From a9ad83c509f5b122980faaa373251f293b636533 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 9 May 2026 12:17:10 +0300 Subject: [PATCH] fix: general equality (=) for all types, add type predicates & collection fns, test suite docs - Fix: = now uses cljEqual/cljMultiEqual instead of numeric-only cljNumEq, so (= :a :a), (= "a" "a"), (= [1 2] [1 2]), (= nil nil) etc return true - Fix: ns forms inside unwrapped (do ...) are skipped instead of emitting broken Nim comments inside cljRepr() - Fix: defmacro forms go to defs section, not wrapped in discard cljRepr() - Add 18 type predicates: keyword?, symbol?, string?, number?, integer?, float?, vector?, map?, set?, list?, seq?, coll?, sequential?, fn?, boolean?, true?, false?, some? - Add collection fns: second, ffirst, nfirst, peek, pop - Add keyword/symbol ops: keyword, symbol, name, namespace, key, val - Update eval interpreter = to handle structural collection equality - Add test_single.py + test_vals.clj for Clojure test suite runner - Add docs/en/07 and docs/bg/07 for cross-dialect test suite compatibility - Update README with test suite documentation link --- README.md | 1 + docs/bg/07-clojure-test-suite.md | 110 +++++++++++++++ docs/bg/index.md | 1 + docs/en/07-clojure-test-suite.md | 110 +++++++++++++++ docs/en/index.md | 1 + lib/cljnim_runtime.nim | 190 ++++++++++++++++++++++++- src/cljnim.nim | 10 +- src/emitter.nim | 229 ++++++++++++++++++++++++------- src/eval.nim | 67 +++++---- test_single.py | 133 ++++++++++++++++++ test_vals.clj | 6 + tests/test_emitter.nim | 2 +- 12 files changed, 777 insertions(+), 83 deletions(-) create mode 100644 docs/bg/07-clojure-test-suite.md create mode 100644 docs/en/07-clojure-test-suite.md create mode 100644 test_single.py create mode 100644 test_vals.clj diff --git a/README.md b/README.md index f773a45..a2901c8 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ make check - **[English](docs/en/index.md)** — Getting started, architecture, AI integration, API reference - **[Български](docs/bg/index.md)** — Първи стъпки, архитектура, AI интеграция, API справочник +- **[Clojure Test Suite](docs/en/07-clojure-test-suite.md)** — Cross-dialect compliance testing with the [jank-lang/clojure-test-suite](https://github.com/jank-lang/clojure-test-suite) ## What is this? diff --git a/docs/bg/07-clojure-test-suite.md b/docs/bg/07-clojure-test-suite.md new file mode 100644 index 0000000..c403c53 --- /dev/null +++ b/docs/bg/07-clojure-test-suite.md @@ -0,0 +1,110 @@ + +[← Обратно към индекса](index.md) + +--- + +# Съвместимост с Clojure Test Suite + +Clojure/Nim участва в между-диалектния **[jank-lang/clojure-test-suite](https://github.com/jank-lang/clojure-test-suite)** — стандартният Clojure тестови пакет за съответствие, който валидира поведението на всички Clojure диалекти. + +## Поддържани диалекти + +Clojure Test Suite се поддържа официално за следните диалекти: + +| Диалект | Статус | Runtime | +|---------|--------|---------| +| Clojure (JVM) | ✅ Официален | JVM | +| ClojureScript | ✅ Официален | JavaScript / Node.js | +| Babashka | ✅ Официален | GraalVM native-image | +| Clojure CLR | ✅ Официален | .NET CLR | +| Basilisp | ✅ Официален | Python | +| **Clojure/Nim** | 🚀 Кандидат | **Nim → C → Native** | + +## Бърз старт + +### 1. Клониране на тестовия пакет + +```bash +git clone https://github.com/jank-lang/clojure-test-suite.git /tmp/clojure-test-suite +``` + +### 2. Пускане на индивидуални тестове + +Използвайте `test_single.py` за пускане на конкретен тестов файл: + +```bash +python3 test_single.py /tmp/clojure-test-suite/test/clojure/core_test/nil_qmark.cljc +``` + +### 3. Пускане на пакет + +```bash +python3 test_single.py +# Пуска: zipmap, zero_qmark, with_out_str +``` + +Редактирайте файла `test_single.py`, за да добавите още тестови файлове към списъка. + +## Формат на тестовете + +Всеки тестов файл е `.cljc` (cross-platform Clojure/ClojureScript) файл със стандартна структура: + +```clojure +(ns clojure.core-test.nil-qmark + (:require [clojure.test :as t :refer [are deftest is testing]] + [clojure.core-test.portability :refer [when-var-exists]])) + +(when-var-exists nil? + (deftest test-nil? + (testing "common" + (are [in ex] (= (nil? in) ex) + nil true + 0 false + false false + "" false)))) +``` + +## Обработка на Reader Conditionals + +`test_single.py` на Clojure/Nim предварително обработва `.cljc` файловете, като премахва `#?` и `#?@` reader conditionals и извлича `:default` клона: + +```clojure +;; Преди +#?(:cljs :refer-macros :default :refer) + +;; След +:refer + +;; Преди +#?@(:cljs [] :default [1 2 3]) + +;; След +[1 2 3] +``` + +Това следва стандартната между-диалектна конвенция — всеки диалект разрешава `:default` според своята платформа. + +## Текущ обхват на тестовете + +Тестовият пакет покрива **212+ функции от `clojure.core`** и **8 функции от `clojure.string`**. Clojure/Nim работи към пълно съответствие. + +Вижте [Roadmap](06-roadmap.md) за статуса на имплементацията. + +--- + +## Настройка на тестове за отделните диалекти + +За справка, ето линкове към официалните ръководства за настройка на тестовете за други платформи: + +| Диалект | Ръководство за настройка | +|---------|--------------------------| +| Clojure (JVM) | [clojure.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/clojure.md) | +| ClojureScript | [clojurescript.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/clojurescript.md) | +| Babashka | [babashka.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/babashka.md) | +| Clojure CLR | [clojureclr.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/clojureclr.md) | +| Basilisp | [basilisp.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/basilisp.md) | +| **Clojure/Nim** | **Този документ** | + +--- + +*Последно обновено: 2026-05-09* diff --git a/docs/bg/index.md b/docs/bg/index.md index 03c8904..cf832af 100644 --- a/docs/bg/index.md +++ b/docs/bg/index.md @@ -15,6 +15,7 @@ - **Build:** `make build && make check` - **Тестове:** 276+ теста в 8 тестови пакета - **AI Интеграция:** DeepSeek API, OpenAI-compatible, Xiaomi MiMo +- **Test Suite:** [Съвместимост с Clojure Test Suite](07-clojure-test-suite.md) — между-диалектно тестване за съответствие ## Защо Clojure/Nim? diff --git a/docs/en/07-clojure-test-suite.md b/docs/en/07-clojure-test-suite.md new file mode 100644 index 0000000..558eb45 --- /dev/null +++ b/docs/en/07-clojure-test-suite.md @@ -0,0 +1,110 @@ + +[← Back to Index](index.md) + +--- + +# Clojure Test Suite Compatibility + +Clojure/Nim participates in the cross-dialect **[jank-lang/clojure-test-suite](https://github.com/jank-lang/clojure-test-suite)** — the standard Clojure compliance test suite that validates behavior across all Clojure dialects. + +## Supported Dialects + +The Clojure Test Suite is officially maintained for these dialects: + +| Dialect | Status | Runtime | +|---------|--------|---------| +| Clojure (JVM) | ✅ Official | JVM | +| ClojureScript | ✅ Official | JavaScript / Node.js | +| Babashka | ✅ Official | GraalVM native-image | +| Clojure CLR | ✅ Official | .NET CLR | +| Basilisp | ✅ Official | Python | +| **Clojure/Nim** | 🚀 Candidate | **Nim → C → Native** | + +## Quick Start + +### 1. Clone the Test Suite + +```bash +git clone https://github.com/jank-lang/clojure-test-suite.git /tmp/clojure-test-suite +``` + +### 2. Run Individual Tests + +Use `test_single.py` to run a specific test file: + +```bash +python3 test_single.py /tmp/clojure-test-suite/test/clojure/core_test/nil_qmark.cljc +``` + +### 3. Run a Batch + +```bash +python3 test_single.py +# Runs: zipmap, zero_qmark, with_out_str +``` + +Edit the `test_single.py` file to add more test files to the batch list. + +## Test Format + +Each test file is a `.cljc` (cross-platform Clojure/ClojureScript) file with a standard structure: + +```clojure +(ns clojure.core-test.nil-qmark + (:require [clojure.test :as t :refer [are deftest is testing]] + [clojure.core-test.portability :refer [when-var-exists]])) + +(when-var-exists nil? + (deftest test-nil? + (testing "common" + (are [in ex] (= (nil? in) ex) + nil true + 0 false + false false + "" false)))) +``` + +## Reader Conditional Handling + +Clojure/Nim's `test_single.py` pre-processes `.cljc` files, stripping `#?` and `#?@` reader conditionals and extracting the `:default` branch: + +```clojure +;; Before +#?(:cljs :refer-macros :default :refer) + +;; After +:refer + +;; Before +#?@(:cljs [] :default [1 2 3]) + +;; After +[1 2 3] +``` + +This follows the standard cross-dialect convention — each dialect resolves `:default` per its platform. + +## Current Test Scope + +The test suite covers **212+ `clojure.core` functions** and **8 `clojure.string` functions**. Clojure/Nim is working toward full compliance. + +See the [Roadmap](06-roadmap.md) for implementation status. + +--- + +## Dialect-Specific Test Setup + +For reference, here are links to the official dialect setup guides for running the test suite on other platforms: + +| Dialect | Setup Guide | +|---------|-------------| +| Clojure (JVM) | [clojure.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/clojure.md) | +| ClojureScript | [clojurescript.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/clojurescript.md) | +| Babashka | [babashka.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/babashka.md) | +| Clojure CLR | [clojureclr.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/clojureclr.md) | +| Basilisp | [basilisp.md](https://github.com/jank-lang/clojure-test-suite/blob/main/doc/basilisp.md) | +| **Clojure/Nim** | **This document** | + +--- + +*Last updated: 2026-05-09* diff --git a/docs/en/index.md b/docs/en/index.md index 7193c35..dc8c988 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -15,6 +15,7 @@ - **Build:** `make build && make check` - **Tests:** 276+ tests across 8 test suites - **AI Integration:** DeepSeek API, OpenAI-compatible, Xiaomi MiMo +- **Test Suite:** [Clojure Test Suite Compatibility](07-clojure-test-suite.md) — cross-dialect compliance testing ## Why Clojure/Nim? diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim index ab64203..19415ed 100644 --- a/lib/cljnim_runtime.nim +++ b/lib/cljnim_runtime.nim @@ -148,6 +148,86 @@ proc cljIsTrue*(v: CljVal): bool = v.kind == ckBool and v.boolVal proc cljIsFalse*(v: CljVal): bool = v.kind == ckBool and not v.boolVal proc cljIsTruthy*(v: CljVal): bool = not cljIsNil(v) and not cljIsFalse(v) +proc cljIsNilP*(v: CljVal): CljVal = cljBool(cljIsNil(v)) +proc cljIsSome*(v: CljVal): CljVal = cljBool(not cljIsNil(v)) +proc cljIsKeyword*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckKeyword) +proc cljIsSymbol*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckSymbol) +proc cljIsString*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckString) +proc cljIsNumber*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and (v.kind == ckInt or v.kind == ckFloat)) +proc cljIsInteger*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckInt) +proc cljIsFloat*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckFloat) +proc cljIsVector*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckVector) +proc cljIsMap*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckMap) +proc cljIsSet*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckSet) +proc cljIsList*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckList) +proc cljIsSeq*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind in {ckList, ckVector}) +proc cljIsColl*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind in {ckList, ckVector, ckMap, ckSet}) +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 cljIsBool*(v: CljVal): CljVal = cljBool(not cljIsNil(v) and v.kind == ckBool) +proc cljIsTrueP*(v: CljVal): CljVal = cljBool(cljIsTrue(v)) +proc cljIsFalseP*(v: CljVal): CljVal = cljBool(cljIsFalse(v)) + +proc cljKeywordFn*(v: CljVal): CljVal = + if v.isNil or v.kind == ckNil: return cljNil() + case v.kind + of ckKeyword: v + of ckString: cljKeyword(v.strVal) + of ckSymbol: cljKeyword(v.symName) + else: cljNil() + +proc cljSymbolFn*(v: CljVal): CljVal = + if v.isNil or v.kind == ckNil: return cljNil() + case v.kind + of ckSymbol: v + of ckString: cljSymbol(v.strVal) + of ckKeyword: cljSymbol(v.kwName) + else: cljNil() + +proc cljName*(v: CljVal): CljVal = + if v.isNil or v.kind == ckNil: return cljNil() + case v.kind + of ckKeyword: cljString(v.kwName) + of ckSymbol: cljString(v.symName) + else: raise newException(CatchableError, "name requires a keyword or symbol") + +proc cljNamespace*(v: CljVal): CljVal = + if v.isNil or v.kind == ckNil: return cljNil() + case v.kind + of ckKeyword: + let idx = v.kwName.find('/') + if idx >= 0: cljString(v.kwName[0..= 0: cljString(v.symName[0..= 2: pvecNth(v.vecData, 0) + else: cljNil() + of ckList: + if v.listItems.len >= 2: v.listItems[0] + else: cljNil() + else: + raise newException(CatchableError, "key requires a map entry (vector or list of 2 items)") + +proc cljEntryVal*(v: CljVal): CljVal = + if v.isNil or v.kind == ckNil: return cljNil() + case v.kind + of ckVector: + if v.vecData.count >= 2: pvecNth(v.vecData, 1) + else: cljNil() + of ckList: + if v.listItems.len >= 2: v.listItems[1] + else: cljNil() + else: + raise newException(CatchableError, "val requires a map entry (vector or list of 2 items)") + # ---- I/O ---- proc cljPrintln*(args: seq[CljVal]): CljVal = @@ -316,6 +396,54 @@ proc cljNumEq*(args: seq[CljVal]): CljVal = return cljBool(false) cljBool(true) +proc cljEqual*(a, b: CljVal): bool = + ## Clojure = equality: structural equality for all types, numeric equality for numbers + if a.isNil and b.isNil: return true + if a.isNil or b.isNil: return false + if a.kind != b.kind: + if a.kind == ckInt and b.kind == ckFloat: + return a.intVal.float64 == b.floatVal + if a.kind == ckFloat and b.kind == ckInt: + return a.floatVal == b.intVal.float64 + return false + case a.kind + of ckNil: true + of ckBool: a.boolVal == b.boolVal + of ckInt: a.intVal == b.intVal + of ckFloat: a.floatVal == b.floatVal + of ckString: a.strVal == b.strVal + of ckKeyword: a.kwName == b.kwName + of ckSymbol: a.symName == b.symName + of ckList: + if a.listItems.len != b.listItems.len: return false + for i in 0.. 0: items.add(cljString(line)) - cljVector(items) + +proc cljZipmap*(args: seq[CljVal]): CljVal = + if args.len < 2: + raise newException(CatchableError, "zipmap requires 2 arguments") + let keys = args[0] + let vals = args[1] + if keys.kind != ckVector and keys.kind != ckList: + raise newException(CatchableError, "zipmap: first argument must be seqable") + var kitems: seq[CljVal] = @[] + var vitems: seq[CljVal] = @[] + case keys.kind + of ckVector: kitems = keys.vecData.toSeq() + of ckList: kitems = keys.listItems + else: discard + case vals.kind + of ckVector: vitems = vals.vecData.toSeq() + of ckList: vitems = vals.listItems + else: discard + var mk: seq[CljVal] = @[] + var mv: seq[CljVal] = @[] + let n = min(kitems.len, vitems.len) + for i in 0..": "cljGt" of "<": "cljLt" of ">=": "cljGe" @@ -74,6 +77,9 @@ proc runtimeName(op: string): string = of "even?": "cljEven" of "odd?": "cljOdd" of "first": "cljFirst" + of "second": "cljSecond" + of "ffirst": "cljFfirst" + of "nfirst": "cljNfirst" of "rest": "cljRest" of "next": "cljNext" of "last": "cljLast" @@ -113,6 +119,34 @@ proc runtimeName(op: string): string = of "deref": "cljDeref" of "reset!": "cljReset" of "swap!": "cljSwap" + of "zipmap": "cljZipmap" + # ---- Type predicates ---- + of "nil?": "cljIsNilP" + of "some?": "cljIsSome" + of "keyword?": "cljIsKeyword" + of "symbol?": "cljIsSymbol" + of "string?": "cljIsString" + of "number?": "cljIsNumber" + of "integer?": "cljIsInteger" + of "float?": "cljIsFloat" + of "vector?": "cljIsVector" + of "map?": "cljIsMap" + of "set?": "cljIsSet" + of "list?": "cljIsList" + of "seq?": "cljIsSeq" + of "coll?": "cljIsColl" + of "sequential?": "cljIsSequential" + of "fn?": "cljIsFn" + of "boolean?": "cljIsBool" + of "true?": "cljIsTrueP" + of "false?": "cljIsFalseP" + # ---- Keyword/Symbol ops ---- + of "keyword": "cljKeywordFn" + of "symbol": "cljSymbolFn" + of "name": "cljName" + of "namespace": "cljNamespace" + of "key": "cljKey" + of "val": "cljEntryVal" # ---- File operations ---- of "file/read": "cljFileRead" of "file/write": "cljFileWrite" @@ -126,6 +160,8 @@ proc runtimeName(op: string): string = of "git/diff": "cljGitDiff" of "git/log": "cljGitLog" of "disj": "cljDisj" + of "peek": "cljPeek" + of "pop": "cljPop" of "slurp": "cljFileRead" of "spit": "cljFileWrite" of "read-line": "cljReadLine" @@ -244,6 +280,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = # quote if hName == "quote" and form.items.len == 2: return form.items[1] + # syntax-quote + if hName == "syntax-quote" and form.items.len == 2: + return eval(expandSyntaxQuote(form.items[1])) # list if hName == "list": var evaluated: seq[CljVal] = @[] @@ -316,7 +355,7 @@ 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], 0) + return sp & "let " & mangleName(name.symName) & " = " & emitExpr(items[2], indent) of "defn": if items.len < 4: @@ -338,9 +377,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = bodyCode = emitExpr(body[0], indent + 1) else: bodyCode = emitBlock(body, indent + 1) - # In a proc body, replace "discard " with "result = " so that - # if/when branches return their last expression properly. - bodyCode = bodyCode.replace("discard ", "result = ") let procName = mangleName(name.symName) let exportMarker = if emitLibMode: "*" else: "" return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode @@ -425,37 +461,23 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = raise newException(EmitterError, "if requires condition, then, and optional else") let condCode = emitExpr(items[1], 0) let thenCode = emitExpr(items[2], indent + 1) - let thenStripped = thenCode.strip() - var thenFinal = thenCode - if not (thenStripped.startsWith("echo ") or thenStripped.startsWith("discard ") or - thenStripped.startsWith("block:") or thenStripped.startsWith("if ") or - thenStripped.startsWith("try:") or thenStripped.startsWith("var ") or - thenStripped.startsWith("let ") or thenStripped.startsWith("proc ") or - thenStripped.contains("\n") or thenStripped.contains(" = ")): - thenFinal = indentStr(indent + 1) & "discard " & thenStripped - var ifBlock = sp & "if cljIsTruthy(" & condCode & "):\n" & thenFinal + var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode if items.len == 4: let elseCode = emitExpr(items[3], indent + 1) - let elseStripped = elseCode.strip() - var elseFinal = elseCode - if not (elseStripped.startsWith("echo ") or elseStripped.startsWith("discard ") or - elseStripped.startsWith("block:") or elseStripped.startsWith("if ") or - elseStripped.startsWith("try:") or elseStripped.startsWith("var ") or - elseStripped.startsWith("let ") or elseStripped.startsWith("proc ") or - elseStripped.contains("\n") or elseStripped.contains(" = ")): - elseFinal = indentStr(indent + 1) & "discard " & elseStripped - ifBlock.add("\n" & sp & "else:\n" & elseFinal) + ifBlock.add("\n" & sp & "else:\n" & elseCode) return ifBlock of "when": if items.len < 3: raise newException(EmitterError, "when requires condition and body") - let condCode = emitExpr(items[1], 0) + let condCode = emitExpr(items[1], indent) var lines: seq[string] = @[] - lines.add(sp & "if cljIsTruthy(" & condCode & "):") + lines.add(sp & "if cljIsTruthy(" & condCode.strip() & "):") let body = items[2..^1] for b in body: lines.add(emitExpr(b, indent + 1)) + lines.add(sp & "else:") + lines.add(indentStr(indent + 1) & "cljNil()") return lines.join("\n") of "when-let": @@ -602,14 +624,42 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = inc ti var lines: seq[string] = @[] lines.add(sp & "try:") - for b in bodyForms: - lines.add(emitExpr(b, indent + 1)) + for i, b in bodyForms: + let isLast = (i == bodyForms.len - 1) + let code = emitExpr(b, indent + 1) + if isLast: + lines.add(code) + else: + let stripped = code.strip() + if stripped.startsWith("echo ") or stripped.startsWith("discard ") or + stripped.startsWith("block:") or stripped.startsWith("if ") or + stripped.startsWith("try:") or stripped.startsWith("var ") or + stripped.startsWith("let ") or stripped.startsWith("proc ") or + stripped.startsWith("result = ") or stripped.contains("\n") or + stripped.contains(" = "): + lines.add(code) + else: + lines.add(indentStr(indent + 1) & "discard " & stripped) for (exType, exVar, exBody) in catchClauses: 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 & "\"))") - for eb in exBody: - lines.add(emitExpr(eb, indent + 1)) + for i, eb in exBody: + let isLast = (i == exBody.len - 1) + let code = emitExpr(eb, indent + 1) + if isLast: + lines.add(code) + else: + let stripped = code.strip() + if stripped.startsWith("echo ") or stripped.startsWith("discard ") or + stripped.startsWith("block:") or stripped.startsWith("if ") or + stripped.startsWith("try:") or stripped.startsWith("var ") or + stripped.startsWith("let ") or stripped.startsWith("proc ") or + stripped.startsWith("result = ") or stripped.contains("\n") or + stripped.contains(" = "): + lines.add(code) + else: + lines.add(indentStr(indent + 1) & "discard " & stripped) if finallyBody.len > 0: lines.add(sp & "finally:") for fb in finallyBody: @@ -631,12 +681,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = of ckList: var parts: seq[string] = @[] for item in quoted.items: - parts.add(emitExpr(item, 0)) + parts.add(emitQuotedForm(item)) return sp & "cljList(@[" & parts.join(", ") & "])" of ckVector: var parts: seq[string] = @[] for item in quoted.items: - parts.add(emitExpr(item, 0)) + parts.add(emitQuotedForm(item)) return sp & "cljVector(@[" & parts.join(", ") & "])" else: return emitExpr(quoted, indent) @@ -978,26 +1028,56 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string = if rn.len > 0: var argParts: seq[string] = @[] for i in 1..", "<", ">=", "<=", "not=", "println", "prn", "print", "str", "pr-str", - "concat", "min", "max", "merge", "interleave"] + "concat", "min", "max", "merge", "interleave", + "zipmap"] var call: string if variadic and argParts.len > 0: call = rn & "(@[" & argParts.join(", ") & "])" else: call = rn & "(" & argParts.join(", ") & ")" - if op in ["println", "prn", "print"]: - return sp & "discard " & call return sp & call # ---- User-defined function call ---- var args: seq[string] = @[] for i in 1.. 0 and - form.items[0].kind == ckSymbol and - form.items[0].symName in ["def", "defn", "defn-"] - if isDef: + proc processForm(i: int, form: CljVal, isLast: bool) = + let headSym = form.kind == ckList and form.items.len > 0 and + form.items[0].kind == ckSymbol + let headName = if headSym: form.items[0].symName else: "" + 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)) else: let code = emitExpr(form, 2) let stripped = code.strip() if stripped.startsWith("echo ") or stripped.startsWith("discard ") or - stripped.startsWith("block:") or stripped.startsWith("if ") or - stripped.startsWith("try:") or stripped.startsWith("var "): + stripped.startsWith("var "): mainForms.add(code) - elif i == forms.len - 1: + elif isLast: mainForms.add(indentStr(2) & "echo cljRepr(" & stripped & ")") else: mainForms.add(indentStr(2) & "discard cljRepr(" & stripped & ")") + proc unwrapAndProcess(i: int, form: CljVal, isLast: bool) = + # Unwrap top-level (do ...) forms so each sub-form is processed individually + if form.kind == ckList and form.items.len > 0 and + form.items[0].kind == ckSymbol and form.items[0].symName == "do": + let subForms = form.items[1..^1] + for j, subForm in subForms: + let subLast = isLast and (j == subForms.len - 1) + unwrapAndProcess(j, subForm, subLast) + else: + processForm(i, form, isLast) + + for i, form in forms: + unwrapAndProcess(i, form, i == forms.len - 1) + var lines = headerLines # Add collected Nim imports for imp in requiredImports: diff --git a/src/eval.nim b/src/eval.nim index 15e3710..caaad3d 100644 --- a/src/eval.nim +++ b/src/eval.nim @@ -373,30 +373,51 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult = return EvalResult(ok: true, value: cljFloat(a / b)) of "=": + proc evalEqual(a, b: CljVal): bool = + if a.isNil and b.isNil: return true + if a.isNil or b.isNil: return false + if a.kind == ckInt and b.kind == ckFloat: + return a.intVal.float64 == b.floatVal + if a.kind == ckFloat and b.kind == ckInt: + return a.floatVal == b.intVal.float64 + if a.kind != b.kind: return false + case a.kind + of ckNil: true + of ckBool: a.boolVal == b.boolVal + of ckInt: a.intVal == b.intVal + of ckFloat: a.floatVal == b.floatVal + of ckString: a.strVal == b.strVal + of ckKeyword: a.kwName == b.kwName + of ckSymbol: a.symName == b.symName + of ckList, ckVector: + if a.items.len != b.items.len: return false + for i in 0..= len(text) or text[j] != '(': + result.append(text[i]) + i += 1 + continue + + # Find matching closing paren + depth = 0 + k = j + while k < len(text): + if text[k] == '(': + depth += 1 + elif text[k] == ')': + depth -= 1 + if depth == 0: + break + k += 1 + if depth != 0: + result.append(text[i]) + i += 1 + continue + + # Extract content between parens + inner = text[j+1:k] + default_val, _ = extract_default(inner, 0) + result.append(default_val) + i = k + 1 + + return ''.join(result) + +def run_test(cljc_path, timeout=30): + with open(cljc_path, 'r') as f: + content = f.read() + content = strip_reader_conditionals(content) + content = content.replace('##Inf', 'inf').replace('##-Inf', '-inf').replace('##NaN', 'nan') + 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) + fd, path = tempfile.mkstemp(suffix='.clj') + try: + os.write(fd, content.encode()) + finally: + os.close(fd) + try: + result = subprocess.run(['./cljnim', 'run', path], capture_output=True, text=True, timeout=timeout) + return result.returncode, result.stdout, result.stderr + finally: + os.unlink(path) + +if len(sys.argv) > 1: + cljc = sys.argv[1] + rc, out, err = run_test(cljc) + print(f'rc={rc}') + if rc != 0: + print('stderr:', err[:500]) + if out.strip(): + print('stdout:', out[:500]) +else: + for test in ['zipmap', 'zero_qmark', 'with_out_str']: + cljc = f'/tmp/clojure-test-suite/test/clojure/core_test/{test}.cljc' + rc, out, err = run_test(cljc) + print(f'{test}: rc={rc}') + if rc != 0: + print(' stderr:', err[:300]) diff --git a/test_vals.clj b/test_vals.clj new file mode 100644 index 0000000..1d43607 --- /dev/null +++ b/test_vals.clj @@ -0,0 +1,6 @@ +(do + (defmacro is [form] + `(let [result# ~form] + (if result# nil (println "FAIL:" (quote ~form))))) + (println [1 2 (is true) 3]) +) diff --git a/tests/test_emitter.nim b/tests/test_emitter.nim index be8736e..80eb223 100644 --- a/tests/test_emitter.nim +++ b/tests/test_emitter.nim @@ -143,7 +143,7 @@ suite "Emitter - Operators": test "emit equality": let v = read("(= 1 1)") let code = emitExpr(v) - check "cljNumEq" in code + check "cljMultiEqual" in code test "emit not=": let v = read("(not= 1 2)")