ba0b300917
- 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
355 lines
11 KiB
Nim
355 lines
11 KiB
Nim
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
|