diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim index 19415ed..ff7c1ec 100644 --- a/lib/cljnim_runtime.nim +++ b/lib/cljnim_runtime.nim @@ -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.. 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(@[], @[]) diff --git a/src/emitter.nim b/src/emitter.nim index c62db75..9baa175 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -7,6 +7,7 @@ var requiredImports* = initHashSet[string]() var emitLibMode* = false var loopStack*: seq[seq[string]] = @[] var nsAliases*: seq[(string, string)] = @[] # (alias, namespace) +var scopeStack*: seq[HashSet[string]] = @[] proc setNsAliases*(aliases: seq[(string, string)]) = nsAliases = aliases @@ -42,7 +43,50 @@ proc mangleName*(name: string): string = of '\'': result.add("_QUOTE_") of '&': result.add("_AMP_") of '#': result.add("_HASH") + of '%': result.add("pct") else: result.add(c) + # Nim rejects trailing underscores + while result.len > 0 and result[^1] == '_': + result = result[0..^2] + # Nim rejects double underscores in identifiers + while result.find("__") != -1: + result = result.replace("__", "_") + # Nim 2.x only allows bare `_` as identifier starting with underscore + if result.len > 1 and result[0] == '_': + result = "x" & result + # Nim identifiers cannot start with a digit + if result.len > 0 and result[0] in {'0'..'9'}: + result = "x" & result + if result.len == 0: + result = "val" + # Nim keywords that would clash + let nimKeywords = ["in", "out", "var", "let", "proc", "func", "type", "ref", "ptr", + "object", "method", "template", "macro", "iterator", "converter", + "discard", "return", "break", "continue", "if", "else", "elif", + "when", "case", "of", "for", "while", "try", "except", "finally", + "raise", "import", "export", "from", "include", "using", "bind", + "mixin", "asm", "defer", "block", "static", "yield", "assert", + "do", "enum", "tuple", "shared", "guard", "concept", "distinct", + "interface", "lambda", "open", "quit", "result", "nil"] + if result in nimKeywords: + result = result & "1" + +proc pushScope() = + scopeStack.add(initHashSet[string]()) + +proc popScope() = + if scopeStack.len > 0: + discard scopeStack.pop() + +proc addToScope(name: string) = + if scopeStack.len > 0: + scopeStack[^1].incl(name) + +proc isLocalVar(name: string): bool = + for s in scopeStack: + if name in s: + return true + return false proc emitExpr*(v: CljVal, indent: int = 0): string proc emitBlock*(items: seq[CljVal], indent: int, useResult: bool = false): string @@ -51,6 +95,19 @@ proc emitQuotedForm*(v: CljVal): string proc indentStr(indent: int): string = " ".repeat(indent) +proc indentCode(code: string, extra: int): string = + if extra <= 0: return code + let prefix = indentStr(extra) + var result = "" + var lines = code.split("\n") + for i, line in lines: + if i > 0: result.add("\n") + if line.len > 0: + result.add(prefix & line) + else: + result.add(line) + return result + proc runtimeName(op: string): string = case op of "+": "cljAdd" @@ -108,6 +165,8 @@ proc runtimeName(op: string): string = of "contains?": "cljContains" of "select-keys": "cljSelectKeys" of "merge": "cljMerge" + of "hash-map": "cljHashMap" + of "hash-set": "cljHashSet" of "identity": "cljIdentity" of "type": "cljType" of "abs": "cljAbs" @@ -120,6 +179,11 @@ proc runtimeName(op: string): string = of "reset!": "cljReset" of "swap!": "cljSwap" of "zipmap": "cljZipmap" + of "object-array": "cljObjectArray" + of "sorted-map": "cljSortedMap" + of "sorted-set": "cljSortedSet" + of "sorted?": "cljSortedQ" + of "array-map": "cljArrayMap" # ---- Type predicates ---- of "nil?": "cljIsNilP" of "some?": "cljIsSome" @@ -143,10 +207,12 @@ proc runtimeName(op: string): string = # ---- Keyword/Symbol ops ---- of "keyword": "cljKeywordFn" of "symbol": "cljSymbolFn" + of "inf": "cljInf" + of "nan": "cljNaN" of "name": "cljName" of "namespace": "cljNamespace" - of "key": "cljKey" - of "val": "cljEntryVal" + # of "key": "cljKey" + # of "val": "cljEntryVal" # ---- File operations ---- of "file/read": "cljFileRead" of "file/write": "cljFileWrite" @@ -355,7 +421,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = let name = items[1] if name.kind != ckSymbol: raise newException(EmitterError, "def name must be a symbol") - return sp & "let " & mangleName(name.symName) & " = " & emitExpr(items[2], indent) + let mangled = mangleName(name.symName) + let valCode = emitExpr(items[2], indent) + if indent == 0: + return sp & "let " & mangled & " = " & valCode + else: + return sp & "((proc (): CljVal =\n" & indentStr(indent + 1) & "let " & mangled & " = " & valCode & "\n" & indentStr(indent + 1) & mangled & ")())" of "defn": if items.len < 4: @@ -367,9 +438,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = if params.kind != ckVector: raise newException(EmitterError, "defn params must be a vector") var paramNames: seq[string] = @[] + pushScope() for p in params.items: if p.kind != ckSymbol: raise newException(EmitterError, "defn params must be symbols") + addToScope(p.symName) paramNames.add(mangleName(p.symName) & ": CljVal") let body = items[3..^1] var bodyCode = "" @@ -377,6 +450,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) + popScope() let procName = mangleName(name.symName) let exportMarker = if emitLibMode: "*" else: "" return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode @@ -391,9 +465,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = if params.kind != ckVector: raise newException(EmitterError, "defn- params must be a vector") var paramNames: seq[string] = @[] + pushScope() for p in params.items: if p.kind != ckSymbol: raise newException(EmitterError, "defn- params must be symbols") + addToScope(p.symName) paramNames.add(mangleName(p.symName) & ": CljVal") let body = items[3..^1] var bodyCode = "" @@ -401,6 +477,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) + popScope() let procName = mangleName(name.symName) return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode @@ -411,9 +488,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = if params.kind != ckVector: raise newException(EmitterError, "fn params must be a vector") var paramNames: seq[string] = @[] + pushScope() for p in params.items: if p.kind != ckSymbol: raise newException(EmitterError, "fn params must be symbols") + addToScope(p.symName) paramNames.add(mangleName(p.symName) & ": CljVal") let body = items[2..^1] var bodyCode = "" @@ -421,7 +500,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) - return sp & "(proc(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")" + popScope() + if bodyCode.find("\n") == -1: + return sp & "(proc (" & paramNames.join(", ") & "): CljVal = " & bodyCode.strip() & ")" + else: + return sp & "(proc (" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")" of "let": if items.len < 3: @@ -433,12 +516,14 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = raise newException(EmitterError, "let bindings must have even number of elements") var lines: seq[string] = @[] lines.add(sp & "block:") + pushScope() var bi = 0 while bi < bindings.items.len: let bname = bindings.items[bi] let bval = bindings.items[bi+1] if bname.kind != ckSymbol: raise newException(EmitterError, "let binding name must be a symbol") + addToScope(bname.symName) lines.add(indentStr(indent + 1) & "let " & mangleName(bname.symName) & " = " & emitExpr(bval, 0)) bi += 2 let body = items[2..^1] @@ -454,6 +539,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = lines.add(bcode) else: lines.add(indentStr(indent + 1) & "discard " & stripped) + popScope() return lines.join("\n") of "if": @@ -671,6 +757,23 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = raise newException(EmitterError, "throw requires exactly 1 argument") return sp & "raise newException(CatchableError, cljStr(" & emitExpr(items[1], 0) & "))" + of "when-var-exists": + if items.len < 3: + raise newException(EmitterError, "when-var-exists requires a symbol and body") + let varSym = items[1] + if varSym.kind != ckSymbol: + raise newException(EmitterError, "when-var-exists requires a symbol as first argument") + let symName = resolveNsAlias(varSym.symName) + let exists = runtimeName(symName).len > 0 or isLocalVar(symName) + if exists: + let body = items[2..^1] + if body.len == 1: + return emitExpr(body[0], indent) + else: + return emitBlock(body, indent) + else: + return sp & "cljNil()" + of "quote": if items.len != 2: raise newException(EmitterError, "quote requires exactly 1 argument") @@ -767,9 +870,18 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = return sp & "cljMapv(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" of "apply": - if items.len != 3: - raise newException(EmitterError, "apply requires exactly 2 arguments") - return sp & "cljApply(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + if items.len < 3: + raise newException(EmitterError, "apply requires at least 2 arguments") + if items.len == 3: + return sp & "cljApply(" & emitExpr(items[1], 0) & ", " & emitExpr(items[2], 0) & ")" + else: + # (apply f x y args) -> (apply (partial f x y) args) + let fnArg = emitExpr(items[1], 0) + var partialArgs: seq[string] = @[] + for i in 2..", "<", ">=", "<=", "not=", "println", "prn", "print", "str", "pr-str", "concat", "min", "max", "merge", "interleave", - "zipmap"] + "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-set", + "array-map", "inf", "nan"] var call: string - if variadic and argParts.len > 0: + if variadic: call = rn & "(@[" & argParts.join(", ") & "])" else: call = rn & "(" & argParts.join(", ") & ")" @@ -1096,16 +1209,17 @@ proc emitExpr*(v: CljVal, indent: int = 0): string = of ckSymbol: let symName = resolveNsAlias(v.symName) let rn = runtimeName(symName) - if rn.len > 0: - # Known function symbol — emit as runtime fn reference + if rn.len > 0 and not isLocalVar(symName): return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))" - return sp & mangleName(symName) + if isLocalVar(symName): + return sp & mangleName(symName) + return sp & "cljSymbol(\"" & symName & "\")" of ckList: if v.items.len == 0: return sp & "cljList(@[])" # Macro-expand before emitting let expanded = macroexpand(v) - if expanded.kind != ckList or expanded == v: + if expanded == v: return emitSpecialForm(v.items, indent) return emitExpr(expanded, indent) of ckVector: @@ -1144,19 +1258,18 @@ proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string var code = emitExpr(item, indent) let stripped = code.strip() if i < items.len - 1: - if stripped.startsWith("echo ") or stripped.startsWith("discard ") or - stripped.startsWith("var ") or stripped.startsWith("let ") or - stripped.startsWith("proc ") or stripped.startsWith("result = "): - discard + let firstNl = code.find("\n") + let firstLine = if firstNl == -1: stripped else: code[0.. 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 "): diff --git a/src/macros.nim b/src/macros.nim index f81d24c..78be7c6 100644 --- a/src/macros.nim +++ b/src/macros.nim @@ -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.. 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] - 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]) + 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[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.. 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.. 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.. 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.. 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] - 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_"))])])])])]) + 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_")) + cljList(@[cljSymbol("let"), cljVector(@[gs, coll]), + 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.. 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.. 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,7 +203,14 @@ proc readList(s: string, i: var int): CljVal = break let form = readForm(s, i) 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) proc readVector(s: string, i: var int): CljVal = @@ -193,7 +225,14 @@ proc readVector(s: string, i: var int): CljVal = break let form = readForm(s, i) 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) proc readMap(s: string, i: var int): CljVal = @@ -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) diff --git a/test_single.py b/test_single.py index f7586fe..592d56a 100644 --- a/test_single.py +++ b/test_single.py @@ -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)