| name | sbcl-usage |
| description | Practical SBCL (Steel Bank Common Lisp) operations guide. Use this skill whenever the user mentions SBCL execution/debugging, --script usage, REPL workflows, backtraces, ASDF loading, save-lisp-and-die, profiling, or SLY-based Common Lisp development. |
| version | 2.1.0 |
Provide end-to-end operational guidance for SBCL: running programs, debugging failures,
profiling performance, and producing executables.
This complements common-lisp-ecosystem by focusing on practical runtime workflows.
<version_info>
<current_version>SBCL 2.5.x (2026 stable)</current_version>
All patterns in this skill apply to SBCL 2.x series. SBCL releases frequently (monthly builds); use nix or Roswell to pin a specific version.
</version_info>
Read - Inspect Lisp/ASDF files, configs, logs
Edit - Update *.lisp / *.asd / run scripts
Bash - Execute sbcl / roswell / qlot / nix commands
mcp__plugin_claude-code-home-manager_context7__query-docs - Verify SBCL / ASDF / CFFI details
SBCL invocation mode selection (REPL / script / non-interactive)
Debugger-centric root cause analysis (backtrace, restarts, inspect, trace)
ASDF, Quicklisp, Roswell, and Qlot execution workflows
Performance measurement (time, sb-profile, sb-sprof)
Executable generation with save-lisp-and-die
SBCL usage in Nix-based environments
Full API tutorials for every external library (use Context7 when needed)
General language architecture topics already covered by common-lisp-ecosystem
<sbcl_cli>
Primary SBCL startup modes and when to use each
Interactive exploration and iterative debugging
sbcl
sbcl --noinform
Reproduce the failure in REPL first, then minimize the input.
Use --noinform when you want less startup noise.
Batch jobs, automation, CI execution
sbcl --script tools/task.lisp
Design explicit exit codes for operational reliability.
Wrap top-level failures with handler-case + sb-ext:exit.
One-liner load-and-run flows in CI or local automation
sbcl --non-interactive \
--eval '(require :asdf)' \
--eval '(asdf:load-system :my-app)' \
--eval '(my-app:main)'
Prefer --non-interactive in CI to avoid hanging on prompts.
Move complex startup logic to --script for maintainability.
Custom core image workflows or strict startup control
sbcl --core my.core
sbcl --disable-debugger --non-interactive --eval '(...)'
Do not disable debugger during root-cause investigation.
Use custom core images sparingly to preserve reproducibility.
<asdf_workflow>
ASDF-centered loading and execution patterns
(require :asdf)
(asdf:load-system :my-app)
Validate load-system success before deeper debugging.
Read the first ASDF failure carefully; avoid chasing secondary errors.
sbcl --non-interactive \
--eval '(require :asdf)' \
--eval '(asdf:test-system :my-app/test)'
Keep a one-line reproducible test command for team sharing.
Prefer Qlot for dependency reproducibility
qlot install
qlot exec sbcl --non-interactive --eval '(require :asdf)' --eval '(asdf:load-system :my-app)'
Use pinned dependency sets to reduce local-vs-CI drift.
<debugging_workflow>
High-efficiency SBCL debugging flow for fast root-cause discovery
Produce a stable minimal reproduction
Fix the execution mode first (REPL or script)
Stable execution mode selected
Strip inputs/environment to a minimal failing case
Minimal reproducible failure case
Observe failure location, not only symptom text
Inspect debugger backtrace and stack frames
Candidate fault location identified
Use inspect/describe for problematic objects
Object state diagnostics captured
Use trace for high-value call-path visibility
Call-path evidence captured
(trace my-app::parse-input)
(untrace my-app::parse-input)
(describe some-object)
(inspect some-object)
Test one root-cause hypothesis at a time
Define observable signals per hypothesis
Hypothesis-to-signal mapping
Use step/break/log checks to prove or reject each signal
Validated or rejected hypotheses
Apply minimal fix and verify non-regression
Re-run the same reproduction command after the fix
Fix effectiveness verified
Add tests that preserve the failure case
Regression protection added
Use restart flows to keep diagnosing while preserving continuity
(restart-case
(dangerous-op x)
(use-default () :report "fallback value" 0)
(retry () :report "retry operation" (dangerous-op x)))
Explicit recovery paths let you observe failures and continue operation
without blindly swallowing diagnostics.
Minimum interactive debugger controls to master
Backtrace and frame navigation
Local variable inspection
Restart selection (abort/retry/use-value)
Explicit invoke-debugger usage when needed
<compile_load_hang_triage>
A distinct class of failures where SBCL stops making progress (no error, no backtrace,
no output) inside compile-file, load, or asdf:load-system rather than signalling. These
are compile-unit and load-order phenomena, not ordinary runtime bugs: the same forms
frequently compile and load fine in isolation but stall when combined in one file or one
image. Diagnose them structurally, and prefer decomposition over per-form workarounds.
<general_principle name="shrink_the_compile_unit">
Treat the compile unit (the file handed to compile-file, or a single ASDF component) as the primary variable. Splitting a stalling file into smaller source files loaded serially is the durable fix; per-form workarounds are stopgaps.
Many stalls arise from compile-time interaction between top-level forms in the same unit — macro-generation feeding later macro invocations, large constant folding, or definition ordering — not from any single form. Reducing the unit removes the interaction.
</general_principle>
<general_principle name="keep_top_level_forms_boring">
Define top-level helpers with plain defun rather than a top-level (setf (symbol-function 'name) (lambda ...)) or an eager (compile nil (lambda ...)) at registration time. Keep constant-heavy work inside runtime helper functions instead of thin top-level wrappers that invite constant folding of large literals.
Top-level symbol-function assignment of a full lambda body, and thin wrappers that return a large constant vector/list, have been observed to trigger compile/eval stalls where the equivalent plain defun (or a non-constant construction path) loads normally.
Specific triggers observed on SBCL 2.6.0 (macOS/Nix); the general guidance — keep top-level forms simple and side-effect-light — is dialect-stable.
;; risky at top level: symbol-function assignment of a full lambda body
(setf (symbol-function '%encode) (lambda (s) #| large body |#))
;; safe: plain defun
(defun %encode (s) #| large body |#)
;; risky: a thin wrapper that folds a large constant vector at compile time
(defun tokens () #(#| hundreds of literal specs |#))
;; safe: build the vector at runtime through a non-constant argument path
(defun tokens (specs) (build-token-vector specs))
;; drop unneeded generated copiers that enlarge a defstruct-heavy compile unit
(defstruct (node (:copier nil)) a b c)
</example>
</general_principle>
<general_principle name="watch_macro_expansion_size">
A macro whose expansion grows combinatorially with its arguments can make macroexpand/compile appear hung. Emit a linear runtime construction instead of enumerating a branch per argument subset, and add a macroexpansion-size regression test for high-arity call sites.
Observed case: a keyword-wrapper macro emitting one direct-call branch per &key presence subset produced on the order of 2^N branches for N keys, so a wrapper with ~18 keys generated hundreds of thousands of branches. The load stall was macroexpansion blow-up, not the wrapped function.
</general_principle>
<general_principle name="load_order_is_a_variable">
A file that compiles alone can stall when compiled after another file has been loaded into the same image. When a stall appears only in-sequence, suspect load-order/compile-unit interaction and re-verify each unit in a fresh image.
Observed with definition-heavy files (e.g. a run of many defstruct forms) that compiled in a fresh image but stalled once an earlier file had been loaded first — evidence the trigger is cross-unit state, not the file's own source.
</general_principle>
<observed_triggers>
Version-scoped field observations (SBCL 2.6.0), recorded as reproduction conditions rather than universal rules. Use them as hypotheses to test, not guarantees, and re-validate before relying on any workaround.
A run of many top-level defstruct forms in one compile unit; adding one more struct crosses a threshold and compile-file stalls. Mitigations: split structs across serially-loaded files; add (:copier nil) to drop unneeded generated copiers.
A thin top-level wrapper returning a large constant vector/list, folded at compile time. Mitigation: build the vector inside a runtime helper or pass the specs through a non-constant argument path.
Top-level (setf (symbol-function 'name) (lambda ...)) with a substantial body. Mitigation: plain defun.
Predicates branching on implementation Unicode category/width tables via member/case under a bootstrap-loaded image. Mitigation observed: bind the return value and compare with explicit eq/or checks.
Forcing sb-ext:evaluator-mode to :interpret across a whole file to dodge a compile stall — frequently just relocates the stall to a later file or to execution time. Treat evaluator-mode guards as a dead end for a durable fix unless paired with a structural decomposition and fresh verification.
</observed_triggers>
</compile_load_hang_triage>
<headless_verification_harness>
A sound, non-interactive harness is a prerequisite for diagnosing the stalls above: if the
timeout mechanism is unsound, a stalled form and a stalled harness are indistinguishable,
producing false positives. Build the harness correctly before trusting any hang observation.
The timeout must run in a parent process that keeps the ability to kill the child. A wrapper that arms an alarm and then exec's SBCL replaces itself with SBCL and cancels the alarm — the timeout never fires, so an ASDF/load hang survives indefinitely and looks like a stalled form. Use fork + wait in the parent, with the parent owning the alarm and the kill.
# Perl fork/wait timeout skeleton: the parent keeps the alarm and can signal the child.
# (exec-after-alarm in a single process would silently cancel the alarm.)
perl -e '
my $pid = fork();
if ($pid == 0) { setpgrp(0,0); exec @ARGV or die; }
local $SIG{ALRM} = sub { kill "KILL", -$pid; exit 124; };
alarm($ENV{TIMEOUT} || 60);
waitpid($pid, 0);
exit($? >> 8);
' -- sbcl --script run.lisp
Signal the child's process group, not just the wrapper's parent PID. A child that has called setpgid/setpgrp is orphaned (not reaped) if only the parent is killed, and keeps holding resources. Put the child in its own group and send TERM/KILL to the group, or let the wrapper live to its deadline and reap the child.
Launch every verification child with a fixed, minimal, non-interactive flag set so results are reproducible and cannot drop into the interactive debugger.
--disable-debugger — never enter the interactive debugger in automation. This does not contradict the root-cause rule: disable it in the batch harness, keep it enabled while actively investigating a single failure interactively.
--no-sysinit --no-userinit — ignore site/user init files so the child does not inherit local state.
Exit with a fully qualified (sb-ext:exit ...) / (sb-ext:quit); an unqualified (quit) can become unsafe after package changes during ASDF loading.
sbcl --no-sysinit --no-userinit --disable-debugger \
--eval '(require :asdf)' \
--load run-one-unit.lisp \
--eval '(sb-ext:exit :code 0)'
Run each file/test in a fresh SBCL process rather than many units in one long-lived image. This both avoids cross-unit compile-state interaction and prevents one stalled unit from blocking the rest.
Whole-suite single-process runs have been observed to hang at function/test boundaries even when each unit passes alone; per-unit fresh processes (chunk size 1) is the stable path. The isolation must be complete — a bootstrap step that itself calls compile-file in the long-lived process defeats a per-file subprocess strategy.
Give each run a private, initialized output-translations / cache root before asdf:load-system. Parallel processes sharing an inherited default FASL cache can race and fail with "Failed to find the TRUENAME of ...fasl". Initialize output translations in the launcher itself, and set a fresh HOME/XDG_CACHE_HOME when reproducing in isolation.
Distinguish a genuine per-file stall from ambient machine contention. When many SBCL sessions run concurrently, baseline load latency can exceed a low per-file timeout and report every file as a timeout. Raise the threshold or reduce concurrency before attributing blame to any single file.
<form_bisect_and_package_preflight>
Techniques for pinning the exact offending form once a stall is confirmed, and for
keeping the reproducer itself from manufacturing false failures.
When narrowing which top-level form stalls compile/load, slice by complete top-level forms, never by raw line ranges. A line-range slice can cut through the middle of a form and produce malformed Lisp that fails to read, masquerading as the original stall (e.g. INPUT-ERROR-IN-LOAD).
Use a read/eval form-trace: read one top-level form at a time, log its head before evaluating and log completion after, and stop on the first form that logs a head but never completes. This respects form boundaries and pinpoints the offending form directly.
;; streaming form-trace: reader sees each in-package before it reads the next form,
;; and the last "head:" without a matching "done:" names the stalling form.
(with-open-file (in path)
(loop for form = (read in nil :eof)
until (eq form :eof)
for head = (and (consp form) (car form))
do (format *error-output* "~&head: ~S~%" head)
(finish-output *error-output*)
(eval form)
(format *error-output* "~&done: ~S~%" head)))
The reader interns every symbol in the current package at read time, before an in-package in the same batch takes effect. Reading a whole file (or a whole --eval) into a list of forms first, then evaluating, interns later symbols in the wrong package and can make package-local functions look undefined — a false failure unrelated to the code under test.
Keep package creation, package switch, and definitions as separate top-level evaluations
(or stream forms so the reader sees in-package before it reads later forms). When a child
process receives a test/symbol name via environment variable or argument, read or resolve
it in the target package, not in CL-USER — otherwise it interns into COMMON-LISP-USER and
the dispatch can miss or hang at the boundary.
Before trusting a "hang", rule out defects in the reproducer: an unbalanced paren in a probe loader can leave a form open so later defuns never become top-level, and a package-mismatched read can fake a missing-symbol error. A malformed harness produces false hangs.
<coverage_measurement_bias>
sb-cover reports low expression coverage for files dominated by top-level defining forms and metadata side effects (defpackage, define-condition, top-level documentation/table assignments), even when the runtime contracts they establish are fully tested. These forms are counted as expressions but are not all attributed as executed by ordinary test runs.
<how_to_apply>Separate genuine runtime gaps from instrumentation bias by comparing a low-coverage file against its shape: definition-heavy files may warrant a few explicit contract tests but need not reach 100%; logic-heavy files are the higher-value targets for additional tests or refactoring. Do not distort public API design solely to satisfy sb-cover on top-level metadata; prefer explicit tests plus a documented exception.</how_to_apply>
sb-cover does not clean its own HTML output directory; after splitting or renaming source files, clear the stale report before reading a new one.
</coverage_measurement_bias>
<performance_profiling>
Standard SBCL performance workflow
(time (my-app:run-once input))
Start with time before introducing complex profiling.
(require :sb-profile)
(sb-profile:profile my-app::hot-fn my-app::other-hot-fn)
(my-app:run-benchmark)
(sb-profile:report)
(sb-profile:unprofile)
Identify hot functions at call-site granularity.
(require :sb-sprof)
(sb-sprof:with-profiling (:max-samples 3000 :report :flat)
(my-app:run-benchmark))
Use when you need lower overhead and broad execution trends.
Apply optimization declarations locally and verify impact
(declaim (optimize (speed 3) (safety 1) (debug 1)))
(defun hot (x y)
(declare (type fixnum x y))
(+ x y))
Avoid safety 0 unless you have hard evidence and strong tests.
<build_and_release>
Executable image generation baseline
(defun main ()
(handler-case
(progn
(my-app:run)
(sb-ext:exit :code 0))
(error (e)
(format *error-output* "fatal: ~a~%" e)
(sb-ext:exit :code 1))))
(sb-ext:save-lisp-and-die "my-app"
:toplevel #'main
:executable t
:compression t)
</example>
<notes>
<item>Always define explicit process exit codes.</item>
<item>Validate ASDF load and tests before image generation.</item>
</notes>
<ecosystem_integration>
In this environment, prefer SLY over SLIME
Assume sly / sly-asdf / sly-macrostep workflows for Emacs integration.
When explaining editor actions, provide SLY-compatible guidance.
Reproducible SBCL execution in Nix environments
nix shell nixpkgs#sbcl
sbcl --version
Pin project environments via shell.nix or flake.nix when needed.
Combine with Qlot for stronger dependency reproducibility.
Simplify implementation management and script execution
ros install sbcl
ros run
ros build app.ros
<decision_tree name="execution_mode_selection">
Which run mode should be selected?
REPL mode (sbcl / --noinform)
--non-interactive + --eval/--script
save-lisp-and-die executable flow
Qlot + Nix (and Roswell when useful)
</decision_tree>
<best_practices>
Do not use --disable-debugger during root-cause analysis; capture backtraces first.
Keep one reproducible command line for before/after fix verification.
Use trace/inspect/describe to convert assumptions into observable evidence.
Require measurement (time/sb-profile/sb-sprof) before performance changes.
Adopt Qlot for projects that need deterministic dependency state.
Design batch jobs with explicit success/failure exit codes.
Avoid SLIME-only advice in SLY-based environments.
Verify every hang observation with a sound fork/wait timeout; an exec-after-alarm wrapper never fires and fabricates false stalls.
Decompose a stalling compile unit into smaller serially-loaded files rather than reaching for per-form workarounds or whole-file evaluator-mode guards.
Bisect compile/load stalls by complete top-level forms, never by line ranges; use a read/eval form-trace to name the offending form.
Run verification one unit per fresh SBCL process (--no-sysinit --no-userinit --disable-debugger) with an isolated FASL cache.
</best_practices>
<anti_patterns>
Disabling debugger before diagnosis removes critical evidence.
Use interactive debugger state first (frames, restarts, object inspection).
Applying optimization declarations without evidence.
Measure hotspots first; optimize only proven bottlenecks.
Swallowing errors with handler-case and hiding root cause.
Preserve diagnostic visibility with logging/rethrow/restart strategies.
Allowing per-machine dependency drift.
Use Qlot (and Nix when appropriate) to lock execution context.
Arming a timeout alarm and then exec'ing SBCL in the same process, which cancels the alarm so the timeout never fires.
Fork the child and wait in the parent; the parent owns the alarm and kills the child's process group on expiry.
Narrowing a compile/load stall by slicing raw line ranges, which can cut through a form and produce malformed Lisp that fails to read.
Bisect by complete top-level forms via a read/eval form-trace that stops on the first form that starts but never completes.
Forcing sb-ext:*evaluator-mode* :interpret over a file to dodge a compile stall and treating it as a durable fix.
Use it only as a scoped diagnostic; the durable fix is a structural decomposition of the compile unit plus fresh verification.
Prioritize observability (backtrace, inspect, trace) during root-cause analysis.
Fix proposals must include reproduction, verification, and failure-mode behavior.
Performance recommendations must be grounded in measured data.
Target SBCL 2.5+ (2.x series); use sb-ext:exit for process exit, not cl:exit or os-exit directly
Confirm ASDF load viability before diving into deeper implementation details.
Select execution mode explicitly from task constraints.
Use save-lisp-and-die with explicit exit-code policy for operational binaries.
Reproduce and record the failure
1. Choose run mode (REPL / script / non-interactive)
Workflow guidance
Step completed
2. Build a minimal reproduction
Workflow guidance
Step completed
3. Capture backtrace and input conditions
Workflow guidance
Step completed
Narrow and validate root-cause hypotheses
1. Observe state with inspect / describe / trace
Workflow guidance
Step completed
2. Define recovery paths using restarts
Workflow guidance
Step completed
3. Test one hypothesis at a time
Workflow guidance
Step completed
Confirm fix quality and regression safety
1. Re-run the exact reproduction command
Workflow guidance
Step completed
2. Run asdf:test-system
Workflow guidance
Step completed
3. Profile if performance side effects are possible
Workflow guidance
Step completed
<error_escalation inherits="core-patterns#error_escalation">
Minor SBCL warning during compilation
Unhandled condition or ASDF load failure
Runtime error or heap exhaustion in production image
Memory corruption or undefined behavior in FFI boundary
</error_escalation>
Debugging guidance must preserve the sequence: reproduce → observe → verify
Keep SLY compatibility in editor integration guidance
Provide Nix/Qlot reproducibility guidance when environment drift is likely
Unmeasured optimization
Layering workaround code without identifying root cause
<related_skills>
CLOS/ASDF/condition-system foundations
Pinned SBCL runtime environments with nix shell/flake
Evidence-driven root-cause methodology
Automated checks and CI quality discipline
</related_skills>
<related_agents>
Locate code patterns and references in this skill domain
Review implementation quality against this skill guidance
Analyze code complexity and suggest refactoring improvements
</related_agents>