Embedding Cookbook

Twelve worked examples showing real-world embedding patterns. Each includes the full annotated C source with build instructions.

Drive mino agents from c host code

Demonstrates the public C-API perimeter for agents: mino_send, mino_send_off, mino_await, mino_await_for, mino_agent_error, mino_restart_agent. The host raises the thread limit so the per-state worker threads can spawn (one per pool), then drives an asynchronous counter without going through eval_string. mino's per-state eval lock means actions across the POOLED and SOLO pools serialize, but the queues are independent: a long send-off action doesn't stall pending sends, and vice versa. Build: cc -std=c99 -I.. -o agents agents.c .c Run: ./agents

Build: cc -std=c99 -I.. -o agents agents.c <lib srcs>.c
agents.c - full source
/*
 * agents.c - drive mino agents from C host code.
 *
 * Demonstrates the public C-API perimeter for agents: mino_send,
 * mino_send_off, mino_await, mino_await_for, mino_agent_error,
 * mino_restart_agent. The host raises the thread limit so the
 * per-state worker threads can spawn (one per pool), then drives
 * an asynchronous counter without going through eval_string.
 *
 * mino's per-state eval lock means actions across the POOLED and
 * SOLO pools serialize, but the queues are independent: a long
 * send-off action doesn't stall pending sends, and vice versa.
 *
 * Build:  cc -std=c99 -I.. -o agents agents.c <lib srcs>.c
 * Run:    ./agents
 */

#include "mino.h"
#include <stdio.h>

int main(void)
{
    mino_state *S   = mino_state_new();
    mino_env   *env = mino_env_new_default(S);

    /* Each agent pool needs its own worker thread (the embedder
     * thread does not count). For both POOLED and SOLO concurrently,
     * grant at least 2; we ask for 4 here to leave headroom for any
     * additional futures. The standalone REPL bumps thread_limit to
     * cpu_count automatically; embedders opt in explicitly. */
    mino_set_thread_limit(S, 4);
    mino_install(S, env, MINO_CAP_AGENT);

    /* (fn [v] (inc v)) - increment the agent's value. */
    mino_val *inc_fn = mino_eval_string(S, "(fn [v] (inc v))", env);

    /* Construct an agent holding 0. The owning state is recorded so
     * cross-state misuse throws MST007 at the C boundary. */
    mino_val *counter = mino_agent(S, mino_int(S, 0));

    /* Fire two sends onto POOLED, one send-off onto SOLO. mino_send
     * and mino_send_off return the agent immediately; the action
     * runs on the worker. */
    mino_send(S, counter, inc_fn, NULL);
    mino_send(S, counter, inc_fn, NULL);
    mino_send_off(S, counter, inc_fn, NULL);

    /* Block until the run-queues drain. The NULL-terminated array
     * lets one await wait on multiple agents at once. */
    {
        mino_val *agents[2];
        agents[0] = counter;
        agents[1] = NULL;
        mino_await(S, agents);
    }

    printf("counter after 2 send + 1 send-off = ");
    mino_println(S, mino_agent_deref(counter));  /* prints 3 */

    /* mino_await_for returns 1 if every agent reaches zero in-flight
     * before the deadline, 0 on timeout. The trivial path returns 1
     * immediately when nothing is queued. */
    {
        mino_val *agents[2];
        agents[0] = counter;
        agents[1] = NULL;
        printf("await_for with empty queue: %d (expect 1)\n",
               mino_await_for(S, 50, agents));
    }

    /* Failure handling. Install a validator that rejects negatives,
     * then queue a (dec) action. The validator throws and the agent
     * latches the error. */
    {
        mino_val *guarded = mino_eval_string(S,
            "(agent 0 :validator (fn [v] (>= v 0)))", env);
        mino_val *dec_fn  = mino_eval_string(S, "(fn [v] (dec v))", env);
        mino_val *err;
        mino_val *agents[2];
        agents[0] = guarded;
        agents[1] = NULL;

        mino_send(S, guarded, dec_fn, NULL);
        mino_await(S, agents);

        err = mino_agent_error(S, guarded);
        printf("guarded agent error: %s\n",
               err != NULL ? "captured" : "(clean)");

        /* Subsequent send to a failed :fail agent throws. The C-API
         * publishes the throw via the state and returns NULL. */
        if (mino_send(S, guarded, dec_fn, NULL) == NULL) {
            printf("send to failed agent threw: %s\n", mino_last_error(S));
        }

        /* Restart with a clean value. clear_actions=1 also drops any
         * actions that were queued before the failure latched. */
        mino_restart_agent(S, guarded, mino_int(S, 5), 1);
        printf("guarded agent after restart: ");
        mino_println(S, mino_agent_deref(guarded));  /* prints 5 */
    }

    /* Quiesce both pool workers before tearing down the state.
     * mino_state_free does this implicitly, but calling it
     * explicitly here mirrors how a long-running embedder shuts
     * down a single mino_state during its lifecycle. */
    mino_eval_string(S, "(shutdown-agents)", env);

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

Assemble persistent vectors / maps / sets one element at a time using the builder facade

Builders wrap transients and keep the work-in-progress collection rooted across GC. Use them when elements arrive incrementally (parsing, network input, host events) rather than already living in a C array. Build: cc -std=c99 -I.. -o build_collections build_collections.c ../mino.c Run: ./build_collections

Build: cc -std=c99 -I.. -o build_collections build_collections.c ../mino.c
build_collections.c - full source
/*
 * build_collections.c -- assemble persistent vectors / maps / sets one
 * element at a time using the builder facade.
 *
 * Builders wrap transients and keep the work-in-progress collection
 * rooted across GC. Use them when elements arrive incrementally
 * (parsing, network input, host events) rather than already living in
 * a C array.
 *
 * Build:  cc -std=c99 -I.. -o build_collections build_collections.c ../mino.c
 * Run:    ./build_collections
 */

#include "mino.h"
#include <stdio.h>

int main(void)
{
    mino_state *S   = mino_state_new();
    mino_env   *env = mino_env_new_default(S);

    /* Vector: open builder, push elements, finish. */
    {
        mino_vec_builder *b = mino_vector_builder_new(S);
        int i;
        for (i = 0; i < 5; i++) {
            mino_vector_builder_push(b, mino_int(S, i * i));
        }
        mino_val *v = mino_vector_builder_finish(b);
        mino_env_set(S, env, "v", v);
    }

    /* Map: open builder, put pairs, finish. */
    {
        mino_map_builder *b = mino_map_builder_new(S);
        mino_map_builder_put(b, mino_keyword(S, "name"),
                                mino_string(S, "alice"));
        mino_map_builder_put(b, mino_keyword(S, "age"),
                                mino_int(S, 42));
        mino_map_builder_put(b, mino_keyword(S, "active"),
                                mino_true(S));
        mino_val *m = mino_map_builder_finish(b);
        mino_env_set(S, env, "m", m);
    }

    /* Set: open builder, add elements, finish. Duplicates collapse. */
    {
        mino_set_builder *b = mino_set_builder_new(S);
        const char *colors[] = { "red", "green", "blue", "red", "green" };
        size_t i;
        for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) {
            mino_set_builder_add(b, mino_keyword(S, colors[i]));
        }
        mino_val *s = mino_set_builder_finish(b);
        mino_env_set(S, env, "s", s);
    }

    /* Pretty-print from mino. The builder products are full-fledged
     * persistent values; everything that works on a map / vec / set
     * literal works here too. */
    mino_eval_string(S, "(println \"v =\" v)", env);
    mino_eval_string(S, "(println \"m =\" m)", env);
    mino_eval_string(S, "(println \"s =\" s)", env);
    mino_eval_string(S, "(println \"v count =\" (count v))", env);
    mino_eval_string(S, "(println \"s count =\" (count s))", env);

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

Load structured configuration from a .clj file

Demonstrates: sandboxed eval (no I/O), extracting C values from a map, using keywords as config keys.
Build: cc -std=c99 -I.. -o config config.c ../mino.c
config.c - full source
/*
 * config.c - load structured configuration from a .clj file.
 *
 * Demonstrates: sandboxed eval (no I/O), extracting C values from a map,
 * using keywords as config keys.
 *
 * Build:  cc -std=c99 -I.. -o config config.c ../mino.c
 * Run:    ./config
 */

#include "mino.h"
#include <stdio.h>

/*
 * A .clj config file is just a map literal:
 *
 *   {:port 8080
 *    :host "0.0.0.0"
 *    :debug true
 *    :workers 4
 *    :routes [{:path "/api" :handler "api_handler"}
 *             {:path "/"    :handler "static_handler"}]}
 */
static const char *config_src =
    "{:port 8080\n"
    " :host \"0.0.0.0\"\n"
    " :debug true\n"
    " :workers 4\n"
    " :routes [{:path \"/api\" :handler \"api_handler\"}\n"
    "          {:path \"/\"    :handler \"static_handler\"}]}";

int main(void)
{
    mino_state *S = mino_state_new();
    mino_env *env = mino_env_new(S);
    mino_val *cfg;
    mino_val *val;

    /* Core bindings only - no I/O. The config file cannot read files,
     * print, or access the network. */
    mino_install(S, env, MINO_CAP_DEFAULT);

    /* Evaluate the config source. */
    cfg = mino_eval_string(S, config_src, env);
    if (cfg == NULL) {
        fprintf(stderr, "config error: %s\n", mino_last_error(S));
        return 1;
    }

    /* Bind the config map so we can query it from mino expressions. */
    mino_env_set(S, env, "cfg", cfg);
    val = mino_eval_string(S, "(get cfg :port)", env);
    if (val != NULL) {
        long long port;
        if (mino_to_int(val, &port)) {
            printf("port: %lld\n", port);
        }
    }

    val = mino_eval_string(S, "(get cfg :host)", env);
    if (val != NULL) {
        const char *host;
        size_t      len;
        if (mino_to_string(val, &host, &len)) {
            printf("host: %.*s\n", (int)len, host);
        }
    }

    val = mino_eval_string(S, "(get cfg :debug)", env);
    if (val != NULL) {
        printf("debug: %s\n", mino_to_bool(val) ? "on" : "off");
    }

    /* Iterate over routes. */
    val = mino_eval_string(S, "(get cfg :routes)", env);
    if (val != NULL) {
        long long count;
        mino_val *c = mino_eval_string(S, "(count (get cfg :routes))", env);
        if (c != NULL && mino_to_int(c, &count)) {
            long long i;
            printf("routes (%lld):\n", count);
            for (i = 0; i < count; i++) {
                char expr[128];
                mino_val *route;
                mino_val *path;
                snprintf(expr, sizeof(expr),
                         "(nth (get cfg :routes) %lld)", i);
                route = mino_eval_string(S, expr, env);
                if (route == NULL) continue;
                mino_env_set(S, env, "__r", route);
                path = mino_eval_string(S, "(get __r :path)", env);
                if (path != NULL) {
                    const char *s;
                    size_t      n;
                    if (mino_to_string(path, &s, &n)) {
                        printf("  %.*s\n", (int)n, s);
                    }
                }
            }
        }
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

Game scripting console

Demonstrates: mutable host state exposed through handles and host functions, execution limits to prevent runaway scripts, the REPL handle for interactive command entry.
Build: cc -std=c99 -I.. -o console console.c ../mino.c
console.c - full source
/*
 * console.c - game scripting console.
 *
 * Demonstrates: mutable host state exposed through handles and host
 * functions, execution limits to prevent runaway scripts, the REPL
 * handle for interactive command entry.
 *
 * Build:  cc -std=c99 -I.. -o console console.c ../mino.c
 * Run:    ./console
 */

#include "mino.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* --- Simulated game world ----------------------------------------------- */

typedef struct {
    double x, y;
    int    hp;
    int    max_hp;
    const char *name;
} entity_t;

static entity_t player = { 0.0, 0.0, 100, 100, "player" };

/* --- Host functions ----------------------------------------------------- */

static mino_val *host_pos(mino_state *S, mino_val *args, mino_env *env)
{
    mino_val *items[2];
    (void)args; (void)env;
    items[0] = mino_float(S, player.x);
    items[1] = mino_float(S, player.y);
    return mino_vector(S, items, 2);
}

static mino_val *host_move(mino_state *S, mino_val *args, mino_env *env)
{
    double dx, dy;
    (void)env;
    if (!mino_is_cons(args) || !mino_is_cons(mino_cdr(args))) {
        return mino_nil(S);
    }
    if (!mino_to_float(mino_car(args), &dx)) {
        long long ix;
        if (mino_to_int(mino_car(args), &ix)) dx = (double)ix;
        else return mino_nil(S);
    }
    if (!mino_to_float(mino_car(mino_cdr(args)), &dy)) {
        long long iy;
        if (mino_to_int(mino_car(mino_cdr(args)), &iy)) dy = (double)iy;
        else return mino_nil(S);
    }
    player.x += dx;
    player.y += dy;
    return host_pos(S, args, env);
}

static mino_val *host_hp(mino_state *S, mino_val *args, mino_env *env)
{
    (void)args; (void)env;
    return mino_int(S, player.hp);
}

static mino_val *host_heal(mino_state *S, mino_val *args, mino_env *env)
{
    long long amount;
    (void)env;
    if (mino_is_cons(args) && mino_to_int(mino_car(args), &amount)) {
        player.hp += (int)amount;
        if (player.hp > player.max_hp) player.hp = player.max_hp;
    }
    return mino_int(S, player.hp);
}

static mino_val *host_damage(mino_state *S, mino_val *args, mino_env *env)
{
    long long amount;
    (void)env;
    if (mino_is_cons(args) && mino_to_int(mino_car(args), &amount)) {
        player.hp -= (int)amount;
        if (player.hp < 0) player.hp = 0;
    }
    return mino_int(S, player.hp);
}

static mino_val *host_status(mino_state *S, mino_val *args, mino_env *env)
{
    mino_val *keys[3], *vals[3];
    (void)args; (void)env;
    keys[0] = mino_keyword(S, "pos");
    vals[0] = host_pos(S, args, env);
    keys[1] = mino_keyword(S, "hp");
    vals[1] = mino_int(S, player.hp);
    keys[2] = mino_keyword(S, "name");
    vals[2] = mino_string(S, player.name);
    return mino_map(S, keys, vals, 3);
}

int main(void)
{
    mino_state *S   = mino_state_new();
    mino_env   *env = mino_env_new_default(S);

    /* Install I/O so scripts can use println. */
    mino_install(S, env, MINO_CAP_IO);

    /* Limit scripts to 100k eval steps to prevent infinite loops. */
    mino_set_limit(S, MINO_LIMIT_STEPS, 100000);

    /* Register game functions. */
    mino_register_fn(S, env, "pos",    host_pos);
    mino_register_fn(S, env, "move",   host_move);
    mino_register_fn(S, env, "hp",     host_hp);
    mino_register_fn(S, env, "heal",   host_heal);
    mino_register_fn(S, env, "damage", host_damage);
    mino_register_fn(S, env, "status", host_status);

    /* Define some convenience commands in mino. */
    mino_eval_string(S,
        "(def teleport (fn [x y]\n"
        "  (move (- x (first (pos)))\n"
        "        (- y (nth (pos) 1)))))\n"
        "(def full-heal (fn [] (heal 9999)))\n",
        env);

    /* Run a scripted demo instead of an interactive loop. */
    printf("=== game console demo ===\n\n");

    {
        static const char *commands[] = {
            "(status)",
            "(move 5 3)",
            "(damage 30)",
            "(heal 10)",
            "(teleport 100 200)",
            "(full-heal)",
            "(status)",
        };
        size_t i;
        for (i = 0; i < sizeof(commands)/sizeof(commands[0]); i++) {
            mino_val *result;
            printf("> %s\n", commands[i]);
            result = mino_eval_string(S, commands[i], env);
            if (result == NULL) {
                printf("error: %s\n", mino_last_error(S));
            } else {
                mino_println(S, result);
            }
            printf("\n");
        }
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

Structured error access and the protected-call matrix from c

Two complementary surfaces: mino_error_kind / mino_error_code / mino_last_error inspect the classified diagnostic after a call that returned NULL. mino_eval_ex / mino_eval_string_ex / mino_pcall run with a try-frame installed so a throw caught here surfaces as out_ex without poisoning mino_last_error for subsequent calls. Build: cc -std=c99 -I.. -o error_handling error_handling.c ../mino.c Run: ./error_handling

Build: cc -std=c99 -I.. -o error_handling error_handling.c ../mino.c
error_handling.c - full source
/*
 * error_handling.c -- structured error access and the protected-call
 * matrix from C.
 *
 * Two complementary surfaces:
 *   mino_error_kind / mino_error_code / mino_last_error  inspect the
 *     classified diagnostic after a call that returned NULL.
 *   mino_eval_ex / mino_eval_string_ex / mino_pcall      run with a
 *     try-frame installed so a throw caught here surfaces as out_ex
 *     without poisoning mino_last_error for subsequent calls.
 *
 * Build:  cc -std=c99 -I.. -o error_handling error_handling.c ../mino.c
 * Run:    ./error_handling
 */

#include "mino.h"
#include <stdio.h>

static void report_diag(mino_state *S, const char *banner)
{
    const char *msg  = mino_last_error(S);
    const char *kind = mino_error_kind(S);
    const char *code = mino_error_code(S);
    printf("%s\n", banner);
    printf("  kind: %s\n", kind != NULL ? kind : "(none)");
    printf("  code: %s\n", code != NULL ? code : "(none)");
    printf("  msg:  %s\n", msg  != NULL ? msg  : "(none)");
    mino_clear_error(S);
}

int main(void)
{
    mino_state *S   = mino_state_new();
    mino_env   *env = mino_env_new_default(S);

    /* Read a malformed form: classified as a reader error. */
    {
        mino_val *r = mino_eval_string(S, "(+ 1", env);
        printf("\n[unbalanced parens] r = %s\n",
               r == NULL ? "NULL (expected)" : "non-NULL");
        report_diag(S, "structured access:");
    }

    /* Type error from arithmetic: classified eval/type. */
    {
        mino_val *r = mino_eval_string(S, "(+ 1 :two)", env);
        printf("\n[type error] r = %s\n",
               r == NULL ? "NULL (expected)" : "non-NULL");
        report_diag(S, "structured access:");
    }

    /* Protected eval_string. The throw is caught via out_ex; the raw
     * payload (the value the user passed to (throw ...)) is surfaced
     * directly instead of via the diagnostic round-trip. */
    {
        mino_val *out = NULL;
        mino_val *ex  = NULL;
        int rc = mino_eval_string_ex(S,
            "(throw (ex-info \"bad input\" {:value -1}))",
            env, &out, &ex);
        printf("\n[pcall caught throw] rc = %d\n", rc);
        if (ex != NULL) {
            char buf[256];
            mino_print_to_buf(S, ex, buf, sizeof(buf));
            printf("  out_ex: %s\n", buf);
        }
    }

    /* Successful protected eval distinguishes "real nil" from "error". */
    {
        mino_val *out = NULL;
        mino_val *ex  = NULL;
        int rc = mino_eval_string_ex(S, "nil", env, &out, &ex);
        printf("\n[pcall returns nil] rc = %d, out = %p, ex = %p\n",
               rc, (void *)out, (void *)ex);
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

The canonical embedder hello-world

Demonstrates: state + env construction with the amalgamation distribution, evaluating a script, extracting the result back to C, tearing down. Five minutes from zero to running.
five_minutes.c - full source
/*
 * five_minutes.c - the canonical embedder hello-world.
 *
 * Demonstrates: state + env construction with the amalgamation
 * distribution, evaluating a script, extracting the result back to C,
 * tearing down. Five minutes from zero to running.
 *
 * Build (using the amalgamation):
 *   cd mino && ./mino task amalgamate
 *   cc -std=c99 -O2 -Imino/dist -c mino/dist/mino.c -o dist/mino.o
 *   cc -std=c99 -O2 -Imino/dist src/cookbook/five_minutes.c \
 *      dist/mino.o -lm -lpthread -o src/cookbook/five_minutes
 *
 * Or via the make recipe used throughout this repo:
 *   make src/cookbook/five_minutes
 *
 * Run:
 *   ./src/cookbook/five_minutes
 */

#include "mino.h"
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    mino_state *S;
    mino_env   *env;
    mino_val   *result;
    long long   n;

    /* Step 1: a state owns the heap and the GC.
     * Step 2: an env is the lexical scope where bindings live. The
     *         _default helper installs the canonical Clojure-core
     *         capability set (numbers, strings, collections, regex,
     *         bignum, plus the bundled clojure.string / clojure.set
     *         / clojure.math namespaces). */
    S   = mino_state_new();
    if (S == NULL) { fprintf(stderr, "state_new failed\n"); return 1; }
    env = mino_env_new_default(S);

    /* Step 3: evaluate a script. The result is borrowed -- it lives
     *         until the next GC cycle unless rooted via mino_ref_new. */
    result = mino_eval_string(S,
        "(reduce + 0 (map (fn [x] (* x x)) (range 1 11)))",
        env);
    if (result == NULL) {
        fprintf(stderr, "eval failed: %s\n", mino_last_error(S));
        return 1;
    }

    /* Step 4: extract the result back to C. mino_to_int returns 1 on
     *         success, 0 on type mismatch. */
    if (!mino_to_int(result, &n)) {
        fprintf(stderr, "result is not an int\n");
        return 1;
    }
    printf("sum of squares 1..10 = %lld\n", n);
    if (n != 385) {
        fprintf(stderr, "expected 385\n");
        return 1;
    }

    /* Step 5: tear down. The state owns everything reachable through
     *         it (env, all live values); free in this order. */
    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

Decision tree for "how do i expose my host type to mino script?"

This example is the answer to the Lua-metatable / Janet-put question: when a host value needs to appear in mino script, which cell shape do you reach for? The short answer is one of three: Value-shaped data -> mino_defrecord + mino_record Identity-shaped resource -> mino_handle (an opaque void*) Mutable identity -> mino_handle wrapped in an atom The decision tree: 1. Does your value have inherent identity (a file descriptor, a socket, a database connection, a GPU resource)? YES -> use mino_handle. The value is opaque from script; the host owns the lifecycle and exposes operations via primitives that take the handle and return new values. If state mutates over time, wrap the handle in an atom for principled access. NO -> proceed to step 2. 2. Does your value look like a Clojure record from script: a named type with named fields, structural equality, hashable, participates in protocol dispatch? YES -> use mino_defrecord to declare the type and mino_record to construct instances. Storage is field slots (not a backing map); script-side (:field-name r) and (= r1 r2) work as expected; protocol dispatch uses the type identity. NO -> proceed to step 3. 3. Is your value a small bag of named fields you don't need to type-name? Use a mino_map (plain map) -- no host-side cell shape required. The script side does not see "your type", it sees a map with the same keys you'd put in a record. What you do NOT do: there is no metatable surface on handles. mino respects Clojure's promise that values do not mutate behind your back. If you reach for the metatable answer, the right response is one of: - record + protocol (script-side dispatch by type) - record + multimethod (script-side dispatch by arbitrary dispatch fn) - atom around a record/map (mutable identity layered on values)

Demonstrates: mino_handle for an "fd-like" opaque resource, mino_defrecord + mino_record for value-shaped Vec3, and atom- wrapped state for mutable identity.
handle_record_atom_choice.c - full source
/*
 * handle_record_atom_choice.c - decision tree for "how do I expose
 * my host type to mino script?".
 *
 * This example is the answer to the Lua-metatable / Janet-`put`
 * question: when a host value needs to appear in mino script, which
 * cell shape do you reach for? The short answer is one of three:
 *
 *   Value-shaped data         -> mino_defrecord + mino_record
 *   Identity-shaped resource  -> mino_handle (an opaque void*)
 *   Mutable identity          -> mino_handle wrapped in an atom
 *
 * The decision tree:
 *
 *   1. Does your value have inherent identity (a file descriptor, a
 *      socket, a database connection, a GPU resource)?
 *
 *        YES -> use mino_handle. The value is opaque from script;
 *               the host owns the lifecycle and exposes operations
 *               via primitives that take the handle and return new
 *               values. If state mutates over time, wrap the handle
 *               in an atom for principled access.
 *
 *        NO  -> proceed to step 2.
 *
 *   2. Does your value look like a Clojure record from script: a
 *      named type with named fields, structural equality, hashable,
 *      participates in protocol dispatch?
 *
 *        YES -> use mino_defrecord to declare the type and
 *               mino_record to construct instances. Storage is
 *               field slots (not a backing map); script-side
 *               (:field-name r) and (= r1 r2) work as expected;
 *               protocol dispatch uses the type identity.
 *
 *        NO  -> proceed to step 3.
 *
 *   3. Is your value a small bag of named fields you don't need to
 *      type-name? Use a mino_map (plain map) -- no host-side cell
 *      shape required. The script side does not see "your type",
 *      it sees a map with the same keys you'd put in a record.
 *
 * What you do NOT do: there is no metatable surface on handles.
 * mino respects Clojure's promise that values do not mutate behind
 * your back. If you reach for the metatable answer, the right
 * response is one of:
 *
 *   - record + protocol  (script-side dispatch by type)
 *   - record + multimethod  (script-side dispatch by arbitrary dispatch fn)
 *   - atom around a record/map  (mutable identity layered on values)
 *
 * Demonstrates: mino_handle for an "fd-like" opaque resource,
 * mino_defrecord + mino_record for value-shaped Vec3, and atom-
 * wrapped state for mutable identity.
 *
 * Build (from repo root):
 *   make src/cookbook/handle_record_atom_choice
 *
 * Or via the amalgamation:
 *   cc -std=c99 -O2 -Imino/dist src/cookbook/handle_record_atom_choice.c \
 *      dist/mino.o -lm -lpthread -o src/cookbook/handle_record_atom_choice
 */

#include "mino.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ------------------------------------------------------------------------- */
/* Path 1: mino_handle for an identity-shaped resource.                      */
/* ------------------------------------------------------------------------- */

typedef struct {
    int  id;
    char name[64];
} host_resource;

/* The handle carries a pointer + a stable type-tag string. Equality
 * and hash are pointer-shaped: two handles are equal iff they wrap
 * the same C pointer. The script side cannot mutate the pointed-to
 * struct; the host exposes operations through primitives. */
static mino_val *make_handle(mino_state *S, host_resource *r)
{
    return mino_handle(S, r, "host-resource");
}

static mino_val *prim_resource_name(mino_state *S, mino_val *args,
                                      mino_env *env)
{
    mino_val      *h;
    host_resource *r;
    (void)env;
    if (mino_args_parse(S, "resource-name", args, "H", &h) != 0) return NULL;
    r = (host_resource *)mino_handle_ptr(h);
    return mino_string(S, r != NULL ? r->name : "<null>");
}

/* ------------------------------------------------------------------------- */
/* Path 2: mino_defrecord for a value-shaped Vec3.                           */
/* ------------------------------------------------------------------------- */

static mino_val *define_vec3(mino_state *S, mino_env *env)
{
    const char *fields[3] = {"x", "y", "z"};
    mino_val   *T = mino_defrecord(S, "user", "Vec3", fields, 3);
    mino_env_set(S, env, "Vec3", T);
    return T;
}

/* ------------------------------------------------------------------------- */
/* main                                                                      */
/* ------------------------------------------------------------------------- */

int main(void)
{
    mino_state *S   = mino_state_new();
    mino_env   *env = mino_env_new_default(S);

    /* Path 1: handle */
    {
        host_resource res = {.id = 42};
        snprintf(res.name, sizeof(res.name), "tcp-socket-42");
        mino_register_fn(S, env, "resource-name", prim_resource_name);
        mino_env_set(S, env, "*res*", make_handle(S, &res));
        mino_val *r = mino_eval_string(S, "(resource-name *res*)", env);
        const char *got = NULL;
        size_t      len = 0;
        if (r == NULL || !mino_to_string(r, &got, &len)) {
            fprintf(stderr, "handle path failed\n"); return 1;
        }
        printf("handle path : (resource-name *res*) = %.*s\n",
               (int)len, got);
    }

    /* Path 2: record */
    {
        mino_val *T    = define_vec3(S, env);
        mino_val *vals[3];
        mino_val *v;
        mino_val *r;
        long long got = 0;
        vals[0] = mino_int(S, 1);
        vals[1] = mino_int(S, 2);
        vals[2] = mino_int(S, 3);
        v = mino_record(S, T, vals, 3);
        mino_env_set(S, env, "v", v);
        r = mino_eval_string(S,
            "(+ (:x v) (:y v) (:z v))", env);
        if (r == NULL || !mino_to_int(r, &got)) {
            fprintf(stderr, "record path failed\n"); return 1;
        }
        printf("record path : (sum of fields) = %lld (expected 6)\n", got);
        if (got != 6) return 1;
    }

    /* Path 3: atom around the record for mutable identity. */
    {
        mino_val *r;
        long long got = 0;
        r = mino_eval_string(S,
            "(do "
            "  (def counter (atom 0)) "
            "  (swap! counter inc) "
            "  (swap! counter inc) "
            "  (swap! counter inc) "
            "  @counter)",
            env);
        if (r == NULL || !mino_to_int(r, &got) || got != 3) {
            fprintf(stderr, "atom path failed\n"); return 1;
        }
        printf("atom path   : (counter after 3 swaps) = %lld\n", got);
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

Walk every kind of mino collection from c with mino_iter

The iterator covers vectors, maps (hashed and sorted), sets, lists, lazy seqs, and chunked seqs through one API. For map-shaped collections both out_k and out_v are populated; for the others, out_k is the element and out_v stays NULL. Build: cc -std=c99 -I.. -o iterate iterate.c ../mino.c Run: ./iterate

Build: cc -std=c99 -I.. -o iterate iterate.c ../mino.c
iterate.c - full source
/*
 * iterate.c -- walk every kind of mino collection from C with mino_iter.
 *
 * The iterator covers vectors, maps (hashed and sorted), sets, lists,
 * lazy seqs, and chunked seqs through one API. For map-shaped
 * collections both out_k and out_v are populated; for the others,
 * out_k is the element and out_v stays NULL.
 *
 * Build:  cc -std=c99 -I.. -o iterate iterate.c ../mino.c
 * Run:    ./iterate
 */

#include "mino.h"
#include <stdio.h>
#ifdef _WIN32
#  include <malloc.h>
#else
#  include <alloca.h>
#endif

static long long count_seq(mino_state *S, mino_val *coll)
{
    mino_iter *it = (mino_iter *)alloca(mino_iter_sizeof());
    long long n = 0;
    mino_iter_init(S, it, coll);
    while (mino_iter_next(it, NULL, NULL)) n++;
    mino_iter_done(it);
    return n;
}

static long long sum_seq(mino_state *S, mino_val *coll)
{
    mino_iter *it = (mino_iter *)alloca(mino_iter_sizeof());
    long long total = 0, x = 0;
    mino_val *v;
    mino_iter_init(S, it, coll);
    while (mino_iter_next(it, &v, NULL)) {
        if (mino_to_int(v, &x)) total += x;
    }
    mino_iter_done(it);
    return total;
}

int main(void)
{
    mino_state *S   = mino_state_new();
    mino_env   *env = mino_env_new_default(S);

    /* Vector walk: out_k is the element, out_v is NULL. */
    {
        mino_val *vec = mino_eval_string(S, "[10 20 30 40 50]", env);
        printf("vector count = %lld\n", count_seq(S, vec));   /* 5 */
        printf("vector sum   = %lld\n", sum_seq(S, vec));     /* 150 */
    }

    /* Map walk: both out_k and out_v populated, insertion order. */
    {
        mino_val *m = mino_eval_string(S,
            "{:a 1 :b 2 :c 3}", env);
        mino_iter *it = (mino_iter *)alloca(mino_iter_sizeof());
        mino_val  *k, *v;
        mino_iter_init(S, it, m);
        printf("map entries:\n");
        while (mino_iter_next(it, &k, &v)) {
            const char *kn; size_t klen;
            long long   vn = 0;
            mino_to_keyword(k, &kn, &klen);
            mino_to_int(v, &vn);
            printf("  :%.*s -> %lld\n", (int)klen, kn, vn);
        }
        mino_iter_done(it);
    }

    /* Lazy seq walk: forced on demand. */
    {
        mino_val *r = mino_eval_string(S, "(range 1 11)", env);
        printf("lazy sum 1..10 = %lld\n", sum_seq(S, r));     /* 55 */
    }

    /* Cons list walk. */
    {
        mino_val *lst = mino_cons(S, mino_int(S, 7),
                          mino_cons(S, mino_int(S, 8),
                          mino_cons(S, mino_int(S, 9), mino_nil(S))));
        printf("list sum     = %lld\n", sum_seq(S, lst));     /* 24 */
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

Transform data through mino expressions

Demonstrates: pushing C data into mino as vectors/maps, running sequence operations, extracting transformed results back to C. Shows how mino can serve as a lightweight ETL / data-wrangling layer.
Build: cc -std=c99 -I.. -o pipeline pipeline.c ../mino.c
pipeline.c - full source
/*
 * pipeline.c - transform data through mino expressions.
 *
 * Demonstrates: pushing C data into mino as vectors/maps, running
 * sequence operations, extracting transformed results back to C.
 * Shows how mino can serve as a lightweight ETL / data-wrangling layer.
 *
 * Build:  cc -std=c99 -I.. -o pipeline pipeline.c ../mino.c
 * Run:    ./pipeline
 */

#include "mino.h"
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    mino_state *S   = mino_state_new();
    mino_env   *env = mino_env_new_default(S);
    mino_val *result;

    /* Build a vector of employee records from C data. */
    {
        static const char *names[]  = { "Alice", "Bob", "Carol", "Dave" };
        static const int   ages[]   = { 30, 45, 28, 52 };
        static const int salaries[] = { 75000, 92000, 68000, 110000 };
        size_t i;
        size_t n = sizeof(names) / sizeof(names[0]);
        mino_val **records = (mino_val **)malloc(n * sizeof(*records));

        for (i = 0; i < n; i++) {
            mino_val *keys[3], *vals[3];
            keys[0] = mino_keyword(S, "name");
            vals[0] = mino_string(S, names[i]);
            keys[1] = mino_keyword(S, "age");
            vals[1] = mino_int(S, ages[i]);
            keys[2] = mino_keyword(S, "salary");
            vals[2] = mino_int(S, salaries[i]);
            records[i] = mino_map(S, keys, vals, 3);
        }
        mino_env_set(S, env, "employees", mino_vector(S, records, n));
        free(records);
    }

    /* Pipeline 1: names of employees over 35. */
    printf("Over 35:\n");
    result = mino_eval_string(S,
        "(->> employees\n"
        "     (filter (fn [e] (> (get e :age) 35)))\n"
        "     (map (fn [e] (get e :name))))",
        env);
    if (result != NULL) {
        printf("  ");
        mino_println(S, result);
    }

    /* Pipeline 2: total salary budget. */
    result = mino_eval_string(S,
        "(reduce + 0 (map (fn [e] (get e :salary)) employees))",
        env);
    if (result != NULL) {
        long long total;
        if (mino_to_int(result, &total)) {
            printf("Total salary: %lld\n", total);
        }
    }

    /* Pipeline 3: sorted by salary descending. */
    printf("By salary (desc):\n");
    result = mino_eval_string(S,
        "(->> employees\n"
        "     (sort)\n"
        "     (reverse)\n"
        "     (map (fn [e] (str (get e :name) \": $\" (get e :salary)))))",
        env);
    if (result != NULL) {
        /* Walk the result list and print each line. */
        mino_val *p = result;
        while (mino_is_cons(p)) {
            const char *s;
            size_t      len;
            if (mino_to_string(mino_car(p), &s, &len)) {
                printf("  %.*s\n", (int)len, s);
            }
            p = mino_cdr(p);
        }
    }

    /* Pipeline 4: group into age brackets using into + map. */
    result = mino_eval_string(S,
        "(let [senior   (filter (fn [e] (>= (get e :age) 40)) employees)\n"
        "      junior   (filter (fn [e] (<  (get e :age) 40)) employees)]\n"
        "  {:senior (map (fn [e] (get e :name)) senior)\n"
        "   :junior (map (fn [e] (get e :name)) junior)})",
        env);
    if (result != NULL) {
        printf("Age groups: ");
        mino_println(S, result);
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

A plugin host that loads .clj files and calls into them

Demonstrates: module loading, calling mino functions from C with mino_call, the resolver callback, sandboxed vs full environments.
Build: cc -std=c99 -I.. -o plugin plugin.c ../mino.c
plugin.c - full source
/*
 * plugin.c - a plugin host that loads .clj files and calls into them.
 *
 * Demonstrates: module loading, calling mino functions from C with
 * mino_call, the resolver callback, sandboxed vs full environments.
 *
 * Build:  cc -std=c99 -I.. -o plugin plugin.c ../mino.c
 * Run:    ./plugin
 *
 * In this example the "plugins" are defined inline for portability,
 * but a real host would load them from disk via mino_load_file.
 */

#include "mino.h"
#include <stdio.h>
#include <string.h>

/* Inline plugin source (normally loaded from a file). */
static const char *greeter_plugin =
    "(def greet\n"
    "  (fn [name]\n"
    "    (str \"Hello, \" name \"!\")))\n"
    "\n"
    "(def farewell\n"
    "  (fn [name]\n"
    "    (str \"Goodbye, \" name \".\")))";

static const char *math_plugin =
    "(def square (fn [x] (* x x)))\n"
    "(def cube   (fn [x] (* x x x)))\n"
    "(def hypot  (fn [a b]\n"
    "              (let [a2 (square a)\n"
    "                    b2 (square b)]\n"
    "                (reduce + 0 (list a2 b2)))))\n";

/* Call a named function in env with a single argument. */
static mino_val *call1(mino_state *S, mino_env *env, const char *name,
                         mino_val *arg)
{
    mino_val *fn   = mino_env_get(env, name);
    mino_val *args = mino_cons(S, arg, mino_nil(S));
    if (fn == NULL) {
        fprintf(stderr, "plugin: function '%s' not found\n", name);
        return NULL;
    }
    return mino_call(S, fn, args, env);
}

static mino_val *call2(mino_state *S, mino_env *env, const char *name,
                         mino_val *a, mino_val *b)
{
    mino_val *fn   = mino_env_get(env, name);
    mino_val *args = mino_cons(S, a, mino_cons(S, b, mino_nil(S)));
    if (fn == NULL) {
        fprintf(stderr, "plugin: function '%s' not found\n", name);
        return NULL;
    }
    return mino_call(S, fn, args, env);
}

int main(void)
{
    mino_state *S = mino_state_new();
    mino_env *env = mino_env_new_default(S);
    mino_val *result;

    /* Load the greeter plugin. */
    if (mino_eval_string(S, greeter_plugin, env) == NULL) {
        fprintf(stderr, "load error: %s\n", mino_last_error(S));
        return 1;
    }

    /* Call plugin functions from C. */
    result = call1(S, env, "greet", mino_string(S, "World"));
    if (result != NULL) {
        const char *s;
        size_t      n;
        if (mino_to_string(result, &s, &n)) {
            printf("%.*s\n", (int)n, s);
        }
    }

    result = call1(S, env, "farewell", mino_string(S, "World"));
    if (result != NULL) {
        const char *s;
        size_t      n;
        if (mino_to_string(result, &s, &n)) {
            printf("%.*s\n", (int)n, s);
        }
    }

    /* Load the math plugin. */
    if (mino_eval_string(S, math_plugin, env) == NULL) {
        fprintf(stderr, "load error: %s\n", mino_last_error(S));
        return 1;
    }

    result = call1(S, env, "square", mino_int(S, 7));
    if (result != NULL) {
        long long v;
        if (mino_to_int(result, &v)) {
            printf("square(7) = %lld\n", v);
        }
    }

    result = call2(S, env, "hypot", mino_int(S, 3), mino_int(S, 4));
    if (result != NULL) {
        long long v;
        if (mino_to_int(result, &v)) {
            printf("hypot(3,4) = %lld (sum of squares)\n", v);
        }
    }

    /* Demonstrate protected call - catches errors gracefully. */
    {
        mino_val *fn  = mino_env_get(env, "square");
        mino_val *bad = mino_cons(S, mino_string(S, "oops"), mino_nil(S));
        mino_val *out = NULL;
        int rc = mino_pcall(S, fn, bad, env, &out, NULL);
        if (rc != 0) {
            printf("pcall caught: %s\n", mino_last_error(S));
        }
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}

A repl served over a tcp socket

Demonstrates: the in-process REPL handle (mino_repl_*), I/O opt-in, hosting a REPL from an event loop without threads.
Build: cc -std=c99 -I.. -o repl_socket repl_socket.c ../mino.c
repl_socket.c - full source
/*
 * repl_socket.c - a REPL served over a TCP socket.
 *
 * Demonstrates: the in-process REPL handle (mino_repl_*), I/O opt-in,
 * hosting a REPL from an event loop without threads.
 *
 * Build:  cc -std=c99 -I.. -o repl_socket repl_socket.c ../mino.c
 * Run:    ./repl_socket        (listens on port 7100)
 *         rlwrap nc 127.0.0.1 7100
 *
 * Single-client for simplicity; a real host would use poll/epoll/kqueue.
 */

#include "mino.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 7100
#define BUFSZ 4096

static void send_str(int fd, const char *s)
{
    size_t len = strlen(s);
    (void)write(fd, s, len);
}

static void send_prompt(int fd, int continuation)
{
    send_str(fd, continuation ? "..> " : "=> ");
}

int main(void)
{
    int         srv, cli;
    struct sockaddr_in addr;
    mino_env *env;
    mino_repl *repl;
    char         buf[BUFSZ];

    /* Set up the listening socket. */
    srv = socket(AF_INET, SOCK_STREAM, 0);
    if (srv < 0) { perror("socket"); return 1; }
    {
        int opt = 1;
        setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }
    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_port        = htons(PORT);
    if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind"); return 1;
    }
    listen(srv, 1);
    printf("listening on 127.0.0.1:%d\n", PORT);

    /* Accept one client. */
    cli = accept(srv, NULL, NULL);
    if (cli < 0) { perror("accept"); return 1; }
    printf("client connected\n");

    /* Create the mino runtime with full capabilities. */
    mino_state *S = mino_state_new();
    env = mino_env_new_default(S);
    mino_install(S, env, MINO_CAP_IO);
    repl = mino_repl_new(S, env);

    send_str(cli, "mino REPL\n");
    send_prompt(cli, 0);

    /* Read lines and feed them to the REPL. */
    for (;;) {
        ssize_t n = read(cli, buf, sizeof(buf) - 1);
        if (n <= 0) break;
        buf[n] = '\0';

        /* Feed line-by-line. */
        {
            char *line = buf;
            char *nl;
            while ((nl = strchr(line, '\n')) != NULL) {
                mino_val *out = NULL;
                int         rc;
                *nl = '\0';
                rc = mino_repl_feed(repl, line, &out);
                switch (rc) {
                case MINO_REPL_OK:
                    if (out != NULL) {
                        /* Print the result back to the client. */
                        char repr[4096];
                        int  len;
                        /* Use a tmpfile to capture printed output. */
                        {
                            FILE *tmp = tmpfile();
                            if (tmp != NULL) {
                                mino_print_to(S, tmp, out);
                                len = (int)ftell(tmp);
                                if (len > 0 && len < (int)sizeof(repr)) {
                                    rewind(tmp);
                                    (void)fread(repr, 1, (size_t)len, tmp);
                                    repr[len] = '\0';
                                    send_str(cli, repr);
                                }
                                fclose(tmp);
                            }
                        }
                        send_str(cli, "\n");
                    }
                    send_prompt(cli, 0);
                    break;
                case MINO_REPL_MORE:
                    send_prompt(cli, 1);
                    break;
                case MINO_REPL_ERROR:
                    send_str(cli, "error: ");
                    send_str(cli, mino_last_error(S));
                    send_str(cli, "\n");
                    send_prompt(cli, 0);
                    break;
                }
                line = nl + 1;
            }
        }
    }

    printf("client disconnected\n");
    mino_repl_free(repl);
    mino_env_free(S, env);
    mino_state_free(S);
    close(cli);
    close(srv);
    return 0;
}

A tiny rules engine driven by mino expressions

Demonstrates: registering host functions, evaluating user-defined predicates, using mino as a decision layer between C data and actions.
Build: cc -std=c99 -I.. -o rules rules.c ../mino.c
rules.c - full source
/*
 * rules.c - a tiny rules engine driven by mino expressions.
 *
 * Demonstrates: registering host functions, evaluating user-defined
 * predicates, using mino as a decision layer between C data and actions.
 *
 * Build:  cc -std=c99 -I.. -o rules rules.c ../mino.c
 * Run:    ./rules
 */

#include "mino.h"
#include <stdio.h>
#include <string.h>

/* --- Simulated application state ---------------------------------------- */

typedef struct {
    const char *user;
    int         age;
    int         purchases;
    double      balance;
} customer_t;

static customer_t current_customer;

/* Host functions exposed to the rules engine. */

static mino_val *host_age(mino_state *S, mino_val *args, mino_env *env)
{
    (void)args; (void)env;
    return mino_int(S, current_customer.age);
}

static mino_val *host_purchases(mino_state *S, mino_val *args, mino_env *env)
{
    (void)args; (void)env;
    return mino_int(S, current_customer.purchases);
}

static mino_val *host_balance(mino_state *S, mino_val *args, mino_env *env)
{
    (void)args; (void)env;
    return mino_float(S, current_customer.balance);
}

/* --- Rules are mino expressions returning a discount tier keyword ------- */

static const char *rules_src =
    "(def discount-tier\n"
    "  (fn []\n"
    "    (cond\n"
    "      (> (purchases) 50) :gold\n"
    "      (> (purchases) 10) :silver\n"
    "      (> (age) 65)       :senior\n"
    "      :else              :standard)))\n";

int main(void)
{
    mino_state *S = mino_state_new();
    mino_env *env = mino_env_new_default(S);
    mino_val *result;

    /* Register host accessors. */
    mino_register_fn(S, env, "age",       host_age);
    mino_register_fn(S, env, "purchases", host_purchases);
    mino_register_fn(S, env, "balance",   host_balance);

    /* Load the rules. */
    if (mino_eval_string(S, rules_src, env) == NULL) {
        fprintf(stderr, "rules error: %s\n", mino_last_error(S));
        return 1;
    }

    /* Evaluate for different customers. */
    {
        static const customer_t customers[] = {
            { "Alice",  30,  5,  120.0 },
            { "Bob",    70, 12, 3400.0 },
            { "Carol",  25, 55,  800.0 },
        };
        size_t i;
        for (i = 0; i < sizeof(customers)/sizeof(customers[0]); i++) {
            current_customer = customers[i];
            result = mino_eval_string(S, "(discount-tier)", env);
            if (result == NULL) {
                fprintf(stderr, "eval error: %s\n", mino_last_error(S));
                continue;
            }
            printf("%-8s -> ", customers[i].user);
            mino_println(S, result);
        }
    }

    mino_env_free(S, env);
    mino_state_free(S);
    return 0;
}