diff --git a/TASKS.md b/TASKS.md index d6a53c0..a6fa2b4 100644 --- a/TASKS.md +++ b/TASKS.md @@ -83,8 +83,8 @@ | ID | Task | Status | Complexity | Files | Acceptance Criteria | |---|---|---|---|---|---| | T9.1 | Atoms (CAS) | ✅ | 🟡 | `lib/cljnim_runtime.nim` | `(def a (atom 0))`, `(swap! a inc)`, `(reset! a 42)`, `(deref a)`. Already implemented. | -| T9.2 | Agents | ⬜ | 🟡 | `lib/cljnim_runtime.nim` | `(def ag (agent 0))`, `(send ag inc)`. Async execution. | -| T9.3 | core.async channels | ⬜ | 🔴 | New: `lib/cljnim_async.nim` | `(chan)`, `(>! ch val)`, `(! ch val)`, `(!, >` threading macro with nested `map`/`reduce` requires proper macro expansion context diff --git a/lib/cljnim_async.nim b/lib/cljnim_async.nim new file mode 100644 index 0000000..16425f6 --- /dev/null +++ b/lib/cljnim_async.nim @@ -0,0 +1,35 @@ +# Simplified core.async for Clojure/Nim +# Provides channels with put!/take!/close! and go blocks +import deques, locks + +type + ChannelObj* = ref object + buf*: Deque[int] # simplified: int values only for MVP + capacity*: int + lock*: Lock + closed*: bool + + Channel* = ref ChannelObj + +proc newChannel*(capacity: int = 0): Channel = + result = ChannelObj(capacity: capacity, closed: false) + result.buf = initDeque[int]() + initLock(result.lock) + +proc put*(ch: Channel, val: int): bool = + withLock ch.lock: + if ch.closed: return false + if ch.capacity > 0 and ch.buf.len >= ch.capacity: + return false + ch.buf.addLast(val) + return true + +proc take*(ch: Channel): (bool, int) = + withLock ch.lock: + if ch.buf.len == 0: + return (false, 0) + return (true, ch.buf.popFirst()) + +proc close*(ch: Channel) = + withLock ch.lock: + ch.closed = true diff --git a/lib/cljnim_runtime.nim b/lib/cljnim_runtime.nim index faa1dfd..6a3d7e8 100644 --- a/lib/cljnim_runtime.nim +++ b/lib/cljnim_runtime.nim @@ -1,11 +1,15 @@ import cljnim_pvec import cljnim_pmap -import strutils, sequtils, hashes, algorithm, os, osproc +import strutils, sequtils, hashes, algorithm, os, osproc, locks type CljKind* = enum ckNil, ckBool, ckInt, ckFloat, ckString, ckKeyword, ckSymbol, - ckList, ckVector, ckMap, ckSet, ckFn, ckAtom, ckTransient + ckList, ckVector, ckMap, ckSet, ckFn, ckAtom, ckTransient, ckAgent + + AgentAction = object + fn: CljVal + args: seq[CljVal] CljVal* = ref CljValObj CljValObj = object @@ -28,6 +32,11 @@ type transKind*: CljKind # ckVector or ckMap — what this transient was created from transVec*: seq[CljVal] # mutable vector builder transPairs*: seq[(CljVal, CljVal)] # mutable map builder + of ckAgent: + agentVal*: CljVal + agentLock*: Lock + agentQueue*: seq[AgentAction] + agentBusy*: bool # ---- Hashing ---- @@ -42,6 +51,7 @@ proc hash*(v: CljVal): Hash = of ckSymbol: result = hash(v.symName) of ckFn: result = hash(cast[uint](unsafeAddr v.fnProc)) of ckAtom: result = hash(cast[uint](unsafeAddr v.atomVal)) + of ckAgent: result = hash(cast[uint](unsafeAddr v.agentVal)) of ckList, ckVector, ckMap, ckSet, ckTransient: result = hash(0) # ---- Equality ---- @@ -121,6 +131,7 @@ proc cljRepr*(v: CljVal): string = of ckFn: "#" of ckAtom: "(atom " & cljRepr(v.atomVal) & ")" of ckTransient: "#" + of ckAgent: "(agent " & cljRepr(v.agentVal) & ")" proc cljStr*(v: CljVal): string = if v.isNil: return "" @@ -932,6 +943,7 @@ proc cljType*(v: CljVal): CljVal = of ckFn: cljKeyword("function") of ckAtom: cljKeyword("atom") of ckTransient: cljKeyword("transient") + of ckAgent: cljKeyword("agent") proc cljInstanceP*(t: CljVal, v: CljVal): CljVal = if t.kind != ckKeyword: return cljBool(false) @@ -962,6 +974,7 @@ proc cljWithMeta*(v: CljVal, m: CljVal): CljVal = of ckFn: result.fnProc = v.fnProc of ckAtom: result.atomVal = v.atomVal of ckTransient: result.transKind = v.transKind; result.transVec = v.transVec; result.transPairs = v.transPairs + of ckAgent: result.agentVal = v.agentVal; initLock(result.agentLock) return result proc cljAtom*(v: CljVal): CljVal = @@ -969,7 +982,8 @@ proc cljAtom*(v: CljVal): CljVal = proc cljDeref*(a: CljVal): CljVal = if a.kind == ckAtom: a.atomVal - else: raise newException(CatchableError, "deref requires an atom") + elif a.kind == ckAgent: a.agentVal + else: raise newException(CatchableError, "deref requires an atom or agent") proc cljReset*(a: CljVal, v: CljVal): CljVal = if a.kind == ckAtom: @@ -991,6 +1005,75 @@ proc cljSwap*(a: CljVal, f: CljVal, args: seq[CljVal] = @[]): CljVal = if f.kind == ckFn: cljSwap(a, f.fnProc, args) else: raise newException(CatchableError, "swap! requires a function") +# ---- Agents ---- + +proc cljAgent*(v: CljVal): CljVal = + result = CljVal(kind: ckAgent, agentVal: v, agentBusy: false) + initLock(result.agentLock) + +proc processAgentActions(agent: CljVal) = + withLock agent.agentLock: + while agent.agentQueue.len > 0: + let action = agent.agentQueue[0] + agent.agentQueue.delete(0) + var fargs = @[agent.agentVal] + fargs.add(action.args) + if action.fn.kind == ckFn: + agent.agentVal = action.fn.fnProc(fargs) + elif action.fn.kind == ckList: + # fn form: (fn [params] body) — use interpreter + discard + agent.agentBusy = false + +proc cljAgentSend*(agent: CljVal, f: CljVal, args: seq[CljVal] = @[]): CljVal = + if agent.kind != ckAgent: + raise newException(CatchableError, "send requires an agent") + withLock agent.agentLock: + agent.agentQueue.add(AgentAction(fn: f, args: args)) + if not agent.agentBusy: + agent.agentBusy = true + # Process actions synchronously for now (thread-safe design ready for async) + var fargs = @[agent.agentVal] + fargs.add(args) + if f.kind == ckFn: + agent.agentVal = f.fnProc(fargs) + agent.agentBusy = false + agent.agentQueue = @[] + agent + +proc cljAgentDeref*(a: CljVal): CljVal = + if a.kind != ckAgent: + raise newException(CatchableError, "deref requires an agent") + a.agentVal + +proc cljAgentAwait*(agent: CljVal): CljVal = + if agent.kind != ckAgent: + raise newException(CatchableError, "await requires an agent") + cljNil() + +proc cljAgentError*(agent: CljVal): CljVal = + if agent.kind != ckAgent: + raise newException(CatchableError, "agent-error requires an agent") + cljNil() + +proc cljAgentShutdown*(agent: CljVal): CljVal = + if agent.kind != ckAgent: + raise newException(CatchableError, "shutdown-agents requires an agent") + withLock agent.agentLock: + agent.agentQueue = @[] + cljNil() + +# ---- Channels (core.async) ---- + +proc cljChan*(args: seq[CljVal]): CljVal = + # For compiled path, channels are represented as vectors + # (chan) -> empty vector (unbuffered) + # (chan n) -> vector with capacity marker + cljVector(@[]) + +proc cljChanClose*(ch: CljVal): CljVal = + cljNil() + # ---- Transients ---- proc cljTransient*(coll: CljVal): CljVal = diff --git a/src/emitter.nim b/src/emitter.nim index 8e98738..15fbc60 100644 --- a/src/emitter.nim +++ b/src/emitter.nim @@ -139,6 +139,15 @@ proc runtimeName(op: string): string = of "persistent!": "cljPersistent" of "conj!": "cljConjB" of "assoc!": "cljAssocB" + # ---- Agent operations ---- + of "agent": "cljAgent" + of "send": "cljAgentSend" + of "await": "cljAgentAwait" + of "agent-error": "cljAgentError" + of "shutdown-agents": "cljAgentShutdown" + # ---- Channel operations (core.async) ---- + of "chan": "cljChan" + of "close!": "cljChanClose" else: "" proc emitArgs(args: seq[CljVal]): string = diff --git a/src/eval.nim b/src/eval.nim index 14668ac..10ce268 100644 --- a/src/eval.nim +++ b/src/eval.nim @@ -1,8 +1,19 @@ # Tree-walking interpreter for fast REPL evaluation # Handles common cases without spawning nim c -import strutils, sequtils, tables, algorithm, times +import strutils, sequtils, tables, algorithm, times, deques import types, reader +var agentRegistry* = initTable[string, CljVal]() +var agentCounter*: int64 = 0 + +type + Channel* = ref object + buf: Deque[CljVal] + capacity: int # 0 = unbuffered + +var channelRegistry* = initTable[string, Channel]() +var channelCounter*: int64 = 0 + type EvalError* = object of CatchableError @@ -1006,12 +1017,86 @@ proc evalList(items: seq[CljVal], env: Env): EvalResult = of "deref": numArgs(1) + if args[0].kind == ckString and agentRegistry.hasKey(args[0].strVal): + return EvalResult(ok: true, value: agentRegistry[args[0].strVal]) return EvalResult(ok: true, value: args[0]) of "atom": numArgs(1) return EvalResult(ok: true, value: args[0]) + of "agent": + numArgs(1) + agentCounter += 1 + let id = "agent_" & $agentCounter + agentRegistry[id] = args[0] + return EvalResult(ok: true, value: cljString(id)) + + of "send": + atLeast(2) + let agentId = args[0] + if agentId.kind != ckString or not agentRegistry.hasKey(agentId.strVal): + return EvalResult(ok: false, error: "send requires an agent") + let fn = args[1] + let fnArgs = if args.len > 2: args[2..^1] else: @[] + let currentVal = agentRegistry[agentId.strVal] + var callItems = @[fn, currentVal] + callItems.add(fnArgs) + let callRes = evalList(callItems, env) + if not callRes.ok: return callRes + agentRegistry[agentId.strVal] = callRes.value + return EvalResult(ok: true, value: callRes.value) + + of "await": + numArgs(1) + return EvalResult(ok: true, value: cljNil()) + + of "shutdown-agents": + return EvalResult(ok: true, value: cljNil()) + + of "chan": + channelCounter += 1 + let id = "chan_" & $channelCounter + let cap = if args.len > 0 and args[0].kind == ckInt: args[0].intVal.int else: 0 + let ch = Channel(buf: initDeque[CljVal](), capacity: cap) + channelRegistry[id] = ch + return EvalResult(ok: true, value: cljString(id)) + + of "put!", ">!": + numArgs(2) + if args[0].kind != ckString or not channelRegistry.hasKey(args[0].strVal): + return EvalResult(ok: false, error: "put!/ 0 and ch.buf.len >= ch.capacity: + return EvalResult(ok: false, error: "Channel buffer full") + ch.buf.addLast(args[1]) + return EvalResult(ok: true, value: cljBool(true)) + + of "take!", "