Finding
docs/EMBEDDING.md:63-67 makes a load-bearing promise:
The runtime is multi-state. A single process can hold multiple EigsState instances concurrently; each one is independent.
Nothing in the repo tests the concurrently half. pthread_create appears nowhere in src/embed_smoke.c or any tests/*.sh. The one multi-state case that exists — embed_smoke.c:364-375, "Multi-state switching on one thread" — is explicitly sequential: it creates st2, switches to it, and evals. That covers switching, not independence.
.claude/rules/c-runtime-memory.md already notes tools/embed_stack_soak.sh is "the only multi-eval-per-EigsState coverage in the repo", so the thinness is known on the single-thread axis; the concurrent axis has none.
The promise does currently hold
I wrote the coverage out of tree before filing, so this is a gap report and not a bug report. Two programs against libeigenscript.a, both passing at 078e759:
Per-state observer thresholds survive concurrency. Two states, one per OS thread, each setting distinct thresholds and spinning 200k iterations:
state A dh_zero: set 0.001 -> read 0.0010
state B dh_zero: set 0.002 -> read 0.0020
MSTATE_OK
Per-thread sandbox budgets stay separate. Thread A runs a 50k-element allocation under a 64 KiB budget (must refuse), thread B the same under 512 MiB (must succeed), 200 interleaved rounds each:
tiny-budget thread : 200/200 rounds refused as expected (0 mismatches)
ample-budget thread: 200/200 rounds succeeded as expected (0 mismatches)
SBISO_OK
Structurally this is why: nearly every g_* name is a macro onto eigs_current->… (82 such macros in eigenscript.h), so state that looks global is per-state or per-thread by construction. That is a good design — but it is a design that a future plain static global silently breaks, and no gate would notice.
Why it is worth a test rather than a shrug
The failure mode is invisible in single-threaded use and arbitrarily bad in embedded use: one host's state reading another's thresholds, budget, or error flag. It is also the exact class of regression that arrives by accident — a new counter added as a file-scope static instead of an EigsThread field looks correct in every existing test.
Concretely, vm.c carried a stale comment asserting the sandbox counters were "plain process globals" (corrected on claude/language-review-9cjuc5). The comment was wrong in the safe direction, but it shows the invariant is maintained by convention and can drift in the docs without anything failing.
Suggested shape
A make embed-concurrent target (sibling to make embed-smoke) running the two programs above, gated into run_all_tests.sh like the other probe-gated sections. The assertions that matter:
- Two states, two threads, distinct observer thresholds → each reads back its own.
- Two threads, distinct
sandbox_run budgets → neither sees the other's charges.
- An uncaught error in state A leaves state B's
has_error clear.
I have (1) and (2) working and can open a PR wiring them in if useful — say the word and I'll add (3) and the Makefile/runner plumbing.
Related: docs/EMBEDDING.md:63-67, src/embed_smoke.c:364, src/eigenscript.h:757-776.
Finding
docs/EMBEDDING.md:63-67makes a load-bearing promise:Nothing in the repo tests the concurrently half.
pthread_createappears nowhere insrc/embed_smoke.cor anytests/*.sh. The one multi-state case that exists —embed_smoke.c:364-375, "Multi-state switching on one thread" — is explicitly sequential: it createsst2, switches to it, and evals. That covers switching, not independence..claude/rules/c-runtime-memory.mdalready notestools/embed_stack_soak.shis "the only multi-eval-per-EigsState coverage in the repo", so the thinness is known on the single-thread axis; the concurrent axis has none.The promise does currently hold
I wrote the coverage out of tree before filing, so this is a gap report and not a bug report. Two programs against
libeigenscript.a, both passing at078e759:Per-state observer thresholds survive concurrency. Two states, one per OS thread, each setting distinct thresholds and spinning 200k iterations:
Per-thread sandbox budgets stay separate. Thread A runs a 50k-element allocation under a 64 KiB budget (must refuse), thread B the same under 512 MiB (must succeed), 200 interleaved rounds each:
Structurally this is why: nearly every
g_*name is a macro ontoeigs_current->…(82 such macros ineigenscript.h), so state that looks global is per-state or per-thread by construction. That is a good design — but it is a design that a future plainstaticglobal silently breaks, and no gate would notice.Why it is worth a test rather than a shrug
The failure mode is invisible in single-threaded use and arbitrarily bad in embedded use: one host's state reading another's thresholds, budget, or error flag. It is also the exact class of regression that arrives by accident — a new counter added as a file-scope
staticinstead of anEigsThreadfield looks correct in every existing test.Concretely,
vm.ccarried a stale comment asserting the sandbox counters were "plain process globals" (corrected onclaude/language-review-9cjuc5). The comment was wrong in the safe direction, but it shows the invariant is maintained by convention and can drift in the docs without anything failing.Suggested shape
A
make embed-concurrenttarget (sibling tomake embed-smoke) running the two programs above, gated intorun_all_tests.shlike the other probe-gated sections. The assertions that matter:sandbox_runbudgets → neither sees the other's charges.has_errorclear.I have (1) and (2) working and can open a PR wiring them in if useful — say the word and I'll add (3) and the Makefile/runner plumbing.
Related:
docs/EMBEDDING.md:63-67,src/embed_smoke.c:364,src/eigenscript.h:757-776.