Skip to content

No test runs two EigsStates concurrently — the "each one is independent" embedding promise is unexercised (it does hold; I verified it out of tree) #885

Description

@InauguralPhysicist

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:

  1. Two states, two threads, distinct observer thresholds → each reads back its own.
  2. Two threads, distinct sandbox_run budgets → neither sees the other's charges.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions