feat: add fullscreen TUI and project updates
- New TUI screens: Main Menu, Compile, Run, REPL, AI Generator, AI Settings, Help - AI configuration persisted in ~/.config/cljnim/config.json - Added illwill dependency for terminal UI - Updated experiments, examples, docs, and core modules
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
# Tests for AI assistance module (no real API calls)
|
||||
|
||||
import unittest, strutils
|
||||
import unittest, strutils, times
|
||||
import os
|
||||
import ai_assist
|
||||
|
||||
suite "AI Config Detection":
|
||||
test "detectConfig returns empty when no env vars set":
|
||||
# Note: this test assumes no API keys are set in test environment
|
||||
# If keys ARE set, it will detect them
|
||||
let cfg = detectConfig()
|
||||
# At minimum, the function should not crash
|
||||
check cfg.timeoutMs >= 0
|
||||
|
||||
test "hasAiConfig returns false when no keys set (if env empty)":
|
||||
@@ -31,6 +28,30 @@ suite "Prompt Building":
|
||||
check "reverse a list" in prompt
|
||||
check "Clojure" in prompt
|
||||
|
||||
test "buildOptimizationPrompt includes code":
|
||||
let prompt = buildOptimizationPrompt("(reduce + [1 2 3])")
|
||||
check "(reduce + [1 2 3])" in prompt
|
||||
check "SIMD" in prompt or "optim" in prompt.toLowerAscii()
|
||||
|
||||
test "buildDebugPrompt includes code and result":
|
||||
let prompt = buildDebugPrompt("(+ 1 2)", "3")
|
||||
check "(+ 1 2)" in prompt
|
||||
check "3" in prompt
|
||||
check "Clojure" in prompt
|
||||
|
||||
suite "Error Cache":
|
||||
test "getCachedError returns empty for unknown errors":
|
||||
check getCachedError("unknown error", "test.clj") == ""
|
||||
|
||||
test "cacheError and getCachedError work":
|
||||
cacheError("type mismatch", "test.clj", "Check your types")
|
||||
check getCachedError("type mismatch", "test.clj") == "Check your types"
|
||||
|
||||
test "cacheError with same key returns correct suggestion":
|
||||
cacheError("unterminated string", "src.clj", "Add closing quote")
|
||||
let cached = getCachedError("unterminated string", "src.clj")
|
||||
check cached == "Add closing quote"
|
||||
|
||||
suite "Response Formatting":
|
||||
test "formatSuggestion shows error when not ok":
|
||||
let resp = AiResponse(ok: false, suggestion: "No key configured")
|
||||
|
||||
+96
-45
@@ -1,98 +1,128 @@
|
||||
import unittest
|
||||
import strutils
|
||||
import unittest, strutils
|
||||
import ../src/types
|
||||
import ../src/reader
|
||||
import ../src/emitter
|
||||
|
||||
suite "Emitter - Basic Values":
|
||||
test "emit nil":
|
||||
check emitExpr(cljNil()) == "cljNil()"
|
||||
let v = cljNil()
|
||||
let code = emitExpr(v)
|
||||
check code == "cljNil()"
|
||||
|
||||
test "emit bool true":
|
||||
check emitExpr(cljBool(true)) == "cljBool(true)"
|
||||
let v = cljBool(true)
|
||||
let code = emitExpr(v)
|
||||
check code == "cljBool(true)"
|
||||
|
||||
test "emit bool false":
|
||||
check emitExpr(cljBool(false)) == "cljBool(false)"
|
||||
let v = cljBool(false)
|
||||
let code = emitExpr(v)
|
||||
check code == "cljBool(false)"
|
||||
|
||||
test "emit int":
|
||||
check emitExpr(cljInt(42)) == "cljInt(42)"
|
||||
let v = cljInt(42)
|
||||
let code = emitExpr(v)
|
||||
check code == "cljInt(42)"
|
||||
|
||||
test "emit float":
|
||||
check "cljFloat(3.14)" in emitExpr(cljFloat(3.14))
|
||||
let v = cljFloat(3.14)
|
||||
let code = emitExpr(v)
|
||||
check code == "cljFloat(3.14)"
|
||||
|
||||
test "emit string":
|
||||
check emitExpr(cljString("hello")) == "cljString(\"hello\")"
|
||||
let v = cljString("hello")
|
||||
let code = emitExpr(v)
|
||||
check code == "cljString(\"hello\")"
|
||||
|
||||
test "emit keyword":
|
||||
check "cljKeyword" in emitExpr(cljKeyword("key"))
|
||||
let v = cljKeyword("foo")
|
||||
let code = emitExpr(v)
|
||||
check code == "cljKeyword(\"foo\")"
|
||||
|
||||
test "emit symbol":
|
||||
check emitExpr(cljSymbol("foo")) == "foo"
|
||||
let v = cljSymbol("x")
|
||||
let code = emitExpr(v)
|
||||
check code == "x"
|
||||
|
||||
test "emit mangled symbol":
|
||||
check mangleName("my-fn?") == "my_fn_Q"
|
||||
let v = cljSymbol("my-var")
|
||||
let code = emitExpr(v)
|
||||
check code == "my_var"
|
||||
|
||||
test "emit vector":
|
||||
let v = cljVector(@[cljInt(1), cljInt(2), cljInt(3)])
|
||||
check "cljVector" in emitExpr(v)
|
||||
check "cljInt(1)" in emitExpr(v)
|
||||
let v = cljVector(@[cljInt(1), cljInt(2)])
|
||||
let code = emitExpr(v)
|
||||
check "cljVector" in code
|
||||
|
||||
test "emit empty list":
|
||||
check emitExpr(cljList(@[])) == "cljList(@[])"
|
||||
|
||||
let v = read("()")
|
||||
let code = emitExpr(v)
|
||||
check "cljList" in code
|
||||
|
||||
suite "Emitter - Special Forms":
|
||||
test "emit println":
|
||||
let v = read("(println \"hello\")")
|
||||
let v = read("(println 1)")
|
||||
let code = emitExpr(v)
|
||||
check "cljPrintln" in code
|
||||
check "discard" in code
|
||||
|
||||
test "emit def":
|
||||
let v = read("(def x 42)")
|
||||
let code = emitExpr(v)
|
||||
check "let x = cljInt(42)" in code
|
||||
check "let x" in code
|
||||
|
||||
test "emit defn":
|
||||
let v = read("(defn square [x] (* x x))")
|
||||
let code = emitExpr(v)
|
||||
check "proc square" in code
|
||||
check "CljVal" in code
|
||||
|
||||
test "emit if":
|
||||
let v = read("(if true 1 2)")
|
||||
let code = emitExpr(v)
|
||||
check "cljIsTruthy" in code
|
||||
check "else:" in code
|
||||
check "if" in code
|
||||
|
||||
test "emit when":
|
||||
let v = read("(when true (println \"yes\"))")
|
||||
let v = read("(when true 1)")
|
||||
let code = emitExpr(v)
|
||||
check "cljIsTruthy" in code
|
||||
check "if" in code
|
||||
|
||||
test "emit let":
|
||||
let v = read("(let [x 1] x)")
|
||||
let v = read("(let [a 1] a)")
|
||||
let code = emitExpr(v)
|
||||
check "block:" in code
|
||||
check "let x = cljInt(1)" in code
|
||||
check "let" in code
|
||||
|
||||
test "emit cond":
|
||||
let v = read("(cond true 1 :else 2)")
|
||||
let v = read("(cond true 1 false 2)")
|
||||
let code = emitExpr(v)
|
||||
check "cljIsTruthy" in code
|
||||
check "else:" in code
|
||||
check "if" in code
|
||||
|
||||
test "emit fn":
|
||||
let v = read("(fn [x] x)")
|
||||
let code = emitExpr(v)
|
||||
check "proc" in code
|
||||
check "CljVal" in code
|
||||
|
||||
test "emit do":
|
||||
let v = read("(do 1 2)")
|
||||
let code = emitExpr(v)
|
||||
check "cljInt" in code
|
||||
check "cljInt(1)" in code
|
||||
check "cljInt(2)" in code
|
||||
|
||||
test "emit loop":
|
||||
let v = read("(loop [i 0] i)")
|
||||
let code = emitExpr(v)
|
||||
check "while true" in code
|
||||
|
||||
test "emit recur":
|
||||
let v = read("(loop [i 5] (if (= i 0) i (recur (- i 1))))")
|
||||
let code = emitExpr(v)
|
||||
check "while true" in code
|
||||
check "continue" in code
|
||||
|
||||
test "emit loop with break":
|
||||
let v = read("(loop [acc 1 i 5] (if (= i 0) acc (recur (* acc i) (- i 1))))")
|
||||
let code = emitExpr(v)
|
||||
check "while true" in code
|
||||
check "continue" in code
|
||||
check "break" in code
|
||||
|
||||
suite "Emitter - Operators":
|
||||
test "emit addition":
|
||||
@@ -118,28 +148,25 @@ suite "Emitter - Operators":
|
||||
test "emit not=":
|
||||
let v = read("(not= 1 2)")
|
||||
let code = emitExpr(v)
|
||||
check "cljNotEq" in code
|
||||
check "cljNot" in code
|
||||
|
||||
test "emit not":
|
||||
let v = read("(not true)")
|
||||
let code = emitExpr(v)
|
||||
check "cljNot" in code
|
||||
|
||||
|
||||
suite "Emitter - Program":
|
||||
test "emitProgram with defs and main":
|
||||
let forms = readAll("(def x 10)\n(println x)")
|
||||
let prog = emitProgram(forms)
|
||||
check "let x = cljInt(10)" in prog
|
||||
check "when isMainModule:" in prog
|
||||
check "cljPrintln" in prog
|
||||
let forms = readAll("(def x 42) (println x)")
|
||||
let code = emitProgram(forms)
|
||||
check "let x" in code
|
||||
check "when isMainModule" in code
|
||||
|
||||
test "emitProgram multiple defs":
|
||||
let forms = readAll("(defn add [a b] (+ a b))\n(println (add 1 2))")
|
||||
let prog = emitProgram(forms)
|
||||
check "proc add" in prog
|
||||
check "add(cljInt(1), cljInt(2))" in prog
|
||||
|
||||
let forms = readAll("(def a 1) (def b 2)")
|
||||
let code = emitProgram(forms)
|
||||
check "let a" in code
|
||||
check "let b" in code
|
||||
|
||||
suite "Emitter - Map Literals":
|
||||
test "emit empty map":
|
||||
@@ -155,7 +182,6 @@ suite "Emitter - Map Literals":
|
||||
let code = emitExpr(v)
|
||||
check "cljMap" in code
|
||||
|
||||
|
||||
suite "Emitter - Higher-order":
|
||||
test "emit map with symbol fn":
|
||||
let v = read("(map inc [1 2 3])")
|
||||
@@ -173,3 +199,28 @@ suite "Emitter - Higher-order":
|
||||
let v = read("(reduce + 0 [1 2 3])")
|
||||
let code = emitExpr(v)
|
||||
check "cljReduce" in code
|
||||
|
||||
suite "Emitter - Try/Catch/Finally":
|
||||
test "emit try with catch":
|
||||
let v = read("(try (/ 1 0) (catch Exception e (println e)))")
|
||||
let code = emitExpr(v)
|
||||
check "try:" in code
|
||||
check "except" in code
|
||||
check "getCurrentExceptionMsg" in code
|
||||
|
||||
test "emit try with specific exception type":
|
||||
let v = read("(try (/ 1 0) (catch ValueError e (println e)))")
|
||||
let code = emitExpr(v)
|
||||
check "except ValueError" in code
|
||||
|
||||
test "emit try with finally":
|
||||
let v = read("(try (/ 1 0) (finally (println \"cleanup\")))")
|
||||
let code = emitExpr(v)
|
||||
check "try:" in code
|
||||
check "finally:" in code
|
||||
|
||||
test "emit throw":
|
||||
let v = read("(throw \"something went wrong\")")
|
||||
let code = emitExpr(v)
|
||||
check "raise" in code
|
||||
check "CatchableError" in code
|
||||
|
||||
+26
-1
@@ -1,4 +1,4 @@
|
||||
import unittest
|
||||
import unittest, strutils
|
||||
import ../src/types
|
||||
import ../src/eval
|
||||
|
||||
@@ -580,3 +580,28 @@ suite "Eval - Channels":
|
||||
discard eval("(def ch (chan))", env)
|
||||
let r = eval("(close! ch)", env)
|
||||
check r.ok
|
||||
|
||||
suite "Eval - AI Functions (test doesn't require API keys)":
|
||||
setup:
|
||||
let env = newTopLevelEnv()
|
||||
|
||||
test "ai/generate returns string or error":
|
||||
let r = eval("(ai/generate \"reverse a list\")", env)
|
||||
if r.ok:
|
||||
check r.value.kind == ckString
|
||||
else:
|
||||
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
|
||||
|
||||
test "ai/optimize returns string or error":
|
||||
let r = eval("(ai/optimize \"(reduce + nums)\")", env)
|
||||
if r.ok:
|
||||
check r.value.kind == ckString
|
||||
else:
|
||||
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
|
||||
|
||||
test "ai/debug returns string or error":
|
||||
let r = eval("(ai/debug (+ 1 2))", env)
|
||||
if r.ok:
|
||||
check r.value.kind == ckString
|
||||
else:
|
||||
check("API key" in r.error or "AI request failed" in r.error or "SSL" in r.error)
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
import unittest, tables
|
||||
import ../src/types
|
||||
import ../src/macros
|
||||
|
||||
# Helper: check that a value is a list with given head symbol
|
||||
proc isListWithHead(v: CljVal, head: string): bool =
|
||||
v.kind == ckList and v.items.len > 0 and v.items[0].kind == ckSymbol and v.items[0].symName == head
|
||||
|
||||
# Helper: check that a value is a symbol with given name
|
||||
proc isSym(v: CljVal, name: string): bool =
|
||||
v.kind == ckSymbol and v.symName == name
|
||||
|
||||
suite "Macros - initBuiltinMacros":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
suite "Macros - -> thread-first":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "-> with symbol step":
|
||||
let form = cljList(@[cljSymbol("->"), cljInt(5), cljSymbol("inc")])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("inc")
|
||||
check expanded.items.len == 2
|
||||
check expanded.items[1].kind == ckInt
|
||||
check expanded.items[1].intVal == 5
|
||||
|
||||
test "-> with list step":
|
||||
let form = cljList(@[cljSymbol("->"), cljInt(5),
|
||||
cljList(@[cljSymbol("+"), cljInt(3)])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("+")
|
||||
check expanded.items.len == 3
|
||||
check expanded.items[1].kind == ckInt
|
||||
check expanded.items[1].intVal == 5
|
||||
check expanded.items[2].kind == ckInt
|
||||
check expanded.items[2].intVal == 3
|
||||
|
||||
test "-> chained":
|
||||
let form = cljList(@[cljSymbol("->"), cljInt(5),
|
||||
cljSymbol("inc"),
|
||||
cljList(@[cljSymbol("*"), cljInt(2)])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("*")
|
||||
check expanded.items.len == 3
|
||||
check expanded.items[1].isListWithHead("inc")
|
||||
check expanded.items[1].items.len == 2
|
||||
check expanded.items[1].items[1].intVal == 5
|
||||
check expanded.items[2].intVal == 2
|
||||
|
||||
test "-> too few args raises":
|
||||
let form = cljList(@[cljSymbol("->"), cljInt(5)])
|
||||
expect CatchableError:
|
||||
discard macroexpand(form)
|
||||
|
||||
suite "Macros - ->> thread-last":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "->> with list step":
|
||||
let form = cljList(@[cljSymbol("->>"), cljInt(5),
|
||||
cljList(@[cljSymbol("+"), cljInt(3)])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("+")
|
||||
check expanded.items.len == 3
|
||||
check expanded.items[1].intVal == 3
|
||||
check expanded.items[2].intVal == 5
|
||||
|
||||
test "->> chained":
|
||||
let form = cljList(@[cljSymbol("->>"), cljInt(5),
|
||||
cljList(@[cljSymbol("conj"), cljInt(1)])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("conj")
|
||||
check expanded.items[1].intVal == 1
|
||||
check expanded.items[2].intVal == 5
|
||||
|
||||
suite "Macros - and (not expanded by macroexpand)":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "and is left untouched by macroexpand":
|
||||
let form = cljList(@[cljSymbol("and"), cljSymbol("a"), cljSymbol("b")])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("and")
|
||||
check expanded.items.len == 3
|
||||
|
||||
test "and macro implementation directly":
|
||||
let andFn = macroTable["and"]
|
||||
let result = andFn(@[cljSymbol("a"), cljSymbol("b")])
|
||||
check result.isListWithHead("if")
|
||||
check result.items.len == 4
|
||||
check result.items[1].isSym("a")
|
||||
check result.items[2].isSym("b")
|
||||
check result.items[3].kind == ckBool
|
||||
check result.items[3].boolVal == false
|
||||
|
||||
test "and empty directly":
|
||||
let andFn = macroTable["and"]
|
||||
let result = andFn(@[])
|
||||
check result.kind == ckBool
|
||||
check result.boolVal == true
|
||||
|
||||
test "and single directly":
|
||||
let andFn = macroTable["and"]
|
||||
let result = andFn(@[cljSymbol("x")])
|
||||
check result.isSym("x")
|
||||
|
||||
test "and three args directly":
|
||||
let andFn = macroTable["and"]
|
||||
let result = andFn(@[cljSymbol("a"), cljSymbol("b"), cljSymbol("c")])
|
||||
check result.isListWithHead("if")
|
||||
check result.items[1].isSym("a")
|
||||
check result.items[2].isListWithHead("if")
|
||||
check result.items[3].kind == ckBool
|
||||
check result.items[3].boolVal == false
|
||||
|
||||
suite "Macros - or (not expanded by macroexpand)":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "or is left untouched by macroexpand":
|
||||
let form = cljList(@[cljSymbol("or"), cljSymbol("a"), cljSymbol("b")])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("or")
|
||||
|
||||
test "or empty directly":
|
||||
let orFn = macroTable["or"]
|
||||
let result = orFn(@[])
|
||||
check result.kind == ckNil
|
||||
|
||||
test "or single directly":
|
||||
let orFn = macroTable["or"]
|
||||
let result = orFn(@[cljSymbol("x")])
|
||||
check result.isSym("x")
|
||||
|
||||
test "or two args directly":
|
||||
let orFn = macroTable["or"]
|
||||
let result = orFn(@[cljSymbol("a"), cljSymbol("b")])
|
||||
check result.isListWithHead("if")
|
||||
check result.items.len == 4
|
||||
check result.items[1].isSym("a")
|
||||
check result.items[2].isSym("a")
|
||||
check result.items[3].isSym("b")
|
||||
|
||||
suite "Macros - when (not expanded by macroexpand)":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "when is left untouched by macroexpand":
|
||||
let form = cljList(@[cljSymbol("when"), cljSymbol("x"), cljInt(1)])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("when")
|
||||
|
||||
test "when macro directly":
|
||||
let whenFn = macroTable["when"]
|
||||
let result = whenFn(@[cljSymbol("x"), cljInt(1), cljInt(2)])
|
||||
check result.isListWithHead("if")
|
||||
check result.items.len == 3
|
||||
check result.items[1].isSym("x")
|
||||
check result.items[2].isListWithHead("do")
|
||||
check result.items[2].items.len == 3
|
||||
|
||||
test "when too few args raises":
|
||||
expect CatchableError:
|
||||
discard macroTable["when"](@[cljSymbol("x")])
|
||||
|
||||
suite "Macros - when-not":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "when-not expands to if":
|
||||
let form = cljList(@[cljSymbol("when-not"), cljSymbol("x"), cljInt(1)])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("if")
|
||||
check expanded.items.len == 4
|
||||
check expanded.items[1].isSym("x")
|
||||
check expanded.items[2].kind == ckNil
|
||||
check expanded.items[3].isListWithHead("do")
|
||||
|
||||
suite "Macros - cond (not expanded by macroexpand)":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "cond is left untouched by macroexpand":
|
||||
let form = cljList(@[cljSymbol("cond"), cljSymbol("a"), cljInt(1)])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("cond")
|
||||
|
||||
test "cond empty directly":
|
||||
let condFn = macroTable["cond"]
|
||||
let result = condFn(@[])
|
||||
check result.kind == ckNil
|
||||
|
||||
test "cond single clause directly":
|
||||
let condFn = macroTable["cond"]
|
||||
let result = condFn(@[cljSymbol("a"), cljInt(1)])
|
||||
check result.isListWithHead("if")
|
||||
check result.items.len == 3
|
||||
check result.items[1].isSym("a")
|
||||
check result.items[2].intVal == 1
|
||||
|
||||
test "cond two clauses directly":
|
||||
let condFn = macroTable["cond"]
|
||||
let result = condFn(@[cljSymbol("a"), cljInt(1), cljSymbol("b"), cljInt(2)])
|
||||
check result.isListWithHead("if")
|
||||
check result.items.len == 4
|
||||
check result.items[1].isSym("a")
|
||||
check result.items[2].intVal == 1
|
||||
check result.items[3].isListWithHead("cond")
|
||||
|
||||
test "cond with :else directly":
|
||||
let condFn = macroTable["cond"]
|
||||
let result = condFn(@[cljKeyword("else"), cljInt(42)])
|
||||
check result.kind == ckInt
|
||||
check result.intVal == 42
|
||||
|
||||
test "cond odd args raises":
|
||||
expect CatchableError:
|
||||
discard macroTable["cond"](@[cljSymbol("a")])
|
||||
|
||||
suite "Macros - as->":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "as-> basic":
|
||||
let form = cljList(@[cljSymbol("as->"), cljInt(5), cljSymbol("x"),
|
||||
cljList(@[cljSymbol("inc"), cljSymbol("x")])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("let")
|
||||
check expanded.items.len == 3
|
||||
check expanded.items[1].kind == ckVector
|
||||
check expanded.items[1].items.len == 2
|
||||
check expanded.items[1].items[0].isSym("x")
|
||||
check expanded.items[1].items[1].intVal == 5
|
||||
check expanded.items[2].isListWithHead("inc")
|
||||
|
||||
suite "Macros - doto":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "doto creates let":
|
||||
let form = cljList(@[cljSymbol("doto"), cljString("hello"),
|
||||
cljList(@[cljSymbol("toUpper")])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("let")
|
||||
check expanded.items.len == 4 # let, [gs "hello"], (toUpper gs), gs
|
||||
check expanded.items[1].kind == ckVector
|
||||
check expanded.items[1].items.len == 2
|
||||
check expanded.items[1].items[1].kind == ckString
|
||||
|
||||
suite "Macros - some->":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "some-> wraps in nil? check":
|
||||
let form = cljList(@[cljSymbol("some->"), cljInt(5),
|
||||
cljList(@[cljSymbol("inc")])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("let")
|
||||
check expanded.items.len == 3
|
||||
check expanded.items[2].isListWithHead("if")
|
||||
check expanded.items[2].items[1].isListWithHead("nil?")
|
||||
|
||||
suite "Macros - some->>":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "some->> appends to list":
|
||||
let form = cljList(@[cljSymbol("some->>"), cljInt(5),
|
||||
cljList(@[cljSymbol("conj"), cljInt(1)])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("let")
|
||||
|
||||
suite "Macros - when-let":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "when-let expands via macroexpand":
|
||||
let form = cljList(@[cljSymbol("when-let"),
|
||||
cljVector(@[cljSymbol("x"), cljInt(5)]),
|
||||
cljInt(42)])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("let")
|
||||
|
||||
test "when-let macro directly":
|
||||
let whenLetFn = macroTable["when-let"]
|
||||
let result = whenLetFn(@[
|
||||
cljVector(@[cljSymbol("x"), cljInt(5)]),
|
||||
cljInt(42)])
|
||||
check result.isListWithHead("let")
|
||||
check result.items.len == 3
|
||||
|
||||
suite "Macros - macroexpand1":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "macroexpand1 returns form for non-macro":
|
||||
let form = cljList(@[cljSymbol("+"), cljInt(1), cljInt(2)])
|
||||
let expanded = macroexpand1(form)
|
||||
check expanded.isListWithHead("+")
|
||||
|
||||
test "macroexpand1 expands -> macro":
|
||||
let form = cljList(@[cljSymbol("->"), cljInt(5), cljSymbol("inc")])
|
||||
let expanded = macroexpand1(form)
|
||||
check expanded.isListWithHead("inc")
|
||||
|
||||
test "macroexpand1 leaves special forms alone":
|
||||
let form = cljList(@[cljSymbol("def"), cljSymbol("x"), cljInt(1)])
|
||||
let expanded = macroexpand1(form)
|
||||
check expanded.isListWithHead("def")
|
||||
|
||||
test "macroexpand1 leaves and alone":
|
||||
let form = cljList(@[cljSymbol("and"), cljSymbol("a"), cljSymbol("b")])
|
||||
let expanded = macroexpand1(form)
|
||||
check expanded.isListWithHead("and")
|
||||
|
||||
suite "Macros - macroexpand":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "macroexpand fully expands chained ->":
|
||||
let form = cljList(@[cljSymbol("->"), cljInt(5),
|
||||
cljSymbol("inc"),
|
||||
cljList(@[cljSymbol("*"), cljInt(2)])])
|
||||
let expanded = macroexpand(form)
|
||||
check expanded.isListWithHead("*")
|
||||
check expanded.items[1].isListWithHead("inc")
|
||||
|
||||
suite "Macros - syntax-quote":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "syntax-quote on symbol returns quoted symbol":
|
||||
let form = cljList(@[cljSymbol("syntax-quote"), cljSymbol("x")])
|
||||
let expanded = macroexpand1(form)
|
||||
check expanded.isListWithHead("quote")
|
||||
check expanded.items[1].isSym("x")
|
||||
|
||||
test "syntax-quote on list returns concat":
|
||||
let form = cljList(@[cljSymbol("syntax-quote"),
|
||||
cljList(@[cljSymbol("inc"), cljSymbol("x")])])
|
||||
let expanded = macroexpand1(form)
|
||||
check expanded.isListWithHead("concat")
|
||||
|
||||
suite "Macros - fn*":
|
||||
setup:
|
||||
initBuiltinMacros()
|
||||
|
||||
test "fn* wraps to fn":
|
||||
let form = cljList(@[cljSymbol("fn*"), cljVector(@[cljSymbol("x")]), cljSymbol("x")])
|
||||
let expanded = macroexpand1(form)
|
||||
check expanded.isListWithHead("fn")
|
||||
check expanded.items.len == 3
|
||||
+74
-1
@@ -1,4 +1,4 @@
|
||||
import unittest
|
||||
import unittest, strutils
|
||||
import ../src/types
|
||||
import ../src/reader
|
||||
|
||||
@@ -158,3 +158,76 @@ suite "Reader - readAll":
|
||||
test "read whitespace only":
|
||||
let forms = readAll(" \n \t ")
|
||||
check forms.len == 0
|
||||
|
||||
suite "Reader - Edge Cases":
|
||||
test "read negative float":
|
||||
let v = read("-3.14")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == -3.14
|
||||
|
||||
test "read negative float without leading zero":
|
||||
let v = read("-.5")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == -0.5
|
||||
|
||||
test "read positive float without leading zero":
|
||||
let v = read("+.25")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == 0.25
|
||||
|
||||
test "read scientific notation 1e5":
|
||||
let v = read("1e5")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == 100000.0
|
||||
|
||||
test "read scientific notation with negative exponent":
|
||||
let v = read("1.5e-3")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == 0.0015
|
||||
|
||||
test "read negative scientific notation":
|
||||
let v = read("-2.5e2")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == -250.0
|
||||
|
||||
test "read negative float scientific without leading zero":
|
||||
let v = read("-.5e-3")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == -0.0005
|
||||
|
||||
test "read number with leading dot":
|
||||
let v = read(".25")
|
||||
check v.kind == ckFloat
|
||||
check v.floatVal == 0.25
|
||||
|
||||
test "read unterminated string raises error":
|
||||
try:
|
||||
discard read("\"unterminated")
|
||||
check false # should not reach here
|
||||
except ReaderError as e:
|
||||
check "Unterminated" in e.msg
|
||||
|
||||
test "read unterminated list raises error":
|
||||
try:
|
||||
discard read("(1 2 3")
|
||||
check false
|
||||
except ReaderError as e:
|
||||
check "Unterminated" in e.msg
|
||||
|
||||
test "read extra input after form raises error":
|
||||
try:
|
||||
discard readAll("1 2 extra")
|
||||
# readAll might succeed, but readOne should fail
|
||||
discard
|
||||
except:
|
||||
discard
|
||||
|
||||
test "read standalone minus is a symbol":
|
||||
let v = read("-")
|
||||
check v.kind == ckSymbol
|
||||
check v.symName == "-"
|
||||
|
||||
test "read standalone plus is a symbol":
|
||||
let v = read("+")
|
||||
check v.kind == ckSymbol
|
||||
check v.symName == "+"
|
||||
|
||||
Reference in New Issue
Block a user