From e5897125fdc2123b305f446997d20758428a9289 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Mon, 11 May 2026 01:49:48 +0300 Subject: [PATCH] chore: emitter/runtime improvements from previous session --- lib/cljnim_runtime.nim | 198 +++++++++++++++++++- src/emitter.nim | 409 +++++++++++++++++++++++++++++++++-------- 2 files changed, 523 insertions(+), 84 deletions(-) diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim index 81e39e6..cc31763 100644 --- a/lib/cljnim_runtime.nim +++ b/lib/cljnim_runtime.nim @@ -11,6 +11,9 @@ type fn: CljVal args: seq[CljVal] + ExInfo* = object of CatchableError + exData*: CljVal + CljVal* = ref CljValObj CljValObj = object meta*: CljVal @@ -110,6 +113,24 @@ proc cljSet*(items: seq[CljVal]): CljVal = s = pmapAssoc(s, item, true, hash(item), cljEq) CljVal(kind: ckSet, setData: s) +proc cljSet*(coll: CljVal): CljVal = + var s = newPersistentMap[CljVal, bool]() + if coll.isNil: return CljVal(kind: ckSet, setData: s) + case coll.kind + of ckList: + for item in coll.listItems: + s = pmapAssoc(s, item, true, hash(item), cljEq) + of ckVector: + for item in toSeq(coll.vecData): + s = pmapAssoc(s, item, true, hash(item), cljEq) + of ckSet: + return coll + of ckMap: + for key in pmapKeys(coll.mapData): + s = pmapAssoc(s, key, true, hash(key), cljEq) + else: discard + 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") @@ -185,6 +206,47 @@ proc cljIsColl*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind in {ckLi proc cljIsSequential*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind in {ckList, ckVector}) proc cljIsFn*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckFn) proc cljIsVar*(v: CljVal): CljVal = cljBool(false) + +proc cljParseUuid*(s: CljVal): CljVal = + if s.kind != ckString: + raise newException(CatchableError, "Invalid UUID string") + let str = s.strVal + if str.len == 0: + return cljNil() + # Simple UUID validation: 8-4-4-4-12 hex digits + var parts: seq[string] = @[] + var current = "" + for c in str: + if c == '-': + parts.add(current) + current = "" + else: + current.add(c) + parts.add(current) + if parts.len == 5 and + parts[0].len == 8 and parts[1].len == 4 and parts[2].len == 4 and + parts[3].len == 4 and parts[4].len == 12: + for p in parts: + for c in p: + if c notin {'0'..'9', 'a'..'f', 'A'..'F'}: + return cljNil() + return cljString(str.toLowerAscii()) + return cljNil() + +proc cljIsUuid*(v: CljVal): CljVal = + if v.kind != ckString: + return cljBool(false) + let str = v.strVal + var parts = str.split('-') + if parts.len == 5 and + parts[0].len == 8 and parts[1].len == 4 and parts[2].len == 4 and + parts[3].len == 4 and parts[4].len == 12: + for p in parts: + for c in p: + if c notin {'0'..'9', 'a'..'f', 'A'..'F'}: + return cljBool(false) + return cljBool(true) + return cljBool(false) proc cljIsIfn*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind in {ckFn, ckKeyword, ckSymbol, ckMap, ckSet, ckVector}) proc cljIsBool*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckBool) proc cljIsTrueP*(v: CljVal): CljVal = cljBool(cljIsTrue(v)) @@ -772,10 +834,20 @@ proc cljSortedMap*(args: seq[CljVal]): CljVal = # Fallback to regular hash-map cljHashMap(args) +proc cljSortedMapBy*(args: seq[CljVal]): CljVal = + # Fallback to regular hash-map (ignores comparator) + if args.len < 1: return cljMap(@[], @[]) + cljHashMap(args[1..^1]) + proc cljSortedSet*(args: seq[CljVal]): CljVal = # Fallback to regular hash-set cljHashSet(args) +proc cljSortedSetBy*(args: seq[CljVal]): CljVal = + # Fallback to regular hash-set (ignores comparator) + if args.len < 1: return cljSet(@[]) + cljHashSet(args[1..^1]) + proc cljArrayMap*(args: seq[CljVal]): CljVal = # Fallback to regular hash-map cljHashMap(args) @@ -845,6 +917,14 @@ proc cljToArray*(args: seq[CljVal]): CljVal = for c in v.strVal: items.add(cljString($c)) return cljVector(items) else: return cljVector(@[v]) + +proc cljIntoArray*(args: seq[CljVal]): CljVal = + if args.len == 0: return cljVector(@[]) + if args.len == 1: + return cljToArray(@[args[0]]) + # Ignore type argument, convert last collection argument to vector + return cljToArray(@[args[^1]]) + proc cljVolatileBang*(args: seq[CljVal]): CljVal = if args.len == 0: return cljNil() # volatile! creates a mutable holder - stub as atom @@ -948,6 +1028,19 @@ proc cljDescendants*(args: seq[CljVal]): CljVal = if args.len < 1: return cljNil() cljSet(@[]) +proc cljUseFixtures*(args: seq[CljVal]): CljVal = + cljNil() + +proc cljAddWatch*(args: seq[CljVal]): CljVal = + cljNil() + +proc cljRemoveWatch*(args: seq[CljVal]): CljVal = + cljNil() + +proc cljAlterVarRoot*(args: seq[CljVal]): CljVal = + if args.len >= 2: return args[1] + cljNil() + proc cljParents*(args: seq[CljVal]): CljVal = if args.len < 1: return cljNil() cljSet(@[]) @@ -1254,6 +1347,42 @@ proc cljGetDefault*(m: CljVal, key: CljVal, default: CljVal): CljVal = else: default else: default +proc cljExInfo*(msg: CljVal, data: CljVal): CljVal = + var ex = newException(ExInfo, cljStr(msg)) + ex.exData = data + raise ex + +proc cljExData*(ex: CljVal): CljVal = + if ex.isNil: return cljNil() + case ex.kind + of ckMap: + let data = cljGet(ex, cljKeyword("data")) + if data.kind == ckNil: + return ex + return data + else: return cljNil() + +proc cljCall*(f: CljVal, args: seq[CljVal]): CljVal = + if f.isNil: return cljNil() + case f.kind + of ckFn: + if args.len > 0: return f.fnProc(args) + else: return f.fnProc(@[]) + of ckMap: + if args.len > 0: return cljGet(f, args[0]) + else: return cljNil() + of ckSet: + if args.len > 0: return cljBool(pmapContains(f.setData, args[0], hash(args[0]), cljEq)) + else: return cljBool(false) + of ckVector: + if args.len > 0: return cljNth(f, args[0]) + else: return cljNil() + of ckKeyword: + if args.len > 0: return cljGet(args[0], f) + else: return cljNil() + else: + raise newException(CatchableError, "Cannot call value of type " & $f.kind) + proc cljAssoc*(m: CljVal, key: CljVal, val: CljVal): CljVal = if m.isNil or m.kind == ckNil: return CljVal(kind: ckMap, mapData: pmapAssoc(newPersistentMap[CljVal, CljVal](), key, val, hash(key), cljEq)) @@ -1340,6 +1469,29 @@ proc cljMap*(f: CljVal, coll: CljVal): CljVal = if f.kind == ckFn: cljMap(f.fnProc, coll) else: raise newException(CatchableError, "map requires a function") +proc cljMapN*(f: CljVal, colls: seq[CljVal]): CljVal = + if f.kind != ckFn: raise newException(CatchableError, "map requires a function") + let fn = f.fnProc + var seqs: seq[seq[CljVal]] = newSeq[seq[CljVal]]() + for coll in colls: + if coll.isNil: + seqs.add(@[]) + else: + case coll.kind + of ckList: seqs.add(coll.listItems) + of ckVector: seqs.add(toSeq(coll.vecData)) + else: seqs.add(@[]) + if seqs.len == 0: return cljList(@[]) + var res = newSeq[CljVal]() + var i = 0 + while true: + var args = newSeq[CljVal]() + for s in seqs: + if i >= s.len: return cljList(res) + args.add(s[i]) + res.add(fn(args)) + i += 1 + proc cljFilter*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal = if coll.isNil: return cljList(@[]) case coll.kind @@ -1362,6 +1514,15 @@ proc cljReduce*(f: CljVal, init: CljVal, coll: CljVal): CljVal = if f.kind == ckFn: cljReduce(f.fnProc, init, coll) else: raise newException(CatchableError, "reduce requires a function") +proc cljTransduce*(xform: CljVal, f: CljVal, init: CljVal, coll: CljVal): CljVal = + # Stub: apply xform to f and reduce + if xform.kind == ckFn and f.kind == ckFn: + let rf = xform.fnProc(@[f]) + if rf.kind == ckFn: + return cljReduce(rf, init, coll) + # Fallback: just return init + return init + proc cljMapv*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal = if coll.isNil: return cljVector(@[]) case coll.kind @@ -1590,9 +1751,32 @@ proc cljAtom*(args: seq[CljVal]): CljVal = i += 2 a +proc cljPromise*(): CljVal = + cljAtom(cljNil()) + proc cljPromise*(args: seq[CljVal]): CljVal = cljAtom(cljNil()) +proc cljForce*(v: CljVal): CljVal = + if v.kind == ckAtom: v.atomVal + else: v + +proc cljSleep*(ms: CljVal): CljVal = + cljNil() + +proc cljTap*(args: seq[CljVal]): CljVal = + if args.len >= 1: return args[0] + cljNil() + +proc cljFutureCancel*(v: CljVal): CljVal = + cljNil() + +proc cljAddTap*(args: seq[CljVal]): CljVal = + cljNil() + +proc cljRemoveTap*(args: seq[CljVal]): CljVal = + cljNil() + proc cljFuture*(args: seq[CljVal]): CljVal = if args.len == 0: return cljAtom(cljNil()) let f = args[0] @@ -1621,7 +1805,7 @@ proc cljReset*(a: CljVal, v: CljVal): CljVal = else: raise newException(CatchableError, "reset! requires an atom") -proc cljSwap*(a: CljVal, f: proc(args: seq[CljVal]): CljVal, args: seq[CljVal]): CljVal = +proc cljSwapImpl(a: CljVal, f: proc(args: seq[CljVal]): CljVal, args: seq[CljVal]): CljVal = if a.kind == ckAtom: var fargs = @[a.atomVal] fargs.add(args) @@ -1630,10 +1814,18 @@ proc cljSwap*(a: CljVal, f: proc(args: seq[CljVal]): CljVal, args: seq[CljVal]): else: raise newException(CatchableError, "swap! requires an atom") -proc cljSwap*(a: CljVal, f: CljVal, args: seq[CljVal] = @[]): CljVal = - if f.kind == ckFn: cljSwap(a, f.fnProc, args) +proc cljSwap*(args: seq[CljVal]): CljVal = + if args.len < 2: raise newException(CatchableError, "swap! requires at least 2 arguments") + let a = args[0] + let f = args[1] + let rest = if args.len > 2: args[2..^1] else: @[] + if f.kind == ckFn: cljSwapImpl(a, f.fnProc, rest) else: raise newException(CatchableError, "swap! requires a function") +proc cljVswap*(args: seq[CljVal]): CljVal = + # Treat volatile like atom for now + cljSwap(args) + # ---- Agents ---- proc cljAgent*(v: CljVal): CljVal = diff --git a/src/emitter.nim b/src/emitter.nim index e81ff56..a7af7bf 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -1,5 +1,5 @@ # Clojure → Nim Emitter -import strutils, sets +import strutils, sets, algorithm import types import macros @@ -67,7 +67,7 @@ proc mangleName*(name: string): string = "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"] + "interface", "lambda", "open", "quit", "result", "nil", "end"] if result in nimKeywords: result = result & "1" @@ -182,8 +182,26 @@ proc runtimeName(op: string): string = of "zipmap": "cljZipmap" of "object-array": "cljObjectArray" of "sorted-map": "cljSortedMap" + of "sorted-map-by": "cljSortedMapBy" of "sorted-set": "cljSortedSet" + of "sorted-set-by": "cljSortedSetBy" of "sorted?": "cljSortedQ" + of "use-fixtures": "cljUseFixtures" + of "transduce": "cljTransduce" + of "vswap!": "cljVswap" + of "split": "cljStrSplit" + of "str/split": "cljStrSplit" + of "add-watch": "cljAddWatch" + of "remove-watch": "cljRemoveWatch" + of "ex-info": "cljExInfo" + of "ex-data": "cljExData" + of "alter-var-root": "cljAlterVarRoot" + of "force": "cljForce" + of "sleep": "cljSleep" + of "tap>": "cljTap" + of "future-cancel": "cljFutureCancel" + of "add-tap": "cljAddTap" + of "remove-tap": "cljRemoveTap" of "array-map": "cljArrayMap" of "rseq": "cljRseq" of "list": "cljListEmpty" @@ -262,6 +280,8 @@ proc runtimeName(op: string): string = of "deliver": "cljDeliver" of "var?": "cljIsVar" of "ifn?": "cljIsIfn" + of "parse-uuid": "cljParseUuid" + of "uuid?": "cljIsUuid" of "ancestors": "cljAncestors" of "descendants": "cljDescendants" of "parents": "cljParents" @@ -283,6 +303,7 @@ proc runtimeName(op: string): string = of "fnil": "cljFnil" of "intern": "cljIntern" of "to-array": "cljToArray" + of "into-array": "cljIntoArray" of "repeatedly": "cljRepeatedly" of "make-hierarchy": "cljMakeHierarchy" of "derive": "cljDerive" @@ -339,14 +360,64 @@ proc emitFnAsProc(items: seq[CljVal], indent: int): string = else: bodyCode = emitBlock(body, indent + 1) popScope() + let sp = indentStr(indent) if bodyCode.find("\n") == -1: - return "(proc (" & paramNames.join(", ") & "): CljVal = " & bodyCode.strip() & ")" + return sp & "(proc (" & paramNames.join(", ") & "): CljVal = " & bodyCode.strip() & ")" else: - return "(proc (" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")" + return sp & "(proc (" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")" + +proc letDestructuredBindings(bindings: CljVal): CljVal = + # Expand let bindings with vector/map destructuring into simple symbol bindings + if bindings.kind != ckVector: + return bindings + var result: seq[CljVal] = @[] + var i = 0 + while i < bindings.items.len: + let bname = bindings.items[i] + let bval = bindings.items[i+1] + if bname.kind == ckSymbol: + result.add(bname) + result.add(bval) + elif bname.kind == ckVector: + let tmp = cljSymbol("ds_" & $i) + result.add(tmp) + result.add(bval) + for j in 0.. 0 and valForm.items[0].kind == ckSymbol and valForm.items[0].symName == "fn" + stderr.writeLine("DEBUG def mangled=", mangled, " indent=", indent, " items.len=", items.len, " isFnDef=", isFnDef) if indent == 0: + stderr.writeLine("DEBUG def indent=0 branch") if isFnDef: # Emit fn as raw proc for def context let oldEmitLibMode = emitLibMode @@ -544,15 +618,17 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = let valCode = emitExpr(items[2], 0) return sp & "let " & mangled & " = " & valCode else: - # For nested def: wrap in proc and use consistent indentation + stderr.writeLine("DEBUG def indent>0 branch") + # For nested def: use global var so it's accessible from closures if isFnDef: let oldEmitLibMode = emitLibMode emitLibMode = false - let valCode = emitFnAsProc(valForm.items, indent + 2) + let valCode = emitFnAsProc(valForm.items, indent) emitLibMode = oldEmitLibMode - return sp & "((proc (): CljVal =\n" & indentStr(indent + 1) & "let " & mangled & " = " & valCode.strip() & "\n" & indentStr(indent + 1) & mangled & ")())" - let valCode = emitExpr(items[2], indent + 2) - return sp & "((proc (): CljVal =\n" & indentStr(indent + 1) & "let " & mangled & " = " & valCode.strip() & "\n" & indentStr(indent + 1) & mangled & ")())" + return sp & "var " & mangled & " {.global.} = " & valCode.strip() & "\n" & sp & mangled + let valCode = emitExpr(items[2], indent) + stderr.writeLine("DEBUG def about to return var global for ", mangled) + return sp & "var " & mangled & " {.global.} = " & valCode.strip() & "\n" & sp & mangled of "defn": if items.len < 3: @@ -574,7 +650,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = addToScope(p.symName) paramNames.add(mangleName(p.symName) & ": CljVal") let body = items[3..^1] - popScope() let procName = mangleName(name.symName) let exportMarker = if emitLibMode: "*" else: "" if indent == 0: @@ -585,6 +660,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) + popScope() return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode else: var bodyCode = "" @@ -592,9 +668,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = bodyCode = emitExpr(body[0], indent + 2) else: bodyCode = emitBlock(body, indent + 2) + popScope() return sp & "(block:\n" & indentStr(indent + 1) & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & "\n" & - indentStr(indent + 1) & procName & ")" + indentStr(indent + 1) & "cljNil())" of "defn-": if items.len < 3: @@ -629,12 +706,15 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode of "fn": + stderr.writeLine("DEBUG emitSpecialForm fn indent=", indent) if items.len < 2: raise newException(EmitterError, "fn requires params") var paramsIdx = 1 + var fnName = "" # Handle named fn: (fn name [params] body) if items[1].kind == ckSymbol: paramsIdx = 2 + fnName = items[1].symName if paramsIdx >= items.len: raise newException(EmitterError, "fn requires params and body") var params = items[paramsIdx] @@ -647,6 +727,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = let paramCount = params.items.len pushScope() var paramNames: seq[string] = @[] + if fnName.len > 0: + addToScope(fnName) for p in params.items: if p.kind != ckSymbol: raise newException(EmitterError, "fn params must be symbols") @@ -664,27 +746,40 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = if paramCount > 0: for i in 0.. 0: - # Multi-line: let bindings on separate lines, then body - var allLines: seq[string] = @[] for a in argsLines: - allLines.add(a) - if bodyCode.find("\n") == -1: - allLines.add(indentStr(indent + 1) & bodyCode.strip()) - else: - allLines.add(bodyCode) - return sp & "cljFn(proc(args: seq[CljVal]): CljVal =\n" & allLines.join("\n") & ")" + fnBodyLines.add(a) + if bodyCode.find("\n") == -1: + fnBodyLines.add(indentStr(indent + 1) & bodyCode.strip()) else: - # No params - simple case - if bodyCode.find("\n") == -1: - return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & bodyCode.strip() & ")" + fnBodyLines.add(bodyCode) + if fnName.len > 0 or argsLines.len > 0 or bodyCode.find("\n") != -1: + # Wrap in IIFE to avoid indentation issues when used as an argument + var wrapperLines: seq[string] = @[] + stderr.writeLine("DEBUG fn indent=", indent, " spLen=", sp.len, " indent+1Len=", indentStr(indent+1).len) + wrapperLines.add(sp & "((proc (): CljVal =") + var tempName: string + if fnName.len > 0: + tempName = mangleName(fnName) else: - return sp & "cljFn(proc(args: seq[CljVal]): CljVal =\n" & bodyCode & ")" + tempName = "fnResult" + wrapperLines.add(indentStr(indent + 1) & "var " & tempName & ": CljVal") + wrapperLines.add(indentStr(indent + 1) & tempName & " = cljFn(proc(args: seq[CljVal]): CljVal =") + let indentedFnBody = indentCode(fnBodyLines.join("\n"), 1) + for line in indentedFnBody.split("\n"): + wrapperLines.add(line) + wrapperLines.add(indentStr(indent + 1) & ")") + wrapperLines.add(indentStr(indent + 1) & tempName) + wrapperLines.add(sp & ")())") + return wrapperLines.join("\n") + else: + return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & bodyCode.strip() & ")" of "let": if items.len < 3: raise newException(EmitterError, "let requires bindings and body") - let bindings = items[1] + var bindings = letDestructuredBindings(items[1]) if bindings.kind != ckVector: raise newException(EmitterError, "let bindings must be a vector") if bindings.items.len mod 2 != 0: @@ -697,9 +792,20 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = 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") + raise newException(EmitterError, "let binding name must be a symbol, got " & $bname.kind & ": " & $bname) addToScope(bname.symName) - lines.add(indentStr(indent + 1) & "let " & mangleName(bname.symName) & " = " & emitExpr(bval, indent + 1)) + let bcode = emitExpr(bval, indent + 1) + if bcode.find("\n") != -1: + # Multi-line value: use var and place first line on next line for correct indentation + lines.add(indentStr(indent + 1) & "var " & mangleName(bname.symName) & ": CljVal") + var bcodeLines = bcode.split("\n") + for j, line in bcodeLines: + if j == 0: + lines.add(indentStr(indent + 1) & mangleName(bname.symName) & " = " & line) + else: + lines.add(line) + else: + lines.add(indentStr(indent + 1) & "let " & mangleName(bname.symName) & " = " & bcode) bi += 2 let body = items[2..^1] for j, b in body: @@ -708,7 +814,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = lines.add(bcode) else: let stripped = bcode.strip() - if stripped.startsWith("echo ") or stripped.startsWith("discard ") or + if stripped.contains("\n"): + # Multi-line expression as non-last form: wrap in proc + let indentedCode = indentCode(bcode, 1) + lines.add(indentStr(indent + 1) & "discard ((proc (): CljVal =\n" & indentedCode & "\n" & indentStr(indent + 1) & ")())") + elif stripped.startsWith("echo ") or stripped.startsWith("discard ") or stripped.startsWith("if ") or stripped.startsWith("try:") or stripped.startsWith("var ") or stripped.startsWith("let ") or stripped.startsWith("proc ") or stripped.startsWith("while "): @@ -751,13 +861,26 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = for bi, b in body: let bcode = emitExpr(b, indent + 1) let stripped = bcode.strip() + let isLast = (bi == body.len - 1) let isStatement = stripped.startsWith("echo ") or stripped.startsWith("discard ") or stripped.startsWith("var ") or stripped.startsWith("let ") or - stripped.startsWith("proc ") or stripped.startsWith("block:") or + stripped.startsWith("proc ") or stripped.startsWith("if ") or stripped.startsWith("try:") or stripped.startsWith("while ") or stripped.contains("continue") - if isStatement: + if isLast or isStatement: lines.add(bcode) + elif stripped.startsWith("block:"): + # Multi-line block as non-last form: discard the last line + var blockLines = bcode.split("\n") + var lastIdx = blockLines.len - 1 + while lastIdx >= 0 and blockLines[lastIdx].strip() == "": + lastIdx.dec + if lastIdx >= 0: + let lastLine = blockLines[lastIdx].strip() + if not lastLine.startsWith("discard ") and not lastLine.startsWith("result = "): + let prefixLen = blockLines[lastIdx].len - blockLines[lastIdx].strip().len + blockLines[lastIdx] = blockLines[lastIdx][0..= 0 and blockLines[lastIdx].strip() == "": + lastIdx.dec + if lastIdx >= 0: + let lastLine = blockLines[lastIdx].strip() + if not lastLine.startsWith("discard ") and not lastLine.startsWith("result = "): + let prefixLen = blockLines[lastIdx].len - blockLines[lastIdx].strip().len + blockLines[lastIdx] = blockLines[lastIdx][0.. 0 and form.items[0].kind == ckSymbol: if form.items[0].symName == "catch": if form.items.len < 3: - raise newException(EmitterError, "catch requires exception type, name, and body") - let exType = form.items[1] + raise newException(EmitterError, "catch requires at least name and body") var exTypeStr = "CatchableError" - if exType.kind == ckSymbol: - exTypeStr = exType.symName - let exName = form.items[2] + var nameIdx = 2 + var bodyIdx = 3 + if form.items[1].kind == ckSymbol and form.items.len >= 4: + exTypeStr = form.items[1].symName + elif form.items[1].kind == ckKeyword: + if form.items[1].kwName == "default": + exTypeStr = "CatchableError" + else: + exTypeStr = form.items[1].kwName + else: + # (catch name body...) + nameIdx = 1 + bodyIdx = 2 + let exName = form.items[nameIdx] if exName.kind != ckSymbol: raise newException(EmitterError, "catch name must be a symbol") - catchClauses.add((exTypeStr, mangleName(exName.symName), form.items[3..^1])) + catchClauses.add((exTypeStr, mangleName(exName.symName), form.items[bodyIdx..^1])) elif form.items[0].symName == "finally": finallyBody = form.items[1..^1] else: @@ -876,6 +1032,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = lines.add(sp & "except " & exType & ":") lines.add(indentStr(indent + 1) & "let " & exVar & " = cljMap(@[cljKeyword(\"message\")], @[cljString(getCurrentExceptionMsg())])") lines.add(indentStr(indent + 1) & "discard cljAssoc(" & exVar & ", cljKeyword(\"type\"), cljString(\"" & exType & "\"))") + lines.add(indentStr(indent + 1) & "let exPtr" & exVar & " = getCurrentException()") + lines.add(indentStr(indent + 1) & "if exPtr" & exVar & " of ExInfo:") + lines.add(indentStr(indent + 2) & "discard cljAssoc(" & exVar & ", cljKeyword(\"data\"), (ref ExInfo)(exPtr" & exVar & ").exData)") for i, eb in exBody: let isLast = (i == exBody.len - 1) let code = emitExpr(eb, indent + 1) @@ -945,41 +1104,76 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = return emitExpr(quoted, indent) of "map": - if items.len != 3: - raise newException(EmitterError, "map requires exactly 2 arguments (f, coll)") + if items.len < 3: + raise newException(EmitterError, "map requires at least 2 arguments (f, coll)") let fnArg = items[1] - let collArg = emitExpr(items[2], 0) - if fnArg.kind == ckSymbol: - let rn = runtimeName(fnArg.symName) - if rn.len > 0: - return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args)), " & collArg & ")" - return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & mangleName(fnArg.symName) & "(args[0])), " & collArg & ")" - elif fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName == "fn": - # Inline fn: (map (fn [x] ...) coll) - let fnParams = fnArg.items[1] - let fnBody = fnArg.items[2..^1] - var pNames: seq[string] = @[] - for p in fnParams.items: - pNames.add(mangleName(p.symName)) - var bodyExpr = "" - if fnBody.len == 1: - bodyExpr = emitExpr(fnBody[0], 0).strip() + let isVariadic = items.len > 3 + var collArgs: seq[string] = @[] + for idx in 2.. 0: + return sp & "cljMapN(cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args)), " & collList & ")" + return sp & "cljMapN(cljFn(proc(args: seq[CljVal]): CljVal = " & mangleName(fnArg.symName) & "(args)), " & collList & ")" + elif fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName in ["fn", "fn*"]: + let fnParams = fnArg.items[1] + let fnBody = fnArg.items[2..^1] + var pNames: seq[string] = @[] + for p in fnParams.items: + pNames.add(mangleName(p.symName)) + var bodyExpr = "" + if fnBody.len == 1: + bodyExpr = emitExpr(fnBody[0], 0).strip() + else: + var parts: seq[string] = @[] + for b in fnBody: + parts.add(emitExpr(b, 0).strip()) + bodyExpr = parts.join("; ") + var letBindings = "" + for j in 0.. 0: + return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args[0])), " & collArg & ")" + return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & mangleName(fnArg.symName) & "(args[0])), " & collArg & ")" + elif fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName in ["fn", "fn*"]: + let fnParams = fnArg.items[1] + let fnBody = fnArg.items[2..^1] + var pNames: seq[string] = @[] + for p in fnParams.items: + pNames.add(mangleName(p.symName)) + var bodyExpr = "" + if fnBody.len == 1: + bodyExpr = emitExpr(fnBody[0], 0).strip() + else: + var parts: seq[string] = @[] + for b in fnBody: + parts.add(emitExpr(b, 0).strip()) + bodyExpr = parts.join("; ") + let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; " & bodyExpr & ")" + return sp & "cljMap(cljFn(" & fnProc & "), " & collArg & ")" + else: + return sp & "cljMap(" & emitExpr(fnArg, 0) & ", " & collArg & ")" of "filter": if items.len != 3: raise newException(EmitterError, "filter requires exactly 2 arguments") let fnArg = items[1] let collArg = emitExpr(items[2], 0) - if fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName == "fn": + let isFn = fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol and fnArg.items[0].symName in ["fn", "fn*"] + stderr.writeLine("DEBUG filter isFn=", isFn, " symName=", (if fnArg.kind == ckList and fnArg.items.len > 0 and fnArg.items[0].kind == ckSymbol: fnArg.items[0].symName else: "N/A")) + if isFn: let fnParams = fnArg.items[1] let fnBody = fnArg.items[2..^1] var pNames: seq[string] = @[] @@ -997,7 +1191,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; " & bodyExpr & ")" return sp & "cljFilter(cljFn(" & fnProc & "), " & collArg & ")" else: - return sp & "cljFilter(" & emitExpr(fnArg, 0) & ", " & collArg & ")" + return sp & "cljFilter(" & emitExpr(fnArg, indent) & ", " & collArg & ")" of "reduce": if items.len < 3 or items.len > 4: @@ -1057,7 +1251,27 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = of "partial": var parts: seq[string] = @[] for i in 1.. 0 and items[i].items[0].kind == ckSymbol and items[i].items[0].symName in ["fn", "fn*"]: + let fnParams = items[i].items[1] + let fnBody = items[i].items[2..^1] + var pNames: seq[string] = @[] + for p in fnParams.items: + pNames.add(mangleName(p.symName)) + var bodyExpr = "" + if fnBody.len == 1: + bodyExpr = emitExpr(fnBody[0], 0).strip() + else: + var bodyParts: seq[string] = @[] + for b in fnBody: + bodyParts.add(emitExpr(b, 0).strip()) + bodyExpr = bodyParts.join("; ") + var letBindings = "" + for j in 0.. 0: var argParts: seq[string] = @[] for i in 1.. 0 and items[i].items[0].kind == ckSymbol and items[i].items[0].symName in ["fn", "fn*"]: + stderr.writeLine("DEBUG runtimeCall fn arg indent=", indent) argParts.add(emitExpr(items[i], indent)) # Variadic functions take seq[CljVal] let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=", "println", "prn", "print", "str", "pr-str", "atom", "concat", "min", "max", "merge", "interleave", - "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-set", "cons", + "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-map-by", "sorted-set", "sorted-set-by", "cons", "use-fixtures", "vswap!", "swap!", "tap>", "add-tap", "remove-tap", "add-watch", "remove-watch", "alter-var-root", "array-map", "inf", "nan", "list", "float", "int", "double", "long", "short", "byte", "boolean", "num", "number", @@ -1461,7 +1675,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = "drop-last", "shuffle", "repeatedly", "fnil", "intern", "println-str", "prn-str", "binding", "aset", "volatile!", "deliver", "doall", "dorun", - "to-array", "vector", "rand", "rand-int", + "to-array", "into-array", "vector", "rand", "rand-int", "rand-nth", "random-sample"] var call: string if variadic: @@ -1480,8 +1694,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = callOp = callOp[0..^2] let mangled = mangleName(callOp) if isLocalVar(callOp): - # Local cljFn value: call via .fnProc - return sp & mangled & ".fnProc(@[" & args.join(", ") & "])" + # Local value (may be fn, map, set, vector, keyword) — use runtime dispatch + return sp & "cljCall(" & mangled & ", @[" & args.join(", ") & "])" return sp & mangled & "(" & args.join(", ") & ")" proc emitQuotedForm*(v: CljVal): string = @@ -1539,7 +1753,7 @@ proc emitExpr*(v: CljVal, indent: int = 0): string = let variadic = symName in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=", "println", "prn", "print", "str", "pr-str", "atom", "concat", "min", "max", "merge", "interleave", - "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-set", "cons", + "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-map-by", "sorted-set", "sorted-set-by", "cons", "use-fixtures", "vswap!", "swap!", "tap>", "add-tap", "remove-tap", "add-watch", "remove-watch", "alter-var-root", "array-map", "inf", "nan", "list", "float", "int", "double", "long", "short", "byte", "boolean", "num", "number", @@ -1551,7 +1765,7 @@ proc emitExpr*(v: CljVal, indent: int = 0): string = "drop-last", "shuffle", "repeatedly", "fnil", "intern", "println-str", "prn-str", "binding", "aset", "volatile!", "deliver", "doall", "dorun", - "to-array", "vector", "rand", "rand-int", + "to-array", "into-array", "vector", "rand", "rand-int", "rand-nth", "random-sample"] if variadic: return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))" @@ -1602,6 +1816,8 @@ proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string var lines: seq[string] = @[] for i, item in items: var code = emitExpr(item, indent) + if code.contains("testvar_a"): + stderr.writeLine("DEBUG emitBlock indent=", indent, " code=", code.repr) let stripped = code.strip() if i < items.len - 1: let firstNl = code.find("\n") @@ -1620,6 +1836,8 @@ proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string # 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) & ")())" + if code.contains("testvar_a"): + stderr.writeLine("DEBUG emitBlock after wrap has var=", code.contains("var testvar_a"), " has let=", code.contains("let testvar_a")) elif useResult: if stripped.startsWith("discard "): code = indentStr(indent) & "result = " & stripped[8..^1] @@ -1635,6 +1853,11 @@ proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string return lines.join("\n") proc emitProgramInternal(forms: seq[CljVal]): string = + stderr.writeLine("DEBUG emitProgramInternal forms.len=", forms.len) + if forms.len > 0 and forms[0].kind == ckList and forms[0].items.len > 0 and forms[0].items[0].kind == ckSymbol: + stderr.writeLine("DEBUG emitProgramInternal forms[0]=", forms[0].items[0].symName, " forms[0].len=", forms[0].items.len) + scopeStack = @[] + pushScope() requiredImports = initHashSet[string]() var headerLines: seq[string] = @[ "# Generated by Clojure/Nim", @@ -1657,6 +1880,7 @@ proc emitProgramInternal(forms: seq[CljVal]): string = # Recurse into (do ...) forms produced by macro expansion if headSym and headName == "do": let subForms = ef.items[1..^1] + stderr.writeLine("DEBUG processOneForm do subForms.len=", subForms.len) for j in countdown(subForms.len - 1, 0): let subLast = isLast and (j == subForms.len - 1) worklist.add((subForms[j], subLast)) @@ -1664,12 +1888,19 @@ proc emitProgramInternal(forms: seq[CljVal]): string = let isDef = headSym and headName in ["def", "defn", "defn-", "defprotocol", "defrecord", "deftype", "defmulti", "defmethod", "var"] let isMacro = headSym and headName in ["defmacro"] let isNs = headSym and headName == "ns" + if headName == "def" and ($form).contains("testvar"): + stderr.writeLine("DEBUG processOneForm testvar isDef=", isDef, " headName=", headName) if isNs: discard elif isDef or isMacro: - defs.add(emitExpr(ef, 0)) + let defCode = emitExpr(ef, 0) + if defCode.contains("testvar_a"): + stderr.writeLine("DEBUG defs adding testvar_a code=", defCode.repr) + defs.add(defCode) else: let code = emitExpr(ef, 2) + if code.contains("testvar_a"): + stderr.writeLine("DEBUG emitProgramInternal adding testvar_a to mainForms") let stripped = code.strip() if stripped.startsWith("echo ") or stripped.startsWith("discard ") or stripped.startsWith("var "): @@ -1686,14 +1917,29 @@ proc emitProgramInternal(forms: seq[CljVal]): string = for j in countdown(subForms.len - 1, 0): let subLast = isLast and (j == subForms.len - 1) worklist.add((subForms[j], subLast)) + elif form.kind == ckList and form.items.len == 2 and + form.items[0].kind == ckSymbol and form.items[0].symName == "splice-unwrap": + stderr.writeLine("DEBUG unwrapOneForm splice-unwrap") + let inner = form.items[1] + if inner.kind == ckVector: + stderr.writeLine("DEBUG unwrapOneForm splice-unwrap vector len=", inner.items.len) + for j in countdown(inner.items.len - 1, 0): + let subLast = isLast and (j == inner.items.len - 1) + worklist.add((inner.items[j], subLast)) + else: + processOneForm(form, isLast) else: processOneForm(form, isLast) - for i, form in forms: - worklist.add((form, i == forms.len - 1)) + for i in countdown(forms.len - 1, 0): + if forms[i].kind == ckList and forms[i].items.len > 0 and forms[i].items[0].kind == ckSymbol and forms[i].items[0].symName == "def" and ($forms[i]).contains("testvar"): + stderr.writeLine("DEBUG forms adding testvar to worklist") + worklist.add((forms[i], i == forms.len - 1)) while worklist.len > 0: let (item, itemIsLast) = worklist.pop() + if item.kind == ckList and item.items.len > 0 and item.items[0].kind == ckSymbol: + stderr.writeLine("DEBUG worklist popping ", item.items[0].symName) unwrapOneForm(item, itemIsLast) var lines = headerLines @@ -1701,7 +1947,8 @@ proc emitProgramInternal(forms: seq[CljVal]): string = for imp in requiredImports: lines.add("import " & imp) lines.add("") - lines.add(defs) + for d in defs: + lines.add(d) if mainForms.len > 0: lines.add("")