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
This commit is contained in:
2026-05-09 12:17:10 +03:00
parent ba0b300917
commit a9ad83c509
12 changed files with 777 additions and 83 deletions
+1
View File
@@ -27,6 +27,7 @@ make check
- **[English](docs/en/index.md)** — Getting started, architecture, AI integration, API reference - **[English](docs/en/index.md)** — Getting started, architecture, AI integration, API reference
- **[Български](docs/bg/index.md)** — Първи стъпки, архитектура, AI интеграция, API справочник - **[Български](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? ## What is this?
+110
View File
@@ -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*
+1
View File
@@ -15,6 +15,7 @@
- **Build:** `make build && make check` - **Build:** `make build && make check`
- **Тестове:** 276+ теста в 8 тестови пакета - **Тестове:** 276+ теста в 8 тестови пакета
- **AI Интеграция:** DeepSeek API, OpenAI-compatible, Xiaomi MiMo - **AI Интеграция:** DeepSeek API, OpenAI-compatible, Xiaomi MiMo
- **Test Suite:** [Съвместимост с Clojure Test Suite](07-clojure-test-suite.md) — между-диалектно тестване за съответствие
## Защо Clojure/Nim? ## Защо Clojure/Nim?
+110
View File
@@ -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*
+1
View File
@@ -15,6 +15,7 @@
- **Build:** `make build && make check` - **Build:** `make build && make check`
- **Tests:** 276+ tests across 8 test suites - **Tests:** 276+ tests across 8 test suites
- **AI Integration:** DeepSeek API, OpenAI-compatible, Xiaomi MiMo - **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? ## Why Clojure/Nim?
+188 -2
View File
@@ -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 cljIsFalse*(v: CljVal): bool = v.kind == ckBool and not v.boolVal
proc cljIsTruthy*(v: CljVal): bool = not cljIsNil(v) and not cljIsFalse(v) 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..<idx])
else: cljNil()
of ckSymbol:
let idx = v.symName.find('/')
if idx >= 0: cljString(v.symName[0..<idx])
else: cljNil()
else: raise newException(CatchableError, "namespace requires a keyword or symbol")
proc cljKey*(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, 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 ---- # ---- I/O ----
proc cljPrintln*(args: seq[CljVal]): CljVal = proc cljPrintln*(args: seq[CljVal]): CljVal =
@@ -316,6 +396,54 @@ proc cljNumEq*(args: seq[CljVal]): CljVal =
return cljBool(false) return cljBool(false)
cljBool(true) 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..<a.listItems.len:
if not cljEqual(a.listItems[i], b.listItems[i]): return false
true
of ckVector:
if a.vecData.count != b.vecData.count: return false
for i in 0..<a.vecData.count:
if not cljEqual(a.vecData.pvecNth(i), b.vecData.pvecNth(i)): return false
true
of ckMap:
if a.mapData.count != b.mapData.count: return false
for (k, v) in a.mapData.pmapItems:
if not b.mapData.pmapContains(k, hash(k), cljEqual): return false
if not cljEqual(v, b.mapData.pmapGet(k, cljNil(), hash(k), cljEqual)): return false
true
of ckSet:
if a.setData.count != b.setData.count: return false
for (k, _) in a.setData.pmapItems:
if not b.setData.pmapContains(k, hash(k), cljEqual): return false
true
else: false
proc cljMultiEqual*(args: seq[CljVal]): CljVal =
if args.len < 2: return cljBool(true)
for i in 1..<args.len:
if not cljEqual(args[i-1], args[i]):
return cljBool(false)
cljBool(true)
proc cljLt*(args: seq[CljVal]): CljVal = proc cljLt*(args: seq[CljVal]): CljVal =
if args.len < 2: return cljBool(true) if args.len < 2: return cljBool(true)
for i in 1..<args.len: for i in 1..<args.len:
@@ -434,6 +562,15 @@ proc cljNext*(v: CljVal): CljVal =
return cljNil() return cljNil()
r r
proc cljSecond*(v: CljVal): CljVal =
cljFirst(cljRest(v))
proc cljFfirst*(v: CljVal): CljVal =
cljFirst(cljFirst(v))
proc cljNfirst*(v: CljVal): CljVal =
cljNext(cljFirst(v))
proc cljLast*(v: CljVal): CljVal = proc cljLast*(v: CljVal): CljVal =
if v.isNil: return cljNil() if v.isNil: return cljNil()
case v.kind case v.kind
@@ -496,6 +633,31 @@ proc cljDisj*(s: CljVal, item: CljVal): CljVal =
return cljSet(@[]) return cljSet(@[])
CljVal(kind: ckSet, setData: pmapDissoc(s.setData, item, hash(item), cljEq)) CljVal(kind: ckSet, setData: pmapDissoc(s.setData, item, hash(item), cljEq))
proc cljPeek*(v: CljVal): CljVal =
if v.isNil: return cljNil()
case v.kind
of ckList:
if v.listItems.len == 0: cljNil()
else: v.listItems[0]
of ckVector:
if v.vecData.count == 0: cljNil()
else: pvecNth(v.vecData, v.vecData.count - 1)
else: cljNil()
proc cljPop*(v: CljVal): CljVal =
if v.isNil: return cljNil()
case v.kind
of ckList:
if v.listItems.len <= 1: cljList(@[])
else: cljList(v.listItems[1..^1])
of ckVector:
if v.vecData.count <= 1: cljVector(@[])
else:
var items = toSeq(v.vecData)
items.setLen(items.len - 1)
cljVector(items)
else: cljNil()
proc cljSeq*(v: CljVal): CljVal = proc cljSeq*(v: CljVal): CljVal =
if v.isNil: return cljNil() if v.isNil: return cljNil()
case v.kind case v.kind
@@ -1129,7 +1291,7 @@ proc cljAssocB*(t: CljVal, key: CljVal, val: CljVal): CljVal =
# ---- Additional functions needed by emitter ---- # ---- Additional functions needed by emitter ----
proc cljNotEq*(args: seq[CljVal]): CljVal = proc cljNotEq*(args: seq[CljVal]): CljVal =
cljNot(cljNumEq(args)) cljNot(cljMultiEqual(args))
proc cljGetIn*(m: CljVal, keys: CljVal, default: CljVal = nil): CljVal = proc cljGetIn*(m: CljVal, keys: CljVal, default: CljVal = nil): CljVal =
if keys.isNil or keys.kind != ckList: if keys.isNil or keys.kind != ckList:
@@ -1410,4 +1572,28 @@ proc cljGitLog*(n: CljVal = cljInt(5)): CljVal =
for line in gitOut.splitLines(): for line in gitOut.splitLines():
if line.len > 0: if line.len > 0:
items.add(cljString(line)) 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..<n:
mk.add(kitems[i])
mv.add(vitems[i])
return cljMap(mk, mv)
+5 -5
View File
@@ -159,7 +159,7 @@ proc runFile*(inputPath: string) =
let aiRes = ai_assist.explainError(compileOut, source, inputPath) let aiRes = ai_assist.explainError(compileOut, source, inputPath)
stderr.writeLine(ai_assist.formatSuggestion(aiRes)) stderr.writeLine(ai_assist.formatSuggestion(aiRes))
quit(1) quit(1)
try: removeDir(buildDir) except: discard # try: removeDir(buildDir) except: discard
let runResult = execCmd(cacheBinPath) let runResult = execCmd(cacheBinPath)
quit(runResult) quit(runResult)
@@ -173,7 +173,7 @@ proc runFile*(inputPath: string) =
let source = readFile(inputPath) let source = readFile(inputPath)
let aiRes = ai_assist.explainError(compileOut, source, inputPath) let aiRes = ai_assist.explainError(compileOut, source, inputPath)
stderr.writeLine(ai_assist.formatSuggestion(aiRes)) stderr.writeLine(ai_assist.formatSuggestion(aiRes))
try: removeDir(buildDir) except: discard # try: removeDir(buildDir) except: discard
quit(1) quit(1)
# Save to cache # Save to cache
@@ -186,7 +186,7 @@ proc runFile*(inputPath: string) =
discard discard
let runResult = execCmd(binPath) let runResult = execCmd(binPath)
try: removeDir(buildDir) except: discard # try: removeDir(buildDir) except: discard
if runResult != 0: if runResult != 0:
stderr.writeLine("Execution failed") stderr.writeLine("Execution failed")
quit(1) quit(1)
@@ -241,10 +241,10 @@ proc main() =
if ai_assist.hasAiConfig(): if ai_assist.hasAiConfig():
let aiRes = ai_assist.explainError(compileOut, code, "-e expression") let aiRes = ai_assist.explainError(compileOut, code, "-e expression")
stderr.writeLine(ai_assist.formatSuggestion(aiRes)) stderr.writeLine(ai_assist.formatSuggestion(aiRes))
try: removeDir(buildDir) except: discard # try: removeDir(buildDir) except: discard
quit(1) quit(1)
let runResult = execCmd(binPath) let runResult = execCmd(binPath)
try: removeDir(buildDir) except: discard # try: removeDir(buildDir) except: discard
quit(runResult) quit(runResult)
case cmd case cmd
+177 -52
View File
@@ -41,10 +41,12 @@ proc mangleName*(name: string): string =
of '.': result.add('_') of '.': result.add('_')
of '\'': result.add("_QUOTE_") of '\'': result.add("_QUOTE_")
of '&': result.add("_AMP_") of '&': result.add("_AMP_")
of '#': result.add("_HASH")
else: result.add(c) else: result.add(c)
proc emitExpr*(v: CljVal, indent: int = 0): string proc emitExpr*(v: CljVal, indent: int = 0): string
proc emitBlock*(items: seq[CljVal], indent: int): string proc emitBlock*(items: seq[CljVal], indent: int, useResult: bool = false): string
proc emitQuotedForm*(v: CljVal): string
proc indentStr(indent: int): string = proc indentStr(indent: int): string =
" ".repeat(indent) " ".repeat(indent)
@@ -55,7 +57,8 @@ proc runtimeName(op: string): string =
of "-": "cljSub" of "-": "cljSub"
of "*": "cljMul" of "*": "cljMul"
of "/": "cljDiv" of "/": "cljDiv"
of "=": "cljNumEq" of "=": "cljMultiEqual"
of "==": "cljNumEq"
of ">": "cljGt" of ">": "cljGt"
of "<": "cljLt" of "<": "cljLt"
of ">=": "cljGe" of ">=": "cljGe"
@@ -74,6 +77,9 @@ proc runtimeName(op: string): string =
of "even?": "cljEven" of "even?": "cljEven"
of "odd?": "cljOdd" of "odd?": "cljOdd"
of "first": "cljFirst" of "first": "cljFirst"
of "second": "cljSecond"
of "ffirst": "cljFfirst"
of "nfirst": "cljNfirst"
of "rest": "cljRest" of "rest": "cljRest"
of "next": "cljNext" of "next": "cljNext"
of "last": "cljLast" of "last": "cljLast"
@@ -113,6 +119,34 @@ proc runtimeName(op: string): string =
of "deref": "cljDeref" of "deref": "cljDeref"
of "reset!": "cljReset" of "reset!": "cljReset"
of "swap!": "cljSwap" 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 ---- # ---- File operations ----
of "file/read": "cljFileRead" of "file/read": "cljFileRead"
of "file/write": "cljFileWrite" of "file/write": "cljFileWrite"
@@ -126,6 +160,8 @@ proc runtimeName(op: string): string =
of "git/diff": "cljGitDiff" of "git/diff": "cljGitDiff"
of "git/log": "cljGitLog" of "git/log": "cljGitLog"
of "disj": "cljDisj" of "disj": "cljDisj"
of "peek": "cljPeek"
of "pop": "cljPop"
of "slurp": "cljFileRead" of "slurp": "cljFileRead"
of "spit": "cljFileWrite" of "spit": "cljFileWrite"
of "read-line": "cljReadLine" of "read-line": "cljReadLine"
@@ -244,6 +280,9 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
# quote # quote
if hName == "quote" and form.items.len == 2: if hName == "quote" and form.items.len == 2:
return form.items[1] return form.items[1]
# syntax-quote
if hName == "syntax-quote" and form.items.len == 2:
return eval(expandSyntaxQuote(form.items[1]))
# list # list
if hName == "list": if hName == "list":
var evaluated: seq[CljVal] = @[] var evaluated: seq[CljVal] = @[]
@@ -316,7 +355,7 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
let name = items[1] let name = items[1]
if name.kind != ckSymbol: if name.kind != ckSymbol:
raise newException(EmitterError, "def name must be a symbol") 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": of "defn":
if items.len < 4: if items.len < 4:
@@ -338,9 +377,6 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
bodyCode = emitExpr(body[0], indent + 1) bodyCode = emitExpr(body[0], indent + 1)
else: else:
bodyCode = emitBlock(body, indent + 1) 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 procName = mangleName(name.symName)
let exportMarker = if emitLibMode: "*" else: "" let exportMarker = if emitLibMode: "*" else: ""
return sp & "proc " & procName & exportMarker & "(" & paramNames.join(", ") & "): CljVal =\n" & bodyCode 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") raise newException(EmitterError, "if requires condition, then, and optional else")
let condCode = emitExpr(items[1], 0) let condCode = emitExpr(items[1], 0)
let thenCode = emitExpr(items[2], indent + 1) let thenCode = emitExpr(items[2], indent + 1)
let thenStripped = thenCode.strip() var ifBlock = sp & "if cljIsTruthy(" & condCode.strip() & "):\n" & thenCode
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
if items.len == 4: if items.len == 4:
let elseCode = emitExpr(items[3], indent + 1) let elseCode = emitExpr(items[3], indent + 1)
let elseStripped = elseCode.strip() ifBlock.add("\n" & sp & "else:\n" & elseCode)
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)
return ifBlock return ifBlock
of "when": of "when":
if items.len < 3: if items.len < 3:
raise newException(EmitterError, "when requires condition and body") raise newException(EmitterError, "when requires condition and body")
let condCode = emitExpr(items[1], 0) let condCode = emitExpr(items[1], indent)
var lines: seq[string] = @[] var lines: seq[string] = @[]
lines.add(sp & "if cljIsTruthy(" & condCode & "):") lines.add(sp & "if cljIsTruthy(" & condCode.strip() & "):")
let body = items[2..^1] let body = items[2..^1]
for b in body: for b in body:
lines.add(emitExpr(b, indent + 1)) lines.add(emitExpr(b, indent + 1))
lines.add(sp & "else:")
lines.add(indentStr(indent + 1) & "cljNil()")
return lines.join("\n") return lines.join("\n")
of "when-let": of "when-let":
@@ -602,14 +624,42 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
inc ti inc ti
var lines: seq[string] = @[] var lines: seq[string] = @[]
lines.add(sp & "try:") lines.add(sp & "try:")
for b in bodyForms: for i, b in bodyForms:
lines.add(emitExpr(b, indent + 1)) 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: for (exType, exVar, exBody) in catchClauses:
lines.add(sp & "except " & exType & ":") lines.add(sp & "except " & exType & ":")
lines.add(indentStr(indent + 1) & "let " & exVar & " = cljMap(@[cljKeyword(\"message\")], @[cljString(getCurrentExceptionMsg())])") 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) & "discard cljAssoc(" & exVar & ", cljKeyword(\"type\"), cljString(\"" & exType & "\"))")
for eb in exBody: for i, eb in exBody:
lines.add(emitExpr(eb, indent + 1)) 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: if finallyBody.len > 0:
lines.add(sp & "finally:") lines.add(sp & "finally:")
for fb in finallyBody: for fb in finallyBody:
@@ -631,12 +681,12 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
of ckList: of ckList:
var parts: seq[string] = @[] var parts: seq[string] = @[]
for item in quoted.items: for item in quoted.items:
parts.add(emitExpr(item, 0)) parts.add(emitQuotedForm(item))
return sp & "cljList(@[" & parts.join(", ") & "])" return sp & "cljList(@[" & parts.join(", ") & "])"
of ckVector: of ckVector:
var parts: seq[string] = @[] var parts: seq[string] = @[]
for item in quoted.items: for item in quoted.items:
parts.add(emitExpr(item, 0)) parts.add(emitQuotedForm(item))
return sp & "cljVector(@[" & parts.join(", ") & "])" return sp & "cljVector(@[" & parts.join(", ") & "])"
else: else:
return emitExpr(quoted, indent) return emitExpr(quoted, indent)
@@ -978,26 +1028,56 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
if rn.len > 0: if rn.len > 0:
var argParts: seq[string] = @[] var argParts: seq[string] = @[]
for i in 1..<items.len: for i in 1..<items.len:
argParts.add(emitExpr(items[i], 0)) argParts.add(emitExpr(items[i], indent))
# Variadic functions take seq[CljVal] # Variadic functions take seq[CljVal]
let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=", let variadic = op in ["+", "-", "*", "/", "=", ">", "<", ">=", "<=", "not=",
"println", "prn", "print", "str", "pr-str", "println", "prn", "print", "str", "pr-str",
"concat", "min", "max", "merge", "interleave"] "concat", "min", "max", "merge", "interleave",
"zipmap"]
var call: string var call: string
if variadic and argParts.len > 0: if variadic and argParts.len > 0:
call = rn & "(@[" & argParts.join(", ") & "])" call = rn & "(@[" & argParts.join(", ") & "])"
else: else:
call = rn & "(" & argParts.join(", ") & ")" call = rn & "(" & argParts.join(", ") & ")"
if op in ["println", "prn", "print"]:
return sp & "discard " & call
return sp & call return sp & call
# ---- User-defined function call ---- # ---- User-defined function call ----
var args: seq[string] = @[] var args: seq[string] = @[]
for i in 1..<items.len: for i in 1..<items.len:
args.add(emitExpr(items[i], 0)) args.add(emitExpr(items[i], indent))
return sp & mangleName(op) & "(" & args.join(", ") & ")" return sp & mangleName(op) & "(" & args.join(", ") & ")"
proc emitQuotedForm*(v: CljVal): string =
case v.kind
of ckSymbol:
return "cljSymbol(\"" & v.symName & "\")"
of ckKeyword:
return "cljKeyword(\"" & v.kwName & "\")"
of ckList:
var parts: seq[string] = @[]
for item in v.items:
parts.add(emitQuotedForm(item))
return "cljList(@[" & parts.join(", ") & "])"
of ckVector:
var parts: seq[string] = @[]
for item in v.items:
parts.add(emitQuotedForm(item))
return "cljVector(@[" & parts.join(", ") & "])"
of ckMap:
var keyParts: seq[string] = @[]
var valParts: seq[string] = @[]
for i in 0..<v.mapKeys.len:
keyParts.add(emitQuotedForm(v.mapKeys[i]))
valParts.add(emitQuotedForm(v.mapVals[i]))
return "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])"
of ckSet:
var parts: seq[string] = @[]
for item in v.setItems:
parts.add(emitQuotedForm(item))
return "cljSet(@[" & parts.join(", ") & "])"
else:
return emitExpr(v, 0)
proc emitExpr*(v: CljVal, indent: int = 0): string = proc emitExpr*(v: CljVal, indent: int = 0): string =
let sp = indentStr(indent) let sp = indentStr(indent)
case v.kind case v.kind
@@ -1031,7 +1111,7 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
of ckVector: of ckVector:
var parts: seq[string] = @[] var parts: seq[string] = @[]
for item in v.items: for item in v.items:
parts.add(emitExpr(item, 0)) parts.add(emitExpr(item, indent))
return sp & "cljVector(@[" & parts.join(", ") & "])" return sp & "cljVector(@[" & parts.join(", ") & "])"
of ckMap: of ckMap:
if v.mapKeys.len == 0: if v.mapKeys.len == 0:
@@ -1039,13 +1119,13 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
var keyParts: seq[string] = @[] var keyParts: seq[string] = @[]
var valParts: seq[string] = @[] var valParts: seq[string] = @[]
for i in 0..<v.mapKeys.len: for i in 0..<v.mapKeys.len:
keyParts.add(emitExpr(v.mapKeys[i], 0)) keyParts.add(emitExpr(v.mapKeys[i], indent))
valParts.add(emitExpr(v.mapVals[i], 0)) valParts.add(emitExpr(v.mapVals[i], indent))
return sp & "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])" return sp & "cljMap(@[" & keyParts.join(", ") & "], @[" & valParts.join(", ") & "])"
of ckSet: of ckSet:
var parts: seq[string] = @[] var parts: seq[string] = @[]
for item in v.setItems: for item in v.setItems:
parts.add(emitExpr(item, 0)) parts.add(emitExpr(item, indent))
return sp & "cljSet(@[" & parts.join(", ") & "])" return sp & "cljSet(@[" & parts.join(", ") & "])"
of ckFn: of ckFn:
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = discard cljNil())" return sp & "cljFn(proc(args: seq[CljVal]): CljVal = discard cljNil())"
@@ -1056,12 +1136,39 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
of ckAgent: of ckAgent:
return sp & "cljAgent(" & emitExpr(v.agentVal, 0) & ")" return sp & "cljAgent(" & emitExpr(v.agentVal, 0) & ")"
proc emitBlock(items: seq[CljVal], indent: int): string = proc emitBlock(items: seq[CljVal], indent: int, useResult: bool = false): string =
if items.len == 0: if items.len == 0:
return indentStr(indent) & "discard cljNil()" return indentStr(indent) & "discard cljNil()"
var lines: seq[string] = @[] var lines: seq[string] = @[]
for item in items: for i, item in items:
lines.add(emitExpr(item, indent)) var code = emitExpr(item, indent)
let stripped = code.strip()
if i < items.len - 1:
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("var ") or stripped.startsWith("let ") or
stripped.startsWith("proc ") or stripped.startsWith("result = "):
discard
else:
# Prepend discard before the first non-empty line
let firstNl = code.find("\n")
if firstNl == -1:
code = indentStr(indent) & "discard " & stripped
else:
let firstLine = code[0..<firstNl].strip()
let rest = code[firstNl..^1]
code = indentStr(indent) & "discard " & firstLine & rest
elif useResult:
if stripped.startsWith("discard "):
code = indentStr(indent) & "result = " & stripped[8..^1]
elif not stripped.startsWith("result = "):
let firstNl = code.find("\n")
if firstNl == -1:
code = indentStr(indent) & "result = " & stripped
else:
let firstLine = code[0..<firstNl].strip()
let rest = code[firstNl..^1]
code = indentStr(indent) & "result = " & firstLine & rest
lines.add(code)
return lines.join("\n") return lines.join("\n")
proc emitProgramInternal(forms: seq[CljVal]): string = proc emitProgramInternal(forms: seq[CljVal]): string =
@@ -1074,24 +1181,42 @@ proc emitProgramInternal(forms: seq[CljVal]): string =
var defs: seq[string] = @[] var defs: seq[string] = @[]
var mainForms: seq[string] = @[] var mainForms: seq[string] = @[]
for i, form in forms: proc processForm(i: int, form: CljVal, isLast: bool) =
let isDef = form.kind == ckList and form.items.len > 0 and let headSym = form.kind == ckList and form.items.len > 0 and
form.items[0].kind == ckSymbol and form.items[0].kind == ckSymbol
form.items[0].symName in ["def", "defn", "defn-"] let headName = if headSym: form.items[0].symName else: ""
if isDef: let isDef = headSym and headName in ["def", "defn", "defn-"]
let isMacro = headSym and headName in ["defmacro"]
let isNs = headSym and headName == "ns"
if isNs:
discard
elif isDef or isMacro:
defs.add(emitExpr(form, 0)) defs.add(emitExpr(form, 0))
else: else:
let code = emitExpr(form, 2) let code = emitExpr(form, 2)
let stripped = code.strip() let stripped = code.strip()
if stripped.startsWith("echo ") or stripped.startsWith("discard ") or if stripped.startsWith("echo ") or stripped.startsWith("discard ") or
stripped.startsWith("block:") or stripped.startsWith("if ") or stripped.startsWith("var "):
stripped.startsWith("try:") or stripped.startsWith("var "):
mainForms.add(code) mainForms.add(code)
elif i == forms.len - 1: elif isLast:
mainForms.add(indentStr(2) & "echo cljRepr(" & stripped & ")") mainForms.add(indentStr(2) & "echo cljRepr(" & stripped & ")")
else: else:
mainForms.add(indentStr(2) & "discard cljRepr(" & stripped & ")") 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 var lines = headerLines
# Add collected Nim imports # Add collected Nim imports
for imp in requiredImports: for imp in requiredImports:
+44 -23
View File
@@ -373,30 +373,51 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
return EvalResult(ok: true, value: cljFloat(a / b)) return EvalResult(ok: true, value: cljFloat(a / b))
of "=": 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..<a.items.len:
if not evalEqual(a.items[i], b.items[i]): return false
true
of ckMap:
if a.mapKeys.len != b.mapKeys.len: return false
for ai in 0..<a.mapKeys.len:
var found = false
for bi in 0..<b.mapKeys.len:
if evalEqual(a.mapKeys[ai], b.mapKeys[bi]) and
evalEqual(a.mapVals[ai], b.mapVals[bi]):
found = true
break
if not found: return false
true
of ckSet:
if a.setItems.len != b.setItems.len: return false
for ai in a.setItems:
var found = false
for bi in b.setItems:
if evalEqual(ai, bi):
found = true
break
if not found: return false
true
else: false
numArgs(2) numArgs(2)
let a = args[0] return EvalResult(ok: true, value: cljBool(evalEqual(args[0], args[1])))
let b = args[1]
if a.kind == ckNil and b.kind == ckNil:
return EvalResult(ok: true, value: cljBool(true))
if a.kind == ckNil or b.kind == ckNil:
return EvalResult(ok: true, value: cljBool(false))
if a.kind == ckInt and b.kind == ckInt:
return EvalResult(ok: true, value: cljBool(a.intVal == b.intVal))
if a.kind == ckFloat and b.kind == ckFloat:
return EvalResult(ok: true, value: cljBool(a.floatVal == b.floatVal))
if (a.kind == ckInt and b.kind == ckFloat) or (a.kind == ckFloat and b.kind == ckInt):
let av = if a.kind == ckInt: a.intVal.float64 else: a.floatVal
let bv = if b.kind == ckInt: b.intVal.float64 else: b.floatVal
return EvalResult(ok: true, value: cljBool(av == bv))
if a.kind == ckString and b.kind == ckString:
return EvalResult(ok: true, value: cljBool(a.strVal == b.strVal))
if a.kind == ckBool and b.kind == ckBool:
return EvalResult(ok: true, value: cljBool(a.boolVal == b.boolVal))
if a.kind == ckKeyword and b.kind == ckKeyword:
return EvalResult(ok: true, value: cljBool(a.kwName == b.kwName))
if a.kind == ckSymbol and b.kind == ckSymbol:
return EvalResult(ok: true, value: cljBool(a.symName == b.symName))
return EvalResult(ok: true, value: cljBool(false))
of "not=": of "not=":
numArgs(2) numArgs(2)
+133
View File
@@ -0,0 +1,133 @@
import subprocess, tempfile, os, re, sys
def extract_default(content, start):
"""Extract the :default branch value from reader conditional content.
Returns (value_string, consumed_length)."""
content = content.strip()
# Find :default keyword
default_idx = content.find(':default')
if default_idx < 0:
return '', len(content)
rest = content[default_idx + len(':default'):].strip()
if not rest:
return '', len(content)
# Read one s-expression
if rest[0] in '([':
open_c = rest[0]
close_c = ']' if open_c == '[' else ')'
depth = 0
k = 0
while k < len(rest):
if rest[k] == open_c:
depth += 1
elif rest[k] == close_c:
depth -= 1
if depth == 0:
return rest[:k+1], k+1
k += 1
return rest, len(rest)
elif rest[0] == '"':
# String
k = 1
while k < len(rest):
if rest[k] == '\\':
k += 1
elif rest[k] == '"':
return rest[:k+1], k+1
k += 1
return rest, len(rest)
else:
# Atom: read until whitespace or )
k = 0
while k < len(rest) and rest[k] not in ' \t\n\r,)':
k += 1
return rest[:k], k
def strip_reader_conditionals(text):
"""Strip #? and #?@ reader conditionals, keeping :default branch content."""
result = []
i = 0
while i < len(text):
# Check for #?@ or #?
if i + 3 <= len(text) and text[i:i+3] == '#?@':
j = i + 3
elif i + 2 <= len(text) and text[i:i+2] == '#?':
j = i + 2
else:
result.append(text[i])
i += 1
continue
# Skip whitespace between #? and (
while j < len(text) and text[j] in ' \t\n\r':
j += 1
if j >= 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])
+6
View File
@@ -0,0 +1,6 @@
(do
(defmacro is [form]
`(let [result# ~form]
(if result# nil (println "FAIL:" (quote ~form)))))
(println [1 2 (is true) 3])
)
+1 -1
View File
@@ -143,7 +143,7 @@ suite "Emitter - Operators":
test "emit equality": test "emit equality":
let v = read("(= 1 1)") let v = read("(= 1 1)")
let code = emitExpr(v) let code = emitExpr(v)
check "cljNumEq" in code check "cljMultiEqual" in code
test "emit not=": test "emit not=":
let v = read("(not= 1 2)") let v = read("(not= 1 2)")