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:
@@ -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?
|
||||||
|
|
||||||
|
|||||||
@@ -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*
|
||||||
@@ -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?
|
||||||
|
|
||||||
|
|||||||
@@ -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*
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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])
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
(do
|
||||||
|
(defmacro is [form]
|
||||||
|
`(let [result# ~form]
|
||||||
|
(if result# nil (println "FAIL:" (quote ~form)))))
|
||||||
|
(println [1 2 (is true) 3])
|
||||||
|
)
|
||||||
@@ -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)")
|
||||||
|
|||||||
Reference in New Issue
Block a user