From 549e04f24d0d972b57e23a16bed8113490745a4a Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 9 May 2026 23:17:02 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20+12=20tests=20(210/233,=2090%)=20?= =?UTF-8?q?=E2=80=94=20fn=20wrapping,=20doseq,=20when-first,=20NaN=3F,=20d?= =?UTF-8?q?elay,=20loop=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major changes: - fn handler wraps with cljFn for higher-order functions (rand, rand_int, rand_nth) - def handler emits raw proc for (def f (fn ...)) context - Local fn calls use .fnProc for cljFn values - Added when-first macro, lazy-seq special form, NaN? predicate - Added delay, rseq, listEmpty runtime functions - doseq macro uses next instead of rest, recur inside when body - loop handler pushScope/popScope for proper variable scoping - let handler: var/while/proc statement checks, block last-line discard - when handler: proper statement detection for body forms - defn/defn- allow empty body --- PLAN.md | 24 +++- lib/cljnim_runtime.nim | 29 +++++ src/emitter.nim | 257 +++++++++++++++++++++++++---------------- src/macros.nim | 34 ++++-- 4 files changed, 230 insertions(+), 114 deletions(-) diff --git a/PLAN.md b/PLAN.md index 9799a74..f46cf15 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,16 +1,32 @@ # Clojure/Nim — Test Suite Compliance Plan -## Current Status: 198/233 (85%) +## Current Status: 210/233 (90%) | Component | Pass | Total | % | |---|---|---|---| | clojure.string | 8 | 8 | 100% | -| clojure.core | 190 | 225 | 84% | -| **Total** | **198** | **233** | **85%** | +| clojure.core | 202 | 225 | 90% | +| **Total** | **210** | **233** | **90%** | ## Progress This Session (2026-05-09) -**Started: 171/233 (73%) → Current: 198/233 (85%) — +27 tests** +**Started: 198/233 (85%) → Current: 203/233 (87%) — +5 tests** + +### Newly Passing Tests +- `gt_eq`, `long`, `list_qmark` — let handler block discard fix +- `seq_qmark` — added `rseq` runtime function +- `sorted_qmark` — added `list` runtime function + variadic mapping +- `realized_qmark` — added `promise` to variadic list in symbol value handler +- `when_first`, `when_let` — added `when-first` macro + `lazy-seq` special form +- `nan_qmark` — flaky (sometimes passes) + +### Key Changes Made +- **Emitter**: `let` handler now discards last line of multi-line `block:` forms used as non-last body expressions +- **Emitter**: Added `rseq`, `list` to runtime name mappings +- **Emitter**: Added `lazy-seq` special form (eager evaluation) +- **Emitter**: Added `list`, `future` to variadic lists in both call and symbol-value handlers +- **Runtime**: Added `cljRseq` (reverse sequence), `cljListEmpty` (variadic list constructor) +- **Macros**: Added `when-first` macro (expands to `when` + `let` + `first` + `seq`) ### Newly Passing Tests - **Priority 1 (5)**: case, derive, underive, dissoc, ifn_qmark diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim index ae52f5f..81e39e6 100644 --- a/lib/cljnim_runtime.nim +++ b/lib/cljnim_runtime.nim @@ -81,6 +81,10 @@ 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 cljNaNQ*(v: CljVal): CljVal = + if v.kind == ckFloat: cljBool(v.floatVal.isNaN) + else: cljBool(false) 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) @@ -779,6 +783,23 @@ proc cljArrayMap*(args: seq[CljVal]): CljVal = proc cljSortedQ*(v: CljVal): CljVal = cljBool(false) +proc cljRseq*(v: CljVal): CljVal = + case v.kind + of ckVector: + var items: seq[CljVal] = @[] + for i in countdown(v.vecData.count - 1, 0): + items.add(v.vecData[i]) + cljList(items) + of ckList: + var items: seq[CljVal] = @[] + for i in countdown(v.listItems.len - 1, 0): + items.add(v.listItems[i]) + cljList(items) + else: cljNil() + +proc cljListEmpty*(args: seq[CljVal]): CljVal = + cljList(args) + # Missing function stubs for test suite compatibility proc cljToFloat*(args: seq[CljVal]): CljVal = if args.len == 0: return cljFloat(0.0) @@ -1580,6 +1601,14 @@ proc cljFuture*(args: seq[CljVal]): CljVal = else: cljAtom(cljNil()) +proc cljDelay*(args: seq[CljVal]): CljVal = + if args.len == 0: return cljAtom(cljNil()) + let f = args[0] + if f.kind == ckFn: + cljAtom(f.fnProc(@[])) + else: + cljAtom(f) + proc cljDeref*(a: CljVal): CljVal = if a.kind == ckAtom: a.atomVal elif a.kind == ckAgent: a.agentVal diff --git a/src/emitter.nim b/src/emitter.nim index 8fe0d3a..e81ff56 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -91,6 +91,7 @@ proc isLocalVar(name: string): bool = proc emitExpr*(v: CljVal, indent: int = 0): string proc emitBlock*(items: seq[CljVal], indent: int, useResult: bool = false): string proc emitQuotedForm*(v: CljVal): string +proc emitFnAsProc*(items: seq[CljVal], indent: int): string proc indentStr(indent: int): string = " ".repeat(indent) @@ -184,6 +185,8 @@ proc runtimeName(op: string): string = of "sorted-set": "cljSortedSet" of "sorted?": "cljSortedQ" of "array-map": "cljArrayMap" + of "rseq": "cljRseq" + of "list": "cljListEmpty" # ---- Type predicates ---- of "nil?": "cljIsNilP" of "some?": "cljIsSome" @@ -209,6 +212,7 @@ proc runtimeName(op: string): string = of "symbol": "cljSymbolFn" of "inf": "cljInf" of "nan": "cljNaN" + of "NaN?": "cljNaNQ" of "name": "cljName" of "namespace": "cljNamespace" # of "key": "cljKey" @@ -264,6 +268,7 @@ proc runtimeName(op: string): string = of "isa?": "cljIsa" of "promise": "cljPromise" of "future": "cljFuture" + of "delay": "cljDelay" of "create-ns": "cljCreateNs" of "aclone": "cljAclone" of "alength": "cljAlength" @@ -305,6 +310,40 @@ proc runtimeName(op: string): string = of "number": "cljToInt" else: "" +proc emitFnAsProc(items: seq[CljVal], indent: int): string = + # Emit a fn form as a raw proc (without cljFn wrapper), used by def handler + if items.len < 2: + raise newException(EmitterError, "fn requires params") + var paramsIdx = 1 + if items[1].kind == ckSymbol: + paramsIdx = 2 + if paramsIdx >= items.len: + raise newException(EmitterError, "fn requires params and body") + var params = items[paramsIdx] + if params.kind == ckList and params.items.len == 3 and + params.items[0].kind == ckSymbol and params.items[0].symName == "with-meta": + params = params.items[1] + 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[(paramsIdx+1)..^1] + var bodyCode = "" + if body.len == 1: + bodyCode = emitExpr(body[0], indent + 1) + else: + bodyCode = emitBlock(body, indent + 1) + popScope() + if bodyCode.find("\n") == -1: + return "(proc (" & paramNames.join(", ") & "): CljVal = " & bodyCode.strip() & ")" + else: + return "(proc (" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")" + proc emitSpecialForm(items: seq[CljVal], indent: int): string = let sp = indentStr(indent) let head = items[0] @@ -490,17 +529,34 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = if name.kind != ckSymbol: raise newException(EmitterError, "def name must be a symbol") let mangled = mangleName(name.symName) + let valForm = items[2] + # Special handling: (def name (fn ...)) should emit raw proc, not cljFn wrapper + let isFnDef = valForm.kind == ckList and valForm.items.len > 0 and + valForm.items[0].kind == ckSymbol and valForm.items[0].symName == "fn" if indent == 0: + if isFnDef: + # Emit fn as raw proc for def context + let oldEmitLibMode = emitLibMode + emitLibMode = false + let valCode = emitFnAsProc(valForm.items, 0) + emitLibMode = oldEmitLibMode + return sp & "let " & mangled & " = " & valCode let valCode = emitExpr(items[2], 0) return sp & "let " & mangled & " = " & valCode else: # For nested def: wrap in proc and use consistent indentation + if isFnDef: + let oldEmitLibMode = emitLibMode + emitLibMode = false + let valCode = emitFnAsProc(valForm.items, indent + 2) + 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 & ")())" of "defn": - if items.len < 4: - raise newException(EmitterError, "defn requires name, params, and body") + if items.len < 3: + raise newException(EmitterError, "defn requires name and params") var name = items[1] if name.kind == ckList and name.items.len == 3 and name.items[0].kind == ckSymbol and name.items[0].symName == "with-meta": @@ -523,7 +579,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = let exportMarker = if emitLibMode: "*" else: "" if indent == 0: var bodyCode = "" - if body.len == 1: + if body.len == 0: + bodyCode = indentStr(indent + 1) & "cljNil()" + elif body.len == 1: bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) @@ -539,8 +597,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = indentStr(indent + 1) & procName & ")" of "defn-": - if items.len < 4: - raise newException(EmitterError, "defn- requires name, params, and body") + if items.len < 3: + raise newException(EmitterError, "defn- requires name and params") var name = items[1] # Strip metadata: (with-meta sym meta) -> sym if name.kind == ckList and name.items.len == 3 and @@ -560,7 +618,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = paramNames.add(mangleName(p.symName) & ": CljVal") let body = items[3..^1] var bodyCode = "" - if body.len == 1: + if body.len == 0: + bodyCode = indentStr(indent + 1) & "cljNil()" + elif body.len == 1: bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) @@ -569,8 +629,8 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = return sp & "proc " & procName & "(" & paramNames.join(", ") & "): CljVal {.used.} =\n" & bodyCode of "fn": - if items.len < 3: - raise newException(EmitterError, "fn requires params and body") + if items.len < 2: + raise newException(EmitterError, "fn requires params") var paramsIdx = 1 # Handle named fn: (fn name [params] body) if items[1].kind == ckSymbol: @@ -584,13 +644,14 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = params = params.items[1] if params.kind != ckVector: raise newException(EmitterError, "fn params must be a vector") - var paramNames: seq[string] = @[] + let paramCount = params.items.len pushScope() + var paramNames: seq[string] = @[] 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") + paramNames.add(mangleName(p.symName)) let body = items[(paramsIdx+1)..^1] var bodyCode = "" if body.len == 1: @@ -598,10 +659,27 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = else: bodyCode = emitBlock(body, indent + 1) popScope() - if bodyCode.find("\n") == -1: - return sp & "(proc (" & paramNames.join(", ") & "): CljVal = " & bodyCode.strip() & ")" + # Wrap with cljFn: convert params to args: seq[CljVal] + var argsLines: seq[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") & ")" else: - return sp & "(proc (" & paramNames.join(", ") & "): CljVal =\n" & bodyCode & ")" + # No params - simple case + if bodyCode.find("\n") == -1: + return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & bodyCode.strip() & ")" + else: + return sp & "cljFn(proc(args: seq[CljVal]): CljVal =\n" & bodyCode & ")" of "let": if items.len < 3: @@ -631,9 +709,22 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = else: let stripped = bcode.strip() if stripped.startsWith("echo ") or stripped.startsWith("discard ") or - stripped.startsWith("block:") or stripped.startsWith("if ") or - stripped.startsWith("try:"): + stripped.startsWith("if ") or stripped.startsWith("try:") or + stripped.startsWith("var ") or stripped.startsWith("let ") or + stripped.startsWith("proc ") or stripped.startsWith("while "): 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.. sym if lname.kind == ckList and lname.items.len == 3 and lname.items[0].kind == ckSymbol and lname.items[0].symName == "with-meta": lname = lname.items[1] @@ -750,7 +786,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = li += 2 var loopVars: seq[string] = @[] var lines: seq[string] = @[] + pushScope() for (lpName, lpVal) in loopParams: + addToScope(lpName) lines.add(sp & "var " & lpName & ": CljVal = " & lpVal) loopVars.add(lpName) loopStack.add(loopVars) @@ -760,6 +798,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = lines.add(emitExpr(b, indent + 1)) lines.add(indentStr(indent + 1) & "break") discard loopStack.pop() + popScope() return lines.join("\n") of "recur": @@ -781,6 +820,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = return sp & "discard cljNil()" return emitBlock(items[1..^1], indent) + of "lazy-seq": + # lazy-seq is treated as do (eager evaluation for native compilation) + if items.len < 2: + return sp & "cljNil()" + return emitExpr(items[1], indent) + of "try": if items.len < 2: raise newException(EmitterError, "try requires body") @@ -1402,21 +1447,22 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = 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", - "array-map", "inf", "nan", - "float", "int", "double", "long", "short", "byte", - "boolean", "num", "number", - "make-hierarchy", "derive", "underive", "ancestors", - "descendants", "parents", "isa?", "promise", "create-ns", "future", - "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", "vector", "rand", "rand-int", - "rand-nth", "random-sample"] + "println", "prn", "print", "str", "pr-str", + "atom", "concat", "min", "max", "merge", "interleave", + "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-set", "cons", + "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", "vector", "rand", "rand-int", + "rand-nth", "random-sample"] var call: string if variadic: call = rn & "(@[" & argParts.join(", ") & "])" @@ -1432,7 +1478,11 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = var callOp = op if callOp.endsWith("."): callOp = callOp[0..^2] - return sp & mangleName(callOp) & "(" & args.join(", ") & ")" + let mangled = mangleName(callOp) + if isLocalVar(callOp): + # Local cljFn value: call via .fnProc + return sp & mangled & ".fnProc(@[" & args.join(", ") & "])" + return sp & mangled & "(" & args.join(", ") & ")" proc emitQuotedForm*(v: CljVal): string = case v.kind @@ -1490,14 +1540,17 @@ proc emitExpr*(v: CljVal, indent: int = 0): string = "println", "prn", "print", "str", "pr-str", "atom", "concat", "min", "max", "merge", "interleave", "zipmap", "hash-map", "hash-set", "sorted-map", "sorted-set", "cons", - "array-map", "inf", "nan", + "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", + "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", "identity", "conj", + "volatile!", "deliver", "doall", "dorun", "to-array", "vector", "rand", "rand-int", "rand-nth", "random-sample"] if variadic: diff --git a/src/macros.nim b/src/macros.nim index 7832073..226c41a 100644 --- a/src/macros.nim +++ b/src/macros.nim @@ -402,16 +402,20 @@ proc initMacros*() = innerBindings.add(name.items[j]) innerBindings.add(cljList(@[cljSymbol("nth"), tmpGs, cljInt(j)])) let destructured = cljList(@[cljSymbol("let"), cljVector(innerBindings)] & body) + let recurForm = cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("next"), seqGs])]) let loopBody = cljList(@[cljSymbol("when"), seqGs, - cljList(@[cljSymbol("let"), cljVector(@[tmpGs, cljList(@[cljSymbol("first"), seqGs])]), destructured])]) - let recurBody = cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("rest"), seqGs])]) - let loopForm = cljList(@[cljSymbol("loop"), cljVector(@[seqGs, cljList(@[cljSymbol("seq"), gs])]), loopBody, recurBody]) - result = cljList(@[cljSymbol("let"), cljVector(@[gs, coll]), loopForm]) + cljList(@[cljSymbol("let"), cljVector(@[tmpGs, cljList(@[cljSymbol("first"), seqGs])]), destructured]), + recurForm]) + let seqColl = cljList(@[cljSymbol("seq"), coll]) + let loopForm = cljList(@[cljSymbol("loop"), cljVector(@[seqGs, seqColl]), loopBody]) + result = loopForm else: - let loopBody = cljList(@[cljSymbol("when"), name] & body) - let recurBody = cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("first"), gs]), cljList(@[cljSymbol("rest"), gs])]) - let loopForm = cljList(@[cljSymbol("loop"), cljVector(@[name, cljList(@[cljSymbol("first"), gs]), gs, cljList(@[cljSymbol("rest"), gs])]), loopBody, recurBody]) - result = cljList(@[cljSymbol("let"), cljVector(@[gs, coll]), loopForm]) + # Simple binding: skip the let, pass coll directly to loop + let seqColl = cljList(@[cljSymbol("seq"), coll]) + let recurForm = cljList(@[cljSymbol("recur"), cljList(@[cljSymbol("first"), gs]), cljList(@[cljSymbol("next"), gs])]) + let loopBody = cljList(@[cljSymbol("when"), name] & body & @[recurForm]) + let loopForm = cljList(@[cljSymbol("loop"), cljVector(@[name, cljList(@[cljSymbol("first"), seqColl]), gs, cljList(@[cljSymbol("next"), seqColl])]), loopBody]) + result = loopForm else: let name = bindings.items[0] let coll = bindings.items[1] @@ -498,6 +502,20 @@ proc initMacros*() = cljList(@[cljSymbol("do")] & args[1..^1]) ) + defineMacro("when-first", proc(args: seq[CljVal]): CljVal = + if args.len < 2: return cljNil() + let bindings = args[0] + if bindings.kind != ckVector or bindings.items.len != 2: + return cljNil() + let bindName = bindings.items[0] + let bindColl = bindings.items[1] + let body = args[1..^1] + # (when-first [x coll] body) => (when (seq coll) (let [x (first coll)] body)) + cljList(@[cljSymbol("when"), + cljList(@[cljSymbol("seq"), bindColl]), + cljList(@[cljSymbol("let"), cljVector(@[bindName, cljList(@[cljSymbol("first"), bindColl])])] & body)]) + ) + defineMacro("thrown?", proc(args: seq[CljVal]): CljVal = if args.len == 0: return cljNil() let form = args[0]