From 4b208dbe55d939c7a0ef2c1217343f6d16e15808 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 12 May 2026 20:02:58 +0300 Subject: [PATCH] fix: threading macros, REPL stability, and CI coverage - fix(emitter): emitFnWrapper now uses runtimeName for builtin operators - fix(emitter): map/reduce handlers support variadic runtime functions (+, -, etc.) - fix(emitter): variadic runtime functions list extracted to variadicRuntimeFns constant - fix(macros): restore correct thread-first behavior (->) and fix result shadowing - fix(macros): and/or/as->/some->/some->> macro result variable fixes - fix(repl): add macroexpand step before interpreter eval - fix(runtime): cljReduceSeq handles nil init (uses first coll element) - fix(eval): add macroexpand/macroexpand-1 special forms to interpreter - fix(eval): add macros module import for macroexpand access - fix(warnings): eliminate ResultShadowed and UnusedImport warnings - chore(.gitignore): remove artifact entries (-o, =0.4.0), unignore lib/bring_http.nim - chore(ci): run all examples + threading macro + macroexpand REPL smoke tests --- .gitignore | 3 -- .gitlab-ci.yml | 8 +++ lib/bring_http.nim | 1 + lib/cljnim_runtime.nim | 14 +++-- src/emitter.nim | 117 ++++++++++++++++++++++++----------------- src/eval.nim | 16 +++++- src/macros.nim | 56 ++++++++++---------- src/repl.nim | 4 +- 8 files changed, 136 insertions(+), 83 deletions(-) create mode 120000 lib/bring_http.nim diff --git a/.gitignore b/.gitignore index 78da813..670001e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,6 @@ test_ai_assist # Artifacts / temp files --o -=0.4.0 __pycache__/ test_single_debug.py @@ -26,4 +24,3 @@ test_single_debug.py src/tui_config experiments/native-lib/test_client/test_client experiments/wasm/tools/zig/ -lib/bring_http.nim diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9c78af5..b5f04f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,8 +14,12 @@ test: - echo "=== math ===" && ./cljnim run examples/math.clj - echo "=== macros ===" && ./cljnim run examples/macros.clj - echo "=== ai_tools ===" && ./cljnim run examples/ai_tools.clj || true + - echo "=== ai_features ===" && ./cljnim run examples/ai_features.clj || true - echo "=== ffi ===" && ./cljnim run examples/ffi.clj - echo "=== interop ===" && ./cljnim run examples/interop.clj + - echo "=== fizzbuzz ===" && ./cljnim run examples/fizzbuzz.clj + - echo "=== jsonista ===" && ./cljnim run examples/jsonista.clj + - echo "=== wordfreq ===" && ./cljnim run examples/wordfreq.clj # Smoke test REPL (interpreter path) - printf '{"op":"eval","form":"(+ 1 2 3)"}\n{"op":"quit"}\n' | timeout 5 ./cljnim repl --json | head -3 # Smoke test REPL (tool-call format) @@ -27,6 +31,10 @@ test: - printf '{"op":"eval","form":"(let [c (chan 3)] (>! c 42) ( [1 2 3] (map inc))"}\n{"op":"quit"}\n' | timeout 5 ./cljnim repl --json | head -3 + - printf '{"op":"eval","form":"(->> [1 2 3] (map inc) (reduce +))"}\n{"op":"quit"}\n' | timeout 5 ./cljnim repl --json | head -3 + - printf '{"op":"eval","form":"(macroexpand (quote (-> [1 2 3] (map inc))))"}\n{"op":"quit"}\n' | timeout 5 ./cljnim repl --json | head -3 # Test compiled path - ./cljnim -e '(+ 10 20)' - ./cljnim -e '(quot 17 3)' diff --git a/lib/bring_http.nim b/lib/bring_http.nim new file mode 120000 index 0000000..add58b2 --- /dev/null +++ b/lib/bring_http.nim @@ -0,0 +1 @@ +/home/ziko/bara-eco/bring/lib/bring_http.nim \ No newline at end of file diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim index b60f6e0..b1eec08 100644 --- a/lib/cljnim_runtime.nim +++ b/lib/cljnim_runtime.nim @@ -1476,9 +1476,17 @@ proc cljFilterSeq*(f: proc(args: seq[CljVal]): CljVal, coll: seq[CljVal]): seq[C result.add(item) proc cljReduceSeq*(f: proc(args: seq[CljVal]): CljVal, init: CljVal, coll: seq[CljVal]): CljVal = - result = init - for item in coll: - result = f(@[result, item]) + if coll.len == 0: + return init + var i = 0 + if init.isNil or init.kind == ckNil: + result = coll[0] + i = 1 + else: + result = init + while i < coll.len: + result = f(@[result, coll[i]]) + i += 1 proc cljMap*(f: proc(args: seq[CljVal]): CljVal, coll: CljVal): CljVal = if coll.isNil: return cljList(@[]) diff --git a/src/emitter.nim b/src/emitter.nim index 4fa7920..982167d 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -1,5 +1,5 @@ # Bara Lang → Nim Emitter -import strutils, sets, algorithm, sequtils, tables +import strutils, sets, sequtils, tables import types import macros @@ -13,16 +13,52 @@ var multiArityFns*: HashSet[string] = initHashSet[string]() var definedFnArities*: Table[string, int] = initTable[string, int]() # -1 = multi-arity/rest var loopResultVar*: string = "" # Set by loop handler when result capture is needed +# Runtime functions that accept seq[CljVal] (variadic wrapper signature) +let variadicRuntimeFns* = ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=", + "println", "prn", "print", "str", "pr-str", + "atom", "concat", "min", "max", "merge", "interleave", + "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", + "make-hierarchy", "derive", "underive", "ancestors", + "descendants", "parents", "isa?", "promise", "create-ns", "future", + "delay", + "aclone", "alength", "aget", "int-array", "identical?", "empty", "identity", + "conj", + "drop-last", "shuffle", "repeatedly", "fnil", "intern", + "println-str", "prn-str", "binding", "aset", + "volatile!", "deliver", "doall", "dorun", + "to-array", "into-array", "vector", "rand", "rand-int", + "rand-nth", "random-sample", + "assoc", "dissoc", "get", "get-in", "update", "assoc-in", + "contains?", "select-keys", "keys", "vals", + "disj", "peek", "pop", + "transduce", "ex-info", + "compare", "subvec", + "require", "eval", "resolve", "random-uuid", + "vreset!", "restart-agent", "with-out-str", + "System/getProperty", + "dosync", "alter"] + proc registerGlobal*(name: string) = definedGlobals.incl(name) proc mangleName*(name: string): string +proc runtimeName(op: string): string proc registerFn*(name: string, arity: int) = definedFnArities[name] = arity proc emitFnWrapper*(name: string): string = ## Emit a cljFn wrapper for a user-defined function when used as a value. + let rtName = runtimeName(name) + if rtName.len > 0: + # Built-in runtime function — emit variadic wrapper if applicable + if name in variadicRuntimeFns: + return "cljFn(" & rtName & ")" + else: + return "cljFn(proc(args: seq[CljVal]): CljVal = " & rtName & "(args[0]))" let mangled = mangleName(name) if name in multiArityFns: # Multi-arity / rest params already take seq[CljVal] @@ -137,15 +173,15 @@ proc indentStr(indent: int): string = proc indentCode(code: string, extra: int): string = if extra <= 0: return code let prefix = indentStr(extra) - var result = "" + var res = "" var lines = code.split("\n") for i, line in lines: - if i > 0: result.add("\n") + if i > 0: res.add("\n") if line.len > 0: - result.add(prefix & line) + res.add(prefix & line) else: - result.add(line) - return result + res.add(line) + return res proc runtimeName(op: string): string = case op @@ -440,26 +476,26 @@ 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 res: 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) + res.add(bname) + res.add(bval) elif bname.kind == ckVector: let tmp = cljSymbol("ds_" & $i) - result.add(tmp) - result.add(bval) + res.add(tmp) + res.add(bval) for j in 0.. 0: - return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args[0])), " & collArg & ")" + let isVar = fnArg.symName in variadicRuntimeFns + if isVar: + return sp & "cljMap(cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args)), " & collArg & ")" + else: + 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] @@ -1574,6 +1614,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false): let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; let " & pNames[1] & " = args[1]; " & bodyExpr & ")" return sp & "cljReduce(cljFn(" & fnProc & "), cljNil(), " & collArg & ")" elif isSymbol: + let rn = runtimeName(fnArg.symName) + if rn.len > 0: + let wrapper = "proc(args: seq[CljVal]): CljVal = " & rn & "(args)" + return sp & "cljReduce(cljFn(" & wrapper & "), cljNil(), " & collArg & ")" let mangled = mangleName(fnArg.symName) let wrapper = "proc(args: seq[CljVal]): CljVal = " & mangled & "(args[0], args[1])" return sp & "cljReduce(cljFn(" & wrapper & "), cljNil(), " & collArg & ")" @@ -1603,6 +1647,10 @@ proc emitSpecialForm(items: seq[CljVal], indent: int, needsValue: bool = false): let fnProc = "proc(args: seq[CljVal]): CljVal = (let " & pNames[0] & " = args[0]; let " & pNames[1] & " = args[1]; " & bodyExpr & ")" return sp & "cljReduce(cljFn(" & fnProc & "), " & initArg & ", " & collArg & ")" elif isSymbol: + let rn = runtimeName(fnArg.symName) + if rn.len > 0: + let wrapper = "proc(args: seq[CljVal]): CljVal = " & rn & "(args)" + return sp & "cljReduce(cljFn(" & wrapper & "), " & initArg & ", " & collArg & ")" let mangled = mangleName(fnArg.symName) let wrapper = "proc(args: seq[CljVal]): CljVal = " & mangled & "(args[0], args[1])" return sp & "cljReduce(cljFn(" & wrapper & "), " & initArg & ", " & collArg & ")" @@ -2202,32 +2250,7 @@ proc emitExpr*(v: CljVal, indent: int = 0, needsValue: bool = false): string = let rn = runtimeName(symName) if rn.len > 0 and not isLocalVar(symName): # Check if the runtime function is variadic (takes seq[CljVal]) - 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-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", - "make-hierarchy", "derive", "underive", "ancestors", - "descendants", "parents", "isa?", "promise", "create-ns", "future", - "delay", - "aclone", "alength", "aget", "int-array", "identical?", "empty", "identity", - "conj", - "drop-last", "shuffle", "repeatedly", "fnil", "intern", - "println-str", "prn-str", "binding", "aset", - "volatile!", "deliver", "doall", "dorun", - "to-array", "into-array", "vector", "rand", "rand-int", - "rand-nth", "random-sample", - "assoc", "dissoc", "get", "get-in", "update", "assoc-in", - "contains?", "select-keys", "keys", "vals", - "disj", "peek", "pop", - "transduce", "ex-info", - "compare", "subvec", - "require", "eval", "resolve", "random-uuid", - "vreset!", "restart-agent", "with-out-str", - "System/getProperty", - "dosync", "alter"] + let variadic = symName in variadicRuntimeFns if variadic: return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))" else: diff --git a/src/eval.nim b/src/eval.nim index caaad3d..62cbaad 100644 --- a/src/eval.nim +++ b/src/eval.nim @@ -1,7 +1,7 @@ # Tree-walking interpreter for fast REPL evaluation # Handles common cases without spawning nim c import strutils, sequtils, tables, algorithm, times, deques -import types, reader, ai_assist +import types, reader, macros, ai_assist var agentRegistry* = initTable[string, CljVal]() var agentCounter*: int64 = 0 @@ -228,6 +228,20 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult = return EvalResult(ok: true, value: res.value) return EvalResult(ok: true, value: cljNil()) + of "macroexpand": + if items.len < 2: + return EvalResult(ok: false, error: "macroexpand requires an expression") + let argRes = evalAst(items[1], env) + if not argRes.ok: return argRes + return EvalResult(ok: true, value: macroexpand(argRes.value)) + + of "macroexpand-1": + if items.len < 2: + return EvalResult(ok: false, error: "macroexpand-1 requires an expression") + let argRes = evalAst(items[1], env) + if not argRes.ok: return argRes + return EvalResult(ok: true, value: macroexpand1(argRes.value)) + of "ai/debug", "ai.debug": if items.len < 2: return EvalResult(ok: false, error: "ai/debug requires an expression") diff --git a/src/macros.nim b/src/macros.nim index 8b5bbb4..2470cb2 100644 --- a/src/macros.nim +++ b/src/macros.nim @@ -149,20 +149,20 @@ proc initMacros*() = defineMacro("and", proc(args: seq[CljVal]): CljVal = if args.len == 0: return cljBool(true) if args.len == 1: return args[0] - var result = args[^1] + var acc = args[^1] for i in countdown(args.len - 2, 0): - result = cljList(@[cljSymbol("if"), args[i], result, cljBool(false)]) - return result + acc = cljList(@[cljSymbol("if"), args[i], acc, cljBool(false)]) + return acc ) # or defineMacro("or", proc(args: seq[CljVal]): CljVal = if args.len == 0: return cljNil() if args.len == 1: return args[0] - var result = args[^1] + var acc = args[^1] for i in countdown(args.len - 2, 0): - result = cljList(@[cljSymbol("if"), args[i], args[i], result]) - return result + acc = cljList(@[cljSymbol("if"), args[i], args[i], acc]) + return acc ) # when-let @@ -242,22 +242,22 @@ proc initMacros*() = let clauses = args[1..^1] if clauses.len mod 2 != 0: raise newException(CatchableError, "cond-> requires pairs of test/expr") - var result = init + var acc = 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]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), test, cljList(@[expr.items[0], gs] & expr.items[1..^1]), gs])]) else: - result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), test, cljList(@[expr, gs]), gs])]) - return result + return acc ) # cond->> @@ -268,22 +268,22 @@ proc initMacros*() = let clauses = args[1..^1] if clauses.len mod 2 != 0: raise newException(CatchableError, "cond->> requires pairs of test/expr") - var result = init + var acc = 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]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), test, cljList(expr.items & @[gs]), gs])]) else: - result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), test, cljList(@[expr, gs]), gs])]) - return result + return acc ) # doto @@ -312,10 +312,10 @@ proc initMacros*() = let init = args[0] let name = args[1] let body = args[2..^1] - var result = init + var acc = init for form in body: - result = cljList(@[cljSymbol("let"), cljVector(@[name, result]), form]) - return result + acc = cljList(@[cljSymbol("let"), cljVector(@[name, acc]), form]) + return acc ) # some-> @@ -324,21 +324,21 @@ proc initMacros*() = raise newException(CatchableError, "some-> requires initial value and body") let init = args[0] let body = args[1..^1] - var result = init + var acc = init for form in body: let gs = cljSymbol(gensymName("st_")) let nilCheck = cljList(@[cljSymbol("nil?"), gs]) if form.kind == ckList and form.items.len > 0: - result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), nilCheck, cljNil(), cljList(@[form.items[0], gs] & form.items[1..^1])])]) else: - result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), nilCheck, cljNil(), cljList(@[form, gs])])]) - return result + return acc ) # some->> @@ -347,20 +347,20 @@ proc initMacros*() = raise newException(CatchableError, "some->> requires initial value and body") let init = args[0] let body = args[1..^1] - var result = init + var acc = 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]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), gs, cljList(form.items & @[gs]), cljNil()])]) else: - result = cljList(@[cljSymbol("let"), cljVector(@[gs, result]), + acc = cljList(@[cljSymbol("let"), cljVector(@[gs, acc]), cljList(@[cljSymbol("if"), gs, cljList(@[form, gs]), cljNil()])]) - return result + return acc ) # for @@ -465,11 +465,11 @@ proc initMacros*() = let loopBody = cljList(@[cljSymbol("when"), name] & innerBody & @[recurForm]) return cljList(@[cljSymbol("loop"), cljVector(@[name, cljList(@[cljSymbol("first"), seqColl]), gs, cljList(@[cljSymbol("next"), seqColl])]), loopBody]) - var result = makeLoop(pairs[^1][0], pairs[^1][1], inner) + var acc = makeLoop(pairs[^1][0], pairs[^1][1], inner) for j in countdown(pairs.len - 2, 0): let innerSeq = @[result] - result = makeLoop(pairs[j][0], pairs[j][1], innerSeq) - return result + acc = makeLoop(pairs[j][0], pairs[j][1], innerSeq) + return acc ) # dotimes diff --git a/src/repl.nim b/src/repl.nim index 13b192d..ad76769 100644 --- a/src/repl.nim +++ b/src/repl.nim @@ -155,8 +155,10 @@ proc evalForm*(state: var ReplState, formStr: string): JsonNode = parsed.items[0].kind == ckSymbol and parsed.items[0].symName in ["def", "defn"] + # Macro-expand before evaluating + let expanded = macroexpand(parsed) # Try in-memory evaluation first (fast path) - let evalResult = evalAst(parsed, state.evalEnv) + let evalResult = evalAst(expanded, state.evalEnv) if evalResult.ok: let elapsed = (epochTime() - startTime) * 1000