Skip to content

Compatibility

  • Implementation: SBCL only. Tested against SBCL 2.6.0.
  • Dependencies: none at runtime. cl-concurrent-kit/test depends on cl-weave v1.1.4, test-only -- its test DSL (describe/it/expect/signals), property-based testing and fuzzing (it-property, it-fuzz, gen-integer, gen-list, gen-boolean), soft assertions (with-soft-assertions, reporting every failing expect in an it block instead of stopping at the first), and benchmark facility (benchmark, used by benchmarks/run-benchmarks.lisp) are all in active use, not just the assertion macros. (v1.1.2 skipped deliberately: its tag exists but release.yml never actually published it -- unrelated to a real, SBCL-only-affecting fix in v1.1.3 that removed an unconditional sb-cover dependency from cl-weave's main system -- so v1.1.4, which additionally guards several ECL portability paths this SBCL-only project never exercises, is the version actually pinned.) That same script additionally depends on cl-cli v1.2.0, benchmark-tooling-only, for its --only/scale argument parsing -- neither it nor cl-weave is reachable from cl-concurrent-kit's own :depends-on (), only from flake.nix's CL_SOURCE_REGISTRY for the script and test system respectively. flake.nix itself is built with cl-nix-forge, the org's Nix packaging library -- a build-time-only, Nix-level dependency with no Lisp component. A fourth nerima-lisp tool, paredit-cli, is not a dependency at all in the ASDF/Nix sense -- it never appears in :depends-on, flake.nix, or CL_SOURCE_REGISTRY -- but is the structure-aware refactoring tool this project's own source history was edited with throughout: renames (refactor rename-function), balance validation after every structural edit (inspect check), and definition discovery (inspect definitions) all ran through it rather than by hand.
  • Platforms: x86_64-linux is the only platform CI actually gates. aarch64-darwin is also declared, for nix develop/nix build on the development machine -- dropped briefly on 2026-08-01 for carrying no CI gate, then re-declared on 2026-08-02 once the org's own PACKAGE_STANDARD.md accepted that trade-off explicitly rather than requiring every declared system to be CI-gated. aarch64-linux and x86_64-darwin are nobody's verification and stay undeclared. See flake.nix.

cl-concurrent-kit wraps sb-thread and sb-ext directly rather than depending on bordeaux-threads; see Architecture for why. Porting to another implementation would mean reimplementing src/primitives.lisp against that implementation's native thread API -- everything above that layer (promise, channel, select, executor, scope) is portable Common Lisp with no sb-* references.

Stability

The public API is exactly src/package.lisp's :export list -- nothing reached only through a package-qualified cl-concurrent-kit:: symbol is covered by semantic versioning. Every exported symbol is exercised by at least one test in t/, and nix flake check (tests, docs, formatting, coverage) gates every merge to main and every tagged release; see .github/workflows/ci.yml and release.yml.

flake.lock pins cl-weave and cl-nix-forge to specific tagged releases (bumped by hand when this package adopts a new one) and nixpkgs/treefmt-nix to a commit refreshed automatically by flake-update.yml's weekly cron, each update going through the same nix flake check gate as any other change before merging.

Production readiness

This is a library, loaded into a caller's own SBCL image -- there is no service to deploy and no SLA to publish; what a caller integrating it needs to know is below. (For round-trip overhead per primitive, run benchmarks/run-benchmarks.lisp, described in the repository's own top-level README.)

  • Error handling: every blocking operation that accepts :timeout signals operation-timed-out (never returns a sentinel value) on expiry; every other failure mode is its own condition (promise-already-fulfilled, channel-closed, task-cancelled, scope-error, latch-count-underflow, barrier-broken, promise-cancelled, promise-empty-input, promise-all-failed, executor-queue-full) subclassing cl-concurrent-kit-error, so a caller can catch that one base condition to handle any failure this library signals without enumerating each one.
  • Thread safety: every public struct (promise, channel, executor, task-scope, countdown-latch, barrier) owns its own lock and is safe to share across threads through its documented operations only; none of them is safe to mutate through slot accessors directly (all writer accessors are internal, %-prefixed).
  • Resource cleanup: make-executor starts worker threads that outlive the call until shutdown-executor is called -- there is no finalizer, by design, matching sb-thread's own contract; a long-running process that creates executors without shutting them down leaks threads exactly as it would leak any other unclosed resource. with-task-scope and future have no equivalent leak: every thread either one starts is guaranteed to have been joined (structured concurrency) or was never blocked on externally (future's own thread exits on its own).
  • Known limitation: cancellation (check-cancelled, with-task-scope) is cooperative, not preemptive -- see Architecture for why forcing it would cost the guarantee a scope exists to make. Where a body really must be bounded whatever it is doing, with-timeout is the preemptive escape hatch (it interrupts the thread outright), with the asynchronous-unwind caveat that comes with one: Architecture.
  • Scope: single SBCL image only. Nothing here coordinates across OS processes or machines; promise/channel/executor/task-scope objects are not serializable and sharing one across images is not a supported use.
  • Timeout audit: every place this project itself runs a command or blocks on a result has an explicit bound, checked directly rather than assumed:
  • flake.nix's three shell invocations (checks.coverage-lcov, checks.benchmark, apps.benchmark) each wrap their sbcl call in timeout --signal=KILL <N>s.
  • Every job in every workflow under .github/workflows/ (ci.yml, docs.yml's two jobs, flake-update.yml, release.yml) declares its own timeout-minutes: -- there is no job relying on GitHub's default.
  • The test suite has a global backstop (run-all :timeout-ms 20000 in t/package.lisp) plus its own per-call :timeout at almost every individual blocking recv/await. The four bare calls without one (t/channel-test.lisp:102,106, t/promise-test.lisp:231, t/select-test.lisp:241) were each checked individually rather than assumed safe: in every case the value is already available -- sent, cancelled, or buffered -- before the blocking call runs, so it resolves synchronously and structurally cannot block.