Skip to content

Commit dfe7e7b

Browse files
leifericfclaude
andcommitted
STM: agents (MVP) -- agent / send / await / agent-error / restart
mino ships agents: agent, agent?, send, send-off, await, await-for, agent-error, restart-agent, set-error-handler!, error-handler, set-error-mode!, error-mode, plus shutdown-agents / release-pending-sends stubs. Watches and validators on agents go through the same watchable_get path as atoms / refs / vars. The MVP runs sends synchronously on the calling thread. mino's eval loop holds a per-state mutex; a worker-pool dispatcher would serialize on it anyway, and the synchronous shape is observably equivalent for any program that does not race against the agent itself. await becomes a trivial no-op. Action throws and watch throws are both captured into agent-error via agent_try_call -- a manual try frame in src/prim/agent.c that swallows the throw without re-raising. mino's mino_pcall would re-throw to any enclosing try via set_eval_diag's longjmp path, which would defeat the catch contract here. Type plumbing: new MINO_AGENT enum tag with a per-cell {val, watches, validator, err, err_handler, err_mode, queue} struct. Print form #agent[VAL]; equality is identity. Wired through GC mark / verify, deref dispatch, watchable_get, type predicate, clone (non-transferable), eval-form self-evaluation. Replaces the prior core.clj `agent` stub that threw :mino/unsupported. Test coverage: tests/agent_test.clj exercises construct / send / send-off / watches / validators / restart / error-mode / await. Internal suite 1512 / 7166 / 0. The agent arms of the upstream add_watch.cljc / remove_watch.cljc tests now pass cleanly, returning the external runner to its pre-STM-branch baseline of 1 fail + 2 errors (test-abs / test-reduce / test-short, all pre-existing). Documented deviations: send-via not implemented (no public Executor type); shutdown-agents / release-pending-sends are stubs; :fail mode rejects further sends until restart-agent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 226f36f commit dfe7e7b

17 files changed

Lines changed: 720 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,40 @@ fire watches anywhere.
361361
upstream, and the atom / ref / var arms pass cleanly. The agent
362362
arm of each still errors (out of scope until agents land).
363363

364+
### Agents (MVP)
365+
366+
mino now ships agents: `agent`, `agent?`, `send`, `send-off`,
367+
`await`, `await-for`, `agent-error`, `restart-agent`,
368+
`set-error-handler!`, `error-handler`, `set-error-mode!`,
369+
`error-mode`, plus `shutdown-agents` / `release-pending-sends`
370+
stubs. Watches and validators on agents go through the same
371+
`watchable_get` machinery as atoms / refs / vars.
372+
373+
The MVP runs sends synchronously on the calling thread. mino's
374+
eval loop holds a per-state mutex so a worker-pool design would
375+
serialize on it anyway; running synchronously is observably
376+
equivalent for any program that does not race against the agent
377+
itself, and `await` becomes a trivial no-op (the queue is always
378+
drained on send return). Action throws and watch throws are both
379+
captured into `agent-error` via a manual try frame in
380+
`src/prim/agent.c` (mino's `mino_pcall` re-throws to any
381+
enclosing try via `set_eval_diag`'s longjmp path, which would
382+
defeat the catch contract here).
383+
384+
Documented deviations: `send-via` is not implemented (no public
385+
Executor type); `shutdown-agents` and `release-pending-sends`
386+
are stubs; the `:fail` error mode is the default and rejects
387+
further sends until `restart-agent` clears the err.
388+
389+
`tests/agent_test.clj` exercises construct / send / send-off /
390+
watches / validators / restart / error-mode / await and is in
391+
the internal run.clj. The agent arms of the upstream
392+
`add_watch.cljc` / `remove_watch.cljc` tests now pass cleanly,
393+
bringing the external runner to **134 / 2680, 1 fail + 2 errors**
394+
(matching the pre-STM baseline; remaining failures are
395+
pre-existing test-abs / test-reduce / test-short, unrelated to
396+
STM or watches).
397+
364398
### Equality of empty lazy seqs
365399

366400
Fix: `(= (filter pred []) (filter pred []))` returned `false`.

lib/mino/tasks/builtin.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"src/prim/sequences.c" "src/prim/lazy.c"
4242
"src/prim/string.c" "src/prim/io.c"
4343
"src/prim/reflection.c" "src/prim/meta.c" "src/prim/regex.c"
44-
"src/prim/stateful.c" "src/prim/stm.c" "src/prim/module.c"
44+
"src/prim/stateful.c" "src/prim/stm.c" "src/prim/agent.c" "src/prim/module.c"
4545
"src/prim/ns.c"
4646
"src/prim/fs.c" "src/prim/proc.c"
4747
"src/prim/host.c" "src/interop/syntax.c"

src/collections/clone.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ static mino_val_t *clone_val(mino_state_t *dst, const mino_val_t *v)
216216
case MINO_HOST_ARRAY:
217217
case MINO_MAP_ENTRY:
218218
case MINO_TX_REF:
219+
case MINO_AGENT:
219220
return NULL;
220221
case MINO_UUID:
221222
return mino_uuid_from_bytes(dst, v->as.uuid.bytes);

src/collections/val.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,9 @@ int mino_eq(const mino_val_t *a, const mino_val_t *b)
10761076
case MINO_TX_REF:
10771077
/* Identity equality matches atoms and Clojure's JVM Ref. */
10781078
return a == b;
1079+
case MINO_AGENT:
1080+
/* Identity equality matches atoms and JVM Agent. */
1081+
return a == b;
10791082
}
10801083
return 0;
10811084
}

src/core.clj

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,21 +2056,12 @@
20562056
(:mino/message ex)
20572057
(:message ex))))
20582058

2059-
(defn agent [& _]
2060-
(throw (ex-info
2061-
(str "agent is not supported on mino — use atoms for"
2062-
" synchronous mutable state or core.async for async"
2063-
" dispatch")
2064-
{:mino/unsupported :agent})))
2065-
2066-
(defn send-to [& _]
2067-
(throw (ex-info
2068-
"send-to is not supported on mino — see (atom) and core.async"
2069-
{:mino/unsupported :send-to})))
2070-
2071-
(defn agent-error [& _]
2072-
(throw (ex-info "agent-error is not supported on mino"
2073-
{:mino/unsupported :agent-error})))
2059+
;; agent / send / send-off / await / agent-error / restart-agent /
2060+
;; set-error-handler! / error-handler / set-error-mode! / error-mode /
2061+
;; agent? / await-for / shutdown-agents / release-pending-sends are
2062+
;; provided by mino_install_agent (src/prim/agent.c). mino's MVP runs
2063+
;; sends synchronously on the calling thread, so await is a no-op.
2064+
;; See /documentation/stm/ for the full deviation list.
20742065

20752066
;; ---------------------------------------------------------------------------
20762067
;; Host threads.

src/eval/print.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,13 @@ void mino_print_to(mino_state_t *S, FILE *out, const mino_val_t *v)
471471
S->print_depth--;
472472
fputc(']', out);
473473
return;
474+
case MINO_AGENT:
475+
fputs("#agent[", out);
476+
S->print_depth++;
477+
mino_print_to(S, out, v->as.agent.val);
478+
S->print_depth--;
479+
fputc(']', out);
480+
return;
474481
case MINO_HOST_ARRAY: {
475482
/* Mirror Clojure JVM's #object[...] form for arrays since
476483
* arrays don't round-trip through the reader. */

src/eval/special.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ mino_val_t *eval_impl(mino_state_t *S, mino_val_t *form, mino_env_t *env, int ta
719719
case MINO_HOST_ARRAY:
720720
case MINO_MAP_ENTRY:
721721
case MINO_TX_REF:
722+
case MINO_AGENT:
722723
return form;
723724
case MINO_SYMBOL:
724725
return eval_symbol(S, form, env);

src/gc/driver.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,13 @@ void gc_trace_children(mino_state_t *S, gc_hdr_t *h)
531531
gc_mark_child_push(S, v->as.tx_ref.watches);
532532
gc_mark_child_push(S, v->as.tx_ref.validator);
533533
break;
534+
case MINO_AGENT:
535+
gc_mark_child_push(S, v->as.agent.val);
536+
gc_mark_child_push(S, v->as.agent.watches);
537+
gc_mark_child_push(S, v->as.agent.validator);
538+
gc_mark_child_push(S, v->as.agent.err);
539+
gc_mark_child_push(S, v->as.agent.err_handler);
540+
break;
534541
default:
535542
break;
536543
}

src/gc/minor.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,13 @@ static void gc_verify_remset_complete(mino_state_t *S)
331331
gc_verify_check(S, h, v->as.tx_ref.watches);
332332
gc_verify_check(S, h, v->as.tx_ref.validator);
333333
break;
334+
case MINO_AGENT:
335+
gc_verify_check(S, h, v->as.agent.val);
336+
gc_verify_check(S, h, v->as.agent.watches);
337+
gc_verify_check(S, h, v->as.agent.validator);
338+
gc_verify_check(S, h, v->as.agent.err);
339+
gc_verify_check(S, h, v->as.agent.err_handler);
340+
break;
334341
default: break;
335342
}
336343
break;

src/mino.h

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ typedef enum {
171171
* seq over the elements. Equality is identity.
172172
* Constructed via object-array, int-array,
173173
* long-array, etc. */
174-
MINO_TX_REF /* Software transactional memory ref: identity cell
174+
MINO_TX_REF, /* Software transactional memory ref: identity cell
175175
* holding a single committed value plus per-cell
176176
* watches and validator. Mutations are confined to
177177
* `dosync` transactions and serialized through a
@@ -182,6 +182,15 @@ typedef enum {
182182
* was already taken by the embedder rooting handle
183183
* (mino_ref_t), so the enum tag is MINO_TX_REF; the
184184
* Clojure-level type keyword is `:ref`. */
185+
MINO_AGENT /* Asynchronous mutable cell with per-agent action
186+
* queue. send / send-off enqueue (fn args); a worker
187+
* thread drains the queue serially, applying actions
188+
* to the state. Watches and validators on agents
189+
* follow the atom/ref shape. Equality is identity.
190+
* Constructed via `(agent v)`. Requires the host
191+
* to grant threads (mino_set_thread_limit > 1) for
192+
* the worker to spawn; with grant=1 send is a
193+
* synchronous-but-deferred no-op equivalent. */
185194
} mino_type_t;
186195

187196
typedef struct mino_val mino_val_t;
@@ -373,6 +382,20 @@ struct mino_val {
373382
* itself outlives all its refs
374383
* and is not a GC value. */
375384
} tx_ref;
385+
struct { /* MINO_AGENT: async mutable cell + queue */
386+
mino_val_t *val; /* current state */
387+
mino_val_t *watches; /* MINO_MAP key->callback, or NULL */
388+
mino_val_t *validator; /* validator fn, or NULL */
389+
mino_val_t *err; /* last failed action's exception,
390+
* or NULL. Set when the worker
391+
* caught a throw; cleared by
392+
* restart-agent. */
393+
mino_val_t *err_handler; /* on-error callback (fn agent ex)
394+
* or NULL */
395+
int err_mode; /* 0=:fail, 1=:continue */
396+
void *queue; /* opaque agent_queue_t * (heap-
397+
* allocated; freed via finalizer) */
398+
} agent;
376399
} as;
377400
};
378401

@@ -525,6 +548,16 @@ mino_val_t *mino_map_entry(mino_state_t *S, mino_val_t *k, mino_val_t *v);
525548
* monotonic ID) is unique within S. */
526549
mino_val_t *mino_tx_ref(mino_state_t *S, mino_val_t *val);
527550

551+
/* Construct an asynchronous agent holding the given initial state.
552+
* Watches, validator, and error handler all start NULL; install them
553+
* via add-watch / set-validator! / set-error-handler! on the returned
554+
* cell. The agent's action queue is heap-allocated and freed via the
555+
* GC sweep finalizer when the agent becomes unreachable. */
556+
mino_val_t *mino_agent(mino_state_t *S, mino_val_t *initial);
557+
558+
/* Return 1 if v is an agent, 0 otherwise. NULL-safe. */
559+
int mino_is_agent(const mino_val_t *v);
560+
528561
/* Return 1 if v is an STM ref (MINO_TX_REF), 0 otherwise. NULL-safe. */
529562
int mino_is_tx_ref(const mino_val_t *v);
530563

@@ -940,6 +973,15 @@ void mino_install_proc(mino_state_t *S, mino_env_t *env);
940973
*/
941974
void mino_install_stm(mino_state_t *S, mino_env_t *env);
942975

976+
/*
977+
* Install the agent surface: agent / agent? / send / send-off / await
978+
* / await-for / agent-error / restart-agent / set-error-handler! /
979+
* error-handler / set-error-mode! / error-mode / shutdown-agents /
980+
* release-pending-sends. mino_install_all calls this; embedders
981+
* calling only mino_new keep agents opt-out.
982+
*/
983+
void mino_install_agent(mino_state_t *S, mino_env_t *env);
984+
943985
/*
944986
* Evaluate one form. Returns NULL on error and writes a message via
945987
* mino_last_error(). Returns mino_nil() for an explicit nil result.

0 commit comments

Comments
 (0)