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:
+57
-2
@@ -1,6 +1,6 @@
|
||||
import cljnim_pvec
|
||||
import cljnim_pmap
|
||||
import strutils, sequtils, hashes, algorithm, os, osproc, locks
|
||||
import strutils, sequtils, hashes, algorithm, os, osproc, locks, math
|
||||
|
||||
type
|
||||
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: int): CljVal = CljVal(kind: ckInt, intVal: v.int64)
|
||||
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 cljKeyword*(v: string): CljVal = CljVal(kind: ckKeyword, kwName: 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)
|
||||
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 ----
|
||||
|
||||
proc cljRepr*(v: CljVal): string =
|
||||
@@ -686,6 +702,30 @@ proc cljVec*(v: CljVal): CljVal =
|
||||
of ckVector: 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 =
|
||||
cljBool(cljCount(v).intVal == 0)
|
||||
|
||||
@@ -852,9 +892,24 @@ proc cljGetDefault*(m: CljVal, key: CljVal, default: CljVal): CljVal =
|
||||
else: default
|
||||
|
||||
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))
|
||||
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 =
|
||||
if m.isNil or m.kind != ckMap: return cljMap(@[], @[])
|
||||
|
||||
+156
-25
@@ -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")
|
||||
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))"
|
||||
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
|
||||
else:
|
||||
# Prepend discard before the first non-empty line
|
||||
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:
|
||||
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 "):
|
||||
|
||||
+272
-211
@@ -48,37 +48,26 @@ proc expandSyntaxQuote*(form: CljVal): CljVal =
|
||||
# Process each element
|
||||
var parts: seq[CljVal] = @[]
|
||||
for item in form.items:
|
||||
if item.kind == ckList and item.items.len == 2 and
|
||||
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]
|
||||
parts.add(expandSyntaxQuote(item))
|
||||
return cljList(@[cljSymbol("concat")] & parts)
|
||||
of ckVector:
|
||||
var parts: seq[CljVal] = @[]
|
||||
for item in form.items:
|
||||
if item.kind == ckList and item.items.len == 2 and
|
||||
item.items[0].kind == ckSymbol and item.items[0].symName == "unquote-splicing":
|
||||
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)])
|
||||
parts.add(expandSyntaxQuote(item))
|
||||
return cljList(@[cljSymbol("apply"), cljSymbol("vector"), cljList(@[cljSymbol("concat")] & parts)])
|
||||
of ckMap:
|
||||
# Maps: expand each key and value
|
||||
return cljList(@[cljSymbol("quote"), form])
|
||||
var parts: seq[CljVal] = @[]
|
||||
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:
|
||||
return cljList(@[cljSymbol("quote"), form])
|
||||
return form
|
||||
|
||||
proc macroexpand1*(form: CljVal): CljVal =
|
||||
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")
|
||||
var acc = args[0]
|
||||
for i in 1..<args.len:
|
||||
let step = args[i]
|
||||
if step.kind == ckList and step.items.len > 0:
|
||||
var newItems = step.items
|
||||
let form = args[i]
|
||||
if form.kind == ckList and form.items.len > 0:
|
||||
if reverse:
|
||||
newItems.add(acc)
|
||||
acc = cljList(form.items & @[acc])
|
||||
else:
|
||||
newItems.insert(acc, 1)
|
||||
acc = cljList(newItems)
|
||||
elif step.kind == ckSymbol:
|
||||
# (-> x f) => (f x)
|
||||
acc = cljList(@[step, acc])
|
||||
acc = cljList(@[form.items[0], acc] & form.items[1..^1])
|
||||
elif form.kind == ckSymbol:
|
||||
acc = cljList(@[form, acc])
|
||||
else:
|
||||
raise newException(CatchableError, "Threading macro requires list or symbol forms")
|
||||
acc
|
||||
raise newException(CatchableError, "Threading macro requires lists or symbols")
|
||||
return acc
|
||||
|
||||
proc initBuiltinMacros*() =
|
||||
# ->
|
||||
proc initMacros*() =
|
||||
# fn* macro
|
||||
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)
|
||||
)
|
||||
|
||||
# ->
|
||||
# -> (thread-first)
|
||||
defineMacro("->", proc(args: seq[CljVal]): CljVal =
|
||||
threadingMacro(args, false)
|
||||
)
|
||||
|
||||
# ->>
|
||||
# ->> (thread-last)
|
||||
defineMacro("->>", proc(args: seq[CljVal]): CljVal =
|
||||
threadingMacro(args, true)
|
||||
)
|
||||
@@ -155,284 +142,294 @@ proc initBuiltinMacros*() =
|
||||
defineMacro("and", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len == 0: return cljBool(true)
|
||||
if args.len == 1: return args[0]
|
||||
# (and a b c) => (if a (if b c false) false)
|
||||
var r = args[^1]
|
||||
var result = cljList(@[cljSymbol("if"), args[^1],
|
||||
args[^1],
|
||||
cljBool(false)])
|
||||
for i in countdown(args.len - 2, 0):
|
||||
r = cljList(@[cljSymbol("if"), args[i], r, cljBool(false)])
|
||||
r
|
||||
result = cljList(@[cljSymbol("if"), args[i], result, cljBool(false)])
|
||||
return result
|
||||
)
|
||||
|
||||
# or
|
||||
defineMacro("or", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len == 0: return cljNil()
|
||||
if args.len == 1: return args[0]
|
||||
# (or a b c) => (if a a (if b b (if c c nil)))
|
||||
var r = args[^1]
|
||||
var result = args[^1]
|
||||
for i in countdown(args.len - 2, 0):
|
||||
r = cljList(@[cljSymbol("if"), args[i], args[i], r])
|
||||
r
|
||||
let gs = cljSymbol(gensymName("or_"))
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, args[i]]),
|
||||
cljList(@[cljSymbol("if"), gs, gs, result])])
|
||||
return result
|
||||
)
|
||||
|
||||
# when-let
|
||||
defineMacro("when-let", proc(args: seq[CljVal]): CljVal =
|
||||
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 body = args[1..^1]
|
||||
if binding.kind != ckVector or binding.items.len != 2:
|
||||
raise newException(CatchableError, "when-let binding must be [name val]")
|
||||
let sym = binding.items[0]
|
||||
raise newException(CatchableError, "when-let requires [name val]")
|
||||
let name = binding.items[0]
|
||||
let val = binding.items[1]
|
||||
let gs = cljSymbol(gensymName("wl_"))
|
||||
cljList(@[cljSymbol("let"), cljVector(@[gs, val]),
|
||||
cljList(@[cljSymbol("if"), gs,
|
||||
cljList(@[cljSymbol("let"), cljVector(@[sym, gs])] & body)])])
|
||||
cljList(@[cljSymbol("when"), gs,
|
||||
cljList(@[cljSymbol("let"), cljVector(@[name, gs])] & body)])])
|
||||
)
|
||||
|
||||
# if-let
|
||||
defineMacro("if-let", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 2:
|
||||
raise newException(CatchableError, "if-let requires binding, then, and optional else")
|
||||
if args.len < 3:
|
||||
raise newException(CatchableError, "if-let requires binding vector, then, and else")
|
||||
let binding = args[0]
|
||||
let thenExpr = args[1]
|
||||
let elseExpr = if args.len > 2: args[2] else: cljNil()
|
||||
let thenBranch = args[1]
|
||||
let elseBranch = args[2]
|
||||
if binding.kind != ckVector or binding.items.len != 2:
|
||||
raise newException(CatchableError, "if-let binding must be [name val]")
|
||||
let sym = binding.items[0]
|
||||
raise newException(CatchableError, "if-let requires [name val]")
|
||||
let name = binding.items[0]
|
||||
let val = binding.items[1]
|
||||
let gs = cljSymbol(gensymName("il_"))
|
||||
cljList(@[cljSymbol("let"), cljVector(@[gs, val]),
|
||||
cljList(@[cljSymbol("if"), gs,
|
||||
cljList(@[cljSymbol("let"), cljVector(@[sym, gs]), thenExpr]),
|
||||
elseExpr])])
|
||||
cljList(@[cljSymbol("let"), cljVector(@[name, gs]), thenBranch]),
|
||||
elseBranch])])
|
||||
)
|
||||
|
||||
# when
|
||||
defineMacro("when", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 2:
|
||||
if args.len < 1:
|
||||
raise newException(CatchableError, "when requires condition and body")
|
||||
let test = args[0]
|
||||
let cond = args[0]
|
||||
let body = args[1..^1]
|
||||
cljList(@[cljSymbol("if"), test, cljList(@[cljSymbol("do")] & body)])
|
||||
cljList(@[cljSymbol("if"), cond,
|
||||
cljList(@[cljSymbol("do")] & body),
|
||||
cljNil()])
|
||||
)
|
||||
|
||||
# when-not
|
||||
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")
|
||||
let test = args[0]
|
||||
let cond = args[0]
|
||||
let body = args[1..^1]
|
||||
cljList(@[cljSymbol("if"), test, cljNil(), cljList(@[cljSymbol("do")] & body)])
|
||||
cljList(@[cljSymbol("if"), cond, cljNil(),
|
||||
cljList(@[cljSymbol("do")] & body)])
|
||||
)
|
||||
|
||||
# cond
|
||||
defineMacro("cond", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len == 0: return cljNil()
|
||||
if args.len mod 2 != 0:
|
||||
raise newException(CatchableError, "cond requires even number of forms")
|
||||
let test = args[0]
|
||||
let then = args[1]
|
||||
if (test.kind == ckKeyword and test.kwName == "else") or
|
||||
(test.kind == ckSymbol and test.symName == ":else"):
|
||||
return then
|
||||
let rest = args[2..^1]
|
||||
if rest.len == 0:
|
||||
return cljList(@[cljSymbol("if"), test, then])
|
||||
cljList(@[cljSymbol("if"), test, then,
|
||||
cljList(@[cljSymbol("cond")] & rest)])
|
||||
raise newException(CatchableError, "cond requires pairs of test/expr")
|
||||
var result = cljNil()
|
||||
for i in countdown((args.len div 2) - 1, 0):
|
||||
let test = args[i * 2]
|
||||
let expr = args[i * 2 + 1]
|
||||
if test.kind == ckKeyword and test.kwName == "else":
|
||||
result = expr
|
||||
else:
|
||||
result = cljList(@[cljSymbol("if"), test, expr, result])
|
||||
return result
|
||||
)
|
||||
|
||||
# cond->
|
||||
defineMacro("cond->", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 1:
|
||||
raise newException(CatchableError, "cond-> requires at least 1 argument")
|
||||
let expr = args[0]
|
||||
raise newException(CatchableError, "cond-> requires initial value and clauses")
|
||||
let init = args[0]
|
||||
let clauses = args[1..^1]
|
||||
if clauses.len mod 2 != 0:
|
||||
raise newException(CatchableError, "cond-> requires pairs of test/expr")
|
||||
var result = init
|
||||
for i in 0..<(clauses.len div 2):
|
||||
let test = clauses[i * 2]
|
||||
let expr = clauses[i * 2 + 1]
|
||||
let gs = cljSymbol(gensymName("ct_"))
|
||||
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])]
|
||||
var i = 1
|
||||
while i + 1 < args.len:
|
||||
let test = args[i]
|
||||
let step = args[i+1]
|
||||
var threaded: CljVal
|
||||
if step.kind == ckList and step.items.len > 0:
|
||||
var newItems = step.items
|
||||
newItems.insert(gs, 1)
|
||||
threaded = cljList(newItems)
|
||||
elif step.kind == ckSymbol:
|
||||
threaded = cljList(@[step, gs])
|
||||
if expr.kind == ckList and expr.items.len > 0:
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), test,
|
||||
cljList(@[expr.items[0], gs] & expr.items[1..^1]),
|
||||
gs])])
|
||||
else:
|
||||
threaded = step
|
||||
forms.add(cljList(@[cljSymbol("if"), test,
|
||||
cljList(@[cljSymbol("set!"), gs, threaded]),
|
||||
gs]))
|
||||
i += 2
|
||||
forms.add(gs)
|
||||
cljList(forms)
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), test,
|
||||
cljList(@[expr, gs]),
|
||||
gs])])
|
||||
return result
|
||||
)
|
||||
|
||||
# cond->>
|
||||
defineMacro("cond->>", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 1:
|
||||
raise newException(CatchableError, "cond->> requires at least 1 argument")
|
||||
let expr = args[0]
|
||||
let gs = cljSymbol(gensymName("ctg_"))
|
||||
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, expr])]
|
||||
var i = 1
|
||||
while i + 1 < args.len:
|
||||
let test = args[i]
|
||||
let step = args[i+1]
|
||||
var threaded: CljVal
|
||||
if step.kind == ckList and step.items.len > 0:
|
||||
var newItems = step.items
|
||||
newItems.add(gs)
|
||||
threaded = cljList(newItems)
|
||||
elif step.kind == ckSymbol:
|
||||
threaded = cljList(@[step, gs])
|
||||
raise newException(CatchableError, "cond->> requires initial value and clauses")
|
||||
let init = args[0]
|
||||
let clauses = args[1..^1]
|
||||
if clauses.len mod 2 != 0:
|
||||
raise newException(CatchableError, "cond->> requires pairs of test/expr")
|
||||
var result = init
|
||||
for i in 0..<(clauses.len div 2):
|
||||
let test = clauses[i * 2]
|
||||
let expr = clauses[i * 2 + 1]
|
||||
let gs = cljSymbol(gensymName("ct_"))
|
||||
if expr.kind == ckList and expr.items.len > 0:
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), test,
|
||||
cljList(expr.items & @[gs]),
|
||||
gs])])
|
||||
else:
|
||||
threaded = step
|
||||
forms.add(cljList(@[cljSymbol("if"), test,
|
||||
cljList(@[cljSymbol("set!"), gs, threaded]),
|
||||
gs]))
|
||||
i += 2
|
||||
forms.add(gs)
|
||||
cljList(forms)
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), test,
|
||||
cljList(@[expr, gs]),
|
||||
gs])])
|
||||
return result
|
||||
)
|
||||
|
||||
# doto
|
||||
defineMacro("doto", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 2:
|
||||
raise newException(CatchableError, "doto requires at least 2 arguments")
|
||||
let x = args[0]
|
||||
let gs = cljSymbol(gensymName("do_"))
|
||||
var forms: seq[CljVal] = @[cljSymbol("let"), cljVector(@[gs, x])]
|
||||
for i in 1..<args.len:
|
||||
let step = args[i]
|
||||
if step.kind == ckList and step.items.len > 0:
|
||||
var newItems = step.items
|
||||
newItems.insert(gs, 1)
|
||||
forms.add(cljList(newItems))
|
||||
elif step.kind == ckSymbol:
|
||||
forms.add(cljList(@[step, gs]))
|
||||
if args.len < 1:
|
||||
raise newException(CatchableError, "doto requires initial value and body")
|
||||
let init = args[0]
|
||||
let body = args[1..^1]
|
||||
let gs = cljSymbol(gensymName("dt_"))
|
||||
var forms: seq[CljVal] = @[]
|
||||
forms.add(cljSymbol("let"))
|
||||
forms.add(cljVector(@[gs, init]))
|
||||
for form in body:
|
||||
if form.kind == ckList and form.items.len > 0:
|
||||
forms.add(cljList(@[form.items[0], gs] & form.items[1..^1]))
|
||||
else:
|
||||
forms.add(cljList(@[form, gs]))
|
||||
forms.add(gs)
|
||||
cljList(forms)
|
||||
return cljList(forms)
|
||||
)
|
||||
|
||||
# as->
|
||||
defineMacro("as->", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 3:
|
||||
raise newException(CatchableError, "as-> requires at least 3 arguments")
|
||||
let expr = args[0]
|
||||
let name = args[1]
|
||||
var acc = expr
|
||||
for i in 2..<args.len:
|
||||
acc = cljList(@[cljSymbol("let"), cljVector(@[name, acc]), args[i]])
|
||||
acc
|
||||
raise newException(CatchableError, "as-> requires name, init, and body")
|
||||
let name = args[0]
|
||||
let init = args[1]
|
||||
let body = args[2..^1]
|
||||
var result = init
|
||||
for form in body:
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[name, result]), form])
|
||||
return result
|
||||
)
|
||||
|
||||
# some->
|
||||
defineMacro("some->", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 2:
|
||||
raise newException(CatchableError, "some-> requires at least 2 arguments")
|
||||
var acc = args[0]
|
||||
for i in 1..<args.len:
|
||||
let step = args[i]
|
||||
if args.len < 1:
|
||||
raise newException(CatchableError, "some-> requires initial value and body")
|
||||
let init = args[0]
|
||||
let body = args[1..^1]
|
||||
var result = init
|
||||
for form in body:
|
||||
let gs = cljSymbol(gensymName("st_"))
|
||||
var threaded: CljVal
|
||||
if step.kind == ckList and step.items.len > 0:
|
||||
var newItems = step.items
|
||||
newItems.insert(gs, 1)
|
||||
threaded = cljList(newItems)
|
||||
elif step.kind == ckSymbol:
|
||||
threaded = cljList(@[step, gs])
|
||||
if form.kind == ckList and form.items.len > 0:
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), gs,
|
||||
cljList(@[form.items[0], gs] & form.items[1..^1]),
|
||||
cljNil()])])
|
||||
else:
|
||||
threaded = step
|
||||
acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]),
|
||||
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]),
|
||||
cljNil(), threaded])])
|
||||
acc
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), gs,
|
||||
cljList(@[form, gs]),
|
||||
cljNil()])])
|
||||
return result
|
||||
)
|
||||
|
||||
# some->>
|
||||
defineMacro("some->>", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 2:
|
||||
raise newException(CatchableError, "some->> requires at least 2 arguments")
|
||||
var acc = args[0]
|
||||
for i in 1..<args.len:
|
||||
let step = args[i]
|
||||
let gs = cljSymbol(gensymName("stg_"))
|
||||
var threaded: CljVal
|
||||
if step.kind == ckList and step.items.len > 0:
|
||||
var newItems = step.items
|
||||
newItems.add(gs)
|
||||
threaded = cljList(newItems)
|
||||
elif step.kind == ckSymbol:
|
||||
threaded = cljList(@[step, gs])
|
||||
if args.len < 1:
|
||||
raise newException(CatchableError, "some->> requires initial value and body")
|
||||
let init = args[0]
|
||||
let body = args[1..^1]
|
||||
var result = init
|
||||
for form in body:
|
||||
let gs = cljSymbol(gensymName("st_"))
|
||||
if form.kind == ckList and form.items.len > 0:
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), gs,
|
||||
cljList(form.items & @[gs]),
|
||||
cljNil()])])
|
||||
else:
|
||||
threaded = step
|
||||
acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]),
|
||||
cljList(@[cljSymbol("if"), cljList(@[cljSymbol("nil?"), gs]),
|
||||
cljNil(), threaded])])
|
||||
acc
|
||||
result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]),
|
||||
cljList(@[cljSymbol("if"), gs,
|
||||
cljList(@[form, gs]),
|
||||
cljNil()])])
|
||||
return result
|
||||
)
|
||||
|
||||
# for (simplified — single binding only)
|
||||
# for
|
||||
defineMacro("for", proc(args: seq[CljVal]): CljVal =
|
||||
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 body = args[1..^1]
|
||||
if bindings.kind != ckVector or bindings.items.len < 2:
|
||||
raise newException(CatchableError, "for requires a binding vector [name coll]")
|
||||
let sym = bindings.items[0]
|
||||
let coll = bindings.items[1]
|
||||
let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body)
|
||||
let gs = cljSymbol(gensymName("f_"))
|
||||
if bindings.kind != ckVector:
|
||||
raise newException(CatchableError, "for bindings must be a vector")
|
||||
let gs = cljSymbol(gensymName("fr_"))
|
||||
cljList(@[cljSymbol("let"), cljVector(@[gs, cljVector(@[])]),
|
||||
cljList(@[cljSymbol("doseq"), cljVector(@[sym, coll]),
|
||||
cljList(@[cljSymbol("set!"), gs,
|
||||
cljList(@[cljSymbol("conj"), gs, bodyExpr])])]),
|
||||
cljList(@[cljSymbol("doseq")] & @[bindings] & @[
|
||||
cljList(@[cljSymbol("set!"), gs, cljList(@[cljSymbol("conj"), gs] & body)])]),
|
||||
gs])
|
||||
)
|
||||
|
||||
# doseq (simplified — single binding only)
|
||||
# doseq
|
||||
defineMacro("doseq", proc(args: seq[CljVal]): CljVal =
|
||||
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 body = args[1..^1]
|
||||
if bindings.kind != ckVector or bindings.items.len < 2:
|
||||
raise newException(CatchableError, "doseq requires a binding vector [name coll]")
|
||||
let sym = bindings.items[0]
|
||||
if bindings.kind != ckVector:
|
||||
raise newException(CatchableError, "doseq bindings must be a vector")
|
||||
let bindingPairs = bindings.items.len div 2
|
||||
if bindingPairs == 1:
|
||||
let name = bindings.items[0]
|
||||
let coll = bindings.items[1]
|
||||
let gs = cljSymbol(gensymName("ds_"))
|
||||
let bodyExpr = if body.len == 1: body[0] else: cljList(@[cljSymbol("do")] & body)
|
||||
cljList(@[cljSymbol("let"), cljVector(@[gs, coll]),
|
||||
cljList(@[cljSymbol("loop"), cljVector(@[cljSymbol(gensymName("i_")), cljInt(0)]),
|
||||
cljList(@[cljSymbol("when"),
|
||||
cljList(@[cljSymbol("<"), cljSymbol(gensymName("i_")), cljList(@[cljSymbol("count"), gs])]),
|
||||
cljList(@[cljSymbol("let"), cljVector(@[sym, cljList(@[cljSymbol("nth"), gs, cljSymbol(gensymName("i_"))])]),
|
||||
bodyExpr]),
|
||||
cljList(@[cljSymbol("recur"),
|
||||
cljList(@[cljSymbol("inc"), cljSymbol(gensymName("i_"))])])])])])
|
||||
cljList(@[cljSymbol("loop"), cljVector(@[name, cljList(@[cljSymbol("first"), gs]),
|
||||
gs, cljList(@[cljSymbol("rest"), gs])]),
|
||||
cljList(@[cljSymbol("when"), name] & body),
|
||||
cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("first"), gs]),
|
||||
cljList(@[cljSymbol("rest"), gs])])])])
|
||||
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
|
||||
defineMacro("dotimes", proc(args: seq[CljVal]): CljVal =
|
||||
if args.len < 2:
|
||||
raise newException(CatchableError, "dotimes requires binding vector and body")
|
||||
let bindings = args[0]
|
||||
raise newException(CatchableError, "dotimes requires binding and body")
|
||||
let binding = args[0]
|
||||
let body = args[1..^1]
|
||||
if bindings.kind != ckVector or bindings.items.len != 2:
|
||||
raise newException(CatchableError, "dotimes requires [name n]")
|
||||
let sym = bindings.items[0]
|
||||
let n = bindings.items[1]
|
||||
cljList(@[cljSymbol("doseq"), cljVector(@[sym, cljList(@[cljSymbol("range"), n])])] & body)
|
||||
if binding.kind != ckVector or binding.items.len != 2:
|
||||
raise newException(CatchableError, "dotimes requires [name count]")
|
||||
let name = binding.items[0]
|
||||
let count = binding.items[1]
|
||||
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
|
||||
defineMacro("defmacro", proc(args: seq[CljVal]): CljVal =
|
||||
# defmacro is handled specially in the compiler, this is a no-op
|
||||
cljNil()
|
||||
if args.len < 3:
|
||||
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
|
||||
@@ -464,3 +461,67 @@ proc initBuiltinMacros*() =
|
||||
cljList(@[cljSymbol("finally"),
|
||||
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()
|
||||
|
||||
+112
-10
@@ -8,7 +8,7 @@ type
|
||||
proc skipWhitespaceAndComments(s: string, i: var int) =
|
||||
while i < s.len:
|
||||
let c = s[i]
|
||||
if c in Whitespace:
|
||||
if c in Whitespace or c == ',':
|
||||
inc i
|
||||
elif c == ';':
|
||||
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
|
||||
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 =
|
||||
var start = i
|
||||
# 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:
|
||||
while i < s.len and s[i] in Digits:
|
||||
inc i
|
||||
consumeNumericSuffix(s, i)
|
||||
consumeRatioDenominator(s, i)
|
||||
return s[start..<i]
|
||||
elif i + 1 < s.len and s[i+1] in Digits:
|
||||
inc i
|
||||
@@ -75,6 +89,8 @@ proc readNumberOrSym(s: string, i: var int): string =
|
||||
if i < s.len and s[i] in Digits:
|
||||
while i < s.len and s[i] in Digits:
|
||||
inc i
|
||||
consumeNumericSuffix(s, i)
|
||||
consumeRatioDenominator(s, i)
|
||||
return s[start..<i]
|
||||
else:
|
||||
# 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:
|
||||
while i < s.len and s[i] in Digits:
|
||||
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]
|
||||
else:
|
||||
while i < s.len and isSymChar(s[i]):
|
||||
@@ -118,31 +138,36 @@ proc readAtom(s: string, i: var int): CljVal =
|
||||
if tok[0] == ':':
|
||||
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
|
||||
var isFloat = false
|
||||
var isNumber = true
|
||||
var startIdx = 0
|
||||
if tok[0] == '-' or tok[0] == '+':
|
||||
if tok.len == 1:
|
||||
if numTok[0] == '-' or numTok[0] == '+':
|
||||
if numTok.len == 1:
|
||||
isNumber = false
|
||||
else:
|
||||
startIdx = 1
|
||||
var sawExp = false
|
||||
for j in startIdx..<tok.len:
|
||||
if tok[j] == '.':
|
||||
for j in startIdx..<numTok.len:
|
||||
if numTok[j] == '.':
|
||||
if isFloat:
|
||||
isNumber = false
|
||||
break
|
||||
isFloat = true
|
||||
elif tok[j] == 'e' or tok[j] == 'E':
|
||||
elif numTok[j] == 'e' or numTok[j] == 'E':
|
||||
if sawExp:
|
||||
isNumber = false
|
||||
break
|
||||
sawExp = 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
|
||||
elif tok[j] notin Digits:
|
||||
elif numTok[j] notin Digits:
|
||||
if sawExp and (tok[j] == '-' or tok[j] == '+'):
|
||||
if j > 0 and (tok[j-1] == 'e' or tok[j-1] == 'E'):
|
||||
continue
|
||||
@@ -157,9 +182,9 @@ proc readAtom(s: string, i: var int): CljVal =
|
||||
break
|
||||
if hasDigit:
|
||||
if isFloat:
|
||||
return cljFloat(parseFloat(tok))
|
||||
return cljFloat(parseFloat(numTok))
|
||||
else:
|
||||
return cljInt(parseInt(tok).int64)
|
||||
return cljInt(parseInt(numTok).int64)
|
||||
|
||||
# symbol
|
||||
return cljSymbol(tok)
|
||||
@@ -178,6 +203,13 @@ proc readList(s: string, i: var int): CljVal =
|
||||
break
|
||||
let form = readForm(s, i)
|
||||
if form != nil:
|
||||
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)
|
||||
|
||||
@@ -193,6 +225,13 @@ proc readVector(s: string, i: var int): CljVal =
|
||||
break
|
||||
let form = readForm(s, i)
|
||||
if form != nil:
|
||||
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)
|
||||
|
||||
@@ -252,6 +291,39 @@ proc readDispatch(s: string, i: var int): CljVal =
|
||||
inc i
|
||||
discard readForm(s, i)
|
||||
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:
|
||||
raise newException(ReaderError, "Unknown dispatch macro: #" & c)
|
||||
|
||||
@@ -282,6 +354,34 @@ proc readDeref(s: string, i: var int): CljVal =
|
||||
let form = readForm(s, i)
|
||||
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 =
|
||||
skipWhitespaceAndComments(s, i)
|
||||
if i >= s.len:
|
||||
@@ -313,6 +413,8 @@ proc readForm(s: string, i: var int): CljVal =
|
||||
return readUnquote(s, i)
|
||||
of '@':
|
||||
return readDeref(s, i)
|
||||
of '\\':
|
||||
return readCharLiteral(s, i)
|
||||
else:
|
||||
return readAtom(s, i)
|
||||
|
||||
|
||||
+6
-5
@@ -44,13 +44,16 @@ def extract_default(content, start):
|
||||
return rest[:k], k
|
||||
|
||||
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 = []
|
||||
i = 0
|
||||
while i < len(text):
|
||||
# Check for #?@ or #?
|
||||
is_splice = False
|
||||
if i + 3 <= len(text) and text[i:i+3] == '#?@':
|
||||
j = i + 3
|
||||
is_splice = True
|
||||
elif i + 2 <= len(text) and text[i:i+2] == '#?':
|
||||
j = i + 2
|
||||
else:
|
||||
@@ -85,6 +88,8 @@ def strip_reader_conditionals(text):
|
||||
# Extract content between parens
|
||||
inner = text[j+1:k]
|
||||
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)
|
||||
i = k + 1
|
||||
|
||||
@@ -98,10 +103,6 @@ def run_test(cljc_path, timeout=30):
|
||||
content = re.sub(r'#\{[^}]*\}', r'@[]', content) # #{} → @[]
|
||||
content = re.sub(r'#:([\w-]+)', r'\1', content) # #:foo → foo
|
||||
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 = re.sub(r'clojure\.test', 'test', content)
|
||||
|
||||
Reference in New Issue
Block a user