Skip to content

Commit a6c5d6b

Browse files
leifericfclaude
andcommitted
release: ship capability-gated install for embedders
Phase 1 of the lean-embed cycle: introduce a capability-gated install API so hosts opt into exactly the surface they need without changing what a mino_install_core-equipped runtime exposes to user Clojure. New surface: - mino_install_minimal: floor (reader, evaluator, GC, persistent collections, numeric ops, basic seq, foundational macros). No core.clj eval. - mino_install_regex / _bignum / _multimethods / _protocols / _transducers: each flips its capability bit and (for the C-backed ones) registers prims into clojure.core. - mino_install_clojure_core: evaluates core.clj against whatever capability bits the host has flipped. Optional sections are wrapped in (when (mino-installed? :cap) ...) and skip cleanly when off. - mino_install_core: back-compat alias that pre-sets the canonical Clojure-core caps (regex + bignum + multimethods + protocols + transducers) then calls mino_install_clojure_core. Existing embedders see no behaviour change. mino_state_t carries a new caps_installed bitmask; MINO_CAP_* constants plus mino_capability_installed / mino_capabilities let host code query what is on. A static capability registry powers mino_capability_for_symbol(name), used by eval_symbol to raise an enriched diagnostic when user code calls a name from a capability the host disabled. New diagnostic code MNS002 (capability-disabled), distinct from MNS001 (unbound symbol). The :mino/data payload carries {:capability :symbol :reason :enable-via} so user-side error handlers can pattern-match on the disabled capability. REPL UX: :capabilities (alias :caps) prints a two-column installed-vs-not table; the banner shows "embedded, N of M capabilities installed" plus a one-liner pointing to :capabilities when the runtime is in a partial-install state. Capability-install ordering rule: a capability that gates a core.clj section must have its bit set before mino_install_clojure_core runs. The back-compat mino_install_core wrapper handles this for the canonical caps; mino_install_all does the same for the I/O / fs / proc / stm / agent / async / host tier. Critical bug found and fixed during integration: each mino_install_<cap> originally installed its prims into the caller-supplied env (a user env from mino_env_new), not the clojure.core ns env. core.clj eval (which runs with current_ns = clojure.core) then could not resolve the capability's prims, raising MNS002 even on a fully-installed runtime. Each install_<cap> now ns_env_ensures clojure.core and installs into that env. tests/embed_caps_test.c exercises three paths: minimal floor, minimal + selective caps (multimethods only) with MNS002 verification on the off caps, and install_all coverage. Standalone test suite (1616 tests, 7527 assertions) green; embed_multi_state green; embed_stm_test pre-existing latent bug unrelated to this change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b4a938 commit a6c5d6b

21 files changed

Lines changed: 945 additions & 99 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ embed_multi_state
6060
embed_multi_state_asan
6161
embed_multi_state_tsan
6262
embed_stm_test
63+
embed_caps_test
64+
embed_caps_test_asan
65+
embed_caps_test_tsan
6366
tests/transient_test
6467
tests/transient_test_asan
6568
tests/transient_test_ubsan

CHANGELOG.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,79 @@
11
# Changelog
22

3+
## v0.146.0 — Capability-Gated Install: Lean Embedded Footprint, Same Standalone Surface
4+
5+
Embedded mino's `mino_install_core` was monolithic — every fresh runtime
6+
parsed and evaluated all ~117 KB of `core.clj` and registered every C
7+
primitive whether the host needed it or not. Hosts that wanted just a
8+
calculator paid the full Clojure-stdlib bill. This release introduces a
9+
capability-gated install API so embedders opt into exactly the surface
10+
they need, without changing what a `mino_install_core`-equipped runtime
11+
exposes to user Clojure code.
12+
13+
The new surface:
14+
15+
- `mino_install_minimal(S, env)` — reader, evaluator, GC, persistent
16+
collections, numeric ops, basic seq, foundational macros. No
17+
`core.clj` evaluation; sub-millisecond install cost.
18+
- `mino_install_regex(S, env)`, `mino_install_bignum`,
19+
`mino_install_multimethods`, `mino_install_protocols`,
20+
`mino_install_transducers` — each flips a capability bit and (for
21+
the C-backed ones) registers its prims into `clojure.core`.
22+
- `mino_install_clojure_core(S, env)` — evaluates `core.clj` against
23+
whatever capability bits the host has set. Optional sections (regex,
24+
multimethods, protocols, transducers, bignum-aware `integer?`) are
25+
wrapped in `(when (mino-installed? :cap) ...)` and skip cleanly when
26+
their capability is off.
27+
- `mino_install_core(S, env)` is preserved as a back-compat alias that
28+
pre-sets the canonical Clojure-core caps (regex + bignum +
29+
multimethods + protocols + transducers) and then calls
30+
`mino_install_clojure_core`. Existing embedders see no behaviour
31+
change.
32+
33+
`mino_state_t` carries a new `caps_installed` bitmask; `MINO_CAP_*`
34+
constants and `mino_capability_installed(S, bit)` / `mino_capabilities(S)`
35+
let host code query what is on. A static capability registry powers
36+
`mino_capability_for_symbol(name)`, used by the symbol-resolution path
37+
to raise an enriched diagnostic when user code calls a name from a
38+
capability the host disabled.
39+
40+
New diagnostic code **MNS002** (capability-disabled), distinct from
41+
**MNS001** (unbound symbol). The `:mino/data` payload carries
42+
`{:capability :symbol :reason :enable-via}` so user-side error handlers
43+
can pattern-match on the disabled capability. Example:
44+
45+
```
46+
error[MNS002]: slurp is not installed in this runtime
47+
(capability 'io' disabled by host)
48+
note: the host can enable this capability by calling
49+
mino_install_io from C before mino_install_core
50+
```
51+
52+
REPL UX gains a `:capabilities` (alias `:caps`) command that prints a
53+
two-column installed-vs-not table, and the banner shows
54+
"embedded, N of M capabilities installed" plus a one-liner pointing
55+
to `:capabilities` when the runtime is in a partial-install state.
56+
57+
Standalone `./mino`, `./mino -e ...`, `./mino script.clj`, REPL, all
58+
remain unchanged at the user-visible surface: same Clojure-core names,
59+
same diagnostics for non-capability errors, same 1616-test suite green.
60+
61+
Embedded surface: a host that wants the full Clojure runtime still
62+
calls `mino_install_core` and gets bit-for-bit identical behaviour. A
63+
host that wants a lean numeric/collection-only mino calls
64+
`mino_install_minimal` and skips the `core.clj` eval entirely.
65+
66+
The capability install ordering rule: a capability that gates a
67+
`core.clj` section must have its bit set **before**
68+
`mino_install_clojure_core` runs. The back-compat
69+
`mino_install_core` wrapper handles this for the canonical caps;
70+
`mino_install_all` does the same for the I/O / fs / proc / stm /
71+
agent / async / host tier.
72+
73+
Subsequent phases of the lean-embed cycle (porting thin `core.clj`
74+
wrappers to C, pre-parsed AST, pre-compiled bytecode, image-based
75+
bootstrap) will compose on top of this surface without breaking it.
76+
377
## v0.145.1 — Task Runner Fix: Pre-Resolve Tasks Outside The BC Doseq Body
478

579
`mino task <name>` raised a confusing `subs: first argument must be

lib/mino/tasks/builtin.clj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,16 @@
336336
- embed_multi_state: 16 mino_state_t x 16 pthreads, asserts the
337337
embedding API is safe under the one-state-per-thread contract.
338338
- embed_stm_test: STM Layer 2a smoke test (mino_tx_run,
339-
mino_tx_alter_c, mino_tx_commute_c, mino_tx_ensure, watches)."
339+
mino_tx_alter_c, mino_tx_commute_c, mino_tx_ensure, watches).
340+
- embed_caps_test: capability-gated install surface -- minimal /
341+
selective caps / install_all paths and MNS002 diagnostics."
340342
[]
341343
(compile-and-run-embed-test "tests/embed_multi_state.c"
342344
"embed_multi_state")
343345
(compile-and-run-embed-test "tests/embed_stm_test.c"
344-
"embed_stm_test"))
346+
"embed_stm_test")
347+
(compile-and-run-embed-test "tests/embed_caps_test.c"
348+
"embed_caps_test"))
345349

346350
;; ---- Architecture quality gates ----
347351

main.c

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,14 +376,51 @@ static void print_repl_help(FILE *out)
376376
{
377377
fputs(
378378
"REPL meta-commands:\n"
379-
" :help Show this help\n"
380-
" :quit Exit the REPL (or press Ctrl-D)\n"
379+
" :help Show this help\n"
380+
" :quit Exit the REPL (or press Ctrl-D)\n"
381+
" :capabilities Show installed vs. available capabilities (alias :caps)\n"
381382
"\n"
382383
"Type a form and press Enter to evaluate it. Multi-line forms are\n"
383384
"gathered until the parens balance.\n",
384385
out);
385386
}
386387

388+
/* Two-column capability listing for the REPL `:capabilities` command.
389+
* The full registry is enumerated; each entry is printed under
390+
* INSTALLED or AVAILABLE depending on the runtime's bitmask. */
391+
static void print_repl_capabilities(FILE *out, mino_state_t *S)
392+
{
393+
const mino_capability_info_t *p;
394+
int total = 0, installed = 0, longest = 0;
395+
396+
for (p = mino_capability_list(); p->name != NULL; p++) {
397+
int n = (int)strlen(p->name);
398+
if (n > longest) longest = n;
399+
total++;
400+
if (mino_capability_installed(S, p->bit)) installed++;
401+
}
402+
403+
fprintf(out, "Capabilities: %d of %d installed\n\n", installed, total);
404+
fprintf(out, " %-*s %-24s %s\n", longest, "name", "C entry point",
405+
"description");
406+
fprintf(out, " %-*s %-24s %s\n", longest, "----",
407+
"-------------",
408+
"-----------");
409+
for (p = mino_capability_list(); p->name != NULL; p++) {
410+
const char *mark = mino_capability_installed(S, p->bit)
411+
? " [x]" : " [ ]";
412+
fprintf(out, "%s %-*s %-24s %s\n",
413+
mark, longest, p->name,
414+
p->install_fn != NULL ? p->install_fn : "",
415+
p->summary != NULL ? p->summary : "");
416+
}
417+
fputc('\n', out);
418+
fputs(" [x] installed [ ] available -- call the C entry point\n",
419+
out);
420+
fputs(" from the embedder to enable. mino-installed? exposes\n", out);
421+
fputs(" the same bits to running code.\n", out);
422+
}
423+
387424
/* Exec a companion binary ("mino-nrepl" / "mino-lsp") from PATH, passing
388425
* remaining argv through. Replaces argv[first] with the binary name so the
389426
* companion observes its own argv[0]. Only returns on failure. */
@@ -948,8 +985,24 @@ int main(int argc, char **argv)
948985
return exit_code;
949986
}
950987

951-
fprintf(stderr, "mino %s\n", mino_version_string());
952-
fputs("Type :help for help, :quit to exit\n", stderr);
988+
{
989+
const mino_capability_info_t *p;
990+
int total = 0, installed = 0;
991+
for (p = mino_capability_list(); p->name != NULL; p++) {
992+
total++;
993+
if (mino_capability_installed(S, p->bit)) installed++;
994+
}
995+
if (installed < total) {
996+
fprintf(stderr,
997+
"mino %s (embedded, %d of %d capabilities installed)\n",
998+
mino_version_string(), installed, total);
999+
fputs("Type :capabilities to see the full list, "
1000+
":help for help, :quit to exit\n", stderr);
1001+
} else {
1002+
fprintf(stderr, "mino %s\n", mino_version_string());
1003+
fputs("Type :help for help, :quit to exit\n", stderr);
1004+
}
1005+
}
9531006
fputs("mino=> ", stderr);
9541007
fflush(stderr);
9551008

@@ -1013,6 +1066,18 @@ int main(int argc, char **argv)
10131066
awaiting_continuation = 0;
10141067
continue;
10151068
}
1069+
if (strcmp(name, "capabilities") == 0
1070+
|| strcmp(name, "caps") == 0) {
1071+
print_repl_capabilities(stderr, S);
1072+
{
1073+
size_t consumed = (size_t)(end - buf);
1074+
size_t remaining = len - consumed;
1075+
memmove(buf, end, remaining + 1);
1076+
len = remaining;
1077+
}
1078+
awaiting_continuation = 0;
1079+
continue;
1080+
}
10161081
}
10171082
if (form == NULL) {
10181083
const char *err = mino_last_error(S);

src/core.clj

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,9 @@
796796

797797
(defn integer?
798798
"Returns true if x is an integer (long or bigint)."
799-
[x] (or (int? x) (bigint? x)))
799+
[x] (if (mino-installed? :bignum)
800+
(or (int? x) (bigint? x))
801+
(int? x)))
800802
;; pos-int? / neg-int? / nat-int? specifically test the long-sized
801803
;; int tier per Clojure's contract: `(neg-int? -1N)` returns false on
802804
;; the JVM because clojure.core/neg-int? composes int? (Long-only),
@@ -1775,6 +1777,14 @@
17751777
(prewalk (fn [x] (if (contains? smap x) (get smap x) x)) form))
17761778

17771779
;; --- Regex ---
1780+
;;
1781+
;; Gated on the :regex capability. When the host did not call
1782+
;; mino_install_regex (or mino_install_clojure_core / mino_install_all),
1783+
;; the entire section is skipped: re-pattern / re-find / re-matches are
1784+
;; not bound, so any code calling them triggers a MNS002 "capability
1785+
;; not installed" diagnostic instead of a bare unbound-symbol error.
1786+
1787+
(when (mino-installed? :regex)
17781788

17791789
;; re-pattern is a C primitive that returns a MINO_REGEX from a
17801790
;; string source (and a no-op on an existing regex). Note that
@@ -1877,6 +1887,8 @@
18771887
{:matcher m})))
18781888
last-match))
18791889

1890+
) ;; end (when (mino-installed? :regex) ...)
1891+
18801892
;; --- Complex macros ---
18811893

18821894
(defmacro condp
@@ -2110,6 +2122,10 @@
21102122

21112123
;; ---------------------------------------------------------------------------
21122124
;; Protocols: polymorphic dispatch on the type of the first argument.
2125+
;; Gated on the :protocols capability -- defprotocol / extend-protocol /
2126+
;; extend-type / satisfies? are absent when the host did not install
2127+
;; protocols. The CollReduce / IKVReduce / Datafiable / Navigable
2128+
;; extension points further down also live in this gate.
21132129
;;
21142130
;; (defprotocol Name
21152131
;; (method1 [this])
@@ -2129,6 +2145,8 @@
21292145
;; (satisfies? Name x) returns true if x's type has implementations for Name.
21302146
;; ---------------------------------------------------------------------------
21312147

2148+
(when (mino-installed? :protocols)
2149+
21322150
(defn protocol-dispatch [dispatch-atom mname & args]
21332151
(let [target (first args)
21342152
t (type target)
@@ -2357,6 +2375,10 @@
23572375
(impl m f init)
23582376
(internal-reduce-kv f init m))))
23592377

2378+
) ;; end (when (mino-installed? :protocols) ...)
2379+
2380+
(when (mino-installed? :multimethods)
2381+
23602382
;; ---------------------------------------------------------------------------
23612383
;; Hierarchies: immutable parent/child/ancestor/descendant relationships.
23622384
;;
@@ -2645,6 +2667,8 @@
26452667

26462668
(set-print-method! print-method)
26472669

2670+
) ;; end (when (mino-installed? :multimethods) ...)
2671+
26482672
(defmacro with-out-str
26492673
"Evaluates body with *out* bound to a fresh string-collecting atom,
26502674
and returns the accumulated string."
@@ -2692,14 +2716,20 @@
26922716
(finally (close ~name)))))))
26932717

26942718
;; ---------------------------------------------------------------------------
2695-
;; Transducers: composable algorithmic transformations.
2719+
;; Transducers: composable algorithmic transformations. Gated on
2720+
;; :transducers -- when off, the C-level `into` primitive handles
2721+
;; (into to from) directly and transducer-aware shapes like
2722+
;; (transduce xf f coll) raise a MNS002 "capability not installed"
2723+
;; diagnostic. Depends on :protocols (reduce-kv lives there).
26962724
;;
26972725
;; A transducer is a function (rf -> rf) where rf is a reducing function
26982726
;; with three arities: ([] init), ([result] completion), ([result input] step).
26992727
;;
27002728
;; Use (transduce xf f coll) or (into to xf from) to apply.
27012729
;; ---------------------------------------------------------------------------
27022730

2731+
(when (mino-installed? :transducers)
2732+
27032733
(defn cat
27042734
"A transducer that concatenates the contents of each input."
27052735
[rf]
@@ -2842,6 +2872,8 @@
28422872
(sequence (first xfs) coll)
28432873
(sequence (apply comp xfs) coll))))
28442874

2875+
) ;; end (when (mino-installed? :transducers) ...)
2876+
28452877
;; --- Array constructors ---
28462878
;;
28472879
;; object-array, int-array, long-array, etc. are now C primitives that

src/eval/special.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include "eval/special_internal.h"
1313
#include "prim/internal.h"
14+
#include "mino.h"
1415

1516
/* --- Evaluator helpers: one per value kind. --- */
1617

@@ -168,6 +169,38 @@ static mino_val_t *eval_symbol(mino_state_t *S, mino_val_t *form, mino_env_t *en
168169
if (amb != NULL) v = mino_env_get_sym(amb, form);
169170
}
170171
if (v == NULL) {
172+
const mino_capability_info_t *cap = mino_capability_for_symbol(data);
173+
if (cap != NULL) {
174+
char msg[400];
175+
char note[200];
176+
mino_val_t *keys[4];
177+
mino_val_t *vals[4];
178+
mino_val_t *data_map;
179+
180+
snprintf(msg, sizeof(msg),
181+
"%s is not installed in this runtime "
182+
"(capability '%s' disabled by host)",
183+
data, cap->name);
184+
snprintf(note, sizeof(note),
185+
"the host can enable this capability by calling %s "
186+
"from C before mino_install_core",
187+
cap->install_fn);
188+
189+
keys[0] = mino_keyword(S, "capability");
190+
vals[0] = mino_keyword(S, cap->name);
191+
keys[1] = mino_keyword(S, "symbol");
192+
vals[1] = mino_symbol(S, data);
193+
keys[2] = mino_keyword(S, "reason");
194+
vals[2] = mino_keyword(S, "not-installed");
195+
keys[3] = mino_keyword(S, "enable-via");
196+
vals[3] = mino_string(S, cap->install_fn);
197+
data_map = mino_map(S, keys, vals, 4);
198+
199+
set_eval_diag_with_data(S,
200+
mino_current_ctx(S)->eval_current_form,
201+
"capability", "MNS002", msg, data_map, note);
202+
return NULL;
203+
}
171204
char msg[300];
172205
snprintf(msg, sizeof(msg), "unbound symbol: %s", data);
173206
set_eval_diag(S, mino_current_ctx(S)->eval_current_form, "name", "MNS001", msg);

0 commit comments

Comments
 (0)