nandi/clonimpublic Fork
⑂ 3dc7d0f
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/clonim.git
git clone ssh://git@rickub.com/nandi/clonim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat: add cooperative Clojure agents

nandithebull committed 2026-09-22T11:19:49-07:00 Browse files
3dc7d0f parent: 7fd797f
modified README.md +5 -0
@@ -154,6 +154,11 @@ Data: nil, bool, int, float, string, keyword, symbol, list, vector, map, set —
154154 persistent, with structural equality, hashing, and Clojure-shaped printing. Atoms, closures, `comp`,
155155 `partial`, `juxt`, the usual seq library, `clojure.string/*`.
156156
157+Agents: `agent`, `send`, `await`, `agent-error`, and `restart-agent`. Agents
158+serialize queued state transitions; this initial implementation is cooperative,
159+so queued actions run when an agent is dereferenced, awaited, or inspected for
160+an error rather than on a background thread.
161+
157162 Lazy seqs: `iterate` `repeat` `repeatedly` `cycle` `doall` `dorun`, and the seq
158163 library above returns them where Clojure does.
159164
@@ -154,6 +154,11 @@ Data: nil, bool, int, float, string, keyword, symbol, list, vector, map, set —
154 persistent, with structural equality, hashing, and Clojure-shaped printing. Atoms, closures, `comp`,154 persistent, with structural equality, hashing, and Clojure-shaped printing. Atoms, closures, `comp`,
155 `partial`, `juxt`, the usual seq library, `clojure.string/*`.155 `partial`, `juxt`, the usual seq library, `clojure.string/*`.
156 156
157+Agents: `agent`, `send`, `await`, `agent-error`, and `restart-agent`. Agents
158+serialize queued state transitions; this initial implementation is cooperative,
159+so queued actions run when an agent is dereferenced, awaited, or inspected for
160+an error rather than on a background thread.
161+
157 Lazy seqs: `iterate` `repeat` `repeatedly` `cycle` `doall` `dorun`, and the seq162 Lazy seqs: `iterate` `repeat` `repeatedly` `cycle` `doall` `dorun`, and the seq
158 library above returns them where Clojure does.163 library above returns them where Clojure does.
159 164
added clonim.nimble +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+version = "0.1.14"
2+author = "clonim contributors"
3+description = "A Clojure compiler hosted on Nim"
4+license = "MIT"
5+srcDir = "src"
6+bin = @["clonim"]
7+
8+requires "nim >= 2.2.12"
new file mode 100644
@@ -0,0 +1,8 @@
1+version = "0.1.14"
2+author = "clonim contributors"
3+description = "A Clojure compiler hosted on Nim"
4+license = "MIT"
5+srcDir = "src"
6+bin = @["clonim"]
7+
8+requires "nim >= 2.2.12"
added examples/agents.clj +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+(def counter (agent 0))
2+(send counter + 2)
3+(send counter * 10)
4+(println @counter)
5+
6+(def awaited (agent 3))
7+(send awaited + 4)
8+(await awaited)
9+(println @awaited)
10+
11+(def broken (agent 1))
12+(send broken (fn [_] (throw "boom")))
13+(println (agent-error broken))
14+(restart-agent broken 7)
15+(send broken inc)
16+(println @broken)
new file mode 100644
@@ -0,0 +1,16 @@
1+(def counter (agent 0))
2+(send counter + 2)
3+(send counter * 10)
4+(println @counter)
5+
6+(def awaited (agent 3))
7+(send awaited + 4)
8+(await awaited)
9+(println @awaited)
10+
11+(def broken (agent 1))
12+(send broken (fn [_] (throw "boom")))
13+(println (agent-error broken))
14+(restart-agent broken 7)
15+(send broken inc)
16+(println @broken)
modified src/compiler.nim +4 -1
@@ -194,6 +194,7 @@ proc quoteLit(v: Value): string =
194194 "mkMap(@[" & parts.join(", ") & "])"
195195 of kCons, kChunk, kLazy: err("Can't quote a lazy seq")
196196 of kFn: err("Can't quote a function")
197+ of kAgent: err("Can't quote an agent")
197198
198199 proc emptySeqFix(s: string, elemType: string): string =
199200 ## `@[]` has no inferable element type in Nim; annotate it.
@@ -949,7 +950,7 @@ proc tryExpr(f: Value, env: Env, c: Ctx): string =
949950 if hv.len == 0: return ""
950951 "call(" & hv & ", " &
951952 (if ids.len == 0: "emptyArgs" else: "[" & ids.join(", ") & "]") & ")"
952- of kFn, kCons, kChunk, kLazy:
953+ of kFn, kAgent, kCons, kChunk, kLazy:
953954 ""
954955
955956 proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
@@ -985,6 +986,8 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
985986 (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")")
986987 of kFn:
987988 err("Can't emit a function literal")
989+ of kAgent:
990+ err("Can't emit an agent literal")
988991 of kCons, kChunk, kLazy:
989992 err("Can't emit a lazy seq literal")
990993 of kList:
@@ -194,6 +194,7 @@ proc quoteLit(v: Value): string =
194 "mkMap(@[" & parts.join(", ") & "])"194 "mkMap(@[" & parts.join(", ") & "])"
195 of kCons, kChunk, kLazy: err("Can't quote a lazy seq")195 of kCons, kChunk, kLazy: err("Can't quote a lazy seq")
196 of kFn: err("Can't quote a function")196 of kFn: err("Can't quote a function")
197+ of kAgent: err("Can't quote an agent")
197 198
198 proc emptySeqFix(s: string, elemType: string): string =199 proc emptySeqFix(s: string, elemType: string): string =
199 ## `@[]` has no inferable element type in Nim; annotate it.200 ## `@[]` has no inferable element type in Nim; annotate it.
@@ -949,7 +950,7 @@ proc tryExpr(f: Value, env: Env, c: Ctx): string =
949 if hv.len == 0: return ""950 if hv.len == 0: return ""
950 "call(" & hv & ", " &951 "call(" & hv & ", " &
951 (if ids.len == 0: "emptyArgs" else: "[" & ids.join(", ") & "]") & ")"952 (if ids.len == 0: "emptyArgs" else: "[" & ids.join(", ") & "]") & ")"
952- of kFn, kCons, kChunk, kLazy:953+ of kFn, kAgent, kCons, kChunk, kLazy:
953 ""954 ""
954 955
955 proc genInto(f: Value, dst: string, env: Env, c: Ctx) =956 proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
@@ -985,6 +986,8 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
985 (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")")986 (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")")
986 of kFn:987 of kFn:
987 err("Can't emit a function literal")988 err("Can't emit a function literal")
989+ of kAgent:
990+ err("Can't emit an agent literal")
988 of kCons, kChunk, kLazy:991 of kCons, kChunk, kLazy:
989 err("Can't emit a lazy seq literal")992 err("Can't emit a lazy seq literal")
990 of kList:993 of kList:
modified src/core.nim +10 -1
@@ -1090,7 +1090,16 @@ proc registerCore*() =
10901090 if args.len == 0: return cell
10911091 cell = args[1]
10921092 cell)
1093- def "deref", proc (a: openArray[Value]): Value = call(a[0], [])
1093+ # ---- agents (cooperative, serialized state transitions)
1094+ def "agent", proc (a: openArray[Value]): Value = mkAgent(a[0])
1095+ def "send", proc (a: openArray[Value]): Value =
1096+ if a.len < 2: err("send expects an agent and an action")
1097+ agentSend(a[0], a[1], a[2 .. ^1])
1098+ def "await", proc (a: openArray[Value]): Value = agentAwait(a)
1099+ def "agent-error", proc (a: openArray[Value]): Value = agentError(a[0])
1100+ def "restart-agent", proc (a: openArray[Value]): Value = restartAgent(a[0], a[1])
1101+ def "deref", proc (a: openArray[Value]): Value =
1102+ if a[0].kind == kAgent: agentDeref(a[0]) else: call(a[0], [])
10941103 def "reset!", proc (a: openArray[Value]): Value = call(a[0], [mkKeyword("set"), a[1]])
10951104 def "swap!", proc (a: openArray[Value]): Value =
10961105 let cur = call(a[0], [])
@@ -1090,7 +1090,16 @@ proc registerCore*() =
1090 if args.len == 0: return cell1090 if args.len == 0: return cell
1091 cell = args[1]1091 cell = args[1]
1092 cell)1092 cell)
1093- def "deref", proc (a: openArray[Value]): Value = call(a[0], [])1093+ # ---- agents (cooperative, serialized state transitions)
1094+ def "agent", proc (a: openArray[Value]): Value = mkAgent(a[0])
1095+ def "send", proc (a: openArray[Value]): Value =
1096+ if a.len < 2: err("send expects an agent and an action")
1097+ agentSend(a[0], a[1], a[2 .. ^1])
1098+ def "await", proc (a: openArray[Value]): Value = agentAwait(a)
1099+ def "agent-error", proc (a: openArray[Value]): Value = agentError(a[0])
1100+ def "restart-agent", proc (a: openArray[Value]): Value = restartAgent(a[0], a[1])
1101+ def "deref", proc (a: openArray[Value]): Value =
1102+ if a[0].kind == kAgent: agentDeref(a[0]) else: call(a[0], [])
1094 def "reset!", proc (a: openArray[Value]): Value = call(a[0], [mkKeyword("set"), a[1]])1103 def "reset!", proc (a: openArray[Value]): Value = call(a[0], [mkKeyword("set"), a[1]])
1095 def "swap!", proc (a: openArray[Value]): Value =1104 def "swap!", proc (a: openArray[Value]): Value =
1096 let cur = call(a[0], [])1105 let cur = call(a[0], [])
modified src/runtime.nim +56 -2
@@ -22,6 +22,7 @@ proc equals*(a, b: Value): bool
2222 proc hashValue*(v: Value): uint32
2323 proc prStr*(v: Value): string
2424 proc toSeq*(v: Value): seq[Value]
25+proc call*(f: Value, args: openArray[Value]): Value
2526
2627 # ------------------------------------------------------- persistent vector
2728 let emptyVNode = VNode(leaf: false, kids: @[])
@@ -297,6 +298,58 @@ proc mkSet*(xs: openArray[Value]): Value = mkSet(@xs)
297298 proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): Value =
298299 Value(kind: kFn, obj: Obj(kind: kFn, fn: f, name: name))
299300
301+proc mkAgent*(state: Value): Value =
302+ Value(kind: kAgent, obj: Obj(kind: kAgent,
303+ agent: Agent(state: state, queue: @[], failure: NilV, draining: false)))
304+
305+proc agentOf(v: Value): Agent =
306+ if v.kind != kAgent: err("Expected an agent, got " & prStr(v))
307+ v.obj.agent
308+
309+proc drainAgent(a: Agent) =
310+ ## A cooperative mailbox: send appends work, while deref and await drain it.
311+ ## Actions remain serialized and an exception stops the mailbox.
312+ if a.draining or not a.failure.isNil: return
313+ a.draining = true
314+ try:
315+ while a.queue.len > 0 and a.failure.isNil:
316+ let action = a.queue[0]
317+ a.queue.delete(0)
318+ try:
319+ a.state = call(action.fn, @[a.state] & action.args)
320+ except CljError as e:
321+ a.failure = mkStr(e.msg)
322+ a.queue.setLen(0)
323+ finally:
324+ a.draining = false
325+
326+proc agentSend*(v, f: Value, args: openArray[Value]): Value =
327+ let a = agentOf(v)
328+ if not a.failure.isNil: err("Agent is failed: " & a.failure.s)
329+ a.queue.add AgentAction(fn: f, args: @args)
330+ v
331+
332+proc agentDeref*(v: Value): Value =
333+ let a = agentOf(v)
334+ drainAgent(a)
335+ a.state
336+
337+proc agentAwait*(vs: openArray[Value]): Value =
338+ for v in vs: drainAgent(agentOf(v))
339+ NilV
340+
341+proc agentError*(v: Value): Value =
342+ let a = agentOf(v)
343+ drainAgent(a)
344+ a.failure
345+
346+proc restartAgent*(v, state: Value): Value =
347+ let a = agentOf(v)
348+ a.state = state
349+ a.failure = NilV
350+ a.queue.setLen(0)
351+ v
352+
300353 # --------------------------------------------------------------- lazy seqs
301354 ## A lazy seq is a thunk that, when forced, yields either nil/`kNil` (the end)
302355 ## or a cons cell whose tail is usually another lazy seq. Forcing is memoized
@@ -474,7 +527,7 @@ proc hashValue*(v: Value): uint32 =
474527 for e in mapEntries(v.m):
475528 h = h xor mixHash(hashValue(e.key), hashValue(e.val))
476529 h
477- of kFn: uint32(hash(cast[int](cast[pointer](v.obj))))
530+ of kFn, kAgent: uint32(hash(cast[int](cast[pointer](v.obj))))
478531
479532 # --------------------------------------------------------------- accessors
480533 proc items*(v: Value): seq[Value] =
@@ -558,7 +611,7 @@ proc equals*(a, b: Value): bool =
558611 for e in mapEntries(a.m):
559612 if not equals(e.val, mapGet(b.m, e.key, missing)): return false
560613 true
561- of kFn: a == b
614+ of kFn, kAgent: a == b
562615 of kList, kVector, kCons, kChunk, kLazy: false # handled above
563616 # ---------------------------------------------------------------- printing
564617 proc escapeStr(s: string): string =
@@ -615,6 +668,7 @@ proc toStr*(v: Value, readable: bool): string =
615668 for (k, val) in v.pairs: parts.add toStr(k, readable) & " " & toStr(val, readable)
616669 "{" & parts.join(", ") & "}"
617670 of kFn: "#<fn " & v.name & ">"
671+ of kAgent: "#<agent>"
618672
619673 proc prStr*(v: Value): string = toStr(v, true)
620674 proc str*(v: Value): string = toStr(v, false)
@@ -22,6 +22,7 @@ proc equals*(a, b: Value): bool
22 proc hashValue*(v: Value): uint3222 proc hashValue*(v: Value): uint32
23 proc prStr*(v: Value): string23 proc prStr*(v: Value): string
24 proc toSeq*(v: Value): seq[Value]24 proc toSeq*(v: Value): seq[Value]
25+proc call*(f: Value, args: openArray[Value]): Value
25 26
26 # ------------------------------------------------------- persistent vector27 # ------------------------------------------------------- persistent vector
27 let emptyVNode = VNode(leaf: false, kids: @[])28 let emptyVNode = VNode(leaf: false, kids: @[])
@@ -297,6 +298,58 @@ proc mkSet*(xs: openArray[Value]): Value = mkSet(@xs)
297 proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): Value =298 proc mkFn*(name: string, f: proc (args: openArray[Value]): Value {.closure.}): Value =
298 Value(kind: kFn, obj: Obj(kind: kFn, fn: f, name: name))299 Value(kind: kFn, obj: Obj(kind: kFn, fn: f, name: name))
299 300
301+proc mkAgent*(state: Value): Value =
302+ Value(kind: kAgent, obj: Obj(kind: kAgent,
303+ agent: Agent(state: state, queue: @[], failure: NilV, draining: false)))
304+
305+proc agentOf(v: Value): Agent =
306+ if v.kind != kAgent: err("Expected an agent, got " & prStr(v))
307+ v.obj.agent
308+
309+proc drainAgent(a: Agent) =
310+ ## A cooperative mailbox: send appends work, while deref and await drain it.
311+ ## Actions remain serialized and an exception stops the mailbox.
312+ if a.draining or not a.failure.isNil: return
313+ a.draining = true
314+ try:
315+ while a.queue.len > 0 and a.failure.isNil:
316+ let action = a.queue[0]
317+ a.queue.delete(0)
318+ try:
319+ a.state = call(action.fn, @[a.state] & action.args)
320+ except CljError as e:
321+ a.failure = mkStr(e.msg)
322+ a.queue.setLen(0)
323+ finally:
324+ a.draining = false
325+
326+proc agentSend*(v, f: Value, args: openArray[Value]): Value =
327+ let a = agentOf(v)
328+ if not a.failure.isNil: err("Agent is failed: " & a.failure.s)
329+ a.queue.add AgentAction(fn: f, args: @args)
330+ v
331+
332+proc agentDeref*(v: Value): Value =
333+ let a = agentOf(v)
334+ drainAgent(a)
335+ a.state
336+
337+proc agentAwait*(vs: openArray[Value]): Value =
338+ for v in vs: drainAgent(agentOf(v))
339+ NilV
340+
341+proc agentError*(v: Value): Value =
342+ let a = agentOf(v)
343+ drainAgent(a)
344+ a.failure
345+
346+proc restartAgent*(v, state: Value): Value =
347+ let a = agentOf(v)
348+ a.state = state
349+ a.failure = NilV
350+ a.queue.setLen(0)
351+ v
352+
300 # --------------------------------------------------------------- lazy seqs353 # --------------------------------------------------------------- lazy seqs
301 ## A lazy seq is a thunk that, when forced, yields either nil/`kNil` (the end)354 ## A lazy seq is a thunk that, when forced, yields either nil/`kNil` (the end)
302 ## or a cons cell whose tail is usually another lazy seq. Forcing is memoized355 ## or a cons cell whose tail is usually another lazy seq. Forcing is memoized
@@ -474,7 +527,7 @@ proc hashValue*(v: Value): uint32 =
474 for e in mapEntries(v.m):527 for e in mapEntries(v.m):
475 h = h xor mixHash(hashValue(e.key), hashValue(e.val))528 h = h xor mixHash(hashValue(e.key), hashValue(e.val))
476 h529 h
477- of kFn: uint32(hash(cast[int](cast[pointer](v.obj))))530+ of kFn, kAgent: uint32(hash(cast[int](cast[pointer](v.obj))))
478 531
479 # --------------------------------------------------------------- accessors532 # --------------------------------------------------------------- accessors
480 proc items*(v: Value): seq[Value] =533 proc items*(v: Value): seq[Value] =
@@ -558,7 +611,7 @@ proc equals*(a, b: Value): bool =
558 for e in mapEntries(a.m):611 for e in mapEntries(a.m):
559 if not equals(e.val, mapGet(b.m, e.key, missing)): return false612 if not equals(e.val, mapGet(b.m, e.key, missing)): return false
560 true613 true
561- of kFn: a == b614+ of kFn, kAgent: a == b
562 of kList, kVector, kCons, kChunk, kLazy: false # handled above615 of kList, kVector, kCons, kChunk, kLazy: false # handled above
563 # ---------------------------------------------------------------- printing616 # ---------------------------------------------------------------- printing
564 proc escapeStr(s: string): string =617 proc escapeStr(s: string): string =
@@ -615,6 +668,7 @@ proc toStr*(v: Value, readable: bool): string =
615 for (k, val) in v.pairs: parts.add toStr(k, readable) & " " & toStr(val, readable)668 for (k, val) in v.pairs: parts.add toStr(k, readable) & " " & toStr(val, readable)
616 "{" & parts.join(", ") & "}"669 "{" & parts.join(", ") & "}"
617 of kFn: "#<fn " & v.name & ">"670 of kFn: "#<fn " & v.name & ">"
671+ of kAgent: "#<agent>"
618 672
619 proc prStr*(v: Value): string = toStr(v, true)673 proc prStr*(v: Value): string = toStr(v, true)
620 proc str*(v: Value): string = toStr(v, false)674 proc str*(v: Value): string = toStr(v, false)
modified src/runtime_types.nim +15 -1
@@ -5,7 +5,18 @@
55 type
66 Kind* = enum
77 kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword, kSymbol,
8- kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn
8+ kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn, kAgent
9+
10+ AgentAction* = object
11+ fn*: Value
12+ args*: seq[Value]
13+
14+ Agent* = ref object
15+ ## Actions are queued in send order and run one at a time when observed.
16+ state*: Value
17+ queue*: seq[AgentAction]
18+ failure*: Value
19+ draining*: bool
920
1021 VNode* = ref object
1122 case leaf*: bool
@@ -72,6 +83,8 @@ type
7283 of kFn:
7384 fn*: proc (args: openArray[Value]): Value {.closure.}
7485 name*: string
86+ of kAgent:
87+ agent*: Agent
7588
7689 Value* = object
7790 kind*: Kind
@@ -121,6 +134,7 @@ proc `=destroy`*(x: var ValueObj) =
121134 of kFn:
122135 `=destroy`(x.fn)
123136 `=destroy`(x.name)
137+ of kAgent: `=destroy`(x.agent)
124138 of kCons:
125139 `=destroy`(x.head)
126140 if not x.tl.isNil: pendingFree.add x.tl
@@ -5,7 +5,18 @@
5 type5 type
6 Kind* = enum6 Kind* = enum
7 kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword, kSymbol,7 kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword, kSymbol,
8- kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn8+ kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn, kAgent
9+
10+ AgentAction* = object
11+ fn*: Value
12+ args*: seq[Value]
13+
14+ Agent* = ref object
15+ ## Actions are queued in send order and run one at a time when observed.
16+ state*: Value
17+ queue*: seq[AgentAction]
18+ failure*: Value
19+ draining*: bool
9 20
10 VNode* = ref object21 VNode* = ref object
11 case leaf*: bool22 case leaf*: bool
@@ -72,6 +83,8 @@ type
72 of kFn:83 of kFn:
73 fn*: proc (args: openArray[Value]): Value {.closure.}84 fn*: proc (args: openArray[Value]): Value {.closure.}
74 name*: string85 name*: string
86+ of kAgent:
87+ agent*: Agent
75 88
76 Value* = object89 Value* = object
77 kind*: Kind90 kind*: Kind
@@ -121,6 +134,7 @@ proc `=destroy`*(x: var ValueObj) =
121 of kFn:134 of kFn:
122 `=destroy`(x.fn)135 `=destroy`(x.fn)
123 `=destroy`(x.name)136 `=destroy`(x.name)
137+ of kAgent: `=destroy`(x.agent)
124 of kCons:138 of kCons:
125 `=destroy`(x.head)139 `=destroy`(x.head)
126 if not x.tl.isNil: pendingFree.add x.tl140 if not x.tl.isNil: pendingFree.add x.tl
added tests/agents.expected +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+20
2+7
3+boom
4+8
new file mode 100644
@@ -0,0 +1,4 @@
1+20
2+7
3+boom
4+8