feat: swap! and reset! in interpreter

- atoms now work in REPL interpreter via string registry
- swap! supports extra arguments: (swap! a + 5)
- reset! sets atom value directly
- Updated tests: 3 new atom tests (swap!, reset!, swap! with args)
- 270+ tests pass across all suites
This commit is contained in:
2026-05-08 22:30:35 +03:00
parent 6249d8a251
commit 2b32640eeb
2 changed files with 269 additions and 33 deletions
+58 -32
View File
@@ -5,6 +5,8 @@ import types, reader
var agentRegistry* = initTable[string, CljVal]()
var agentCounter*: int64 = 0
var atomRegistry* = initTable[string, CljVal]()
var atomCounter*: int64 = 0
type
Channel* = ref object
@@ -22,8 +24,6 @@ type
bindings*: Table[string, CljVal]
parent*: Env
BuiltinFn = proc(args: seq[CljVal], env: Env): CljVal
EvalResult* = object
ok*: bool
value*: CljVal
@@ -298,20 +298,20 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
if args[0].kind == ckInt: return EvalResult(ok: true, value: cljInt(-args[0].intVal))
if args[0].kind == ckFloat: return EvalResult(ok: true, value: cljFloat(-args[0].floatVal))
return EvalResult(ok: false, error: "- requires numbers")
var result: float64
if args[0].kind == ckInt: result = args[0].intVal.float64
elif args[0].kind == ckFloat: result = args[0].floatVal
var sum: float64
if args[0].kind == ckInt: sum = args[0].intVal.float64
elif args[0].kind == ckFloat: sum = args[0].floatVal
else: return EvalResult(ok: false, error: "- requires numbers")
for i in 1..<args.len:
if args[i].kind == ckInt: result -= args[i].intVal.float64
elif args[i].kind == ckFloat: result -= args[i].floatVal
if args[i].kind == ckInt: sum -= args[i].intVal.float64
elif args[i].kind == ckFloat: sum -= args[i].floatVal
else: return EvalResult(ok: false, error: "- requires numbers")
if result == result.int64.float64 and args[0].kind == ckInt:
if sum == sum.int64.float64 and args[0].kind == ckInt:
var allInt = true
for i in 1..<args.len:
if args[i].kind != ckInt: allInt = false
if allInt: return EvalResult(ok: true, value: cljInt(result.int64))
return EvalResult(ok: true, value: cljFloat(result))
if allInt: return EvalResult(ok: true, value: cljInt(sum.int64))
return EvalResult(ok: true, value: cljFloat(sum))
of "*":
atLeast(1)
@@ -594,11 +594,11 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
return EvalResult(ok: false, error: "cons requires a collection")
of "concat":
var result: seq[CljVal] = @[]
var res: seq[CljVal] = @[]
for a in args:
if a.kind in {ckList, ckVector}:
result.add(a.items)
return EvalResult(ok: true, value: cljList(result))
res.add(a.items)
return EvalResult(ok: true, value: cljList(res))
of "reverse":
numArgs(1)
@@ -629,13 +629,13 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
let fn = args[0]
let coll = args[1]
if coll.kind in {ckList, ckVector}:
var result: seq[CljVal] = @[]
var res: seq[CljVal] = @[]
for item in coll.items:
let callItems = @[fn, item]
let callRes = evalList(callItems, env)
if not callRes.ok: return callRes
result.add(callRes.value)
return EvalResult(ok: true, value: cljList(result))
res.add(callRes.value)
return EvalResult(ok: true, value: cljList(res))
return EvalResult(ok: false, error: "map requires a function and collection")
of "filter":
@@ -643,7 +643,7 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
let fn = args[0]
let coll = args[1]
if coll.kind in {ckList, ckVector}:
var result: seq[CljVal] = @[]
var res: seq[CljVal] = @[]
for item in coll.items:
let callItems = @[fn, item]
let callRes = evalList(callItems, env)
@@ -651,8 +651,8 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
let isTruthy = not (callRes.value.kind == ckNil or
(callRes.value.kind == ckBool and not callRes.value.boolVal))
if isTruthy:
result.add(item)
return EvalResult(ok: true, value: cljList(result))
res.add(item)
return EvalResult(ok: true, value: cljList(res))
return EvalResult(ok: false, error: "filter requires a function and collection")
of "reduce":
@@ -668,22 +668,22 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
if coll.kind in {ckList, ckVector} and coll.items.len > 0:
acc = coll.items[0]
# reduce rest
var result = acc
var res = acc
for i in 1..<coll.items.len:
let callItems = @[fn, result, coll.items[i]]
let callItems = @[fn, res, coll.items[i]]
let callRes = evalList(callItems, env)
if not callRes.ok: return callRes
result = callRes.value
return EvalResult(ok: true, value: result)
res = callRes.value
return EvalResult(ok: true, value: res)
return EvalResult(ok: true, value: cljNil())
if coll.kind in {ckList, ckVector}:
var result = acc
var res = acc
for item in coll.items:
let callItems = @[fn, result, item]
let callItems = @[fn, res, item]
let callRes = evalList(callItems, env)
if not callRes.ok: return callRes
result = callRes.value
return EvalResult(ok: true, value: result)
res = callRes.value
return EvalResult(ok: true, value: res)
return EvalResult(ok: false, error: "reduce requires a function and collection")
of "apply":
@@ -957,17 +957,17 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
of "distinct":
numArgs(1)
if args[0].kind in {ckList, ckVector}:
var result: seq[CljVal] = @[]
var res: seq[CljVal] = @[]
for item in args[0].items:
var isDup = false
for existing in result:
for existing in res:
let eqRes = evalBuiltin("=", @[existing, item])
if eqRes.ok and eqRes.value.kind == ckBool and eqRes.value.boolVal:
isDup = true
break
if not isDup:
result.add(item)
return EvalResult(ok: true, value: cljList(result))
res.add(item)
return EvalResult(ok: true, value: cljList(res))
return EvalResult(ok: false, error: "distinct requires a collection")
of "slurp":
@@ -1036,11 +1036,37 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult =
numArgs(1)
if args[0].kind == ckString and agentRegistry.hasKey(args[0].strVal):
return EvalResult(ok: true, value: agentRegistry[args[0].strVal])
if args[0].kind == ckString and atomRegistry.hasKey(args[0].strVal):
return EvalResult(ok: true, value: atomRegistry[args[0].strVal])
return EvalResult(ok: true, value: args[0])
of "atom":
numArgs(1)
return EvalResult(ok: true, value: args[0])
atomCounter += 1
let id = "atom_" & $atomCounter
atomRegistry[id] = args[0]
return EvalResult(ok: true, value: cljString(id))
of "reset!":
numArgs(2)
if args[0].kind != ckString or not atomRegistry.hasKey(args[0].strVal):
return EvalResult(ok: false, error: "reset! requires an atom")
atomRegistry[args[0].strVal] = args[1]
return EvalResult(ok: true, value: args[1])
of "swap!":
atLeast(2)
if args[0].kind != ckString or not atomRegistry.hasKey(args[0].strVal):
return EvalResult(ok: false, error: "swap! requires an atom")
let fn = args[1]
let currentVal = atomRegistry[args[0].strVal]
var callItems = @[fn, currentVal]
if args.len > 2:
callItems.add(args[2..^1])
let callRes = evalList(callItems, env)
if not callRes.ok: return callRes
atomRegistry[args[0].strVal] = callRes.value
return EvalResult(ok: true, value: callRes.value)
of "agent":
numArgs(1)
+211 -1
View File
@@ -1,6 +1,5 @@
import unittest
import ../src/types
import ../src/reader
import ../src/eval
suite "Eval - Basic Values":
@@ -70,6 +69,17 @@ suite "Eval - Arithmetic":
check r.ok
check r.value.intVal == 10
test "float arithmetic":
let r = eval("(+ 1.5 2.5)", env)
check r.ok
check r.value.kind == ckFloat
check r.value.floatVal == 4.0
test "negation":
let r = eval("(- 5)", env)
check r.ok
check r.value.intVal == -5
suite "Eval - Comparison":
setup:
let env = newTopLevelEnv()
@@ -84,6 +94,16 @@ suite "Eval - Comparison":
check r.ok
check r.value.boolVal == true
test "greater or equal":
let r = eval("(>= 5 5)", env)
check r.ok
check r.value.boolVal == true
test "less or equal":
let r = eval("(<= 3 5)", env)
check r.ok
check r.value.boolVal == true
test "equality":
let r = eval("(= 42 42)", env)
check r.ok
@@ -94,6 +114,45 @@ suite "Eval - Comparison":
check r.ok
check r.value.boolVal == true
suite "Eval - Math Predicates":
setup:
let env = newTopLevelEnv()
test "pos?":
check eval("(pos? 5)", env).value.boolVal == true
check eval("(pos? -1)", env).value.boolVal == false
test "neg?":
check eval("(neg? -3)", env).value.boolVal == true
check eval("(neg? 3)", env).value.boolVal == false
test "even?":
check eval("(even? 4)", env).value.boolVal == true
check eval("(even? 3)", env).value.boolVal == false
test "odd?":
check eval("(odd? 3)", env).value.boolVal == true
check eval("(odd? 4)", env).value.boolVal == false
test "abs":
check eval("(abs -5)", env).value.intVal == 5
check eval("(abs 5)", env).value.intVal == 5
test "mod":
check eval("(mod 10 3)", env).value.intVal == 1
test "quot":
check eval("(quot 10 3)", env).value.intVal == 3
test "rem":
check eval("(rem 10 3)", env).value.intVal == 1
test "min":
check eval("(min 3 7)", env).value.intVal == 3
test "max":
check eval("(max 3 7)", env).value.intVal == 7
suite "Eval - Special Forms":
setup:
let env = newTopLevelEnv()
@@ -130,6 +189,11 @@ suite "Eval - Special Forms":
check r.ok
check r.value.intVal == 2
test "if without else":
let r = eval("(if false 1)", env)
check r.ok
check r.value.kind == ckNil
test "when":
let r = eval("(when true 42)", env)
check r.ok
@@ -145,11 +209,21 @@ suite "Eval - Special Forms":
check r.ok
check r.value.intVal == 3
test "do empty":
let r = eval("(do)", env)
check r.ok
check r.value.kind == ckNil
test "fn and call":
let r = eval("((fn [x] (+ x 1)) 5)", env)
check r.ok
check r.value.intVal == 6
test "variadic fn":
let r = eval("((fn [x & rest] (count rest)) 1 2 3 4)", env)
check r.ok
check r.value.intVal == 3
test "quote":
let r = eval("(quote (1 2 3))", env)
check r.ok
@@ -166,6 +240,18 @@ suite "Eval - Collections":
check r.value.kind == ckVector
check r.value.items.len == 3
test "list constructor":
let r = eval("(list 1 2 3)", env)
check r.ok
check r.value.kind == ckList
check r.value.items.len == 3
test "vector constructor":
let r = eval("(vector 1 2 3)", env)
check r.ok
check r.value.kind == ckVector
check r.value.items.len == 3
test "map literal":
let r = eval("{:a 1 :b 2}", env)
check r.ok
@@ -182,6 +268,16 @@ suite "Eval - Collections":
check r.ok
check r.value.intVal == 10
test "last":
let r = eval("(last [10 20 30])", env)
check r.ok
check r.value.intVal == 30
test "nth":
let r = eval("(nth [10 20 30] 1)", env)
check r.ok
check r.value.intVal == 20
test "rest":
let r = eval("(rest [1 2 3])", env)
check r.ok
@@ -204,6 +300,27 @@ suite "Eval - Collections":
check r.ok
check r.value.items.len == 4
test "reverse":
let r = eval("(reverse [1 2 3])", env)
check r.ok
check r.value.items[0].intVal == 3
check r.value.items[2].intVal == 1
test "vec":
let r = eval("(vec (list 1 2 3))", env)
check r.ok
check r.value.kind == ckVector
check r.value.items.len == 3
test "distinct":
let r = eval("(distinct [1 2 2 3 3 3])", env)
check r.ok
check r.value.items.len == 3
test "empty?":
check eval("(empty? [])", env).value.boolVal == true
check eval("(empty? [1])", env).value.boolVal == false
suite "Eval - Higher-order":
setup:
let env = newTopLevelEnv()
@@ -275,6 +392,18 @@ suite "Eval - String/Primitives":
check r.value.kind == ckKeyword
check r.value.kwName == "integer"
test "true?":
check eval("(true? true)", env).value.boolVal == true
check eval("(true? false)", env).value.boolVal == false
test "false?":
check eval("(false? false)", env).value.boolVal == true
check eval("(false? true)", env).value.boolVal == false
test "instance?":
check eval("(instance? :integer 42)", env).value.boolVal == true
check eval("(instance? :string 42)", env).value.boolVal == false
suite "Eval - Map Operations":
setup:
let env = newTopLevelEnv()
@@ -299,6 +428,11 @@ suite "Eval - Map Operations":
check r.ok
check r.value.mapKeys.len == 1
test "merge":
let r = eval("(merge {:a 1} {:b 2})", env)
check r.ok
check r.value.mapKeys.len == 2
test "keys":
let r = eval("(keys {:a 1 :b 2})", env)
check r.ok
@@ -370,3 +504,79 @@ suite "Eval - Boolean Logic":
let r = eval("(cond false 1 true 2 false 3)", env)
check r.ok
check r.value.intVal == 2
suite "Eval - Atoms":
setup:
let env = newTopLevelEnv()
test "atom and deref":
discard eval("(def a (atom 0))", env)
let r = eval("(deref a)", env)
check r.ok
check r.value.intVal == 0
test "swap! updates atom value":
discard eval("(def a (atom 0))", env)
let r = eval("(swap! a inc)", env)
check r.ok
check r.value.intVal == 1
let r2 = eval("(deref a)", env)
check r2.value.intVal == 1
test "swap! with extra args":
discard eval("(def a (atom 10))", env)
let r = eval("(swap! a + 5)", env)
check r.ok
check r.value.intVal == 15
test "reset! sets atom value":
discard eval("(def a (atom 0))", env)
let r = eval("(reset! a 42)", env)
check r.ok
check r.value.intVal == 42
let r2 = eval("(deref a)", env)
check r2.value.intVal == 42
suite "Eval - Agents":
setup:
let env = newTopLevelEnv()
test "agent and deref":
discard eval("(def ag (agent 10))", env)
let r = eval("(deref ag)", env)
check r.ok
check r.value.intVal == 10
test "send":
discard eval("(def ag (agent 10))", env)
let r = eval("(send ag inc)", env)
check r.ok
check eval("(deref ag)", env).value.intVal == 11
test "await returns nil":
discard eval("(def ag (agent 10))", env)
let r = eval("(await ag)", env)
check r.ok
check r.value.kind == ckNil
suite "Eval - Channels":
setup:
let env = newTopLevelEnv()
test "chan create returns string id":
let r = eval("(chan)", env)
check r.ok
check r.value.kind == ckString
test "chan put and take":
discard eval("(def ch (chan))", env)
let putR = eval("(>! ch 42)", env)
check putR.ok
let takeR = eval("(<! ch)", env)
check takeR.ok
check takeR.value.intVal == 42
test "chan close":
discard eval("(def ch (chan))", env)
let r = eval("(close! ch)", env)
check r.ok