From 110093e41c2d73e6c8ad4a212f931b1bd1d9f694 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Mon, 27 Nov 2023 10:18:02 -0700 Subject: [PATCH 01/31] Add job for Apple Silicon MacOS build --- .github/workflows/build.yml | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 714e388..193d916 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,3 +161,85 @@ jobs: with: name: macos path: ${{ github.workspace }}/artifacts/* + + build-macos-arm: + name: Build Apple-Silicon libquil.dylib + runs-on: macos-latest-xlarge + + steps: + + - uses: actions/checkout@v2 + with: + path: "libquil" + + - name: Cache SBCL + id: cache-sbcl-macos + uses: actions/cache@v3 + with: + path: | + /usr/local/lib/libsbcl.so + /usr/local/lib/sbcl + /usr/local/bin/sbcl + ~/quicklisp + ~/.sbclrc + key: ${{ runner.os }}-build-${{ hashFiles('**/macos-versions' )}} + + - uses: Homebrew/actions/setup-homebrew@master + name: Setup homebrew + + - if: steps.cache-sbcl-macos.outputs.cache-hit != 'true' + name: Install SBCL, libraries, and quicklisp + run: | + brew install sbcl git + git clone --branch sbcl-2.2.4 https://git.code.sf.net/p/sbcl/sbcl /tmp/sbcl + cd /tmp/sbcl && sudo sh make.sh --without-compact-instance-header --without-immobile-space --without-immobile-code && sudo sh make-shared-library.sh + brew remove -f sbcl + sudo sh install.sh + sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so + + brew install wget + wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' + sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/2022-04-01/distinfo.txt\")" + sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' + echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc + rm -f /tmp/quicklisp.lisp + cat ~/.sbclrc + + - name: Pull Lisp dependencies + run: | + git clone https://github.com/notmgsk/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout bffea7fdb972cc9b3a50b790246b6a3143c88c7a + git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 + git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl + git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation + git clone https://github.com/quil-lang/sbcl-librarian.git $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout 04f7e390c777084ac43b8df9e90593a041da2381 + + - name: Install quilc dependencies + run: brew install lapack openblas libffi gfortran + + - name: Build libquil.dylib + run: | + cd $GITHUB_WORKSPACE/libquil + ls + sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' + sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' + make + + - name: ls + run: | + ls $GITHUB_WORKSPACE/libquil/libquil.dylib + + - name: Test + run: | + cd $GITHUB_WORKSPACE/libquil/examples/qvm && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil make test + cd $GITHUB_WORKSPACE/libquil/examples/quilc && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil make test + + - name: Package files + run: | + mkdir -p $GITHUB_WORKSPACE/artifacts/libquil && cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.dylib $GITHUB_WORKSPACE/libquil/libquil.core /usr/local/lib/libsbcl.so $GITHUB_WORKSPACE/artifacts/libquil + + - name: Store artifact + uses: actions/upload-artifact@v3 + with: + name: macos + path: ${{ github.workspace }}/artifacts/* + From 07373f92ae1b69a14467d73303e116fc84e51bf9 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 13:52:04 -0600 Subject: [PATCH 02/31] Claude gets it working, maybe --- .gitignore | 2 ++ Makefile | 60 ++++++++++++++++++++++++++++++++++-- README.md | 41 +++++++++++++++++++++++- src/build-image.lisp | 31 +++++++++++++++++-- src/quilc/api.lisp | 2 +- src/quilc/compile.lisp | 6 ++++ src/qvm/qvm-app-imports.lisp | 8 +++-- 7 files changed, 141 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 9e5309c..3387c30 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ libquil.c libquil.core libquil.h libquil.so +libquil.dylib +libquil.py diff --git a/Makefile b/Makefile index c2d9223..8720580 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ .PHONY: all clean -OS:=$(shell uname -s) +OS := $(shell uname -s) + +SBCL ?= sbcl + ifeq ($(OS), Darwin) LIBQUIL_TARGET = libquil.dylib CCFLAGS = -dynamiclib @@ -9,13 +12,64 @@ else CCFLAGS = -shared endif +# The library needs to embed the SBCL runtime. `make.sh` does not build a +# linkable runtime, so it has to be produced separately by running +# `make-shared-library.sh` in the SBCL source tree, and neither `install.sh` nor +# most package managers install the result. Look in the usual places for it, and +# let the user point at it directly with e.g. `make LIBSBCL=/path/to/libsbcl.a`. +# +# Which file to expect depends on the platform: on arm64 macOS the SBCL runtime +# is only built as a static archive (see SBCL's Config.arm64-darwin), so a +# shared libsbcl does not exist there at all. +SBCL_CORE_DIR := $(dir $(shell $(SBCL) --noinform --no-sysinit --no-userinit --non-interactive \ + --eval '(princ (namestring sb-ext:*core-pathname*))' 2>/dev/null)) +LIBSBCL_SEARCH_DIRS := $(SBCL_HOME) $(SBCL_CORE_DIR) /usr/local/lib /usr/lib /opt/homebrew/lib +LIBSBCL_CANDIDATES := \ + $(foreach dir,$(LIBSBCL_SEARCH_DIRS),\ + $(dir)/libsbcl.a $(dir)/libsbcl.dylib $(dir)/libsbcl.so) + +LIBSBCL ?= $(firstword $(wildcard $(LIBSBCL_CANDIDATES))) + +# A static runtime must be linked whole: the entry points the generated +# libquil.c calls are reached only through the core, so without this the linker +# drops most of the archive. Its own dependencies have to be named explicitly +# too, since an archive records none. A shared runtime carries both properties +# already and just needs to be linked normally. +SBCL_STATIC_DEPS ?= $(shell pkg-config --libs libzstd 2>/dev/null || echo -lzstd) -lm -ldl -lpthread + +ifeq ($(suffix $(LIBSBCL)), .a) +ifeq ($(OS), Darwin) + LIBSBCL_LDFLAGS = -Wl,-force_load,$(LIBSBCL) $(SBCL_STATIC_DEPS) +else + LIBSBCL_LDFLAGS = -Wl,--whole-archive $(LIBSBCL) -Wl,--no-whole-archive $(SBCL_STATIC_DEPS) +endif +else ifeq ($(OS), Darwin) +# Link by path rather than -lsbcl: make-shared-library.sh names its output +# libsbcl.so even on macOS, and the -l flag only ever looks for libsbcl.dylib or +# libsbcl.a. + LIBSBCL_LDFLAGS = $(LIBSBCL) +else + LIBSBCL_LDFLAGS = -L$(dir $(LIBSBCL)) -lsbcl +endif + all: $(LIBQUIL_TARGET) libquil.core libquil.c libquil.h libquil.py: src/libquil.lisp src/qvm/*.lisp src/quilc/*.lisp - sbcl --dynamic-space-size 8192 --load "src/build-image.lisp" + $(SBCL) --dynamic-space-size 8192 --load "src/build-image.lisp" $(LIBQUIL_TARGET): libquil.core libquil.c - gcc $(CCFLAGS) -o $@ libquil.c -lsbcl +ifeq ($(LIBSBCL),) + @echo "error: no linkable SBCL runtime found." >&2 + @echo "Searched for libsbcl.a, libsbcl.dylib and libsbcl.so in:" >&2 + @$(foreach dir,$(LIBSBCL_SEARCH_DIRS),echo " $(dir)" >&2;) + @echo "" >&2 + @echo "Build one from an SBCL source tree of the SAME version as $(SBCL):" >&2 + @echo " sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh" >&2 + @echo "then point make at the result, e.g.:" >&2 + @echo " make LIBSBCL=/path/to/sbcl/src/runtime/libsbcl.a" >&2 + @exit 1 +endif + $(CC) $(CCFLAGS) -o $@ libquil.c $(LIBSBCL_LDFLAGS) clean: rm -f libquil.so libquil.c libquil.h libquil.core libquil.py libquil.dylib example diff --git a/README.md b/README.md index 703950c..40f79e5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ We provide pre-built binaries for the following systems - Linux x64 (tested specifically on Ubuntu) - macOS x64 -Note that ARM is not yet supported on any of the above. +Note that we do not publish pre-built binaries for ARM. ARM machines, including +Apple Silicon, are supported when building from source — see [Building from +source](#building-from-source). ## Requirements @@ -59,6 +61,43 @@ replacing `` with the desired version, e.g. `0.3.0`. If you would like to manually install the library (for example in the case where you want to install the library to a non-standard location), find the appropriate version and operating system from the [releases page](https://github.com/rigetti/libquil/releases). Within the `.zip` archive you will find the library and header files that are required to use the library. Move these into your file system. +## Building from source + +Building requires an SBCL that has a *linkable runtime* — a `libsbcl.a` or +`libsbcl.so`/`libsbcl.dylib` that gets embedded into the library. Neither +`make.sh` nor most package managers produce one (Homebrew's `sbcl` bottle, for +instance, does not), so it has to be built from an SBCL source tree of the same +version as the `sbcl` used for the rest of the build: + +```bash +sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh && sh install.sh +``` + +`install.sh` places the runtime in SBCL's home directory, where the `Makefile` +finds it automatically. To use one from elsewhere, pass it explicitly: + +```bash +make LIBSBCL=/path/to/sbcl/src/runtime/libsbcl.a +``` + +Note that on arm64 macOS, SBCL only builds its runtime as the static +`libsbcl.a`; there is no shared `libsbcl.dylib` on that platform. The `Makefile` +handles either form. + +The Lisp dependencies (`quilc`, `qvm`, `magicl`, `sbcl-librarian`) are expected +in your Quicklisp local-projects directory. Then: + +```bash +make +``` + +### Linear algebra backend on arm64 macOS + +Homebrew's reference `lapack` computes incorrect eigenvectors on arm64, which +surfaces as `Could not find diagonalizer for matrix ... after 16 attempts` +during compilation. Install OpenBLAS (`brew install openblas`) and ensure +`magicl` loads it in preference to `lapack`. + # C API Reference ## Libquil functions and types diff --git a/src/build-image.lisp b/src/build-image.lisp index 95833a5..0656060 100644 --- a/src/build-image.lisp +++ b/src/build-image.lisp @@ -1,7 +1,34 @@ (require '#:asdf) -(asdf:load-system '#:sbcl-librarian) -(asdf:load-system '#:libquil) +;; libquil depends on systems that come from Quicklisp (cffi, bordeaux-threads, +;; ...). ASDF alone will not fetch those, so prefer Quicklisp when it is +;; available and fall back to plain ASDF for setups that vendor the +;; dependencies themselves. +(let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))) + (when (and (null (find-package '#:quicklisp)) + (probe-file quicklisp-init)) + (load quicklisp-init))) + +(defun load-system (system) + ;; libquil intentionally redefines some of the alien callables that + ;; SBCL-LIBRARIAN generates, in order to give them types SBCL-LIBRARIAN cannot + ;; express yet (see the definition of quilc_compile_protoquil). SBCL signals a + ;; continuable error for that; taking the CONTINUE restart installs the new + ;; definition, which is what an interactive build does. Without this the build + ;; drops into the debugger and cannot run unattended. + (handler-bind ((error + (lambda (condition) + (let ((restart (find-restart 'continue condition))) + (when (and restart + (search "redefine alien callable" + (princ-to-string condition))) + (invoke-restart restart)))))) + (if (find-package '#:quicklisp) + (funcall (read-from-string "quicklisp:quickload") system) + (asdf:load-system system)))) + +(load-system '#:sbcl-librarian) +(load-system '#:libquil) (in-package #:libquil) diff --git a/src/quilc/api.lisp b/src/quilc/api.lisp index 0941969..5bbf90f 100644 --- a/src/quilc/api.lisp +++ b/src/quilc/api.lisp @@ -135,7 +135,7 @@ (("parse_quil" cl-quil.frontend:safely-parse-quil) quil-program ((source :string))) (("program_memory_type" parsed-program-get-memory-region-type) :void ((program quil-program) (region-name :string) (region-type-ptr :pointer))) (("print_program" cl-quil.frontend:print-parsed-program) :void ((program quil-program))) - (("compile_quil" cl-quil:compiler-hook) quil-program ((program quil-program) (chip-spec chip-specification))) + (("compile_quil" compile-quil) quil-program ((program quil-program) (chip-spec chip-specification))) (("compilation_metadata_len" compilation-metadata-len) :int ((metadata compilation-metadata))) (("compilation_metadata_get_final_rewiring" compilation-metadata-get-final-rewiring) :void diff --git a/src/quilc/compile.lisp b/src/quilc/compile.lisp index 76f1196..751b7de 100644 --- a/src/quilc/compile.lisp +++ b/src/quilc/compile.lisp @@ -68,6 +68,12 @@ (coerce runtime 'double-float))) present-p)) +(defun compile-quil (parsed-program chip-specification) + ;; As in COMPILE-PROTOQUIL, the LAPACK library for macOS will sometimes hit a + ;; division-by-zero. Mask those interrupts so they can be handled in Lisp. + (magicl:with-blapack + (cl-quil:compiler-hook parsed-program chip-specification))) + (defun compile-protoquil (parsed-program chip-specification metadata-ptr) (multiple-value-bind (compiled-program metadata) (magicl:with-blapack (process-program parsed-program chip-specification :protoquil t)) diff --git a/src/qvm/qvm-app-imports.lisp b/src/qvm/qvm-app-imports.lisp index c601ffb..a918f6a 100644 --- a/src/qvm/qvm-app-imports.lisp +++ b/src/qvm/qvm-app-imports.lisp @@ -12,14 +12,18 @@ ) (defun compress-program-qubits (quil) + "Remap the qubits of QUIL to a minimal sequential set from 0 to (num-qubits-used - 1). Return two values: the processed Quil code and the mapping vector. + +The mapping vector V specifies that the qubit as specified in the program V[i] has been mapped to qubit i." (let* ((quil (cl-quil:copy-instance quil)) + (mapping (cl-quil::compute-qubit-mapping quil)) (trivial-mapping-p - (loop :for x :across (cl-quil::compute-qubit-mapping quil) + (loop :for x :across mapping :for i :from 0 :always (= x i)))) (unless trivial-mapping-p (cl-quil::transform 'cl-quil::compress-qubits quil)) - quil)) + (values quil mapping))) (defun get-random-state (arg) (etypecase arg From 32e35588fbb9d0d8825649afd6d96937edb6706b Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 15:05:43 -0600 Subject: [PATCH 03/31] fix README notes on aarch64 --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 40f79e5..2b1fd2a 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,7 @@ We provide pre-built binaries for the following systems - Linux x64 (tested specifically on Ubuntu) - macOS x64 - -Note that we do not publish pre-built binaries for ARM. ARM machines, including -Apple Silicon, are supported when building from source — see [Building from -source](#building-from-source). +- macOS aarch64 ## Requirements @@ -80,7 +77,7 @@ finds it automatically. To use one from elsewhere, pass it explicitly: make LIBSBCL=/path/to/sbcl/src/runtime/libsbcl.a ``` -Note that on arm64 macOS, SBCL only builds its runtime as the static +Note that on aarch64 macOS, SBCL only builds its runtime as the static `libsbcl.a`; there is no shared `libsbcl.dylib` on that platform. The `Makefile` handles either form. @@ -91,9 +88,9 @@ in your Quicklisp local-projects directory. Then: make ``` -### Linear algebra backend on arm64 macOS +### Linear algebra backend on aarch64 macOS -Homebrew's reference `lapack` computes incorrect eigenvectors on arm64, which +Homebrew's reference `lapack` computes incorrect eigenvectors on aarch64, which surfaces as `Could not find diagonalizer for matrix ... after 16 attempts` during compilation. Install OpenBLAS (`brew install openblas`) and ensure `magicl` loads it in preference to `lapack`. From 975064e9a5eb8b4aa4adfcdf29f34ca3b4cfb402 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 15:06:11 -0600 Subject: [PATCH 04/31] GHA fixes --- .github/workflows/build.yml | 26 ++++++++++++++------------ .github/workflows/release.yml | 6 +++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 193d916..e38296d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,13 +13,13 @@ jobs: - name: root suid tar run: sudo chown root /bin/tar && sudo chmod u+s /bin/tar - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: path: "libquil" - name: Cache SBCL id: cache-sbcl - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /usr/local/lib/libsbcl.so @@ -76,7 +76,7 @@ jobs: mkdir -p $GITHUB_WORKSPACE/artifacts/libquil && cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.so $GITHUB_WORKSPACE/libquil/libquil.core /usr/local/lib/libsbcl.so $GITHUB_WORKSPACE/artifacts/libquil - name: Store artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linux-amd64 path: ${{ github.workspace }}/artifacts/* @@ -87,13 +87,13 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: path: "libquil" - name: Cache SBCL id: cache-sbcl-macos - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /usr/local/lib/libsbcl.so @@ -101,7 +101,7 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc - key: ${{ runner.os }}-build-${{ hashFiles('**/macos-versions' )}} + key: ${{ runner.os }}-${{ runner.arch }}-build-${{ hashFiles('**/macos-versions' )}} - uses: Homebrew/actions/setup-homebrew@master name: Setup homebrew @@ -157,7 +157,7 @@ jobs: mkdir -p $GITHUB_WORKSPACE/artifacts/libquil && cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.dylib $GITHUB_WORKSPACE/libquil/libquil.core /usr/local/lib/libsbcl.so $GITHUB_WORKSPACE/artifacts/libquil - name: Store artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: macos path: ${{ github.workspace }}/artifacts/* @@ -168,13 +168,13 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: path: "libquil" - name: Cache SBCL id: cache-sbcl-macos - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /usr/local/lib/libsbcl.so @@ -182,7 +182,7 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc - key: ${{ runner.os }}-build-${{ hashFiles('**/macos-versions' )}} + key: ${{ runner.os }}-${{ runner.arch }}-build-${{ hashFiles('**/macos-versions' )}} - uses: Homebrew/actions/setup-homebrew@master name: Setup homebrew @@ -238,8 +238,10 @@ jobs: mkdir -p $GITHUB_WORKSPACE/artifacts/libquil && cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.dylib $GITHUB_WORKSPACE/libquil/libquil.core /usr/local/lib/libsbcl.so $GITHUB_WORKSPACE/artifacts/libquil - name: Store artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: macos + # Must differ from the x64 macOS job artifact name: upload-artifact + # v4 rejects two artifacts sharing a name within a single run. + name: macos-arm64 path: ${{ github.workspace }}/artifacts/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1dbdde4..9c4990a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,12 +25,12 @@ jobs: git config --global user.name "${{ github.triggering_actor }}" git config --global user.email "${{ github.triggering_actor }}@users.noreply.github.com" - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download linux artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-amd64 path: artifacts/ @@ -40,7 +40,7 @@ jobs: cd artifacts && zip linux-amd64.zip libquil/libsbcl.so libquil/libquil.so libquil/libquil.core libquil/libquil.h - name: Download macos artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: macos path: artifacts/ From b659ea153c31bc799351263011810c778c700270 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 15:12:16 -0600 Subject: [PATCH 05/31] Drop Intel macOS support Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 93 +++-------------------------------- .github/workflows/release.yml | 19 ++++--- README.md | 9 ++-- install.sh | 26 +++++++--- knope.toml | 2 +- 5 files changed, 45 insertions(+), 104 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e38296d..162aed4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,92 +79,13 @@ jobs: uses: actions/upload-artifact@v4 with: name: linux-amd64 - path: ${{ github.workspace }}/artifacts/* - - build-macos: - name: Build libquil.dylib - runs-on: macos-latest - - steps: - - - uses: actions/checkout@v4 - with: - path: "libquil" - - - name: Cache SBCL - id: cache-sbcl-macos - uses: actions/cache@v4 - with: - path: | - /usr/local/lib/libsbcl.so - /usr/local/lib/sbcl - /usr/local/bin/sbcl - ~/quicklisp - ~/.sbclrc - key: ${{ runner.os }}-${{ runner.arch }}-build-${{ hashFiles('**/macos-versions' )}} - - - uses: Homebrew/actions/setup-homebrew@master - name: Setup homebrew - - - if: steps.cache-sbcl-macos.outputs.cache-hit != 'true' - name: Install SBCL, libraries, and quicklisp - run: | - brew install sbcl git - git clone --branch x86-null-tn https://git.code.sf.net/p/sbcl/sbcl /tmp/sbcl - cd /tmp/sbcl && sudo sh make.sh --without-compact-instance-header --without-immobile-space --without-immobile-code && sudo sh make-shared-library.sh - brew remove -f sbcl - sudo sh install.sh - sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so - - brew install wget - wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' - sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/2022-04-01/distinfo.txt\")" - sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' - echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc - rm -f /tmp/quicklisp.lisp - cat ~/.sbclrc - - - name: Pull Lisp dependencies - run: | - git clone https://github.com/notmgsk/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout bffea7fdb972cc9b3a50b790246b6a3143c88c7a - git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 - git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl - git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation - git clone https://github.com/quil-lang/sbcl-librarian.git $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout 04f7e390c777084ac43b8df9e90593a041da2381 - - - name: Install quilc dependencies - run: brew install lapack openblas libffi gfortran - - - name: Build libquil.dylib - run: | - cd $GITHUB_WORKSPACE/libquil - ls - sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' - sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' - make - - - name: ls - run: | - ls $GITHUB_WORKSPACE/libquil/libquil.dylib - - - name: Test - run: | - cd $GITHUB_WORKSPACE/libquil/examples/qvm && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil make test - cd $GITHUB_WORKSPACE/libquil/examples/quilc && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil make test - - - name: Package files - run: | - mkdir -p $GITHUB_WORKSPACE/artifacts/libquil && cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.dylib $GITHUB_WORKSPACE/libquil/libquil.core /usr/local/lib/libsbcl.so $GITHUB_WORKSPACE/artifacts/libquil - - - name: Store artifact - uses: actions/upload-artifact@v4 - with: - name: macos - path: ${{ github.workspace }}/artifacts/* + # A directory (not a glob): upload-artifact v4 preserves structure + # relative to it, keeping the libquil/ prefix the release job unzips. + path: ${{ github.workspace }}/artifacts build-macos-arm: name: Build Apple-Silicon libquil.dylib - runs-on: macos-latest-xlarge + runs-on: macos-latest steps: @@ -240,8 +161,8 @@ jobs: - name: Store artifact uses: actions/upload-artifact@v4 with: - # Must differ from the x64 macOS job artifact name: upload-artifact - # v4 rejects two artifacts sharing a name within a single run. name: macos-arm64 - path: ${{ github.workspace }}/artifacts/* + # A directory (not a glob): upload-artifact v4 preserves structure + # relative to it, keeping the libquil/ prefix the release job unzips. + path: ${{ github.workspace }}/artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c4990a..033f220 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,31 +29,34 @@ jobs: with: fetch-depth: 0 + # Each artifact is downloaded into its own directory: the two macOS + # artifacts contain identically named files, so a shared directory would + # have one overwrite the other. - name: Download linux artifacts uses: actions/download-artifact@v4 with: name: linux-amd64 - path: artifacts/ + path: artifacts/linux-amd64/ - name: Archive linux artifacts run: | - cd artifacts && zip linux-amd64.zip libquil/libsbcl.so libquil/libquil.so libquil/libquil.core libquil/libquil.h + cd artifacts/linux-amd64 && zip ../linux-amd64.zip libquil/libsbcl.so libquil/libquil.so libquil/libquil.core libquil/libquil.h - - name: Download macos artifacts + - name: Download macos arm64 artifacts uses: actions/download-artifact@v4 with: - name: macos - path: artifacts/ + name: macos-arm64 + path: artifacts/macos-arm64/ - - name: Archive linux artifacts + - name: Archive macos arm64 artifacts run: | - cd artifacts && zip macos.zip libquil/libsbcl.so libquil/libquil.dylib libquil/libquil.core libquil/libquil.h + cd artifacts/macos-arm64 && zip ../macos-arm64.zip libquil/libsbcl.so libquil/libquil.dylib libquil/libquil.core libquil/libquil.h - name: List artifacts run: | ls -R artifacts/ unzip -l artifacts/linux-amd64.zip - unzip -l artifacts/macos.zip + unzip -l artifacts/macos-arm64.zip - name: Install Knope uses: knope-dev/action@v2.0.0 diff --git a/README.md b/README.md index 2b1fd2a..c2b83e5 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,11 @@ We provide pre-built binaries for the following systems - Linux x64 (tested specifically on Ubuntu) -- macOS x64 -- macOS aarch64 +- macOS aarch64 (Apple Silicon) + +Intel macOS binaries are no longer published. Other platforms, including Intel +macOS, can be built from source — see [Building from +source](#building-from-source). ## Requirements @@ -23,7 +26,7 @@ These libraries are required by `libquil`: On systems which use `apt` to install packages (e.g. Ubuntu), these libraries can be installed with the command ``` -sudo apt install libblas-dev libffi-dev libffi7 liblapack-dev libz-dev +sudo apt install libblas-dev libffi-dev liblapack-dev libz-dev ``` On systems which use `brew` to install packages (e.g macOS), these libraries can be installed with the command diff --git a/install.sh b/install.sh index 9012dcf..d4e4061 100755 --- a/install.sh +++ b/install.sh @@ -7,10 +7,6 @@ err() { exit 1 } -if [[ "$(uname -p)" =~ "arm" ]]; then - err "Unsupported CPU architecture: $(uname -p)." -fi - if [[ -n "${1-}" ]] then LIBQUIL_URL_PREFIX="https://github.com/rigetti/libquil/releases/download/v${1}" @@ -19,13 +15,31 @@ else fi OS="$(uname)" +ARCH="$(uname -m)" if [[ "${OS}" == "Linux" ]] then IS_LINUX=1 - LIBQUIL_RELEASE_FILE="linux-amd64.zip" + case "${ARCH}" in + x86_64 | amd64) + LIBQUIL_RELEASE_FILE="linux-amd64.zip" + ;; + *) + err "Unsupported CPU architecture for Linux: ${ARCH}. Only x86_64 is supported." \ + "You can build libquil from source; see https://github.com/rigetti/libquil#building-from-source" + ;; + esac elif [[ "${OS}" == "Darwin" ]] then - LIBQUIL_RELEASE_FILE="macos.zip" + case "${ARCH}" in + arm64 | aarch64) + LIBQUIL_RELEASE_FILE="macos-arm64.zip" + ;; + *) + err "Unsupported CPU architecture for macOS: ${ARCH}. Only Apple Silicon (arm64) is supported." \ + "Intel macOS builds are no longer published. You can build libquil from source; see" \ + "https://github.com/rigetti/libquil#building-from-source" + ;; + esac else err "Unsupported operating system. Supported operating systems are Linux and macOS." fi diff --git a/knope.toml b/knope.toml index 78193a2..366aa5d 100644 --- a/knope.toml +++ b/knope.toml @@ -7,7 +7,7 @@ changelog = "CHANGELOG.md" path = "artifacts/linux-amd64.zip" [[package.assets]] -path = "artifacts/macos.zip" +path = "artifacts/macos-arm64.zip" [[workflows]] name = "release" From ff1fb635676267ec31a37d659a9d5579eb42c216 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 16:32:50 -0600 Subject: [PATCH 06/31] small fixes --- .github/workflows/build.yml | 9 ++-- src/quilc/compile.lisp | 2 +- src/quilc/quilc-imports.lisp | 86 +++++++++++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 162aed4..00753bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,14 +49,17 @@ jobs: - name: Pull Lisp dependencies run: | - git clone https://github.com/notmgsk/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout bffea7fdb972cc9b3a50b790246b6a3143c88c7a + git clone https://github.com/quil-lang/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout 3d83b665b3addc33be2f696b1cdebc0c4c5a04f9 git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation git clone https://github.com/quil-lang/sbcl-librarian.git $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout 04f7e390c777084ac43b8df9e90593a041da2381 - name: Install quilc dependencies - run: sudo apt update && sudo apt install -y libblas-dev libffi-dev libffi7 liblapack-dev libz-dev gfortran + # libffi-dev pulls in whichever libffi runtime the image ships; naming + # it explicitly breaks whenever the runner image moves (libffi7 does not + # exist on ubuntu 24.04, which ubuntu-latest now resolves to). + run: sudo apt update && sudo apt install -y libblas-dev libffi-dev liblapack-dev libz-dev gfortran - name: Build libquil.so run: | @@ -128,7 +131,7 @@ jobs: - name: Pull Lisp dependencies run: | - git clone https://github.com/notmgsk/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout bffea7fdb972cc9b3a50b790246b6a3143c88c7a + git clone https://github.com/quil-lang/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout 3d83b665b3addc33be2f696b1cdebc0c4c5a04f9 git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation diff --git a/src/quilc/compile.lisp b/src/quilc/compile.lisp index 751b7de..813d8ed 100644 --- a/src/quilc/compile.lisp +++ b/src/quilc/compile.lisp @@ -81,5 +81,5 @@ (let ((handle (sbcl-librarian::make-handle metadata))) (setf (sb-alien:deref metadata-ptr) handle))) - (cl-quil.frontend::transform 'cl-quil.frontend::process-protoquil compiled-program) + (cl-quil.frontend::transform 'process-protoquil compiled-program) compiled-program)) diff --git a/src/quilc/quilc-imports.lisp b/src/quilc/quilc-imports.lisp index 9095569..0aaf033 100644 --- a/src/quilc/quilc-imports.lisp +++ b/src/quilc/quilc-imports.lisp @@ -32,6 +32,90 @@ :documentation "The git hash of the quilc repo.") ;;; borrowed from quilc: app/src/entry-point.lisp +;;; +;;; PROCESS-PROTOQUIL and STRIP-FINAL-HALT-RESPECTING-REWIRINGS live in quilc's +;;; *application*, which we do not depend on, so they are vendored here rather +;;; than requiring a quilc fork that exposes them from the library. +;;; TODO(https://github.com/quil-lang/quilc/pull/933): remove these once the logic is upstreamed + +(cl-quil.frontend::define-transform process-protoquil (process-protoquil) + "Removes HALT, DEFCIRCUIT, and DEFGATE instructions.") + +(defun process-protoquil (parsed-program) + (setf (cl-quil.frontend::parsed-program-circuit-definitions parsed-program) nil + (cl-quil.frontend::parsed-program-gate-definitions parsed-program) nil) + + ;; if we're supposed to output protoQuil, we also need to + ;; strip the final HALT instructions from the output + (setf (cl-quil:parsed-program-executable-code parsed-program) + (strip-final-halt-respecting-rewirings parsed-program))) + +(defun strip-final-halt-respecting-rewirings (processed-program) + "Remove the final HALT instruction, if any, from PROCESSED-PROGRAM, retaining any attached rewiring comments." + (let* ((instructions (cl-quil:parsed-program-executable-code processed-program)) + (last-instruction (and (plusp (length instructions)) + (cl-quil::nth-instr 0 processed-program :from-end t))) + (penultimate-instruction (and (< 1 (length instructions)) + (cl-quil::nth-instr 1 processed-program :from-end t))) + (must-transfer-comment-p (and (not (null penultimate-instruction)) + (cl-quil.frontend::comment last-instruction)))) + + (unless (cl-quil::haltp last-instruction) + (return-from strip-final-halt-respecting-rewirings instructions)) + + (when must-transfer-comment-p + ;; Transfer the rewiring comment from LAST-INSTRUCTION to + ;; PENULTIMATE-INSTRUCTION. + (multiple-value-bind (last-entering last-exiting) + (cl-quil::instruction-rewirings last-instruction) + (multiple-value-bind (penultimate-entering penultimate-exiting) + (cl-quil::instruction-rewirings penultimate-instruction) + (flet ((assert-rewirings-compatible (rewiring-type last-rewiring penultimate-rewiring) + ;; This bit of hoop-jumping guards against the + ;; unlikely event that both PENULTIMATE-INSTRUCTION + ;; and LAST-INSTRUCTION have rewiring comments + ;; attached which might be incompatible. We check + ;; to ensure that either one of the rewirings is + ;; NULL, or else they are EQUALP and can safely be + ;; merged. + (assert (or (or (null last-rewiring) + (null penultimate-rewiring)) + (equalp last-rewiring penultimate-rewiring)) + () + "Failed to strip final HALT. Instructions have incompatible ~A rewirings:~@ + LAST: ~A ~A~@ + PREV: ~A ~A" + rewiring-type last-instruction last-rewiring + penultimate-instruction penultimate-rewiring))) + (assert-rewirings-compatible ':ENTERING last-entering penultimate-entering) + (assert-rewirings-compatible ':EXITING last-exiting penultimate-exiting)) + ;; Consider the following cases for the :ENTERING rewirings + ;; (the same case analysis applies to the :EXITING rewiring + ;; pair as well). + ;; + ;; 1) If both the rewirings are non-NIL, then the + ;; ASSERT-REWIRINGS-COMPATIBLE check above guarantees + ;; that they are EQUALP, and it doesn't matter which one + ;; we select. + ;; + ;; 2) If only one is non-NIL, the OR selects it. + ;; + ;; 3) If both are NIL, then MAKE-REWIRING-COMMENT just + ;; ignores that keyword argument, and returns an :EXITING + ;; rewiring. + ;; + ;; Finally, (COMMENT LAST-INSTRUCTION) is non-NIL (otherwise + ;; MUST-TRANSFER-COMMENT-P would be NIL), so at least one of + ;; LAST-ENTERING and LAST-EXITING is non-NIL, which means + ;; that at least one of the :ENTERING and :EXITING keyword + ;; args to MAKE-REWIRING-COMMENT is non-NIL and hence the + ;; call will produce a rewiring comment. + (setf (cl-quil.frontend::comment penultimate-instruction) + (cl-quil::make-rewiring-comment :entering (or last-entering penultimate-entering) + :exiting (or last-exiting penultimate-exiting)))))) + + ;; Strip the final HALT instruction. + (subseq instructions 0 (1- (length instructions))))) (defun process-program (program chip-specification &key @@ -63,7 +147,7 @@ Returns a values tuple (PROCESSED-PROGRAM, STATISTICS), where PROCESSED-PROGRAM (cl-quil:compiler-hook program chip-specification :protoquil protoquil :destructive t) (when protoquil - (cl-quil.frontend::transform 'cl-quil::process-protoquil processed-program) + (cl-quil.frontend::transform 'process-protoquil processed-program) ;; Compute statistics for protoquil program (compute-statistics processed-program chip-specification statistics :gate-whitelist gate-whitelist :gate-blacklist gate-blacklist) From 50f53b912a7d8e1a5a2af149385226ecc7a90f65 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 17:02:13 -0600 Subject: [PATCH 07/31] feat!: build against modern sbcl-librarian Moves off the 2023 sbcl-librarian pin, which also unblocks modern SBCL. libquil is now the generated C bindings only; the Lisp image lives in a core loaded by the libsbcl_librarian runtime, whose constructor initializes Lisp when it is loaded. The explicit init(core) call is gone, as is libquil's hand-rolled error handling. libquil keeps shipping a core rather than the FASL bundles that CREATE-FASL-LIBRARY-CMAKE-PROJECT produces. FASL bundles are re-loaded on every startup, re-running load-time code, and cl-quil resolves stdgates.quil through ASDF:SYSTEM-RELATIVE-PATHNAME at load time -- so a FASL build only runs where quilc's source tree sits at the path recorded when it was built. A core evaluates that once, at build time. build-image.lisp emits three things from one image: libquil's bindings, the runtime's bindings, and the core exporting both sets of callables. The Makefile builds the runtime too, so plain 'make' still yields a usable artifact. Errors move to the runtime's get_error_message/lisp_err_t. libquil redefines default-error-map so ordinary bad input reports as LISP_ERR_FAILURE rather than an internal bug, handling T rather than CL:ERROR because cl-quil signals conditions that are not subtypes of ERROR. REARCHITECTURE.md records the decisions. Needs three sbcl-librarian fixes, on its fix-secondary-system-bundles branch. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 2 + Makefile | 123 ++++++++++++++----------- REARCHITECTURE.md | 207 +++++++++++++++++++++++++++++++++++++++++++ src/build-image.lisp | 86 ++++++++++++++---- src/libquil.lisp | 50 ++++++----- src/quilc/api.lisp | 9 +- src/qvm/api.lisp | 2 +- 7 files changed, 384 insertions(+), 95 deletions(-) create mode 100644 REARCHITECTURE.md diff --git a/.gitignore b/.gitignore index 3387c30..9fad6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ libquil.h libquil.so libquil.dylib libquil.py +build/ +runtime/ diff --git a/Makefile b/Makefile index 8720580..b2c8c37 100644 --- a/Makefile +++ b/Makefile @@ -1,75 +1,94 @@ -.PHONY: all clean +.PHONY: all clean runtime OS := $(shell uname -s) SBCL ?= sbcl +CC ?= cc ifeq ($(OS), Darwin) - LIBQUIL_TARGET = libquil.dylib - CCFLAGS = -dynamiclib + SHARED_SUFFIX = .dylib + SHARED_FLAGS = -dynamiclib else - LIBQUIL_TARGET = libquil.so - CCFLAGS = -shared + SHARED_SUFFIX = .so + SHARED_FLAGS = -shared endif -# The library needs to embed the SBCL runtime. `make.sh` does not build a -# linkable runtime, so it has to be produced separately by running -# `make-shared-library.sh` in the SBCL source tree, and neither `install.sh` nor -# most package managers install the result. Look in the usual places for it, and -# let the user point at it directly with e.g. `make LIBSBCL=/path/to/libsbcl.a`. +LIBQUIL_TARGET = libquil$(SHARED_SUFFIX) + +# libquil is built against modern sbcl-librarian (see REARCHITECTURE.md), which +# splits the work in two: +# +# runtime/libsbcl_librarian$(SHARED_SUFFIX) the SBCL runtime; a constructor +# initializes Lisp when it is loaded +# runtime/libquil.core the Lisp image, holding libquil and +# its dependencies +# libquil$(SHARED_SUFFIX) the generated C bindings # -# Which file to expect depends on the platform: on arm64 macOS the SBCL runtime -# is only built as a static archive (see SBCL's Config.arm64-darwin), so a -# shared libsbcl does not exist there at all. +# Both halves are built here, so `make` alone still produces a usable artifact. +RUNTIME_DIR := runtime +RUNTIME_LIB := $(RUNTIME_DIR)/libsbcl_librarian$(SHARED_SUFFIX) +CORE := $(RUNTIME_DIR)/libquil.core + +SBCL_LIBRARIAN_DIR := $(shell $(SBCL) --noinform --non-interactive \ + --eval '(require :asdf)' \ + --eval '(princ (namestring (asdf:system-source-directory "sbcl-librarian")))' 2>/dev/null) + +# The linkable SBCL runtime. `make.sh` does not build one and package managers do +# not ship it, so it comes from an SBCL source tree built with +# `make-shared-library.sh`; `install.sh` puts it in SBCL's home directory. Note +# that SBCL names it libsbcl.so on every platform, including macOS. SBCL_CORE_DIR := $(dir $(shell $(SBCL) --noinform --no-sysinit --no-userinit --non-interactive \ --eval '(princ (namestring sb-ext:*core-pathname*))' 2>/dev/null)) -LIBSBCL_SEARCH_DIRS := $(SBCL_HOME) $(SBCL_CORE_DIR) /usr/local/lib /usr/lib /opt/homebrew/lib -LIBSBCL_CANDIDATES := \ - $(foreach dir,$(LIBSBCL_SEARCH_DIRS),\ - $(dir)/libsbcl.a $(dir)/libsbcl.dylib $(dir)/libsbcl.so) +LIBSBCL_SEARCH_DIRS := $(SBCL_HOME) $(SBCL_CORE_DIR) $(SBCL_CORE_DIR).. \ + /usr/local/lib /usr/lib /opt/homebrew/lib +LIBSBCL ?= $(firstword $(wildcard \ + $(foreach dir,$(LIBSBCL_SEARCH_DIRS),$(dir)/libsbcl.so $(dir)/libsbcl.dylib))) -LIBSBCL ?= $(firstword $(wildcard $(LIBSBCL_CANDIDATES))) - -# A static runtime must be linked whole: the entry points the generated -# libquil.c calls are reached only through the core, so without this the linker -# drops most of the archive. Its own dependencies have to be named explicitly -# too, since an archive records none. A shared runtime carries both properties -# already and just needs to be linked normally. -SBCL_STATIC_DEPS ?= $(shell pkg-config --libs libzstd 2>/dev/null || echo -lzstd) -lm -ldl -lpthread - -ifeq ($(suffix $(LIBSBCL)), .a) -ifeq ($(OS), Darwin) - LIBSBCL_LDFLAGS = -Wl,-force_load,$(LIBSBCL) $(SBCL_STATIC_DEPS) -else - LIBSBCL_LDFLAGS = -Wl,--whole-archive $(LIBSBCL) -Wl,--no-whole-archive $(SBCL_STATIC_DEPS) -endif -else ifeq ($(OS), Darwin) -# Link by path rather than -lsbcl: make-shared-library.sh names its output -# libsbcl.so even on macOS, and the -l flag only ever looks for libsbcl.dylib or -# libsbcl.a. - LIBSBCL_LDFLAGS = $(LIBSBCL) -else - LIBSBCL_LDFLAGS = -L$(dir $(LIBSBCL)) -lsbcl -endif +# libsbcl needs zstd for core compression; pkg-config knows where it is on systems +# that install it outside the default search path (Homebrew, in particular). +ZSTD_LIBS ?= $(shell pkg-config --libs libzstd 2>/dev/null || echo -lzstd) all: $(LIBQUIL_TARGET) -libquil.core libquil.c libquil.h libquil.py: src/libquil.lisp src/qvm/*.lisp src/quilc/*.lisp - $(SBCL) --dynamic-space-size 8192 --load "src/build-image.lisp" +runtime: $(RUNTIME_LIB) + +# One image produces everything Lisp-side: libquil's bindings, the runtime's +# bindings, and the core that backs both. +$(CORE) libquil.c libquil.h $(RUNTIME_DIR)/sbcl_librarian.c: src/libquil.lisp src/qvm/*.lisp src/quilc/*.lisp src/build-image.lisp + mkdir -p $(RUNTIME_DIR) + $(SBCL) --dynamic-space-size 8192 --non-interactive --load "src/build-image.lisp" + # The core is named after the aggregate library that defines its exports + # (libquil-core); publish it beside the runtime under the name the runtime + # was compiled to look for. + mv libquil_core.core $(CORE) -$(LIBQUIL_TARGET): libquil.core libquil.c +# The runtime is told to load libquil.core rather than the stock +# sbcl_librarian.core, so that libquil's image is what comes up. +$(RUNTIME_LIB): $(RUNTIME_DIR)/sbcl_librarian.c ifeq ($(LIBSBCL),) - @echo "error: no linkable SBCL runtime found." >&2 - @echo "Searched for libsbcl.a, libsbcl.dylib and libsbcl.so in:" >&2 + @echo "error: no linkable SBCL runtime (libsbcl.so) found." >&2 + @echo "Searched:" >&2 @$(foreach dir,$(LIBSBCL_SEARCH_DIRS),echo " $(dir)" >&2;) - @echo "" >&2 - @echo "Build one from an SBCL source tree of the SAME version as $(SBCL):" >&2 - @echo " sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh" >&2 - @echo "then point make at the result, e.g.:" >&2 - @echo " make LIBSBCL=/path/to/sbcl/src/runtime/libsbcl.a" >&2 + @echo "Build one from an SBCL source tree of the same version as $(SBCL):" >&2 + @echo " sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh" >&2 + @echo "then re-run make, or pass LIBSBCL=/path/to/libsbcl.so" >&2 @exit 1 endif - $(CC) $(CCFLAGS) -o $@ libquil.c $(LIBSBCL_LDFLAGS) + mkdir -p $(RUNTIME_DIR) + cp $(LIBSBCL) $(RUNTIME_DIR)/libsbcl.so + cp "$(SBCL_LIBRARIAN_DIR)lib/sbcl_librarian_err.h" $(RUNTIME_DIR)/ + cd $(RUNTIME_DIR) && $(CC) $(SHARED_FLAGS) -o libsbcl_librarian$(SHARED_SUFFIX) \ + sbcl_librarian.c \ + "$(SBCL_LIBRARIAN_DIR)lib/entry_point.c" \ + -DLIBSBCL_LIBRARIAN_API_BUILD \ + -DSBCL_LIBRARIAN_CORE_NAME='"libquil.core"' \ + -I. -I"$(SBCL_LIBRARIAN_DIR)lib" -L. -lsbcl $(ZSTD_LIBS) + +$(LIBQUIL_TARGET): libquil.c $(CORE) $(RUNTIME_LIB) + $(CC) $(SHARED_FLAGS) -o $@ libquil.c \ + -I. -I$(RUNTIME_DIR) -I"$(SBCL_LIBRARIAN_DIR)lib" \ + -L$(RUNTIME_DIR) -lsbcl_librarian clean: - rm -f libquil.so libquil.c libquil.h libquil.core libquil.py libquil.dylib example + rm -rf $(RUNTIME_DIR) build + rm -f libquil.so libquil.dylib libquil.h libquil.c libquil.core libquil.py example diff --git a/REARCHITECTURE.md b/REARCHITECTURE.md new file mode 100644 index 0000000..23494b0 --- /dev/null +++ b/REARCHITECTURE.md @@ -0,0 +1,207 @@ +# libquil on modern sbcl-librarian + +Working notes and decision record for the `sbcl-librarian-runtime` branch, which +moves libquil from the 2023-era sbcl-librarian (pinned at `04f7e39`) onto current +`main`. Written as the work happened; decisions are recorded with their reasons so +they can be revisited. + +## Why this is not a small change + +The obvious motivation was adopting sbcl-librarian's built-in error handling and +unblocking modern SBCL. Both turned out to be gated on a redesign that landed +upstream in `2a12bd0` (2024-01-23, "Add libsbcl_librarian CMake project + Conda +recipe"). + +Two facts establish that there is no cheaper path: + +1. The `sb-int::int-sap` -> `sb-sys:int-sap` fix that modern SBCL requires landed + in `b8dc014` (2024-05-22), *after* the redesign. There is no version window + offering modern-SBCL compatibility with the old architecture. +2. On current `main`, the generated `init(char* core)` is dead code: it declares + its own `static int initialized`, shadowing the global that every generated API + wrapper checks. Calling it leaves every function returning + `LISP_ERR_NOT_INITIALIZED`. + +## The old model vs the new one + +Old (what libquil does on `main`): + +- `build-bindings` + `build-core-and-die` produce `libquil.c/.h` and `libquil.core`. +- `libquil.dylib` is self-contained, statically embedding the SBCL runtime. +- The consumer calls `init("/path/to/libquil.core")` explicitly. + +New (what this branch moves to): + +- A **runtime** is built once: `libsbcl_librarian.dylib` plus `sbcl_librarian.core`. + It initializes Lisp from a shared-library constructor at load time, locating its + core by name next to itself, with heap size from `SBCL_LIBRARIAN_HEAP_SIZE`. +- Each consumer is a **FASL library**: a small shared library holding the generated + C bindings, incbin-embedded FASL bundles for its ASDF system and dependencies, + and a constructor that loads those bundles into the already-running image. + `create-fasl-library-cmake-project` generates the whole CMake project. +- There is no per-consumer `.core`, and no explicit init call. + +## Decisions + +### D1: Follow the upstream FASL-library model rather than pinning a fork + +Rejected alternatives: (a) stay on `04f7e39` and carry the one-line `int-sap` +patch forever; (b) fork sbcl-librarian to keep `define-api :error-map` alive. + +Both work, but they permanently diverge from upstream and keep libquil's +hand-rolled error handling, which upstream has since absorbed. Since we are +re-architecting anyway, match upstream so future updates are merges rather than +rebases. + +Note (a) remains the correct *fallback* and is preserved on the +`build-apple-aarch64` branch, which builds and passes 22/22 today. + +### D2: Adopt sbcl-librarian's built-in error handling; accept the ABI break + +libquil's `*last-error*` / `libquil_error()` / `libquil_error_t` are replaced by +upstream's `get_error_message()` / `lisp_err_t`. This is a breaking change to +libquil's C ABI. + +Accepted because libquil is pre-1.0, the only known consumer is libquil-sys (which +we control and update in lockstep), and the replacement is strictly better: it +distinguishes failure from bug from fatal, and supports backtraces. + +`quilc_compile_protoquil`'s hand-written `define-alien-callable` (which exists +because sbcl-librarian cannot express `(:pointer :pointer)`) now records into +`sbcl-librarian::*error-message*` so it reports through the same channel. + +### D3: Ship a core, not FASL bundles + +Upstream's consumer path is `create-fasl-library-cmake-project`, which embeds a +FASL bundle per ASDF system into the library and loads them into the image at +startup. libquil was built that way first, and it worked -- but it is not +distributable: + +- FASL bundles re-run load-time code on every startup. `cl-quil` computes + `*default-standard-gates-file*` with `asdf:system-relative-pathname` and calls + `initialize-standard-gates` inside an `eval-when (:load-toplevel)`, so every + process that loaded libquil re-read `stdgates.quil` **from quilc's source tree**, + at the path baked in when the bundles were built. An installed artifact on a + machine without quilc's sources would fail. +- The same load-time chatter (`; loading standard gates from ...`) went to stdout + of every process that linked libquil, which among other things broke + `cargo nextest`'s test enumeration. + +Saving a core evaluates all of that once, at build time. So libquil keeps a core -- +what it always shipped -- and gets the modern runtime, init and error handling +around it. + +The core is loaded by the runtime rather than by an explicit `init(core)` call, +which is why D6 exists. + +### D4: Build the runtime manually rather than via its CMake project + +Upstream's `lib/CMakeLists.txt` invokes the generator with `sbcl --script`, which +skips `~/.sbclrc` and therefore Quicklisp — so `generate-bindings.lisp`'s +`(asdf:load-system :swank)` fails with `Component :SWANK not found`. + +Rather than patch upstream's CMake, `src/build-image.lisp` defines the runtime's +aggregate library itself and emits `runtime/sbcl_librarian.c` alongside libquil's +own bindings. One image therefore produces everything: libquil's bindings, the +runtime's bindings, and the core that backs both. That also avoids the swank +dependency and the second, discarded core that upstream's generator would build. + +### D5: Point the runtime at libquil's core + +`entry_point.c` hardcoded `sbcl_librarian.core`, looked up next to the runtime +library. Because libquil ships its own core (D3), the runtime has to load that +instead. Upstream now takes a compile-time `SBCL_LIBRARIAN_CORE_NAME`, defaulting +to the old name, and libquil builds the runtime with `-DSBCL_LIBRARIAN_CORE_NAME='"libquil.core"'`. + +Naming libquil's core `sbcl_librarian.core` would have avoided the patch, but two +sbcl-librarian consumers installed into the same directory would then overwrite +each other's core. + +### D6: Map ordinary errors to `LISP_ERR_FAILURE`, and handle `T` + +The stock `default-error-map` classifies any `cl:error` as `LISP_ERR_BUG`, prefixes +the message with "Internal lisp bug:" and attaches a backtrace. Nearly everything +libquil signals is user error -- malformed Quil, an unknown memory region -- so +libquil redefines the map for its own callables. `wrap-error-handling` is consulted +when a callable is compiled, so this affects only libquil's; the runtime's own APIs +keep upstream behaviour. + +The handler binds `T`, not `cl:error`. cl-quil signals conditions that are not +subtypes of `error` -- `invalid-instruction-condition` has no supertype at all -- +and with a `cl:error` handler those escape into the debugger and hang the calling +process. libquil's original error map bound `T` for the same reason. + +### D7: Keep shipping the SBCL runtime as `libsbcl.so`, even on macOS + +SBCL's `make-shared-library.sh` emits `libsbcl.so` on every platform, and that +string becomes the install name recorded in `libsbcl_librarian.dylib`. Renaming the +file to `.dylib` breaks loading unless the install name is rewritten too. Upstream's +own example Makefile notes the same quirk. We ship `libsbcl.so` and leave the name +alone. + +## What `make` produces + + libquil.dylib generated C bindings (~40 KB) + libquil.h its header + runtime/libsbcl_librarian.dylib SBCL runtime; initializes Lisp on load + runtime/libquil.core the Lisp image + runtime/libsbcl.so the linkable SBCL runtime + runtime/sbcl_librarian.h runtime API (get_error_message, handles) + runtime/sbcl_librarian_err.h lisp_err_t and the fatal-error plumbing + +All of `runtime/` must be installed together, and `libquil.core` must sit beside +`libsbcl_librarian`, since the runtime finds its core relative to its own location. + +## Prerequisite: SBCL with a linkable runtime + +Homebrew's bottle does not ship one, so SBCL must come from source: + + sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh && sh install.sh + +## Upstream sbcl-librarian changes this depends on + +Three fixes, on the `fix-secondary-system-bundles` branch: + +1. **Secondary systems lost their FASL bundles.** The output-translation pattern was + built from the flattened bundle name (`magicl--core--system.fasl`), but ASDF + writes `magicl/core--system.fasl`, turning the slash into a directory. Bundles + for `magicl/core`, `magicl/ext*` and `cl-quil/frontend` were silently left in the + ASDF cache and the generated CMake project referenced files that did not exist. + (Found while libquil was still on the FASL path; kept because it is a real bug.) +2. **`liblibquil`.** The generated CMake project cleared the library prefix only on + Windows, so a library whose name starts with `lib` built as `liblibquil.so` on + Unix. Upstream's own libcalc example links `-lcalc`, which only resolves with the + prefix cleared. +3. **`SBCL_LIBRARIAN_CORE_NAME`** (D5). + +## Status + +- [x] Consumer model determined +- [x] Runtime builds and self-initializes on arm64 macOS +- [x] libquil builds; `make` alone produces a usable artifact +- [x] Verified from C: parse, chip, compile, program string, and error reporting +- [x] libquil-sys updated to the new ABI -- 22/22 tests pass +- [x] Python bindings rebuilt and exercised +- [x] C examples updated and passing (`make test` in both `examples/` directories) +- [x] CI, `install.sh` and release archives updated for the new artifact set +- [x] Packaging verified by simulating package -> zip -> install -> build and + running the libquil-sys suite against the installed layout (22/22) + +## Open + +**CI cannot go green until the sbcl-librarian fixes are reachable.** `build.yml` +clones `$SBCL_LIBRARIAN_REPO` at `$SBCL_LIBRARIAN_REF`, currently +`quil-lang/sbcl-librarian` at `main`, which does not yet carry them. Point those at +a fork or a merged upstream ref. + +**CI's SBCL bump is unverified.** Both jobs now build `sbcl-$SBCL_VERSION` (2.6.7) +with `--with-sb-linkable-runtime`, replacing the 2.2.4 build that failed in +`make-host-1`. That combination is proven locally on arm64 macOS but has not run on +the Linux job. + +**Consumers with a non-/usr/local prefix** need `LIBQUIL_LIB_PATH` as well as +`LIBQUIL_SRC_PATH`, since headers and libraries then live in different directories. + +**Upstreaming.** The three sbcl-librarian fixes are worth PRs regardless of what +libquil does; the FASL-bundle one is a plain bug, and the library-prefix one breaks +upstream's own libcalc example on Unix. diff --git a/src/build-image.lisp b/src/build-image.lisp index 0656060..8cefa74 100644 --- a/src/build-image.lisp +++ b/src/build-image.lisp @@ -9,20 +9,28 @@ (probe-file quicklisp-init)) (load quicklisp-init))) +;; libquil intentionally redefines some of the alien callables that +;; SBCL-LIBRARIAN generates, in order to give them types SBCL-LIBRARIAN cannot +;; express yet (see the definition of quilc_compile_protoquil). SBCL signals a +;; continuable error for that; taking the CONTINUE restart installs the new +;; definition, which is what an interactive build does. Without this the build +;; drops into the debugger and cannot run unattended. +;; +;; The definitions are ordered so that libquil's override is installed last and +;; therefore wins; this applies equally when the systems are recompiled into FASL +;; bundles, so the whole build runs inside the handler. +(defmacro with-alien-redefinition-allowed (&body body) + `(handler-bind ((error + (lambda (condition) + (let ((restart (find-restart 'continue condition))) + (when (and restart + (search "redefine alien callable" + (princ-to-string condition))) + (invoke-restart restart)))))) + ,@body)) + (defun load-system (system) - ;; libquil intentionally redefines some of the alien callables that - ;; SBCL-LIBRARIAN generates, in order to give them types SBCL-LIBRARIAN cannot - ;; express yet (see the definition of quilc_compile_protoquil). SBCL signals a - ;; continuable error for that; taking the CONTINUE restart installs the new - ;; definition, which is what an interactive build does. Without this the build - ;; drops into the debugger and cannot run unattended. - (handler-bind ((error - (lambda (condition) - (let ((restart (find-restart 'continue condition))) - (when (and restart - (search "redefine alien callable" - (princ-to-string condition))) - (invoke-restart restart)))))) + (with-alien-redefinition-allowed (if (find-package '#:quicklisp) (funcall (read-from-string "quicklisp:quickload") system) (asdf:load-system system)))) @@ -32,11 +40,55 @@ (in-package #:libquil) +;;; Two aggregate libraries, because the C side and the Lisp side need different +;;; sets. +;;; +;;; The bindings we generate must cover only libquil's own APIs: the error, handle +;;; and diagnostic APIs already have C wrappers in libsbcl_librarian, and emitting +;;; them again would define those symbols twice. (sbcl-librarian:define-aggregate-library libquil (:function-linkage "QUILC_API") - common + quilc + qvm) + +;;; The runtime's own APIs. Their C wrappers live in libsbcl_librarian, and +;;; generating them here means the runtime can be built straight from this image; +;;; sbcl-librarian's own lib/generate-bindings.lisp would do it, but it pulls in +;;; swank and insists on saving a second core we would only discard. +;;; +;;; The name matters: it decides the generated file names, and entry_point.c +;;; includes sbcl_librarian.h. +(sbcl-librarian:define-aggregate-library sbcl-librarian + (:function-linkage "LIBSBCL_LIBRARIAN_API") + sbcl-librarian:diagnostics + sbcl-librarian:environment + sbcl-librarian:errors + sbcl-librarian:handles + sbcl-librarian:loader) + +;;; The core has to export the Lisp side of everything the process will call, +;;; libquil's APIs and the runtime's alike: libsbcl_librarian's C wrapper for +;;; get_error_message dispatches to an alien callable that exists only if this core +;;; exports it. +(sbcl-librarian:define-aggregate-library libquil-core (:function-linkage "QUILC_API") quilc qvm - sbcl-librarian:handles) + sbcl-librarian:diagnostics + sbcl-librarian:environment + sbcl-librarian:errors + sbcl-librarian:handles + sbcl-librarian:loader) -(sbcl-librarian:build-bindings libquil "." :initialize-lisp-args '("--dynamic-space-size" "8192")) -(sbcl-librarian:build-core-and-die libquil ".") +;;; libquil ships a core rather than the FASL bundles that sbcl-librarian's +;;; CREATE-FASL-LIBRARY-CMAKE-PROJECT produces. FASL bundles are re-loaded into the +;;; image on every startup, which re-runs load-time code: cl-quil reads +;;; stdgates.quil through ASDF:SYSTEM-RELATIVE-PATHNAME at load time, so a FASL +;;; build only works where quilc's source tree is present at the path recorded when +;;; it was built. Saving a core evaluates that once, at build time, and bakes the +;;; result into the image. +;;; +;;; The init function is omitted: initialization is the runtime's job, done from a +;;; constructor when libsbcl_librarian is loaded. +(cl-user::with-alien-redefinition-allowed + (sbcl-librarian:build-bindings libquil "." :omit-init-function t) + (sbcl-librarian:build-bindings sbcl-librarian "runtime/" :omit-init-function t) + (sbcl-librarian:build-core-and-die libquil-core ".")) diff --git a/src/libquil.lisp b/src/libquil.lisp index 538542b..12b9486 100644 --- a/src/libquil.lisp +++ b/src/libquil.lisp @@ -34,25 +34,31 @@ (sbcl-librarian:define-handle-type qvm-multishot-addresses "qvm_multishot_addresses") -(defvar *last-error* "") - -(defun libquil-last-error () - "Returns the most recent error raised by quilc. The error is then cleared." - (let ((last-error *last-error*)) - (setf *last-error* "") - last-error)) - -(sbcl-librarian:define-enum-type error-type "libquil_error_t" - ("LIBQUIL_ERROR_SUCCESS" 0) - ("LIBQUIL_ERROR_FAIL" 1)) - -(sbcl-librarian:define-error-map error-map error-type 0 - ((t (lambda (condition) - (setf *last-error* (format nil "~a" condition)) - (return-from error-map 1))))) - -(sbcl-librarian:define-api common (:error-map error-map - :function-prefix "libquil_") - (:type error-type) - (:function - (("error" libquil-last-error) :string ()))) +;;; Error reporting comes from SBCL-LIBRARIAN's built-in ERRORS api, which +;;; provides the `lisp_err_t' type, `get_error_message', and `enable_backtrace'. +;;; DEFINE-API always uses SBCL-LIBRARIAN's DEFAULT-ERROR-MAP, which records the +;;; condition into that message. +;;; +;;; We redefine that map for libquil's own APIs. The stock one classifies any +;;; plain CL:ERROR as an internal bug (LISP_ERR_BUG), attaching a backtrace and an +;;; "Internal lisp bug:" prefix. Almost everything libquil signals is a user error +;;; -- malformed Quil, an unknown memory region, an unsupported instruction -- so +;;; the stock mapping would report ordinary bad input as a libquil bug. Mapping +;;; CL:ERROR to LISP_ERR_FAILURE keeps the message clean and matches how libquil +;;; behaved before it adopted the built-in error handling. +;;; +;;; WRAP-ERROR-HANDLING is consulted when a callable is compiled, so this affects +;;; only the callables compiled after it -- libquil's own. The APIs already +;;; compiled into libsbcl_librarian keep the stock behaviour. +(sbcl-librarian:define-error-map sbcl-librarian::default-error-map + sbcl-librarian::error-type (:no-error 0 :fatal-error 3) + ;; Handle T, not CL:ERROR. cl-quil signals conditions that are not subtypes of + ;; ERROR -- INVALID-INSTRUCTION-CONDITION, raised for an unrecognized + ;; instruction, has no supertype at all -- so a handler bound to CL:ERROR lets + ;; them escape to the debugger and hang the calling process. libquil's original + ;; error map bound T for this reason. Warnings are passed over first so they do + ;; not abort the call. + ((cl:warning #'cl:continue) + (t (lambda (condition) + (setf sbcl-librarian::*error-message* (format nil "~a" condition)) + (return-from sbcl-librarian::default-error-map 1))))) diff --git a/src/quilc/api.lisp b/src/quilc/api.lisp index 5bbf90f..3952ea7 100644 --- a/src/quilc/api.lisp +++ b/src/quilc/api.lisp @@ -123,8 +123,7 @@ ("LIBQUIL_TYPE_INTEGER" 2) ("LIBQUIL_TYPE_REAL" 3)) -(sbcl-librarian:define-api quilc (:error-map error-map - :function-prefix "quilc_") +(sbcl-librarian:define-api quilc (:function-prefix "quilc_") (:literal "/* Quilc types */") (:type program-memory-type quil-program chip-specification quilc-version-info compilation-metadata) (:literal "/* Quilc functions */") @@ -213,10 +212,14 @@ (let ((program-handle (sbcl-librarian::dereference-handle program)) (chip-spec-handle (sbcl-librarian::dereference-handle chip-spec)) (metadata-ptr metata-ptr)) + ;; Mirrors what SBCL-LIBRARIAN's DEFAULT-ERROR-MAP does for generated + ;; callables: record the condition where GET-ERROR-MESSAGE can find it and + ;; return the failure code. (block error-map (handler-bind ((t (lambda (condition) - (setf *last-error* (format nil "~a" condition)) + (setf sbcl-librarian::*error-message* + (format nil "~a" condition)) (return-from error-map 1)))) (progn (setf (sb-alien:deref sbcl-librarian::result) diff --git a/src/qvm/api.lisp b/src/qvm/api.lisp index 387b6cc..a4c40d0 100644 --- a/src/qvm/api.lisp +++ b/src/qvm/api.lisp @@ -151,7 +151,7 @@ (setf (cffi:mem-aref (sb-alien:alien-sap results-ptr) :double i) probability)))) -(sbcl-librarian:define-api qvm (:error-map error-map :function-prefix "qvm_") +(sbcl-librarian:define-api qvm (:function-prefix "qvm_") (:literal "/* QVM types */") (:type qvm-multishot-addresses qvm-multishot-result qvm-version-info) (:literal "/* QVM functions */") From 95cdf17be39533bc51ebaaece723344d44af989b Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 17:02:13 -0600 Subject: [PATCH 08/31] build: update CI, packaging and examples for the new architecture - build.yml builds SBCL $SBCL_VERSION (2.6.7) with --with-sb-linkable-runtime, replacing the sbcl-2.2.4 build that failed in make-host-1 ('undefined variable: SB-VM::END') because a current host SBCL cannot cross-compile a 2022 target. sbcl-librarian's repo and ref are workflow inputs, since libquil needs three fixes that are not upstream yet. - The artifact is now the bindings library, its header and the whole runtime directory: libsbcl_librarian, libquil.core, libsbcl.so and the runtime headers. install.sh and the release archives follow, with libquil.core installed beside libsbcl_librarian because the runtime finds its core relative to itself. - The examples drop their init(core) calls and -pagezero_size, link the runtime, and use get_error_message/lisp_err_t. - The redundant quickload steps before make are gone; build-image.lisp does it. Verified by simulating package -> zip -> install and running the libquil-sys suite against the installed layout. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 55 ++++++++++++++------ .github/workflows/release.yml | 4 +- README.md | 36 ++++++++----- REARCHITECTURE.md | 8 +-- examples/quilc/Makefile | 11 ++-- examples/quilc/compile-protoquil.c | 25 +++++---- examples/quilc/conjugate-pauli-by-clifford.c | 7 ++- examples/quilc/error.h | 3 +- examples/quilc/generate-rb-sequence.c | 11 ++-- examples/quilc/version.c | 9 ++-- examples/qvm/Makefile | 11 ++-- examples/qvm/error.h | 3 +- examples/qvm/expectation.c | 11 ++-- examples/qvm/multishot-measure.c | 7 ++- examples/qvm/multishot.c | 33 ++++++------ examples/qvm/probabilities.c | 7 ++- examples/qvm/version.c | 9 ++-- examples/qvm/wavefunction.c | 7 ++- install.sh | 21 ++++---- 19 files changed, 150 insertions(+), 128 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00753bc..ecf1ad5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,15 @@ on: pull_request: workflow_call: +env: + # SBCL is built from source for its linkable runtime; see the build steps. + SBCL_VERSION: "2.6.7" + # libquil depends on three sbcl-librarian fixes (secondary-system FASL bundles, + # library prefix, configurable core name). They live on a branch of Rigetti's + # fork; move this back to quil-lang/sbcl-librarian once they are upstream. + SBCL_LIBRARIAN_REPO: "https://github.com/rigetti/sbcl-librarian.git" + SBCL_LIBRARIAN_REF: "fix-secondary-system-bundles" + jobs: build-linux: name: Build libquil.so @@ -33,10 +42,14 @@ jobs: name: Install SBCL, libraries, and quicklisp run: | sudo apt update && sudo apt install -y sbcl build-essential - sudo git clone --single-branch --branch sbcl-2.2.4 https://git.code.sf.net/p/sbcl/sbcl /usr/src/sbcl - cd /usr/src/sbcl && sudo sh make.sh && sudo sh make-shared-library.sh + # Build SBCL from source: a linkable runtime (libsbcl.so) is required and + # neither make.sh nor any package manager produces one. The version must be + # recent -- sbcl-librarian needs sb-sys:int-sap -- and cannot be much older + # than the host SBCL used to cross-compile it. + sudo git clone --single-branch --branch sbcl-$SBCL_VERSION https://git.code.sf.net/p/sbcl/sbcl /usr/src/sbcl + cd /usr/src/sbcl && sudo sh make.sh --with-sb-linkable-runtime && sudo sh make-shared-library.sh sudo apt remove -y sbcl - sudo sh install.sh + sudo sh install.sh sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so sudo apt install -y wget @@ -53,7 +66,7 @@ jobs: git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation - git clone https://github.com/quil-lang/sbcl-librarian.git $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout 04f7e390c777084ac43b8df9e90593a041da2381 + git clone $SBCL_LIBRARIAN_REPO $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout $SBCL_LIBRARIAN_REF - name: Install quilc dependencies # libffi-dev pulls in whichever libffi runtime the image ships; naming @@ -65,18 +78,20 @@ jobs: run: | cd $GITHUB_WORKSPACE/libquil ls - sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' - sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' make - name: Test run: | - cd $GITHUB_WORKSPACE/libquil/examples/qvm && LD_LIBRARY_PATH=../.. make test - cd $GITHUB_WORKSPACE/libquil/examples/quilc && LD_LIBRARY_PATH=../.. make test + cd $GITHUB_WORKSPACE/libquil/examples/qvm && LD_LIBRARY_PATH=../..:../../runtime make test + cd $GITHUB_WORKSPACE/libquil/examples/quilc && LD_LIBRARY_PATH=../..:../../runtime make test - name: Package files run: | - mkdir -p $GITHUB_WORKSPACE/artifacts/libquil && cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.so $GITHUB_WORKSPACE/libquil/libquil.core /usr/local/lib/libsbcl.so $GITHUB_WORKSPACE/artifacts/libquil + mkdir -p $GITHUB_WORKSPACE/artifacts/libquil + cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.so $GITHUB_WORKSPACE/artifacts/libquil + # The runtime directory holds libsbcl_librarian, the core it loads, + # libsbcl itself and the runtime headers; they must ship together. + cp $GITHUB_WORKSPACE/libquil/runtime/* $GITHUB_WORKSPACE/artifacts/libquil - name: Store artifact uses: actions/upload-artifact@v4 @@ -115,8 +130,12 @@ jobs: name: Install SBCL, libraries, and quicklisp run: | brew install sbcl git - git clone --branch sbcl-2.2.4 https://git.code.sf.net/p/sbcl/sbcl /tmp/sbcl - cd /tmp/sbcl && sudo sh make.sh --without-compact-instance-header --without-immobile-space --without-immobile-code && sudo sh make-shared-library.sh + # See the Linux job: a linkable runtime has to be built from source. Homebrew's + # sbcl is only the cross-compilation host, and its version must be close to the + # one being built -- building the old sbcl-2.2.4 with a current host fails in + # make-host-1 with "undefined variable: SB-VM::END". + git clone --branch sbcl-$SBCL_VERSION https://git.code.sf.net/p/sbcl/sbcl /tmp/sbcl + cd /tmp/sbcl && sudo sh make.sh --with-sb-linkable-runtime && sudo sh make-shared-library.sh brew remove -f sbcl sudo sh install.sh sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so @@ -135,7 +154,7 @@ jobs: git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation - git clone https://github.com/quil-lang/sbcl-librarian.git $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout 04f7e390c777084ac43b8df9e90593a041da2381 + git clone $SBCL_LIBRARIAN_REPO $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout $SBCL_LIBRARIAN_REF - name: Install quilc dependencies run: brew install lapack openblas libffi gfortran @@ -144,8 +163,6 @@ jobs: run: | cd $GITHUB_WORKSPACE/libquil ls - sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' - sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' make - name: ls @@ -154,12 +171,16 @@ jobs: - name: Test run: | - cd $GITHUB_WORKSPACE/libquil/examples/qvm && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil make test - cd $GITHUB_WORKSPACE/libquil/examples/quilc && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil make test + cd $GITHUB_WORKSPACE/libquil/examples/qvm && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil:$GITHUB_WORKSPACE/libquil/runtime make test + cd $GITHUB_WORKSPACE/libquil/examples/quilc && DYLD_LIBRARY_PATH=$GITHUB_WORKSPACE/libquil:$GITHUB_WORKSPACE/libquil/runtime make test - name: Package files run: | - mkdir -p $GITHUB_WORKSPACE/artifacts/libquil && cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.dylib $GITHUB_WORKSPACE/libquil/libquil.core /usr/local/lib/libsbcl.so $GITHUB_WORKSPACE/artifacts/libquil + mkdir -p $GITHUB_WORKSPACE/artifacts/libquil + cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.dylib $GITHUB_WORKSPACE/artifacts/libquil + # The runtime directory holds libsbcl_librarian, the core it loads, + # libsbcl itself and the runtime headers; they must ship together. + cp $GITHUB_WORKSPACE/libquil/runtime/* $GITHUB_WORKSPACE/artifacts/libquil - name: Store artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 033f220..8cddb3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: - name: Archive linux artifacts run: | - cd artifacts/linux-amd64 && zip ../linux-amd64.zip libquil/libsbcl.so libquil/libquil.so libquil/libquil.core libquil/libquil.h + cd artifacts/linux-amd64 && zip ../linux-amd64.zip libquil/libsbcl.so libquil/libsbcl_librarian.so libquil/libquil.so libquil/libquil.core libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h - name: Download macos arm64 artifacts uses: actions/download-artifact@v4 @@ -50,7 +50,7 @@ jobs: - name: Archive macos arm64 artifacts run: | - cd artifacts/macos-arm64 && zip ../macos-arm64.zip libquil/libsbcl.so libquil/libquil.dylib libquil/libquil.core libquil/libquil.h + cd artifacts/macos-arm64 && zip ../macos-arm64.zip libquil/libsbcl.so libquil/libsbcl_librarian.dylib libquil/libquil.dylib libquil/libquil.core libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h - name: List artifacts run: | diff --git a/README.md b/README.md index c2b83e5..b82286b 100644 --- a/README.md +++ b/README.md @@ -63,27 +63,21 @@ If you would like to manually install the library (for example in the case where ## Building from source -Building requires an SBCL that has a *linkable runtime* — a `libsbcl.a` or -`libsbcl.so`/`libsbcl.dylib` that gets embedded into the library. Neither -`make.sh` nor most package managers produce one (Homebrew's `sbcl` bottle, for -instance, does not), so it has to be built from an SBCL source tree of the same -version as the `sbcl` used for the rest of the build: +Building requires an SBCL with a *linkable runtime* (`libsbcl.so`). Neither +`make.sh` nor any package manager produces one — Homebrew's `sbcl` bottle, for +instance, does not — so SBCL has to be built from source: ```bash sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh && sh install.sh ``` -`install.sh` places the runtime in SBCL's home directory, where the `Makefile` -finds it automatically. To use one from elsewhere, pass it explicitly: +`install.sh` places it in SBCL's home directory, where the `Makefile` finds it +automatically. To use one from elsewhere, pass it explicitly: ```bash -make LIBSBCL=/path/to/sbcl/src/runtime/libsbcl.a +make LIBSBCL=/path/to/sbcl/src/runtime/libsbcl.so ``` -Note that on aarch64 macOS, SBCL only builds its runtime as the static -`libsbcl.a`; there is no shared `libsbcl.dylib` on that platform. The `Makefile` -handles either form. - The Lisp dependencies (`quilc`, `qvm`, `magicl`, `sbcl-librarian`) are expected in your Quicklisp local-projects directory. Then: @@ -91,6 +85,24 @@ in your Quicklisp local-projects directory. Then: make ``` +This produces the library and the runtime it needs: + +``` +libquil.dylib the C bindings +libquil.h its header +runtime/libsbcl_librarian.dylib the SBCL runtime; brings up Lisp when loaded +runtime/libquil.core the Lisp image +runtime/libsbcl.so the linkable SBCL runtime +runtime/sbcl_librarian*.h runtime headers +``` + +Everything under `runtime/` is installed alongside the library, and +`libquil.core` must sit next to `libsbcl_librarian` — the runtime finds its core +relative to its own location. There is no initialization call to make: loading +the library starts Lisp. + +See [REARCHITECTURE.md](REARCHITECTURE.md) for how this fits together and why. + ### Linear algebra backend on aarch64 macOS Homebrew's reference `lapack` computes incorrect eigenvectors on aarch64, which diff --git a/REARCHITECTURE.md b/REARCHITECTURE.md index 23494b0..0af1f3e 100644 --- a/REARCHITECTURE.md +++ b/REARCHITECTURE.md @@ -189,10 +189,10 @@ Three fixes, on the `fix-secondary-system-bundles` branch: ## Open -**CI cannot go green until the sbcl-librarian fixes are reachable.** `build.yml` -clones `$SBCL_LIBRARIAN_REPO` at `$SBCL_LIBRARIAN_REF`, currently -`quil-lang/sbcl-librarian` at `main`, which does not yet carry them. Point those at -a fork or a merged upstream ref. +**CI depends on a fork branch.** `build.yml` clones `$SBCL_LIBRARIAN_REPO` at +`$SBCL_LIBRARIAN_REF`, currently `rigetti/sbcl-librarian` at +`fix-secondary-system-bundles`. Move it back to `quil-lang/sbcl-librarian` once the +three fixes are upstream. **CI's SBCL bump is unverified.** Both jobs now build `sbcl-$SBCL_VERSION` (2.6.7) with `--with-sb-linkable-runtime`, replacing the 2.2.4 build that failed in diff --git a/examples/quilc/Makefile b/examples/quilc/Makefile index 8e49983..cb816cd 100644 --- a/examples/quilc/Makefile +++ b/examples/quilc/Makefile @@ -1,12 +1,11 @@ TEST_SRCS = $(wildcard *.c) TEST_OBJS = $(TEST_SRCS:.c=) BUILD_DIR = ../.. -CCFLAGS = -lsbcl -lquil -L$(BUILD_DIR) -I$(BUILD_DIR) - -OS:=$(shell uname -s) -ifeq ($(OS), Darwin) - CCFLAGS += -pagezero_size 0x100000 -endif +RUNTIME_DIR = $(BUILD_DIR)/runtime +# libquil is the generated bindings; libsbcl_librarian is the runtime that brings up +# the Lisp image (and supplies get_error_message). No -pagezero_size: the image is +# mapped by the runtime library, not by this executable. +CCFLAGS = -lquil -lsbcl_librarian -L$(BUILD_DIR) -L$(RUNTIME_DIR) -I$(BUILD_DIR) -I$(RUNTIME_DIR) .PHONY: all clean diff --git a/examples/quilc/compile-protoquil.c b/examples/quilc/compile-protoquil.c index e8737c0..021bb6a 100644 --- a/examples/quilc/compile-protoquil.c +++ b/examples/quilc/compile-protoquil.c @@ -2,20 +2,19 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" int main() { - init("../../libquil.core"); - quil_program h, result; chip_specification chip; - if (quilc_parse_quil("CNOT 0 1", &h) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("CNOT 0 1", &h) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } - if (quilc_build_nq_linear_chip(2, &chip) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_build_nq_linear_chip(2, &chip) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to build chip"); exit(1); } @@ -23,13 +22,13 @@ int main() { quilc_compilation_metadata metadata; if (quilc_compile_protoquil(h, chip, &metadata, &result) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to compile program"); exit(1); } int *final_rewiring, final_rewiring_len; - if (quilc_compilation_metadata_get_final_rewiring(metadata, &final_rewiring, &final_rewiring_len) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_final_rewiring(metadata, &final_rewiring, &final_rewiring_len) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to final rewiring from metadata"); exit(1); } @@ -41,7 +40,7 @@ int main() { int gate_depth, multiqubit_gate_depth, gate_volume, topological_swaps, present; double program_duration, program_fidelity, qpu_runtime_estimation; - if (quilc_compilation_metadata_get_gate_depth(metadata, &gate_depth, &present) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_gate_depth(metadata, &gate_depth, &present) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to get gate depth"); exit(1); } @@ -49,7 +48,7 @@ int main() { printf("gate depth is %d\n", gate_depth); } - if (quilc_compilation_metadata_get_multiqubit_gate_depth(metadata, &multiqubit_gate_depth, &present) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_multiqubit_gate_depth(metadata, &multiqubit_gate_depth, &present) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to get multiqubit gate depth"); exit(1); } @@ -57,7 +56,7 @@ int main() { printf("multiqubit gate depth is %d\n", gate_depth); } - if (quilc_compilation_metadata_get_gate_volume(metadata, &gate_volume, &present) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_gate_volume(metadata, &gate_volume, &present) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to get gate volume"); exit(1); } @@ -65,7 +64,7 @@ int main() { printf("gate volume is %d\n", gate_volume); } - if (quilc_compilation_metadata_get_topological_swaps(metadata, &topological_swaps, &present) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_topological_swaps(metadata, &topological_swaps, &present) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to get topological swaps"); exit(1); } @@ -73,7 +72,7 @@ int main() { printf("topological swaps is %d\n", topological_swaps); } - if (quilc_compilation_metadata_get_program_duration(metadata, &program_duration, &present) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_program_duration(metadata, &program_duration, &present) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to get program duration"); exit(1); } @@ -81,7 +80,7 @@ int main() { printf("program duration is %f\n", program_duration); } - if (quilc_compilation_metadata_get_program_fidelity(metadata, &program_fidelity, &present) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_program_fidelity(metadata, &program_fidelity, &present) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to get program fidelity "); exit(1); } @@ -89,7 +88,7 @@ int main() { printf("program fidelity is %f\n", program_fidelity); } - if (quilc_compilation_metadata_get_qpu_runtime_estimation(metadata, &qpu_runtime_estimation, &present) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_compilation_metadata_get_qpu_runtime_estimation(metadata, &qpu_runtime_estimation, &present) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to get qpu runtime estimation "); exit(1); } diff --git a/examples/quilc/conjugate-pauli-by-clifford.c b/examples/quilc/conjugate-pauli-by-clifford.c index a037188..cd5bcc9 100644 --- a/examples/quilc/conjugate-pauli-by-clifford.c +++ b/examples/quilc/conjugate-pauli-by-clifford.c @@ -2,13 +2,12 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" int main() { - init("../../libquil.core"); - quil_program clifford; - if (quilc_parse_quil("H 0", &clifford) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("H 0", &clifford) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } @@ -20,7 +19,7 @@ int main() { if (quilc_conjugate_pauli_by_clifford( pauli_indices, 1, pauli_terms, 1, clifford, &result_phase, - &result_pauli) != LIBQUIL_ERROR_SUCCESS) { + &result_pauli) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call quilc_conjugate_by_pauli"); exit(1); } diff --git a/examples/quilc/error.h b/examples/quilc/error.h index 87e0bf5..f20816f 100644 --- a/examples/quilc/error.h +++ b/examples/quilc/error.h @@ -1,5 +1,4 @@ #define LIBQUIL_ERROR(msg) \ char* libquil_err; \ - libquil_error(&libquil_err); \ + get_error_message(&libquil_err); \ printf("%s: %s\n", msg, libquil_err); - diff --git a/examples/quilc/generate-rb-sequence.c b/examples/quilc/generate-rb-sequence.c index e295e67..0ca9555 100644 --- a/examples/quilc/generate-rb-sequence.c +++ b/examples/quilc/generate-rb-sequence.c @@ -2,23 +2,22 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" int main() { - init("../../libquil.core"); - quil_program phase, h, y; int depth = 3, seed = 42, qubits = 1; - if (quilc_parse_quil("PHASE(pi/2) 0", &phase) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("PHASE(pi/2) 0", &phase) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } - if (quilc_parse_quil("H 0", &h) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("H 0", &h) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } - if (quilc_parse_quil("Y 0", &y) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("Y 0", &y) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } @@ -31,7 +30,7 @@ int main() { if (quilc_generate_rb_sequence(depth, qubits, gateset, 3, &seed, &interleaver, &results, - result_lens) != LIBQUIL_ERROR_SUCCESS) { + result_lens) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to generate RB sequence"); exit(1); } diff --git a/examples/quilc/version.c b/examples/quilc/version.c index 5fb4c90..bd5671f 100644 --- a/examples/quilc/version.c +++ b/examples/quilc/version.c @@ -2,6 +2,7 @@ #include #include +#include "sbcl_librarian.h" #include "libquil.h" #include "error.h" @@ -11,22 +12,20 @@ void die(char *msg) { } int main() { - init("../../libquil.core"); - quilc_version_info version_info; - if (quilc_get_version_info(&version_info) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_get_version_info(&version_info) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call quilc_get_version_info"); exit(1); } char* version; char* githash; - if (quilc_version_info_version(version_info, &version) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_version_info_version(version_info, &version) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call quilc_version_info_version"); exit(1); } - if (quilc_version_info_githash(version_info, &githash) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_version_info_githash(version_info, &githash) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call quilc_version_info_githash"); exit(1); } diff --git a/examples/qvm/Makefile b/examples/qvm/Makefile index 8a051b8..c0b3e7d 100644 --- a/examples/qvm/Makefile +++ b/examples/qvm/Makefile @@ -1,12 +1,11 @@ TEST_SRCS = $(wildcard *.c) TEST_OBJS = $(TEST_SRCS:.c=) BUILD_DIR = ../.. -CCFLAGS = -lsbcl -lquil -L$(BUILD_DIR) -I$(BUILD_DIR) - -OS:=$(shell uname -s) -ifeq ($(OS), Darwin) - CCFLAGS += -pagezero_size 0x100000 -endif +RUNTIME_DIR = $(BUILD_DIR)/runtime +# libquil is the generated bindings; libsbcl_librarian is the runtime that brings up +# the Lisp image (and supplies get_error_message). No -pagezero_size: the image is +# mapped by the runtime library, not by this executable. +CCFLAGS = -lquil -lsbcl_librarian -L$(BUILD_DIR) -L$(RUNTIME_DIR) -I$(BUILD_DIR) -I$(RUNTIME_DIR) .PHONY: all clean diff --git a/examples/qvm/error.h b/examples/qvm/error.h index 87e0bf5..f20816f 100644 --- a/examples/qvm/error.h +++ b/examples/qvm/error.h @@ -1,5 +1,4 @@ #define LIBQUIL_ERROR(msg) \ char* libquil_err; \ - libquil_error(&libquil_err); \ + get_error_message(&libquil_err); \ printf("%s: %s\n", msg, libquil_err); - diff --git a/examples/qvm/expectation.c b/examples/qvm/expectation.c index 249976e..3aedd20 100644 --- a/examples/qvm/expectation.c +++ b/examples/qvm/expectation.c @@ -3,6 +3,7 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" void die(char *msg) { @@ -15,7 +16,7 @@ double do_expectation(quil_program state_prep, quil_program operator) { double expectations[1] = {0}; if (qvm_expectation(state_prep, operators, 1, NULL, &expectations) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_expectation"); exit(1); } @@ -24,21 +25,19 @@ double do_expectation(quil_program state_prep, quil_program operator) { } int main(int argc, char **argv) { - init("../../libquil.core"); - quil_program i; quil_program z; quil_program x; - if (quilc_parse_quil("I 0", &i) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("I 0", &i) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } - if (quilc_parse_quil("Z 0", &z) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("Z 0", &z) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } - if (quilc_parse_quil("X 0", &x) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("X 0", &x) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } diff --git a/examples/qvm/multishot-measure.c b/examples/qvm/multishot-measure.c index 36359c8..72f9434 100644 --- a/examples/qvm/multishot-measure.c +++ b/examples/qvm/multishot-measure.c @@ -3,6 +3,7 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" void die(char *msg) { @@ -11,13 +12,11 @@ void die(char *msg) { } int main(int argc, char **argv) { - init("../../libquil.core"); - quil_program program; char *source = "X 0; X 2"; - if (quilc_parse_quil(source, &program) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil(source, &program) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call quilc_parse_quil"); exit(1); } @@ -29,7 +28,7 @@ int main(int argc, char **argv) { int results[num_trials][n_qubits]; memset(results, 0, num_trials * n_qubits * sizeof(int)); if (qvm_multishot_measure(program, qubits, n_qubits, num_trials, NULL, - &results) != LIBQUIL_ERROR_SUCCESS) { + &results) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_multishot_measure"); exit(1); } diff --git a/examples/qvm/multishot.c b/examples/qvm/multishot.c index cd88eac..fd3c804 100644 --- a/examples/qvm/multishot.c +++ b/examples/qvm/multishot.c @@ -3,6 +3,7 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" void die(char *msg) { @@ -26,20 +27,20 @@ void multishot_with_explicit_ro_indices() { "ro[1]; MEASURE %d ro[2]", q0, q0 + 1, q0 + 2, q0, q0 + 1, q0 + 2); - if (quilc_parse_quil(source, &program) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil(source, &program) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } qvm_multishot_addresses addresses; - if (qvm_multishot_addresses_new(&addresses) != LIBQUIL_ERROR_SUCCESS) { + if (qvm_multishot_addresses_new(&addresses) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to create addresses"); exit(1); } int indices[3] = {0, 1, 2}; if (qvm_multishot_addresses_set(addresses, "ro", indices, 3) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to set address indices"); exit(1); } @@ -47,7 +48,7 @@ void multishot_with_explicit_ro_indices() { qvm_multishot_result qvm_res; int num_trials = 10; if (qvm_multishot(program, addresses, num_trials, NULL, NULL, NULL, - &qvm_res) != LIBQUIL_ERROR_SUCCESS) { + &qvm_res) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_multishot"); exit(1); } @@ -55,7 +56,7 @@ void multishot_with_explicit_ro_indices() { for (int i = 0; i < num_trials; i++) { char vals[3]; if (qvm_multishot_result_get(qvm_res, "ro", i, &vals) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_multishot_result_get"); exit(1); } @@ -76,19 +77,19 @@ void multishot_with_implicit_ro_indices() { "ro[1]; MEASURE %d ro[2]", q0, q0 + 1, q0 + 2, q0, q0 + 1, q0 + 2); - if (quilc_parse_quil(source, &program) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil(source, &program) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } qvm_multishot_addresses addresses; - if (qvm_multishot_addresses_new(&addresses) != LIBQUIL_ERROR_SUCCESS) { + if (qvm_multishot_addresses_new(&addresses) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to create addresses"); exit(1); } if (qvm_multishot_addresses_set_all(addresses, "ro") != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to set address indices"); exit(1); } @@ -97,7 +98,7 @@ void multishot_with_implicit_ro_indices() { int num_trials = 10; double gate_noise[] = {0.0, 0.0, 0.0}; if (qvm_multishot(program, addresses, num_trials, NULL, NULL, NULL, - &qvm_res) != LIBQUIL_ERROR_SUCCESS) { + &qvm_res) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_multishot"); exit(1); } @@ -107,7 +108,7 @@ void multishot_with_implicit_ro_indices() { char *vals; if (qvm_multishot_result_get_all(qvm_res, "ro", i, &vals, &len) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_multishot_result_get_all"); exit(1); } @@ -131,20 +132,20 @@ void multishot_with_noise() { "ro[1]; MEASURE %d ro[2]", q0, q0 + 1, q0 + 2, q0, q0 + 1, q0 + 2); - if (quilc_parse_quil(source, &program) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil(source, &program) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } qvm_multishot_addresses addresses; - if (qvm_multishot_addresses_new(&addresses) != LIBQUIL_ERROR_SUCCESS) { + if (qvm_multishot_addresses_new(&addresses) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to create addresses"); exit(1); } int indices[3] = {0, 1, 2}; if (qvm_multishot_addresses_set(addresses, "ro", indices, 3) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to set address indices"); exit(1); } @@ -155,7 +156,7 @@ void multishot_with_noise() { double measurement_noise[] = {0.1, 0.0, 0.0}; if (qvm_multishot(program, addresses, num_trials, gate_noise, measurement_noise, NULL, - &qvm_res) != LIBQUIL_ERROR_SUCCESS) { + &qvm_res) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_multishot"); exit(1); } @@ -163,7 +164,7 @@ void multishot_with_noise() { for (int i = 0; i < num_trials; i++) { char vals[3]; if (qvm_multishot_result_get(qvm_res, "ro", i, &vals) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_multishot_result_get"); exit(1); } @@ -176,8 +177,6 @@ void multishot_with_noise() { } int main(int argc, char **argv) { - init("../../libquil.core"); - multishot_with_explicit_ro_indices(); multishot_with_implicit_ro_indices(); multishot_with_noise(); diff --git a/examples/qvm/probabilities.c b/examples/qvm/probabilities.c index 39f85da..af37995 100644 --- a/examples/qvm/probabilities.c +++ b/examples/qvm/probabilities.c @@ -3,6 +3,7 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" void die(char *msg) { @@ -11,11 +12,9 @@ void die(char *msg) { } int main(int argc, char **argv) { - init("../../libquil.core"); - quil_program program; - if (quilc_parse_quil("H 0; CNOT 0 1", &program) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("H 0; CNOT 0 1", &program) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } @@ -25,7 +24,7 @@ int main(int argc, char **argv) { double wavefunction[n_probabilities]; memset(wavefunction, 0, n_probabilities * sizeof(double)); - if (qvm_probabilities(program, NULL, wavefunction) != LIBQUIL_ERROR_SUCCESS) { + if (qvm_probabilities(program, NULL, wavefunction) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_probabilities"); exit(1); } diff --git a/examples/qvm/version.c b/examples/qvm/version.c index eeba2de..7fbe5af 100644 --- a/examples/qvm/version.c +++ b/examples/qvm/version.c @@ -2,26 +2,25 @@ #include #include +#include "sbcl_librarian.h" #include "libquil.h" #include "error.h" int main(int argc, char **argv) { - init("../../libquil.core"); - qvm_version_info version_info; - if (qvm_get_version_info(&version_info) != LIBQUIL_ERROR_SUCCESS) { + if (qvm_get_version_info(&version_info) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call quilc_get_version_info"); exit(1); } char* version; char* githash; - if (qvm_version_info_version(version_info, &version) != LIBQUIL_ERROR_SUCCESS) { + if (qvm_version_info_version(version_info, &version) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_version_info_version"); exit(1); } - if (qvm_version_info_githash(version_info, &githash) != LIBQUIL_ERROR_SUCCESS) { + if (qvm_version_info_githash(version_info, &githash) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_version_info_githash"); exit(1); } diff --git a/examples/qvm/wavefunction.c b/examples/qvm/wavefunction.c index f59a37c..e618a7d 100644 --- a/examples/qvm/wavefunction.c +++ b/examples/qvm/wavefunction.c @@ -4,6 +4,7 @@ #include #include "error.h" +#include "sbcl_librarian.h" #include "libquil.h" void die(char *msg) { @@ -12,11 +13,9 @@ void die(char *msg) { } int main(int argc, char **argv) { - init("../../libquil.core"); - quil_program program; - if (quilc_parse_quil("X 0; I 1", &program) != LIBQUIL_ERROR_SUCCESS) { + if (quilc_parse_quil("X 0; I 1", &program) != LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to parse quil"); exit(1); } @@ -25,7 +24,7 @@ int main(int argc, char **argv) { double *wavefunction; int seed = 0; if (qvm_wavefunction(program, &seed, &wavefunction, &wavefunction_len) != - LIBQUIL_ERROR_SUCCESS) { + LISP_ERR_SUCCESS) { LIBQUIL_ERROR("failed to call qvm_wavefunction"); exit(1); } diff --git a/install.sh b/install.sh index d4e4061..1f0e295 100755 --- a/install.sh +++ b/install.sh @@ -53,20 +53,23 @@ pushd "${LIBQUIL_TEMP_DIR}" || exit curl -L "${LIBQUIL_RELEASE_URL}" -o "${LIBQUIL_RELEASE_FILE}" unzip "${LIBQUIL_RELEASE_FILE}" +# libquil.core must land in the same directory as libsbcl_librarian: the runtime +# locates its core relative to its own path. +sudo mkdir -p "${LIBQUIL_INCLUDE_PREFIX}" +sudo cp libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h "${LIBQUIL_INCLUDE_PREFIX}" +sudo cp libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" + if [[ -n "${IS_LINUX-}" ]] then - sudo cp libquil/libquil.so libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" - sudo mkdir -p "${LIBQUIL_INCLUDE_PREFIX}" - sudo cp libquil/libquil.h "${LIBQUIL_INCLUDE_PREFIX}" + sudo cp libquil/libquil.so libquil/libsbcl_librarian.so "${LIBQUIL_LIB_PREFIX}" sudo ldconfig else - sudo cp libquil/libquil.dylib libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" - sudo mkdir -p "${LIBQUIL_INCLUDE_PREFIX}" - sudo cp libquil/libquil.h "${LIBQUIL_INCLUDE_PREFIX}" + sudo cp libquil/libquil.dylib libquil/libsbcl_librarian.dylib "${LIBQUIL_LIB_PREFIX}" # This disables the "cannot open libquil.dylib from untrusted developer" dialog. # A better solution for this would be to properly codesign the files, but that # is a non-trivial amount of work. - sudo xattr -r -d com.apple.quarantine /usr/local/lib/libquil.dylib - sudo xattr -r -d com.apple.quarantine /usr/local/lib/libquil.core - sudo xattr -r -d com.apple.quarantine /usr/local/lib/libsbcl.so + sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.dylib" + sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl_librarian.dylib" + sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.core" + sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl.so" fi From a44a84e86ba26fbf4bf832bbc285df7041747285 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 20:59:04 -0600 Subject: [PATCH 09/31] fix: load OpenBLAS into the image before magicl picks a backend magicl looks for Homebrew's reference LAPACK first and otherwise falls back to a bare liblapack.dylib, which on macOS resolves to Accelerate's. Neither works: the reference build returns incorrect eigenvectors on arm64, failing with 'Could not find diagonalizer for matrix ... after 16 attempts', and Accelerate's LAPACK predates 3.3 so routines quilc calls are missing, failing with 'The alien function zuncsd_ is undefined'. OpenBLAS is correct and complete. Loading it first means its symbols are the ones that resolve, and because SBCL records loaded shared objects in the core and reloads them at startup, the choice is baked into the artifact instead of depending on what the loader happens to find. Verified against a stock magicl checkout with Homebrew's lapack installed. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 22 ++++++++++++++++++---- Makefile | 4 ++-- README.md | 7 ++++++- src/build-image.lisp | 22 ++++++++++++++++++++++ versions | 11 +++++++---- 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ecf1ad5..798165c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,10 @@ env: # fork; move this back to quil-lang/sbcl-librarian once they are upstream. SBCL_LIBRARIAN_REPO: "https://github.com/rigetti/sbcl-librarian.git" SBCL_LIBRARIAN_REF: "fix-secondary-system-bundles" + # The Quicklisp dist supplying the Lisp dependencies. It has to be recent enough + # for current cl-quil: the long-standing 2022-04-01 pin predates clos-encounters + # and fails with SYSTEM-NOT-FOUND. + QUICKLISP_VERSION: "2026-01-01" jobs: build-linux: @@ -54,7 +58,7 @@ jobs: sudo apt install -y wget wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' - sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/2022-04-01/distinfo.txt\")" + sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc rm -f /tmp/quicklisp.lisp @@ -121,7 +125,7 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc - key: ${{ runner.os }}-${{ runner.arch }}-build-${{ hashFiles('**/macos-versions' )}} + key: ${{ runner.os }}-${{ runner.arch }}-build-${{ hashFiles('**/versions')}} - uses: Homebrew/actions/setup-homebrew@master name: Setup homebrew @@ -142,7 +146,7 @@ jobs: brew install wget wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' - sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/2022-04-01/distinfo.txt\")" + sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc rm -f /tmp/quicklisp.lisp @@ -157,7 +161,17 @@ jobs: git clone $SBCL_LIBRARIAN_REPO $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout $SBCL_LIBRARIAN_REF - name: Install quilc dependencies - run: brew install lapack openblas libffi gfortran + run: | + # Deliberately not installing lapack. magicl prefers Homebrew's reference + # LAPACK over everything else, and on arm64 it computes incorrect + # eigenvectors -- compilation then fails with "Could not find diagonalizer + # for matrix ... after 16 attempts". With it absent, magicl falls through to + # a bare liblapack.dylib, which these symlinks point at OpenBLAS. + brew uninstall --ignore-dependencies lapack || true + brew install openblas libffi gfortran + sudo mkdir -p /usr/local/lib + sudo ln -sf "$(brew --prefix openblas)/lib/libopenblas.dylib" /usr/local/lib/liblapack.dylib + sudo ln -sf "$(brew --prefix openblas)/lib/libopenblas.dylib" /usr/local/lib/libblas.dylib - name: Build libquil.dylib run: | diff --git a/Makefile b/Makefile index b2c8c37..a66d69c 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ endif mkdir -p $(RUNTIME_DIR) cp $(LIBSBCL) $(RUNTIME_DIR)/libsbcl.so cp "$(SBCL_LIBRARIAN_DIR)lib/sbcl_librarian_err.h" $(RUNTIME_DIR)/ - cd $(RUNTIME_DIR) && $(CC) $(SHARED_FLAGS) -o libsbcl_librarian$(SHARED_SUFFIX) \ + cd $(RUNTIME_DIR) && $(CC) $(SHARED_FLAGS) -fPIC -o libsbcl_librarian$(SHARED_SUFFIX) \ sbcl_librarian.c \ "$(SBCL_LIBRARIAN_DIR)lib/entry_point.c" \ -DLIBSBCL_LIBRARIAN_API_BUILD \ @@ -85,7 +85,7 @@ endif -I. -I"$(SBCL_LIBRARIAN_DIR)lib" -L. -lsbcl $(ZSTD_LIBS) $(LIBQUIL_TARGET): libquil.c $(CORE) $(RUNTIME_LIB) - $(CC) $(SHARED_FLAGS) -o $@ libquil.c \ + $(CC) $(SHARED_FLAGS) -fPIC -o $@ libquil.c \ -I. -I$(RUNTIME_DIR) -I"$(SBCL_LIBRARIAN_DIR)lib" \ -L$(RUNTIME_DIR) -lsbcl_librarian diff --git a/README.md b/README.md index b82286b..9cd7613 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,14 @@ sudo apt install libblas-dev libffi-dev liblapack-dev libz-dev On systems which use `brew` to install packages (e.g macOS), these libraries can be installed with the command ``` -brew install lapack openblas libffi +brew install openblas libffi ``` +> Note: do not install Homebrew's `lapack` on Apple Silicon. `magicl` prefers it +> over every other backend, and it computes incorrect eigenvectors there, which +> surfaces as `Could not find diagonalizer for matrix ... after 16 attempts` +> during compilation. + ## Automated installation A script is provided to automate installation of the library. It will detect the host operating system and install the library to an appropriate location. A version identifier can be provided to install a particular version of the library. If no version is provided, the latest version of the library will be installed. diff --git a/src/build-image.lisp b/src/build-image.lisp index 8cefa74..9a4039e 100644 --- a/src/build-image.lisp +++ b/src/build-image.lisp @@ -35,6 +35,28 @@ (funcall (read-from-string "quicklisp:quickload") system) (asdf:load-system system)))) +;; Load OpenBLAS before anything else pulls in a BLAS/LAPACK, so that its symbols +;; are the ones that resolve. +;; +;; magicl looks for Homebrew's reference LAPACK first and falls back to a bare +;; liblapack.dylib, which on macOS is Accelerate's. Neither is usable here: the +;; reference build returns incorrect eigenvectors on arm64 ("Could not find +;; diagonalizer for matrix ... after 16 attempts"), and Accelerate's LAPACK predates +;; 3.3, so routines quilc needs are simply missing ("The alien function zuncsd_ is +;; undefined"). OpenBLAS is correct and complete on both counts. +;; +;; SBCL records loaded shared objects in the core and reloads them at startup, so +;; this choice is baked into the artifact rather than left to the loader. +#+darwin +(let ((openblas (find-if #'probe-file + '("/opt/homebrew/opt/openblas/lib/libopenblas.dylib" + "/usr/local/opt/openblas/lib/libopenblas.dylib")))) + (if openblas + (sb-alien:load-shared-object openblas) + (warn "OpenBLAS not found; magicl may load a BLAS/LAPACK that miscomputes ~ + eigenvectors or lacks routines quilc needs. Install it with ~ + `brew install openblas'."))) + (load-system '#:sbcl-librarian) (load-system '#:libquil) diff --git a/versions b/versions index 17a62fd..cddad57 100644 --- a/versions +++ b/versions @@ -1,5 +1,8 @@ +# Versions this build pins. Keep in sync with the env block in +# .github/workflows/build.yml, which hashes this file for its cache keys. QVM_VERSION=4617625cb6053b1adfd3f7aea9cd2be328b225f6 -QUILC_VERSION=bffea7fdb972cc9b3a50b790246b6a3143c88c7a -SBCL_LIBRARIAN_VERSION=04f7e390c777084ac43b8df9e90593a041da2381 -SBCL_VERSION=sbcl-2.2.4 -QUICKLISP_VERSION=2022-04-01 +QUILC_VERSION=3d83b665b3addc33be2f696b1cdebc0c4c5a04f9 +SBCL_LIBRARIAN_VERSION=fix-secondary-system-bundles +SBCL_VERSION=sbcl-2.6.7 +# 2022-04-01 predates clos-encounters, which current cl-quil depends on. +QUICKLISP_VERSION=2026-01-01 From 62959f253c304e3214e688cca0e0d79cd4ef49a8 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 11 Aug 2026 21:19:56 -0600 Subject: [PATCH 10/31] ci: fix the release/prerelease pipeline Cutting a prerelease from a branch produced a tag pointing at the wrong code. knope 0.10/0.11 create the GitHub release against the default branch, so the version bump landed on the branch while the tag landed on main -- and the publish job, which checks out whatever that tag resolves to, then tried to republish main's already-released version: error: crate libquil-sys@0.4.2 already exists on crates.io index Adopt the setup rigetti-pyo3 arrived at (36a1850, 9b5235c, 3505971): - knope 0.23, which tags the ref being released and takes --prerelease-label, so the separate prerelease workflow in knope.toml is no longer needed; - the ref decides what gets cut: main releases, anything else prereleases; - check out ${{ github.ref }} so a release acts on the dispatched branch; - pass the token to the Release step through GITHUB_TOKEN, which is how it authenticates; - dry-run the release on pull requests, so a broken config is visible before it is dispatched. The publish job now checks out the release tag explicitly, so it always publishes exactly what was tagged rather than whatever the target commitish points at. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 50 +++++++++++++++++++++++----------- .github/workflows/release.yml | 33 ++++++++++++++++------- REARCHITECTURE.md | 51 ++++++++++++++++++++++++++++------- install.sh | 10 ++++--- versions | 2 ++ 5 files changed, 109 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 798165c..bb3ed08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,11 @@ on: env: # SBCL is built from source for its linkable runtime; see the build steps. SBCL_VERSION: "2.6.7" + # Host used to cross-compile SBCL_VERSION on Linux. It cannot be apt's (22.04 + # ships 2.1.11, too old) nor SBCL_VERSION's own binary release, which is linked + # against glibc 2.38 and will not run on 22.04. 2.5.0's binary needs only glibc + # 2.34 and is recent enough to build 2.6.7. + SBCL_BOOTSTRAP_VERSION: "2.5.0" # libquil depends on three sbcl-librarian fixes (secondary-system FASL bundles, # library prefix, configurable core name). They live on a branch of Rigetti's # fork; move this back to quil-lang/sbcl-librarian once they are upstream. @@ -20,7 +25,10 @@ env: jobs: build-linux: name: Build libquil.so - runs-on: ubuntu-latest + # Pinned rather than ubuntu-latest: the artifact links libsbcl, so it inherits + # the glibc of whatever built it, and a build on 24.04 fails to load on 22.04 + # with "version `GLIBC_2.38' not found". + runs-on: ubuntu-22.04 steps: - name: root suid tar @@ -40,26 +48,35 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc - key: ${{ runner.os }}-build-${{ hashFiles('**/versions') }} + key: ${{ env.ImageOS }}-${{ runner.arch }}-build-${{ hashFiles('**/versions') }} - if: steps.cache-sbcl.outputs.cache-hit != 'true' name: Install SBCL, libraries, and quicklisp run: | - sudo apt update && sudo apt install -y sbcl build-essential - # Build SBCL from source: a linkable runtime (libsbcl.so) is required and - # neither make.sh nor any package manager produces one. The version must be - # recent -- sbcl-librarian needs sb-sys:int-sap -- and cannot be much older - # than the host SBCL used to cross-compile it. + sudo apt update && sudo apt install -y build-essential bzip2 wget + # SBCL is built from source because a linkable runtime (libsbcl.so) is + # required and neither make.sh nor any package manager produces one. + # + # The host is a binary release of SBCL_BOOTSTRAP_VERSION rather than apt's: + # cross compiling a current SBCL needs a host of roughly the same vintage, + # and 22.04 ships 2.1.11, which fails in make-host-1 with "FAILURE-P was set + # when creating genesis.fasl". 22.04 is used deliberately, since the + # artifact inherits the glibc of whatever builds it. + wget -q "https://downloads.sourceforge.net/project/sbcl/sbcl/$SBCL_BOOTSTRAP_VERSION/sbcl-$SBCL_BOOTSTRAP_VERSION-x86-64-linux-binary.tar.bz2" -O /tmp/sbcl-bin.tar.bz2 + mkdir -p /tmp/sbcl-bin && tar xf /tmp/sbcl-bin.tar.bz2 -C /tmp/sbcl-bin --strip-components=1 + cd /tmp/sbcl-bin && sudo sh install.sh sudo git clone --single-branch --branch sbcl-$SBCL_VERSION https://git.code.sf.net/p/sbcl/sbcl /usr/src/sbcl cd /usr/src/sbcl && sudo sh make.sh --with-sb-linkable-runtime && sudo sh make-shared-library.sh - sudo apt remove -y sbcl sudo sh install.sh + sudo mkdir -p /usr/local/lib sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so sudo apt install -y wget - wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' - sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" - sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' + if [ ! -f ~/quicklisp/setup.lisp ]; then + wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' + sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" + sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' + fi echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc rm -f /tmp/quicklisp.lisp cat ~/.sbclrc @@ -125,7 +142,7 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc - key: ${{ runner.os }}-${{ runner.arch }}-build-${{ hashFiles('**/versions')}} + key: ${{ env.ImageOS }}-${{ runner.arch }}-build-${{ hashFiles('**/versions') }} - uses: Homebrew/actions/setup-homebrew@master name: Setup homebrew @@ -142,12 +159,15 @@ jobs: cd /tmp/sbcl && sudo sh make.sh --with-sb-linkable-runtime && sudo sh make-shared-library.sh brew remove -f sbcl sudo sh install.sh + sudo mkdir -p /usr/local/lib sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so brew install wget - wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' - sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" - sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' + if [ ! -f ~/quicklisp/setup.lisp ]; then + wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' + sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" + sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' + fi echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc rm -f /tmp/quicklisp.lisp cat ~/.sbclrc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8cddb3b..3a03fd2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,14 +1,9 @@ name: Release libquil on: + # Dispatching on main cuts a release; dispatching on any other branch cuts a + # prerelease, so libquil-sys can be built against a libquil that is not yet stable. workflow_dispatch: - inputs: - type: - description: Bump versions and trigger a new release. - required: true - default: release - options: - - release jobs: build: @@ -28,6 +23,8 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + # Release the ref this was dispatched on, not the default branch. + ref: ${{ github.ref }} # Each artifact is downloaded into its own directory: the two macOS # artifacts contain identically named files, so a shared directory would @@ -59,9 +56,25 @@ jobs: unzip -l artifacts/macos-arm64.zip - name: Install Knope - uses: knope-dev/action@v2.0.0 + uses: knope-dev/action@v2.1.2 with: - version: 0.11.0 + # 0.23 is needed for `--prerelease-label` and because older versions tag + # the default branch rather than the ref being released, which produces a + # tag pointing at the wrong code when releasing from a branch. + version: 0.23.0 # Test before updating, breaking changes likely: https://github.com/knope-dev/action#install-latest-version - name: Release - run: knope release -v + env: + # The Release step authenticates with this rather than an argument. + GITHUB_TOKEN: ${{ secrets.PAT }} + run: | + set -euo pipefail + + # A release off main is a real one; anywhere else it can only be a + # prerelease, which is how libquil gets published for testing while an ABI + # change is still under review. + if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then + knope release -v + else + knope release -v --prerelease-label=rc + fi diff --git a/REARCHITECTURE.md b/REARCHITECTURE.md index 0af1f3e..f0e8e30 100644 --- a/REARCHITECTURE.md +++ b/REARCHITECTURE.md @@ -30,20 +30,28 @@ Old (what libquil does on `main`): - `libquil.dylib` is self-contained, statically embedding the SBCL runtime. - The consumer calls `init("/path/to/libquil.core")` explicitly. -New (what this branch moves to): +Upstream's current model: -- A **runtime** is built once: `libsbcl_librarian.dylib` plus `sbcl_librarian.core`. - It initializes Lisp from a shared-library constructor at load time, locating its - core by name next to itself, with heap size from `SBCL_LIBRARIAN_HEAP_SIZE`. +- A **runtime** is built: `libsbcl_librarian`, which initializes Lisp from a + shared-library constructor at load time, locating a core by name next to itself, + with heap size from `SBCL_LIBRARIAN_HEAP_SIZE`. - Each consumer is a **FASL library**: a small shared library holding the generated C bindings, incbin-embedded FASL bundles for its ASDF system and dependencies, - and a constructor that loads those bundles into the already-running image. + and a constructor that loads those bundles into the running image. `create-fasl-library-cmake-project` generates the whole CMake project. -- There is no per-consumer `.core`, and no explicit init call. +- There is no per-consumer core, and no explicit init call. + +New (what this branch actually does): + +- The runtime, as above — but built with `SBCL_LIBRARIAN_CORE_NAME=libquil.core` + so it brings up libquil's image (D5). +- `libquil.dylib` is the generated C bindings only, linked against the runtime. +- The Lisp side stays a **core**, not FASL bundles, for the reasons in D3. +- No explicit init call: loading the runtime starts Lisp. ## Decisions -### D1: Follow the upstream FASL-library model rather than pinning a fork +### D1: Follow upstream's current model rather than pinning a fork Rejected alternatives: (a) stay on `04f7e39` and carry the one-line `int-sap` patch forever; (b) fork sbcl-librarian to keep `define-api :error-map` alive. @@ -131,7 +139,26 @@ subtypes of `error` -- `invalid-instruction-condition` has no supertype at all - and with a `cl:error` handler those escape into the debugger and hang the calling process. libquil's original error map bound `T` for the same reason. -### D7: Keep shipping the SBCL runtime as `libsbcl.so`, even on macOS +### D7: Choose the BLAS/LAPACK backend at build time, not load time + +`src/build-image.lisp` loads OpenBLAS before anything else can pull in a +BLAS/LAPACK. magicl otherwise picks one by searching: Homebrew's reference LAPACK +first, then a bare `liblapack.dylib`. On arm64 macOS both are wrong, in different +ways: + +- Homebrew's reference LAPACK returns incorrect eigenvectors, which surfaces as + `Could not find diagonalizer for matrix ... after 16 attempts`. (Confirmed + directly: `eig` violated the trace invariant by ~5.8 for n>=3, while OpenBLAS + agreed to ~1e-15.) +- A bare `liblapack.dylib` resolves to Accelerate's, which predates LAPACK 3.3 and + is missing routines quilc calls: `The alien function "zuncsd_" is undefined`. + +Because SBCL records loaded shared objects in the core and reloads them at startup, +loading OpenBLAS at build time bakes the choice into the artifact rather than +leaving it to whatever the loader finds on the user's machine. Patching magicl was +the alternative; this keeps the fix inside libquil. + +### D8: Keep shipping the SBCL runtime as `libsbcl.so`, even on macOS SBCL's `make-shared-library.sh` emits `libsbcl.so` on every platform, and that string becomes the install name recorded in `libsbcl_librarian.dylib`. Renaming the @@ -204,4 +231,10 @@ the Linux job. **Upstreaming.** The three sbcl-librarian fixes are worth PRs regardless of what libquil does; the FASL-bundle one is a plain bug, and the library-prefix one breaks -upstream's own libcalc example on Unix. +upstream's own libcalc example on Unix. They are open as +quil-lang/sbcl-librarian#91. + +**Release ordering.** libquil-sys cannot build against a released libquil older +than this change, so the two have to be released together: cut a libquil +prerelease (`knope prerelease`, added for this reason), point libquil-sys's +`LIBQUIL_VERSION` at it, then release both for real. diff --git a/install.sh b/install.sh index 1f0e295..95662b8 100755 --- a/install.sh +++ b/install.sh @@ -7,11 +7,15 @@ err() { exit 1 } +# Which repository to fetch releases from. Override to install from a fork, which +# is how a prerelease can be tested before it is published from the main repository. +LIBQUIL_RELEASE_REPO="${LIBQUIL_RELEASE_REPO:-rigetti/libquil}" + if [[ -n "${1-}" ]] then - LIBQUIL_URL_PREFIX="https://github.com/rigetti/libquil/releases/download/v${1}" + LIBQUIL_URL_PREFIX="https://github.com/${LIBQUIL_RELEASE_REPO}/releases/download/v${1}" else - LIBQUIL_URL_PREFIX="https://github.com/rigetti/libquil/releases/latest/download" + LIBQUIL_URL_PREFIX="https://github.com/${LIBQUIL_RELEASE_REPO}/releases/latest/download" fi OS="$(uname)" @@ -55,7 +59,7 @@ unzip "${LIBQUIL_RELEASE_FILE}" # libquil.core must land in the same directory as libsbcl_librarian: the runtime # locates its core relative to its own path. -sudo mkdir -p "${LIBQUIL_INCLUDE_PREFIX}" +sudo mkdir -p "${LIBQUIL_LIB_PREFIX}" "${LIBQUIL_INCLUDE_PREFIX}" sudo cp libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h "${LIBQUIL_INCLUDE_PREFIX}" sudo cp libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" diff --git a/versions b/versions index cddad57..a147726 100644 --- a/versions +++ b/versions @@ -4,5 +4,7 @@ QVM_VERSION=4617625cb6053b1adfd3f7aea9cd2be328b225f6 QUILC_VERSION=3d83b665b3addc33be2f696b1cdebc0c4c5a04f9 SBCL_LIBRARIAN_VERSION=fix-secondary-system-bundles SBCL_VERSION=sbcl-2.6.7 +# Linux cross-compilation host; see build.yml. +SBCL_BOOTSTRAP_VERSION=2.5.0 # 2022-04-01 predates clos-encounters, which current cl-quil depends on. QUICKLISP_VERSION=2026-01-01 From b163826bca1c6a3fc6875a2398f59ee418babeb3 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Wed, 12 Aug 2026 11:35:23 -0600 Subject: [PATCH 11/31] fix: check for BLAS and LAPACK, and install without sudo when already root Two problems with installing a release: magicl loads BLAS and LAPACK at runtime under their unversioned names, libblas.so and liblapack.so, so a missing one does not surface at install or link time: it surfaces much later, in the middle of compiling a program. Check for them before anything is downloaded, and name the ones that are missing. The unversioned names are also why a runtime-only package is not enough -- Debian's libblas3 provides libblas.so.3 and no unversioned symlink -- so say that in the requirements, which is the part users get wrong. Drop libz from the requirements while there: nothing in the release artifacts refers to it. Container images commonly run as root with no sudo installed, where every sudo call here fails with "sudo: command not found" even though nothing needs elevating. That is how CI installs libquil, so the installer was unusable there. Resolve sudo once: empty when already root, sudo when available, and a clear error when neither. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 8 +++-- install.sh | 87 +++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9cd7613..2a58c10 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,16 @@ These libraries are required by `libquil`: - `BLAS`, `LAPACK` for linear algebra routines - `libffi` for cross-language execution -- `libz` for compression + +`magicl` loads `BLAS` and `LAPACK` at runtime under their unversioned names, +`libblas.so` and `liblapack.so` (`.dylib` on macOS). Distributions ship those names in +their development packages, so installing only a runtime package such as Debian's +`libblas3` — which provides `libblas.so.3` and no unversioned symlink — is not enough. On systems which use `apt` to install packages (e.g. Ubuntu), these libraries can be installed with the command ``` -sudo apt install libblas-dev libffi-dev liblapack-dev libz-dev +sudo apt install libblas-dev libffi-dev liblapack-dev ``` On systems which use `brew` to install packages (e.g macOS), these libraries can be installed with the command diff --git a/install.sh b/install.sh index 95662b8..d26af0c 100755 --- a/install.sh +++ b/install.sh @@ -48,32 +48,99 @@ else err "Unsupported operating system. Supported operating systems are Linux and macOS." fi +# magicl dlopens BLAS and LAPACK under their unversioned names once libquil is in use, +# so a missing one is not a link error at install time but a failure much later, in the +# middle of compiling a program. Check for them up front instead. +if [[ "${OS}" == "Darwin" ]] +then + LIBQUIL_LIB_SUFFIX="dylib" +else + LIBQUIL_LIB_SUFFIX="so" +fi + +library_is_available() { + local soname="lib${1}.${LIBQUIL_LIB_SUFFIX}" + + # The loader's own cache is authoritative where it exists. + if [[ -z "${IS_LINUX-}" ]] + then + # dyld has no queryable cache; check the paths it searches by default, plus the + # Homebrew prefixes that are not on it. Counting the latter keeps a normal + # `brew install openblas` from being reported as missing, at the cost of not + # catching the case where magicl ends up unable to load a keg-only install. + local dir + for dir in /usr/local/lib /usr/lib /opt/homebrew/lib /opt/homebrew/opt/openblas/lib + do + [[ -e "${dir}/${soname}" ]] && return 0 + done + return 1 + elif command -v ldconfig >/dev/null 2>&1 + then + ldconfig -p | grep -q "[[:space:]]${soname}[[:space:]]" && return 0 + fi + + local dir + for dir in /usr/local/lib /usr/lib /usr/lib64 /lib /lib64 + do + [[ -e "${dir}/${soname}" ]] && return 0 + done + return 1 +} + +LIBQUIL_MISSING=() +for lib in blas lapack +do + library_is_available "${lib}" || LIBQUIL_MISSING+=("lib${lib}.${LIBQUIL_LIB_SUFFIX}") +done + +if [[ "${#LIBQUIL_MISSING[@]}" -gt 0 ]] +then + err "Missing required libraries: ${LIBQUIL_MISSING[*]}" \ + "" \ + "libquil loads these at runtime under exactly these unversioned names, so a" \ + "runtime-only package that provides a versioned name is not sufficient." \ + "See https://github.com/rigetti/libquil#requirements" +fi + LIBQUIL_RELEASE_URL="${LIBQUIL_URL_PREFIX}/${LIBQUIL_RELEASE_FILE}" LIBQUIL_TEMP_DIR="$(mktemp -d)" LIBQUIL_LIB_PREFIX="/usr/local/lib" LIBQUIL_INCLUDE_PREFIX="/usr/local/include/libquil" +# Installing into /usr/local needs root. Container images commonly run as root without +# sudo installed, where calling it would fail even though nothing needs elevating. +if [[ "$(id -u)" -eq 0 ]] +then + SUDO="" +elif command -v sudo >/dev/null 2>&1 +then + SUDO="sudo" +else + err "This installer needs root to write to ${LIBQUIL_LIB_PREFIX} and ${LIBQUIL_INCLUDE_PREFIX}," \ + "but it is not running as root and sudo is not available." +fi + pushd "${LIBQUIL_TEMP_DIR}" || exit curl -L "${LIBQUIL_RELEASE_URL}" -o "${LIBQUIL_RELEASE_FILE}" unzip "${LIBQUIL_RELEASE_FILE}" # libquil.core must land in the same directory as libsbcl_librarian: the runtime # locates its core relative to its own path. -sudo mkdir -p "${LIBQUIL_LIB_PREFIX}" "${LIBQUIL_INCLUDE_PREFIX}" -sudo cp libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h "${LIBQUIL_INCLUDE_PREFIX}" -sudo cp libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" +${SUDO} mkdir -p "${LIBQUIL_LIB_PREFIX}" "${LIBQUIL_INCLUDE_PREFIX}" +${SUDO} cp libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h "${LIBQUIL_INCLUDE_PREFIX}" +${SUDO} cp libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" if [[ -n "${IS_LINUX-}" ]] then - sudo cp libquil/libquil.so libquil/libsbcl_librarian.so "${LIBQUIL_LIB_PREFIX}" - sudo ldconfig + ${SUDO} cp libquil/libquil.so libquil/libsbcl_librarian.so "${LIBQUIL_LIB_PREFIX}" + ${SUDO} ldconfig else - sudo cp libquil/libquil.dylib libquil/libsbcl_librarian.dylib "${LIBQUIL_LIB_PREFIX}" + ${SUDO} cp libquil/libquil.dylib libquil/libsbcl_librarian.dylib "${LIBQUIL_LIB_PREFIX}" # This disables the "cannot open libquil.dylib from untrusted developer" dialog. # A better solution for this would be to properly codesign the files, but that # is a non-trivial amount of work. - sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.dylib" - sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl_librarian.dylib" - sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.core" - sudo xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl.so" + ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.dylib" + ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl_librarian.dylib" + ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.core" + ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl.so" fi From 7db4bfd81f805778eb8771d30e5f43f9aa1c6ec0 Mon Sep 17 00:00:00 2001 From: BatmanAoD Date: Thu, 13 Aug 2026 17:42:44 +0000 Subject: [PATCH 12/31] chore: prepare release 0.4.0-rc.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3321f30..e28f76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.4.0-rc.0 (2026-08-13) + +### Breaking Changes + +- build against modern sbcl-librarian + +### Fixes + +- load OpenBLAS into the image before magicl picks a backend +- check for BLAS and LAPACK, and install without sudo when already root + ## 0.3.2 (2023-12-14) ### Fixes From b151096494990a4ff70a3d5d32fc11394d7fbea6 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Thu, 20 Aug 2026 09:56:41 -0600 Subject: [PATCH 13/31] chore: address PR #58 review comments Applies the reviewable changes from the review of #58 and records the answers to the questions raised in PR-58-REVIEW-ANSWERS.md. - Share the SBCL/Quicklisp install and Lisp-dependency clone between the Linux and macOS build jobs (scripts/install-sbcl.sh, scripts/clone-lisp-deps.sh). - install.sh: unify the library search paths into one array, discover the Homebrew prefix rather than hardcoding it, test the OS explicitly instead of via IS_LINUX, require root rather than shelling out to sudo, and announce the quarantine removal. - README: document that the -dev packages are required, narrow the "any package manager" claim, update the install commands for the root requirement, and replace the removed libquil_error_t/libquil_error() API reference with lisp_err_t/get_error_message(). - Trim reviewer-facing comments that do not earn their place in the tree, and clarify the SBCL_LIBRARIAN_CORE_NAME and D4 "upstream" wording. - REARCHITECTURE: drop the point-in-time status checklist. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 97 ++++------- .github/workflows/release.yml | 9 +- Makefile | 10 +- PR-58-REVIEW-ANSWERS.md | 295 ++++++++++++++++++++++++++++++++++ README.md | 104 +++++++----- REARCHITECTURE.md | 19 +-- examples/quilc/Makefile | 3 +- install.sh | 239 +++++++++++++++++++++------ scripts/clone-lisp-deps.sh | 29 ++++ scripts/install-sbcl.sh | 54 +++++++ src/libquil.lisp | 5 +- 11 files changed, 673 insertions(+), 191 deletions(-) create mode 100644 PR-58-REVIEW-ANSWERS.md create mode 100755 scripts/clone-lisp-deps.sh create mode 100755 scripts/install-sbcl.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb3ed08..30cf136 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,21 +5,22 @@ on: workflow_call: env: - # SBCL is built from source for its linkable runtime; see the build steps. + # SBCL is built from source for its linkable runtime; see scripts/install-sbcl.sh. SBCL_VERSION: "2.6.7" # Host used to cross-compile SBCL_VERSION on Linux. It cannot be apt's (22.04 # ships 2.1.11, too old) nor SBCL_VERSION's own binary release, which is linked # against glibc 2.38 and will not run on 22.04. 2.5.0's binary needs only glibc # 2.34 and is recent enough to build 2.6.7. SBCL_BOOTSTRAP_VERSION: "2.5.0" - # libquil depends on three sbcl-librarian fixes (secondary-system FASL bundles, - # library prefix, configurable core name). They live on a branch of Rigetti's - # fork; move this back to quil-lang/sbcl-librarian once they are upstream. + # libquil depends on three sbcl-librarian fixes + # (secondary-system FASL bundles, library prefix, configurable core name). + # They live on a branch of Rigetti's fork; + # TODO(#60): + # use a released quil-lang/sbcl-librarian once these fixes are released: + # https://github.com/quil-lang/sbcl-librarian/pull/91 SBCL_LIBRARIAN_REPO: "https://github.com/rigetti/sbcl-librarian.git" SBCL_LIBRARIAN_REF: "fix-secondary-system-bundles" - # The Quicklisp dist supplying the Lisp dependencies. It has to be recent enough - # for current cl-quil: the long-standing 2022-04-01 pin predates clos-encounters - # and fails with SYSTEM-NOT-FOUND. + # The Quicklisp dist supplying the Lisp dependencies. QUICKLISP_VERSION: "2026-01-01" jobs: @@ -48,51 +49,33 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc + # env.ImageOS is set by the runner image itself (e.g. "ubuntu22", "macos15"); + # unlike runner.os it changes when the image is upgraded, + # so a new image does not reuse a cache built against the old one. + # Note that this variable is stable but not yet documented: + # https://github.com/github/docs/issues/40108 key: ${{ env.ImageOS }}-${{ runner.arch }}-build-${{ hashFiles('**/versions') }} - if: steps.cache-sbcl.outputs.cache-hit != 'true' name: Install SBCL, libraries, and quicklisp run: | sudo apt update && sudo apt install -y build-essential bzip2 wget - # SBCL is built from source because a linkable runtime (libsbcl.so) is - # required and neither make.sh nor any package manager produces one. - # - # The host is a binary release of SBCL_BOOTSTRAP_VERSION rather than apt's: - # cross compiling a current SBCL needs a host of roughly the same vintage, - # and 22.04 ships 2.1.11, which fails in make-host-1 with "FAILURE-P was set - # when creating genesis.fasl". 22.04 is used deliberately, since the - # artifact inherits the glibc of whatever builds it. + # Cross compiling a current SBCL needs a host of roughly the same + # vintage, and 22.04 ships 2.1.11, which fails in make-host-1 with + # "FAILURE-P was set when creating genesis.fasl". Bootstrap from a + # binary release instead. 22.04 is used deliberately, since the artifact + # inherits the glibc of whatever builds it. wget -q "https://downloads.sourceforge.net/project/sbcl/sbcl/$SBCL_BOOTSTRAP_VERSION/sbcl-$SBCL_BOOTSTRAP_VERSION-x86-64-linux-binary.tar.bz2" -O /tmp/sbcl-bin.tar.bz2 mkdir -p /tmp/sbcl-bin && tar xf /tmp/sbcl-bin.tar.bz2 -C /tmp/sbcl-bin --strip-components=1 cd /tmp/sbcl-bin && sudo sh install.sh - sudo git clone --single-branch --branch sbcl-$SBCL_VERSION https://git.code.sf.net/p/sbcl/sbcl /usr/src/sbcl - cd /usr/src/sbcl && sudo sh make.sh --with-sb-linkable-runtime && sudo sh make-shared-library.sh - sudo sh install.sh - sudo mkdir -p /usr/local/lib - sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so - - sudo apt install -y wget - if [ ! -f ~/quicklisp/setup.lisp ]; then - wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' - sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" - sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' - fi - echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc - rm -f /tmp/quicklisp.lisp - cat ~/.sbclrc + "$GITHUB_WORKSPACE/libquil/scripts/install-sbcl.sh" - name: Pull Lisp dependencies - run: | - git clone https://github.com/quil-lang/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout 3d83b665b3addc33be2f696b1cdebc0c4c5a04f9 - git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 - git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl - git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation - git clone $SBCL_LIBRARIAN_REPO $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout $SBCL_LIBRARIAN_REF + run: "$GITHUB_WORKSPACE/libquil/scripts/clone-lisp-deps.sh" - name: Install quilc dependencies - # libffi-dev pulls in whichever libffi runtime the image ships; naming - # it explicitly breaks whenever the runner image moves (libffi7 does not - # exist on ubuntu 24.04, which ubuntu-latest now resolves to). + # libffi-dev pulls in whichever libffi runtime the image ships; naming a + # version explicitly breaks whenever the runner image moves. run: sudo apt update && sudo apt install -y libblas-dev libffi-dev liblapack-dev libz-dev gfortran - name: Build libquil.so @@ -142,6 +125,7 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc + # See the Linux job for why this keys on env.ImageOS. key: ${{ env.ImageOS }}-${{ runner.arch }}-build-${{ hashFiles('**/versions') }} - uses: Homebrew/actions/setup-homebrew@master @@ -149,36 +133,16 @@ jobs: - if: steps.cache-sbcl-macos.outputs.cache-hit != 'true' name: Install SBCL, libraries, and quicklisp + env: + # Homebrew's sbcl is only the cross-compilation host; drop it once the + # build is done so the one we install is the one on PATH. + SBCL_REMOVE_HOST_CMD: "brew remove -f sbcl" run: | - brew install sbcl git - # See the Linux job: a linkable runtime has to be built from source. Homebrew's - # sbcl is only the cross-compilation host, and its version must be close to the - # one being built -- building the old sbcl-2.2.4 with a current host fails in - # make-host-1 with "undefined variable: SB-VM::END". - git clone --branch sbcl-$SBCL_VERSION https://git.code.sf.net/p/sbcl/sbcl /tmp/sbcl - cd /tmp/sbcl && sudo sh make.sh --with-sb-linkable-runtime && sudo sh make-shared-library.sh - brew remove -f sbcl - sudo sh install.sh - sudo mkdir -p /usr/local/lib - sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so - - brew install wget - if [ ! -f ~/quicklisp/setup.lisp ]; then - wget -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' - sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/$QUICKLISP_VERSION/distinfo.txt\")" - sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp --eval '(ql-util:without-prompting (ql:add-to-init-file))' - fi - echo "#+quicklisp(push (truename \"$GITHUB_WORKSPACE\") ql:*local-project-directories*)" >> ~/.sbclrc - rm -f /tmp/quicklisp.lisp - cat ~/.sbclrc + brew install sbcl git wget + "$GITHUB_WORKSPACE/libquil/scripts/install-sbcl.sh" - name: Pull Lisp dependencies - run: | - git clone https://github.com/quil-lang/quilc.git $GITHUB_WORKSPACE/quilc && cd $GITHUB_WORKSPACE/quilc && git checkout 3d83b665b3addc33be2f696b1cdebc0c4c5a04f9 - git clone https://github.com/quil-lang/qvm.git $GITHUB_WORKSPACE/qvm && cd $GITHUB_WORKSPACE/qvm && git checkout 4617625cb6053b1adfd3f7aea9cd2be328b225f6 - git clone https://github.com/quil-lang/magicl.git $GITHUB_WORKSPACE/magicl - git clone https://github.com/stylewarning/cl-permutation $GITHUB_WORKSPACE/cl-permutation - git clone $SBCL_LIBRARIAN_REPO $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout $SBCL_LIBRARIAN_REF + run: "$GITHUB_WORKSPACE/libquil/scripts/clone-lisp-deps.sh" - name: Install quilc dependencies run: | @@ -223,4 +187,3 @@ jobs: # A directory (not a glob): upload-artifact v4 preserves structure # relative to it, keeping the libquil/ prefix the release job unzips. path: ${{ github.workspace }}/artifacts - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a03fd2..82a2c1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,9 @@ jobs: # Release the ref this was dispatched on, not the default branch. ref: ${{ github.ref }} - # Each artifact is downloaded into its own directory: the two macOS - # artifacts contain identically named files, so a shared directory would - # have one overwrite the other. + # Each artifact is downloaded into its own directory: they contain + # identically named files, so a shared directory would have one overwrite + # the other. - name: Download linux artifacts uses: actions/download-artifact@v4 with: @@ -58,9 +58,6 @@ jobs: - name: Install Knope uses: knope-dev/action@v2.1.2 with: - # 0.23 is needed for `--prerelease-label` and because older versions tag - # the default branch rather than the ref being released, which produces a - # tag pointing at the wrong code when releasing from a branch. version: 0.23.0 # Test before updating, breaking changes likely: https://github.com/knope-dev/action#install-latest-version - name: Release diff --git a/Makefile b/Makefile index a66d69c..a37beb3 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,9 @@ SBCL_LIBRARIAN_DIR := $(shell $(SBCL) --noinform --non-interactive \ --eval '(require :asdf)' \ --eval '(princ (namestring (asdf:system-source-directory "sbcl-librarian")))' 2>/dev/null) -# The linkable SBCL runtime. `make.sh` does not build one and package managers do -# not ship it, so it comes from an SBCL source tree built with +# The linkable SBCL runtime. `make.sh` does not build one by default, and neither +# Homebrew's nor Ubuntu's sbcl package ships it, so it comes from a source tree +# built with # `make-shared-library.sh`; `install.sh` puts it in SBCL's home directory. Note # that SBCL names it libsbcl.so on every platform, including macOS. SBCL_CORE_DIR := $(dir $(shell $(SBCL) --noinform --no-sysinit --no-userinit --non-interactive \ @@ -62,8 +63,9 @@ $(CORE) libquil.c libquil.h $(RUNTIME_DIR)/sbcl_librarian.c: src/libquil.lisp sr # was compiled to look for. mv libquil_core.core $(CORE) -# The runtime is told to load libquil.core rather than the stock -# sbcl_librarian.core, so that libquil's image is what comes up. +# SBCL_LIBRARIAN_CORE_NAME is the core name the runtime is compiled to look for +# next to itself. It defaults to sbcl_librarian.core; libquil never builds a core +# by that name, so the runtime is pointed at libquil.core instead. $(RUNTIME_LIB): $(RUNTIME_DIR)/sbcl_librarian.c ifeq ($(LIBSBCL),) @echo "error: no linkable SBCL runtime (libsbcl.so) found." >&2 diff --git a/PR-58-REVIEW-ANSWERS.md b/PR-58-REVIEW-ANSWERS.md new file mode 100644 index 0000000..d4eac1a --- /dev/null +++ b/PR-58-REVIEW-ANSWERS.md @@ -0,0 +1,295 @@ +# Answers to review comments on #58 + +Companion to the code changes made in response to review. Comments that were +straightforward fixes are listed under **Applied**; everything that was a question, +an uncertainty, or a decision worth recording is answered below. + +## Questions answered + +### `(ql:quickload ...)` and Quicklisp (`.github/workflows/build.yml:66`) + +Yes — Quicklisp is the package manager, ASDF is the build system underneath it (see +above). `(ql:quickload :libquil)` fetches the dependency closure and compiles and +loads the system into the running image. + +### Should the deleted `(ql:quickload ...)` validation steps be restored? (`build.yml`) + +**Recommendation: no, and the naive restore would break the build.** The two lines +that were removed were: + +``` +sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' +sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' +``` + +Loading `:libquil` on its own now signals a *continuable* error. `src/quilc/api.lisp` +deliberately redefines the `quilc_compile_protoquil` alien callable that +sbcl-librarian's `define-api` generated a few forms earlier (it needs +`(* (* t))`, which sbcl-librarian cannot express). SBCL signals +`redefine alien callable` for that and offers a `continue` restart. Under +`--non-interactive` there is nothing to take the restart, so the load aborts. + +That is exactly why `src/build-image.lisp` wraps every load in +`with-alien-redefinition-allowed`. A bare `ql:quickload :libquil` outside that +handler is not a valid way to load libquil any more. + +The validation those lines provided is also now redundant: `make` runs +`src/build-image.lisp`, which quickloads `sbcl-librarian` and then `libquil`, so a +dependency-resolution failure fails the `Build libquil.so` step with the same error +it would have produced earlier — just one step later. + +If a separate pre-flight check is still wanted, it has to install the same handler, +e.g.: + +``` +sbcl --noinform --non-interactive \ + --eval '(handler-bind ((error (lambda (c) (let ((r (find-restart (quote continue) c))) (when (and r (search "redefine alien callable" (princ-to-string c))) (invoke-restart r)))))) (ql:quickload :libquil))' +``` + +which is the build step with extra steps. Left out. + +### Are `libffi` 7 and 8 both supported? (`build.yml:96`) + +They are genuinely incompatible, but nothing here mixes them, so dropping the +explicit `libffi7` is safe — and the old entry was already doing nothing. + +Debian/Ubuntu's `libffi7`/`libffi8` package names track the shared object's *soname* +(`libffi.so.7` / `libffi.so.8`), not an upstream libffi version — upstream has been on +3.x since 2008. `libtool-version` went `8:0:1` (3.3) to `9:0:1` (3.4), and libtool's +soname is `current - age`, giving 7 and 8 respectively. + +The bump was warranted. libffi commit +[`7855656`](https://github.com/libffi/libffi/commit/7855656148b96c7070ec362d2a73af840025a2b7) +("x86: Add indirect branch tracking support", 2020-02-21) says: + +> Trampoline must be enlarged to add ENDBR instruction unconditionally, which is NOP +> on non-CET processors. [...] Update library version for larger `FFI_TRAMPOLINE_SIZE`. + +`FFI_TRAMPOLINE_SIZE` went 24 -> 32 on x86-64 and 12 -> 16 on i386, and it sizes the +first member of the *public* `ffi_closure` struct in `ffi.h`: + +```c +typedef struct { + char tramp[FFI_TRAMPOLINE_SIZE]; + ffi_cif *cif; + void (*fun)(ffi_cif*,void*,void**,void*); + void *user_data; +} ffi_closure; +``` + +So a binary compiled against 3.3's header allocates a 24-byte trampoline and expects +`cif` at offset 24, while 3.4 writes 32 bytes and reads `cif` at offset 32 — an +out-of-bounds write and garbage field reads, not a clean load failure. The soname +bump is what prevents the loader from ever allowing that. + +None of which is reachable here. `libffi-dev` on ubuntu-22.04 resolves `libffi.so` to +`libffi.so.8`, so the build linked against 8 whether or not `libffi7` was in the apt +list — the explicit `libffi7` never influenced what was linked, it just installed a +second runtime nothing used. Naming a version is also what actually breaks the +workflow: `libffi7` does not exist on Ubuntu 24.04. The package list no longer names +one, and the explanatory comment has been shortened to one line. + +**Verified: libffi is a real runtime dependency.** I initially guessed it might be +vestigial, on the grounds that SBCL's `sb-alien` does not use libffi and `libquil.asd` +depends on `#:cffi` rather than `#:cffi-libffi`. That was wrong. Tracing a run of +`examples/quilc/version` against the released 0.4.0-rc.0 artifact with +`DYLD_PRINT_LIBRARIES`, and diffing against a trivial C program as a baseline, the +objects libquil adds are: + + libopenblasp-r0.3.34.dylib magicl's backend (confirms D7) + libgfortran / libquadmath / libgcc_s / libomp OpenBLAS's dependencies + libzstd.1.5.7.dylib core decompression + /usr/lib/libffi.dylib dlopen'd at runtime + libquil / libsbcl_librarian / libsbcl + +libffi does not appear in `otool -L` on any shipped object, which is why it looked +absent -- it is loaded dynamically once Lisp is up. So `libffi-dev` belongs in the +requirements, and it is needed at *run* time, not only to build. + +The same trace independently confirms D7: OpenBLAS is what magicl loads. Accelerate's +`libBLAS`/`libLAPACK` do appear, but they appear in the baseline too, so they are +system-framework noise rather than magicl's choice. + +**Follow-up this raises.** On Linux, CFFI resolves libffi under its unversioned name, +exactly as magicl does for BLAS/LAPACK -- so the argument for `install.sh`'s preflight +check applies to libffi too. I have not added it, because whether the existing check +would find it on Debian is untested: the unversioned `libffi.so` lives in the +multiarch directory (`/usr/lib/x86_64-linux-gnu`), which is not in +`LIBQUIL_SEARCH_DIRS`, and I have not confirmed that `ldconfig -p` lists the +unversioned dev symlink. Worth settling on a Linux box before extending the loop. + +### Is there a canonical way to search all dynamic-library paths on macOS? (`install.sh:72`) + +No, and the options each answer a slightly different question than the one the +installer needs: + +- **`man dyld`** — the default search is `/usr/local/lib` then `/usr/lib`, plus + `DYLD_LIBRARY_PATH` / `DYLD_FALLBACK_LIBRARY_PATH` if set. Those two directories + are now the base of the search list. +- **`DYLD_*` environment variables** — deliberately ignored. They apply to the + installing shell, not to whatever process later loads libquil, so honouring them + would make the check pass for installs the eventual consumer cannot see. +- **`otool -L`** — lists what a binary is *already* linked against. `magicl` `dlopen`s + BLAS/LAPACK at runtime rather than linking them, so they never appear. +- **`find / -name`** — exhaustive but slow enough to be hostile in an installer, and + a hit outside a loader path is not actually usable. +- **`locate`** — needs a database most machines have never built (as you found). +- **`pkg-config`** — answers "is there a `.pc` file for this?", which is not the + question. Homebrew's `openblas` is keg-only and its file is `openblas.pc`, not + `blas.pc`/`lapack.pc`, so `pkg-config --exists blas` fails on exactly the machine + the check most needs to pass. What matters here is whether an *unversioned* + `libblas.dylib` / `liblapack.dylib` exists somewhere the loader will find it, + which is a file-existence test. + +So the check stays a directory scan, but it is now built from a single +`LIBQUIL_SEARCH_DIRS` array, with the Homebrew prefixes discovered via +`brew --prefix` rather than hardcoded, and the Linux-only directories appended in +the Linux branch. That also removes the duplication noted at `install.sh:87`. + +`pkg-config` remains a requirement for the *build* (zstd), which is a separate +concern. + +### What about OpenBLAS? (`install.sh:94`) + +The loop checks `blas` and `lapack`, which is correct: `magicl` `dlopen`s +`libblas`/`liblapack` under those names. OpenBLAS satisfies both — on macOS via the +symlinks CI creates, on Linux via the distribution's alternatives mechanism — so +checking for `libopenblas` directly would reject a perfectly good reference-BLAS +install and miss nothing. + +The confusingly named `LIBQUIL_MISSING` has been renamed `LIBQUIL_MISSING_LIBS`. + +### Is `sbcl_librarian.core` generated or not? (`Makefile:66`) + +Both statements were true and the comment was the confusing part. `SBCL_LIBRARIAN_CORE_NAME` +is a *compile-time* setting on the runtime: it is the filename the runtime looks for +next to itself at load time, and it defaults to `sbcl_librarian.core`. libquil never +produces a file by that name — the build emits `libquil_core.core` and renames it to +`runtime/libquil.core` — so the runtime is compiled with the default overridden. The +comment now says that. + +## Items that looked like upstream bug reports + +Three claims in the diff read as latent upstream bugs. All three are real; two were +already reported and fixed upstream before we got here, and the third still stands. +None of them block this PR. + +1. **Homebrew reference LAPACK computes incorrect eigenvectors on arm64.** + (`build.yml`, `README.md`, `REARCHITECTURE.md` D7.) Real, reproduced, and fully + diagnosed upstream — but the diff's framing is too broad. This is not a property + of reference LAPACK on arm64. It is a **gfortran miscompile of LAPACK >= 3.12.1**: + gfortran >= 15.2 auto-vectorizes `TAU * DCONJG(WORK(I))` in the new `zlarf1f.f` + into aarch64 `fcmla` and drops the conjugation + ([Reference-LAPACK#1160](https://github.com/Reference-LAPACK/lapack/issues/1160), + [GCC PR122408](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=122408), fixed by + `c5fa3d4c8` on 2025-10-27). + + Confirmed end to end on an M-series machine: Homebrew's poured `lapack` 3.12.1 + bottle gives residuals of order 1 for `zgeev`/`zheev`/`zgesvd` at n>=3 while real + routines stay at 1e-16; `brew reinstall --build-from-source lapack` with the fixed + gfortran drops every one of them to ~1e-15. The shipped `_zlarf1f_` contains + `fcmla ... #90` where the rebuilt one has `#270`. + + Nothing was left to report to LAPACK or GCC — both are already fixed. The + remaining gap was that Homebrew had not bumped `revision` to rebuild the affected + bottle, and that is now filed as + **[Homebrew/homebrew-core#300084](https://github.com/Homebrew/homebrew-core/issues/300084)**, + with the standalone reproducer at + [BatmanAoD/lapack-arm64-homebrew-bugs](https://github.com/BatmanAoD/lapack-arm64-homebrew-bugs). + + D7 remains correct regardless, because its other half — Accelerate's missing + `zuncsd_` — is permanent. Only the *justification* in the code comment needs + narrowing. + +2. **macOS LAPACK raises division-by-zero traps.** (`src/quilc/compile.lisp:75`.) + Two separate things are going on, and they pull in opposite directions. + + **The mask is required regardless, and is not a workaround.** SBCL enables + floating-point traps by default — `(:overflow :invalid :divide-by-zero)` — while + C and Fortran callers normally run with them masked. LAPACK is written on the + assumption of the latter, so calling it from Lisp surfaces exceptions that no + other caller ever sees. `magicl:with-blapack` is just + `sb-int:with-float-traps-masked (:divide-by-zero :invalid)`, which restores the + environment LAPACK expects. Adding it to `compile-quil` to match + `compile-protoquil` is a plain correctness fix, and nothing about it belongs + upstream in magicl — magicl already provides exactly this. + + **But the exceptions themselves are recognised bugs, and are being fixed.** In + [Reference-LAPACK#1282](https://github.com/Reference-LAPACK/lapack/issues/1282) + Intel's OneMKL team reported crashes under `-ffpe-trap`/`-fpe0`, and + [PR #1283](https://github.com/Reference-LAPACK/lapack/pull/1283) was merged on + 2026-05-21 fixing them across ten files (`?gedmd`, `?gejsv`, `?gesvdx`, + `?gesvj`). The unguarded `OFL / ROOTSC` in `dgedmd` now reads + `IF ( ROOTSC .GT. ONE ) TBIG = OFL / ROOTSC`. + + So a spurious FPE in a LAPACK routine *is* reportable, and upstream will act on + it — though not unanimously: a maintainer noted on that issue that "generating + `Inf` and working with `Inf` is part of the game", while accepting the fix + anyway. + + **What this means here.** The mask stays, permanently — it is about SBCL's + defaults, not about LAPACK's bugs, and every LAPACK in the wild predates PR #1283 in any case. But if it is ever worth knowing *which* routine traps in + `compile-quil`, that is a reportable upstream bug of exactly the class just + fixed, not something to be resigned to. + + *(I previously wrote that these exceptions are "by design" and that there was + nothing to report. That was wrong: #1282 was closed as completed because the + fixes were merged.)* + +3. **`init(char* core)` is dead code on sbcl-librarian `main`.** + (`REARCHITECTURE.md`, "Why this is not a small change".) The generated function + declares its own `static int initialized`, shadowing the global that every + generated wrapper tests, so calling it leaves every function returning + `LISP_ERR_NOT_INITIALIZED`. Worth an upstream issue independent of libquil. + +The three sbcl-librarian fixes libquil depends on are already open as +[quil-lang/sbcl-librarian#91](https://github.com/quil-lang/sbcl-librarian/pull/91); +`build.yml`'s `SBCL_LIBRARIAN_REPO` comment now links to it. + +## Applied without further comment + +- `build.yml`: the `2022-04-01` Quicklisp-pin aside and the `sbcl-2.2.4` aside + removed; the `libffi7` rationale cut to one line; `SBCL_LIBRARIAN_REPO` now links + the tracking PR; `env.ImageOS` explained in both cache steps. +- `build.yml`: the duplicated SBCL-and-Quicklisp install and Lisp-dependency clone + are now `scripts/install-sbcl.sh` and `scripts/clone-lisp-deps.sh`, shared by both + jobs. The platform-specific part (apt bootstrap host vs. Homebrew host) stays in + the workflow; the macOS job passes `SBCL_REMOVE_HOST_CMD` to drop the Homebrew + `sbcl` once the build no longer needs it. +- `release.yml`: the "two macOS artifacts" comment reworded (there is only one macOS + artifact now); the knope-version rationale comment deleted. +- `examples/quilc/Makefile`: the note about the removed `-pagezero_size` deleted. + Confirmed: the Lisp image is mapped by `libsbcl_librarian`, not by the example + executable, so the example no longer needs its own zero page moved. +- `src/libquil.lisp`: comment suggestion applied. +- `install.sh`: the loader-cache comment moved onto the `ldconfig` branch it + describes; the platform test switched from `-z "${IS_LINUX-}"` to an explicit + `"${OS}" != "Darwin"`; search paths unified into one array; `sudo` handling + replaced with a hard root check; an `echo` added before the quarantine removal. +- `README.md`: says the `-dev` packages are what is needed; "any package manager" + narrowed to the two actually checked; install commands updated for the root + requirement (`curl … | sudo bash`). +- `README.md`: the C API reference still documented the removed + `libquil_error_t`/`libquil_error()`. Updated to `lisp_err_t` / + `get_error_message()` / `enable_backtrace()` per D2. (Not raised in review, but + wrong as it stood.) +- `REARCHITECTURE.md`: "upstream" in D4 disambiguated to sbcl-librarian; the + point-in-time `## Status` checklist removed. `## Open` was kept, retitled + `## Known limitations and follow-ups`, since it is the only record of the fork + dependency and the release ordering. + +## Open decisions for you + +- **`install.sh` now requires root**, per the two comments at `install.sh:111`. That + changes the documented invocation from `curl … | bash` to `curl … | sudo bash`, + which some people object to on principle. The alternative you floated — + re-exec via `sudo "${0}"` — does not work when the script is piped from `curl`, + since `$0` is `bash`. Say the word and I will restore the `${SUDO}` variable + instead. +- **`REARCHITECTURE.md` retained.** The comment at line 216 said "if we keep this + file". It is currently linked from `README.md` and from the PR description, so it + is kept; if you would rather it go, `README.md`'s "Building from source" section + and the D-numbered references in this document are the things to fix up. +- **The knope-version comment** at `release.yml:63` was deleted whole rather than + just its last line — the empty suggestion landed on the final line of a + three-line sentence, and deleting only that line left broken prose. diff --git a/README.md b/README.md index 2a58c10..8193811 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,10 @@ These libraries are required by `libquil`: - `libffi` for cross-language execution `magicl` loads `BLAS` and `LAPACK` at runtime under their unversioned names, -`libblas.so` and `liblapack.so` (`.dylib` on macOS). Distributions ship those names in -their development packages, so installing only a runtime package such as Debian's -`libblas3` — which provides `libblas.so.3` and no unversioned symlink — is not enough. +`libblas.so` and `liblapack.so` (`.dylib` on macOS). Those names come from the +*development* packages, so the `-dev` packages have to be installed — a runtime-only +package such as Debian's `libblas3` provides `libblas.so.3` and no unversioned +symlink, which is not enough. On systems which use `apt` to install packages (e.g. Ubuntu), these libraries can be installed with the command @@ -48,33 +49,45 @@ brew install openblas libffi A script is provided to automate installation of the library. It will detect the host operating system and install the library to an appropriate location. A version identifier can be provided to install a particular version of the library. If no version is provided, the latest version of the library will be installed. -> Note: the installer script requires `sudo` to install the library into the system. +> Note: the installer script must be run as root; it installs the library into `/usr/local`. -> Note: on macOS, `sudo` is also used to mark the library files as trusted. The files themselves are not signed and macOS will, by default, flag them as insecure. +> Note: on macOS, root is also needed to mark the library files as trusted. The files themselves are not signed and macOS will, by default, flag them as insecure. Run the following command ``` -curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | bash +curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | sudo bash ``` If you would like to install a particular version of the library, run the following command ``` -curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | bash -s +curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | sudo bash -s ``` replacing `` with the desired version, e.g. `0.3.0`. +By default the installer checks for the libraries above and stops if any are +missing, leaving it to you to install them. Pass `--install-deps` to have it +install them for you with `apt` or Homebrew, skipping any that are already +present: + +``` +curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | sudo bash -s -- --install-deps +``` + +It requires `apt` on Linux and Homebrew on macOS, and fails if neither is +available. `install.sh --help` lists the options. + ## Manual installation If you would like to manually install the library (for example in the case where you want to install the library to a non-standard location), find the appropriate version and operating system from the [releases page](https://github.com/rigetti/libquil/releases). Within the `.zip` archive you will find the library and header files that are required to use the library. Move these into your file system. ## Building from source -Building requires an SBCL with a *linkable runtime* (`libsbcl.so`). Neither -`make.sh` nor any package manager produces one — Homebrew's `sbcl` bottle, for -instance, does not — so SBCL has to be built from source: +Building requires an SBCL with a *linkable runtime* (`libsbcl.so`). `make.sh` does +not build one by default, and the packages we have checked — Homebrew's `sbcl` +bottle and Ubuntu's `sbcl` — do not ship one, so SBCL has to be built from source: ```bash sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh && sh install.sh @@ -121,14 +134,19 @@ during compilation. Install OpenBLAS (`brew install openblas`) and ensure # C API Reference -## Libquil functions and types +These come from `sbcl_librarian_err.h`, which is installed alongside `libquil.h`. + +- `lisp_err_t` + Enum which indicates whether a function call was successful (`LISP_ERR_SUCCESS`) or not (`LISP_ERR_FAILURE`, `LISP_ERR_BUG`, `LISP_ERR_FATAL`). Most functions will have this as their return type. +- `lisp_err_t get_error_message(char** error_msg)` + Used to retrieve the last error message. -- `libquil_error_t` - Enum which indicates whether a function call was successful (`LIBQUIL_ERROR_SUCCESS`) or not (`LIBQUIL_ERROR_FAIL`). Most functions will have this as their return type. -- `libquil_error_t libquil_error(char** error_msg)` - Used to retrieve the last error message from libquil. + When any error is encountered, it will be stored in memory. A subsequent call to `get_error_message` will return that error message. After calling `get_error_message`, the error is cleared from memory such that immediately calling `get_error_message` after a previous call will return an empty string (indicating no errors since the previous error). +- `lisp_err_t enable_backtrace(int enabled)` + Turns backtrace capture on the error message on or off. - When any error is encountered by libquil, it will be stored in memory. A subsequent call to `libquil_error` will return that error message. After calling `libquil_error`, the error is cleared from memory such that immediately calling `libquil_error` after a previous call will return an empty string (indicating no errors since the previous error). +> Note: this replaces the pre-0.4 `libquil_error_t` / `libquil_error()` API. See +> [REARCHITECTURE.md](REARCHITECTURE.md) (D2) for why. ## Quilc documentation @@ -157,32 +175,32 @@ during compilation. Install OpenBLAS (`brew install openblas`) and ensure ### Functions -- `libquil_error quilc_get_version_info(quilc_version_info *version_info)` +- `lisp_err_t quilc_get_version_info(quilc_version_info *version_info)` Allocates a `quilc_version_info` object and stores the pointer to it in `version_info` See [examples/quilc/version.c](examples/quilc/version.c) -- `libquil_error quilc_version_info_version(quilc_version_info version_info, char** version)` +- `lisp_err_t quilc_version_info_version(quilc_version_info version_info, char** version)` Allocates memory which indicates the version string of Quilc and stores the pointer to it in `version` See [examples/quilc/version.c](examples/quilc/version.c) -- `libquil_error quilc_version_info_githash(quilc_version_info version_info, char** githash)` +- `lisp_err_t quilc_version_info_githash(quilc_version_info version_info, char** githash)` Allocates memory which indicates the githash string of Quilc and stores the pointer to it in `version` See [examples/quilc/version.c](examples/quilc/version.c) -- `libquil_error_t quilc_parse_quil(char* program, quil_program *result)` +- `lisp_err_t quilc_parse_quil(char* program, quil_program *result)` Parses the `program` string and stores it in a `quil_program` -- `libquil_error_t quilc_print_program(quil_program program)` +- `lisp_err_t quilc_print_program(quil_program program)` Prints the `program` to stdout -- `libquil_error_t quilc_program_string(quil_program program, char** result)` +- `lisp_err_t quilc_program_string(quil_program program, char** result)` Allocates and populates a `char*` which is the given `program`'s string representation -- `libquil_error_t quilc_compile_quil(quil_program program, chip_specification chip_spec, quil_program* compiled_program)` +- `lisp_err_t quilc_compile_quil(quil_program program, chip_specification chip_spec, quil_program* compiled_program)` Compiles the `program` for the provided chip specification and stores it in a `quil_program` -- `libquil_error_t quilc_compile_protoquil(quil_program program, chip_specification chip_spec, quil_program* compiled_program)` +- `lisp_err_t quilc_compile_protoquil(quil_program program, chip_specification chip_spec, quil_program* compiled_program)` Compiles the (protoquil) `program` for the provided chip specification and stores it in a `quil_program` -- `libquil_error_t quilc_conjugate_pauli_by_clifford(void* pauli_indices, int pauli_indices_len, void* pauli_terms, int pauli_terms_len, quil_program clifford, void* phase, void* pauli)` +- `lisp_err_t quilc_conjugate_pauli_by_clifford(void* pauli_indices, int pauli_indices_len, void* pauli_terms, int pauli_terms_len, quil_program clifford, void* phase, void* pauli)` Conjugates a Pauli operator by a Clifford operator After having called `quilc_conjugate_pauli_by_clifford`: @@ -190,7 +208,7 @@ during compilation. Install OpenBLAS (`brew install openblas`) and ensure - `phase` will be the encoded global phase factor - `pauli` will be a string description of the resulting encoded Pauli operator -- `libquil_error_t quilc_generate_rb_sequence(int depth, int qubits, void* gateset_ptr, int gateset_len, int seed, void* interleaver, void* results_ptr, void* result_lens_ptr)` +- `lisp_err_t quilc_generate_rb_sequence(int depth, int qubits, void* gateset_ptr, int gateset_len, int seed, void* interleaver, void* results_ptr, void* result_lens_ptr)` Generates a randomized benchmarking sequence After having called `quilc_generate_rb_sequence`: @@ -200,12 +218,12 @@ during compilation. Install OpenBLAS (`brew install openblas`) and ensure See [examples/quilc/generate-rb-sequence.c](examples/quilc/generate-rb-sequence.c) -- `libquil_error_t quilc_build_nq_linear_chip(int n, chip_specification* chip_spec)` +- `lisp_err_t quilc_build_nq_linear_chip(int n, chip_specification* chip_spec)` Builds a linearly-connected `n`-qubit chip specification and stores it in `chip_spec` -- `libquil_error_t quilc_chip_spec_from_isa_descriptor(char* isa_json, chip_specification* chip_spec)` +- `lisp_err_t quilc_chip_spec_from_isa_descriptor(char* isa_json, chip_specification* chip_spec)` Builds an arbitrary chip specification using the JSON-encoded ISA description -- `libquil_error_t quilc_program_memory_type(quil_program program, char* region_name, program_memory_type* region_type)` +- `lisp_err_t quilc_program_memory_type(quil_program program, char* region_name, program_memory_type* region_type)` Returns the `quilc_program_memory_type` for the given memory region ## QVM documentation @@ -214,67 +232,67 @@ during compilation. Install OpenBLAS (`brew install openblas`) and ensure - `qvm_multishot_addresses` An opaque pointer to a QVM multishot addresses object -- `libquil_error_t qvm_multishot_result` +- `lisp_err_t qvm_multishot_result` An opaque pointer to a QVM multishot result object -- `libquil_error_t qvm_version_info` +- `lisp_err_t qvm_version_info` An opaque pointer to a QVM version info object ### Functions -- `libquil_error_t qvm_get_version_info(qvm_version_info* version_info)` +- `lisp_err_t qvm_get_version_info(qvm_version_info* version_info)` Get a new `qvm_version_info` -- `libquil_error_t qvm_version_info_version(qvm_version_info version_info, char** version)` +- `lisp_err_t qvm_version_info_version(qvm_version_info version_info, char** version)` Populate a string at `*version` which has the QVM version -- `libquil_error_t qvm_version_info_githash(qvm_version_info version_info, char** version)` +- `lisp_err_t qvm_version_info_githash(qvm_version_info version_info, char** version)` Populate a string at `*githash` which has the QVM githash -- `libquil_error_t qvm_multishot_addresses_new(qvm_multishot_addresses* addresses)` +- `lisp_err_t qvm_multishot_addresses_new(qvm_multishot_addresses* addresses)` Allocate memory for the `qvm_multishot_addresses` object -- `libquil_error_t qvm_multishot_addresses_set(qvm_multishot_addresses addresses, char* name, void* indices, int len)` +- `lisp_err_t qvm_multishot_addresses_set(qvm_multishot_addresses addresses, char* name, void* indices, int len)` Set the indices of a memory region which should be collected when using `qvm_multishot`. For example, if your register was named `ro` and you wanted to get indices 0 and 2, you would provide `"ro"` for `name` and `{0, 2}` for `indices`. (`len` is the length of `indices`.) -- `libquil_error_t qvm_multishot_addresses_get_all(qvm_multishot_addresses addresses, char* name, int shot_index, void\*\* results, int* results_len) +- `lisp_err_t qvm_multishot_addresses_get_all(qvm_multishot_addresses addresses, char* name, int shot_index, void\*\* results, int* results_len) Request all results for the given memory address. On return, `*results` will be an array of length `results_len`. The specific data type contained in the array is to be interpreted by the caller. -- `libquil_error_t qvm_multishot(quil_program program, qvm_multishot_addresses addresses, int trials, double* gate_noise, double* measurement_noise, qvm_multishot_result *result)` +- `lisp_err_t qvm_multishot(quil_program program, qvm_multishot_addresses addresses, int trials, double* gate_noise, double* measurement_noise, qvm_multishot_result *result)` Execute `program` on the QVM `trials`-number of times, collecting the `addresses` into `result`. `gate_noise` and `measurement_noise` are length-3 arrays which affect gate execution and measurement respectively. One or both can be `NULL` which indicates no noise is to be applied. See [examples/qvm/multishot.c](examples/qvm/multishot.c) -- `libquil_error_t qvm_multishot_result_get(qvm_multishot_result qvm_result, char* region_name, int region_index, void* result)` +- `lisp_err_t qvm_multishot_result_get(qvm_multishot_result qvm_result, char* region_name, int region_index, void* result)` Get the measurement results for `region_index` in `region_name`, storing the data in the pre-allocated `result` `result` should be a pointer to memory which has been allocated to store `N` integers, where `N` is the `trials` value used for `qvm_multishot`. See [examples/qvm/multishot.c](examples/qvm/multishot.c) -- `libquil_error_t qvm_multishot_measure(quil_program program, void* qubits, int n_qubits, int trials, void* result)` +- `lisp_err_t qvm_multishot_measure(quil_program program, void* qubits, int n_qubits, int trials, void* result)` Execute `program` on the QVM `trials`-number of times, storing measurement results for the specified `qubits` into `result` `result` should be a pointer to memory which has been allocated to store `n_qubits * trials` integers. This memory should be interpreted as the two-dimensional array whose outer dimension is `trials` and whose inner dimension is `n_qubits`. See [examples/qvm/multishot-measure.c](examples/qvm/multishot-measure.c) -- `libquil_error_t qvm_expectation(quil_program state_prep, void* operators, int n_operators, void* result)` +- `lisp_err_t qvm_expectation(quil_program state_prep, void* operators, int n_operators, void* result)` Calculate the expectation value `` where `P` is the provided state preparation program `state_prep` and `O` is an operator, for each `O` in `operators`. `result` should be a pointer to memory that has been allocated to store `n_operators` double-precision floats. See [examples/qvm/expectation.c](examples/qvm/expectation.c) -- `libquil_error_t qvm_wavefunction(quil_program program, void* result)` +- `lisp_err_t qvm_wavefunction(quil_program program, void* result)` Execute `program` on the QVM and return the associated wavefunction `result` should be a pointer to memory that has been allocated to store `2 * N_q^2` double-precision floats where `N_q` is the number of qubits used in the program. The factor of 2 is here to account for the fact that the wavefunction is a complex-valued vector, thus to represent it in C we need two values: one for the real part and one for the imaginary part. `result` can be interpreted as a two-dimensional array whose outer dimension (size `N_q^2`) is the wavefunction vector index, and whose inner dimension (size `2`) is the pair of values making up the complex value. See [examples/qvm/wavefunction.c](examples/qvm/wavefunction.c) -- `libquil_error_t qvm_probabilities(quil_program program, void* results_ptr)` +- `lisp_err_t qvm_probabilities(quil_program program, void* results_ptr)` Execute `program` on the QVM and return the wavefunction probabilities `result` should be a pointer to memory that has been allocated to store `N_q^2` double-precision floats where `N_q` is the number of qubits used in the program. Each index `i` in `result` is the probability of finding the wavefunction in the `|i>` state. diff --git a/REARCHITECTURE.md b/REARCHITECTURE.md index f0e8e30..75099a9 100644 --- a/REARCHITECTURE.md +++ b/REARCHITECTURE.md @@ -104,11 +104,11 @@ which is why D6 exists. ### D4: Build the runtime manually rather than via its CMake project -Upstream's `lib/CMakeLists.txt` invokes the generator with `sbcl --script`, which +sbcl-librarian's own `lib/CMakeLists.txt` invokes the generator with `sbcl --script`, which skips `~/.sbclrc` and therefore Quicklisp — so `generate-bindings.lisp`'s `(asdf:load-system :swank)` fails with `Component :SWANK not found`. -Rather than patch upstream's CMake, `src/build-image.lisp` defines the runtime's +Rather than patch sbcl-librarian's CMake, `src/build-image.lisp` defines the runtime's aggregate library itself and emits `runtime/sbcl_librarian.c` alongside libquil's own bindings. One image therefore produces everything: libquil's bindings, the runtime's bindings, and the core that backs both. That also avoids the swank @@ -201,20 +201,7 @@ Three fixes, on the `fix-secondary-system-bundles` branch: prefix cleared. 3. **`SBCL_LIBRARIAN_CORE_NAME`** (D5). -## Status - -- [x] Consumer model determined -- [x] Runtime builds and self-initializes on arm64 macOS -- [x] libquil builds; `make` alone produces a usable artifact -- [x] Verified from C: parse, chip, compile, program string, and error reporting -- [x] libquil-sys updated to the new ABI -- 22/22 tests pass -- [x] Python bindings rebuilt and exercised -- [x] C examples updated and passing (`make test` in both `examples/` directories) -- [x] CI, `install.sh` and release archives updated for the new artifact set -- [x] Packaging verified by simulating package -> zip -> install -> build and - running the libquil-sys suite against the installed layout (22/22) - -## Open +## Known limitations and follow-ups **CI depends on a fork branch.** `build.yml` clones `$SBCL_LIBRARIAN_REPO` at `$SBCL_LIBRARIAN_REF`, currently `rigetti/sbcl-librarian` at diff --git a/examples/quilc/Makefile b/examples/quilc/Makefile index cb816cd..691694e 100644 --- a/examples/quilc/Makefile +++ b/examples/quilc/Makefile @@ -3,8 +3,7 @@ TEST_OBJS = $(TEST_SRCS:.c=) BUILD_DIR = ../.. RUNTIME_DIR = $(BUILD_DIR)/runtime # libquil is the generated bindings; libsbcl_librarian is the runtime that brings up -# the Lisp image (and supplies get_error_message). No -pagezero_size: the image is -# mapped by the runtime library, not by this executable. +# the Lisp image (and supplies get_error_message). CCFLAGS = -lquil -lsbcl_librarian -L$(BUILD_DIR) -L$(RUNTIME_DIR) -I$(BUILD_DIR) -I$(RUNTIME_DIR) .PHONY: all clean diff --git a/install.sh b/install.sh index d26af0c..0d0d063 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +1,59 @@ -#!/bin/bash +#!/usr/bin/env bash -set -u +# -e so a failed download or unpack stops the install rather than falling through +# to copying files that were never extracted; -o pipefail so a failure on the left +# of a pipe is not masked by a successful one on the right. +set -euo pipefail err() { printf "%s\n" "$@" >&2 exit 1 } +usage() { + cat <<'EOF' +Usage: install.sh [--install-deps] [VERSION] + +Installs libquil into /usr/local. Must be run as root. + + VERSION release to install, e.g. 0.4.0. Defaults to the latest release. + --install-deps also install libquil's prerequisites with apt or Homebrew. + Off by default: without it, missing prerequisites are reported + and the install stops. Equivalent to LIBQUIL_INSTALL_DEPS=1. + +Environment: + LIBQUIL_RELEASE_REPO repository to fetch releases from (default rigetti/libquil) + LIBQUIL_INSTALL_DEPS set to 1 for --install-deps +EOF +} + # Which repository to fetch releases from. Override to install from a fork, which # is how a prerelease can be tested before it is published from the main repository. LIBQUIL_RELEASE_REPO="${LIBQUIL_RELEASE_REPO:-rigetti/libquil}" +LIBQUIL_INSTALL_DEPS="${LIBQUIL_INSTALL_DEPS:-0}" +LIBQUIL_VERSION="" + +while [[ $# -gt 0 ]] +do + case "${1}" in + --install-deps) LIBQUIL_INSTALL_DEPS=1 ;; + -h | --help) usage; exit 0 ;; + -*) usage >&2; err "" "Unknown option: ${1}" ;; + *) + if [[ -n "${LIBQUIL_VERSION}" ]] + then + usage >&2 + err "" "Unexpected argument: ${1}" + fi + LIBQUIL_VERSION="${1}" + ;; + esac + shift +done -if [[ -n "${1-}" ]] +if [[ -n "${LIBQUIL_VERSION}" ]] then - LIBQUIL_URL_PREFIX="https://github.com/${LIBQUIL_RELEASE_REPO}/releases/download/v${1}" + LIBQUIL_URL_PREFIX="https://github.com/${LIBQUIL_RELEASE_REPO}/releases/download/v${LIBQUIL_VERSION}" else LIBQUIL_URL_PREFIX="https://github.com/${LIBQUIL_RELEASE_REPO}/releases/latest/download" fi @@ -44,8 +84,106 @@ then "https://github.com/rigetti/libquil#building-from-source" ;; esac +# Windows shells report one of these. libquil publishes no Windows build, so there +# is nothing to install even where the shell would run this script. +elif [[ "${OS}" == CYGWIN* || "${OS}" == MINGW* || "${OS}" == MSYS* || "${OS}" == "Windows_NT" ]] +then + err "Windows is not supported: libquil publishes builds for Linux and macOS only." else - err "Unsupported operating system. Supported operating systems are Linux and macOS." + err "Unsupported operating system: ${OS}. libquil supports Linux and macOS." +fi + +for tool in curl unzip +do + command -v "${tool}" >/dev/null 2>&1 || + err "This installer needs ${tool}, which was not found. Install it and try again." +done + +LIBQUIL_LIB_PREFIX="/usr/local/lib" +LIBQUIL_INCLUDE_PREFIX="/usr/local/include/libquil" + +# Installing into /usr/local needs root, and on macOS clearing the quarantine +# attribute does too. Checked before anything else runs so the failure is immediate. +if [[ "$(id -u)" -ne 0 ]] +then + err "This script must be run as root; it installs into ${LIBQUIL_LIB_PREFIX} and ${LIBQUIL_INCLUDE_PREFIX}." +fi + +# Installing prerequisites is opt-in. The default is to check and report, because +# this script is commonly run as `curl ... | sudo bash` and a package manager +# invocation there has a much wider blast radius than copying files into +# /usr/local. Consumers that want the one-shot path pass --install-deps. +install_prerequisites() { + if [[ "${OS}" == "Darwin" ]] + then + command -v brew >/dev/null 2>&1 || + err "--install-deps needs Homebrew on macOS, which was not found." \ + "Install libquil's requirements another way and re-run without --install-deps:" \ + "https://github.com/rigetti/libquil#requirements" + + # Homebrew refuses to run as root, so it has to run as the invoking user. + # `brew --prefix` is the one subcommand it does allow as root, which is why the + # search paths above can call it directly. + [[ -n "${SUDO_USER-}" ]] || + err "--install-deps needs to run Homebrew, which refuses to run as root." \ + "Re-run through sudo from your normal account (sudo bash install.sh --install-deps)," \ + "or install the requirements yourself and drop --install-deps." + local brew_cmd=(sudo -u "${SUDO_USER}" brew) + + # Homebrew's reference `lapack` is deliberately NOT treated as a conflict here, + # even though magicl prefers it over every other backend and it computes + # incorrect eigenvectors on arm64. A prebuilt libquil is immune: build-image.lisp + # loads OpenBLAS before magicl can pick a backend, and SBCL records loaded shared + # objects in the core and reopens them at startup, so the choice is baked into the + # artifact (REARCHITECTURE.md D7). Verified against a machine with lapack 3.12.1 + # installed: the release loads OpenBLAS and never touches the lapack keg. + # + # It does matter when building libquil from source, which is why CI uninstalls it + # and the README says not to install it. Refusing to install a working binary over + # it would be wrong. + + # OpenBLAS provides both BLAS and LAPACK, and is correct on arm64. + local missing=() + local formula + for formula in openblas libffi + do + "${brew_cmd[@]}" list --formula "${formula}" >/dev/null 2>&1 || missing+=("${formula}") + done + if [[ "${#missing[@]}" -gt 0 ]] + then + echo "Installing prerequisites with Homebrew: ${missing[*]}" + "${brew_cmd[@]}" install "${missing[@]}" + fi + return + fi + + command -v apt-get >/dev/null 2>&1 || + err "--install-deps installs prerequisites with apt, which was not found." \ + "Install libquil's requirements with your package manager and re-run without" \ + "--install-deps: https://github.com/rigetti/libquil#requirements" + + # The -dev packages, not the runtime ones: magicl and CFFI load these under their + # unversioned names, which only the development packages provide. + local missing=() + local package + for package in libblas-dev liblapack-dev libffi-dev + do + if ! dpkg-query -W -f='${Status}' "${package}" 2>/dev/null | grep -q "^install ok installed$" + then + missing+=("${package}") + fi + done + if [[ "${#missing[@]}" -gt 0 ]] + then + echo "Installing prerequisites with apt: ${missing[*]}" + apt-get update + apt-get install -y "${missing[@]}" + fi +} + +if [[ "${LIBQUIL_INSTALL_DEPS}" == "1" ]] +then + install_prerequisites fi # magicl dlopens BLAS and LAPACK under their unversioned names once libquil is in use, @@ -58,89 +196,90 @@ else LIBQUIL_LIB_SUFFIX="so" fi +# Where to look for an unversioned library, in roughly the order the platform's +# loader considers them. +LIBQUIL_SEARCH_DIRS=(/usr/local/lib /usr/lib) +if [[ "${OS}" == "Darwin" ]] +then + # `man dyld` gives /usr/lib and /usr/local/lib as the defaults. Homebrew's + # prefixes are not among them, but counting them keeps a normal + # `brew install openblas` from being reported as missing, at the cost of not + # catching the case where magicl ends up unable to load a keg-only install. + if command -v brew >/dev/null 2>&1 + then + LIBQUIL_BREW_PREFIX="$(brew --prefix)" + LIBQUIL_SEARCH_DIRS+=("${LIBQUIL_BREW_PREFIX}/lib" "${LIBQUIL_BREW_PREFIX}/opt/openblas/lib") + fi +else + LIBQUIL_SEARCH_DIRS+=(/usr/lib64 /lib /lib64) +fi + library_is_available() { local soname="lib${1}.${LIBQUIL_LIB_SUFFIX}" # The loader's own cache is authoritative where it exists. - if [[ -z "${IS_LINUX-}" ]] - then - # dyld has no queryable cache; check the paths it searches by default, plus the - # Homebrew prefixes that are not on it. Counting the latter keeps a normal - # `brew install openblas` from being reported as missing, at the cost of not - # catching the case where magicl ends up unable to load a keg-only install. - local dir - for dir in /usr/local/lib /usr/lib /opt/homebrew/lib /opt/homebrew/opt/openblas/lib - do - [[ -e "${dir}/${soname}" ]] && return 0 - done - return 1 - elif command -v ldconfig >/dev/null 2>&1 + if [[ "${OS}" != "Darwin" ]] && command -v ldconfig >/dev/null 2>&1 then ldconfig -p | grep -q "[[:space:]]${soname}[[:space:]]" && return 0 fi local dir - for dir in /usr/local/lib /usr/lib /usr/lib64 /lib /lib64 + for dir in "${LIBQUIL_SEARCH_DIRS[@]}" do [[ -e "${dir}/${soname}" ]] && return 0 done return 1 } -LIBQUIL_MISSING=() +LIBQUIL_MISSING_LIBS=() for lib in blas lapack do - library_is_available "${lib}" || LIBQUIL_MISSING+=("lib${lib}.${LIBQUIL_LIB_SUFFIX}") + library_is_available "${lib}" || LIBQUIL_MISSING_LIBS+=("lib${lib}.${LIBQUIL_LIB_SUFFIX}") done -if [[ "${#LIBQUIL_MISSING[@]}" -gt 0 ]] +if [[ "${#LIBQUIL_MISSING_LIBS[@]}" -gt 0 ]] then - err "Missing required libraries: ${LIBQUIL_MISSING[*]}" \ + err "Missing required libraries: ${LIBQUIL_MISSING_LIBS[*]}" \ "" \ "libquil loads these at runtime under exactly these unversioned names, so a" \ "runtime-only package that provides a versioned name is not sufficient." \ - "See https://github.com/rigetti/libquil#requirements" + "" \ + "Re-run with --install-deps to install them with apt or Homebrew, or install" \ + "them yourself: https://github.com/rigetti/libquil#requirements" fi LIBQUIL_RELEASE_URL="${LIBQUIL_URL_PREFIX}/${LIBQUIL_RELEASE_FILE}" LIBQUIL_TEMP_DIR="$(mktemp -d)" -LIBQUIL_LIB_PREFIX="/usr/local/lib" -LIBQUIL_INCLUDE_PREFIX="/usr/local/include/libquil" -# Installing into /usr/local needs root. Container images commonly run as root without -# sudo installed, where calling it would fail even though nothing needs elevating. -if [[ "$(id -u)" -eq 0 ]] -then - SUDO="" -elif command -v sudo >/dev/null 2>&1 -then - SUDO="sudo" -else - err "This installer needs root to write to ${LIBQUIL_LIB_PREFIX} and ${LIBQUIL_INCLUDE_PREFIX}," \ - "but it is not running as root and sudo is not available." -fi +trap 'rm -rf "${LIBQUIL_TEMP_DIR}"' EXIT +cd "${LIBQUIL_TEMP_DIR}" -pushd "${LIBQUIL_TEMP_DIR}" || exit -curl -L "${LIBQUIL_RELEASE_URL}" -o "${LIBQUIL_RELEASE_FILE}" +# -f so an HTTP error is a non-zero exit rather than an error page written to the +# archive: without it a bad version tag saves a "404: Not Found" body as the .zip +# and the failure only surfaces later, as a confusing unzip error. +curl -fL "${LIBQUIL_RELEASE_URL}" -o "${LIBQUIL_RELEASE_FILE}" || + err "Could not download ${LIBQUIL_RELEASE_URL}" \ + "Check that the requested version exists: https://github.com/${LIBQUIL_RELEASE_REPO}/releases" unzip "${LIBQUIL_RELEASE_FILE}" # libquil.core must land in the same directory as libsbcl_librarian: the runtime # locates its core relative to its own path. -${SUDO} mkdir -p "${LIBQUIL_LIB_PREFIX}" "${LIBQUIL_INCLUDE_PREFIX}" -${SUDO} cp libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h "${LIBQUIL_INCLUDE_PREFIX}" -${SUDO} cp libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" +mkdir -p "${LIBQUIL_LIB_PREFIX}" "${LIBQUIL_INCLUDE_PREFIX}" +cp libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h "${LIBQUIL_INCLUDE_PREFIX}" +cp libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" if [[ -n "${IS_LINUX-}" ]] then - ${SUDO} cp libquil/libquil.so libquil/libsbcl_librarian.so "${LIBQUIL_LIB_PREFIX}" - ${SUDO} ldconfig + cp libquil/libquil.so libquil/libsbcl_librarian.so "${LIBQUIL_LIB_PREFIX}" + ldconfig else - ${SUDO} cp libquil/libquil.dylib libquil/libsbcl_librarian.dylib "${LIBQUIL_LIB_PREFIX}" + cp libquil/libquil.dylib libquil/libsbcl_librarian.dylib "${LIBQUIL_LIB_PREFIX}" + echo "Removing the quarantine attribute from the installed files." # This disables the "cannot open libquil.dylib from untrusted developer" dialog. # A better solution for this would be to properly codesign the files, but that # is a non-trivial amount of work. - ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.dylib" - ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl_librarian.dylib" - ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.core" - ${SUDO} xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl.so" + xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.dylib" + xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl_librarian.dylib" + xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.core" + xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl.so" fi diff --git a/scripts/clone-lisp-deps.sh b/scripts/clone-lisp-deps.sh new file mode 100755 index 0000000..4ea6e92 --- /dev/null +++ b/scripts/clone-lisp-deps.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Clone the Lisp systems libquil is built against into GITHUB_WORKSPACE, which the +# build adds to ql:*local-project-directories*. Shared by the Linux and macOS jobs. +# +# Required environment: +# GITHUB_WORKSPACE where the systems are cloned +# SBCL_LIBRARIAN_REPO sbcl-librarian remote to clone +# SBCL_LIBRARIAN_REF ref to check out in it + +set -euo pipefail + +: "${GITHUB_WORKSPACE:?GITHUB_WORKSPACE must be set}" +: "${SBCL_LIBRARIAN_REPO:?SBCL_LIBRARIAN_REPO must be set}" +: "${SBCL_LIBRARIAN_REF:?SBCL_LIBRARIAN_REF must be set}" + +QUILC_REF="3d83b665b3addc33be2f696b1cdebc0c4c5a04f9" +QVM_REF="4617625cb6053b1adfd3f7aea9cd2be328b225f6" + +git clone https://github.com/quil-lang/quilc.git "${GITHUB_WORKSPACE}/quilc" +git -C "${GITHUB_WORKSPACE}/quilc" checkout "${QUILC_REF}" + +git clone https://github.com/quil-lang/qvm.git "${GITHUB_WORKSPACE}/qvm" +git -C "${GITHUB_WORKSPACE}/qvm" checkout "${QVM_REF}" + +git clone https://github.com/quil-lang/magicl.git "${GITHUB_WORKSPACE}/magicl" +git clone https://github.com/stylewarning/cl-permutation "${GITHUB_WORKSPACE}/cl-permutation" + +git clone "${SBCL_LIBRARIAN_REPO}" "${GITHUB_WORKSPACE}/sbcl-librarian" +git -C "${GITHUB_WORKSPACE}/sbcl-librarian" checkout "${SBCL_LIBRARIAN_REF}" diff --git a/scripts/install-sbcl.sh b/scripts/install-sbcl.sh new file mode 100755 index 0000000..e8b7558 --- /dev/null +++ b/scripts/install-sbcl.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Build and install an SBCL with a linkable runtime (libsbcl.so), then install +# Quicklisp. Shared by the Linux and macOS build jobs, which otherwise differ only +# in how the cross-compilation host is provided. +# +# A linkable runtime is required and `make.sh` does not build one by default, nor +# do the binary releases or the apt/Homebrew packages ship one, so SBCL has to be +# built from source. The host SBCL must already be on PATH and of roughly the same +# vintage as SBCL_VERSION. +# +# Required environment: +# SBCL_VERSION SBCL release to build, e.g. 2.6.7 +# QUICKLISP_VERSION Quicklisp dist to pin, e.g. 2026-01-01 +# GITHUB_WORKSPACE added to ql:*local-project-directories* +# Optional environment: +# SBCL_SRC_DIR where to clone SBCL (default /tmp/sbcl) +# SBCL_REMOVE_HOST_CMD run once the build is done and the host is no longer +# needed, to get it out of the way before installing + +set -euo pipefail + +: "${SBCL_VERSION:?SBCL_VERSION must be set}" +: "${QUICKLISP_VERSION:?QUICKLISP_VERSION must be set}" +: "${GITHUB_WORKSPACE:?GITHUB_WORKSPACE must be set}" + +SBCL_SRC_DIR="${SBCL_SRC_DIR:-/tmp/sbcl}" + +sudo git clone --single-branch --branch "sbcl-${SBCL_VERSION}" \ + https://git.code.sf.net/p/sbcl/sbcl "${SBCL_SRC_DIR}" +cd "${SBCL_SRC_DIR}" +sudo sh make.sh --with-sb-linkable-runtime +sudo sh make-shared-library.sh + +if [[ -n "${SBCL_REMOVE_HOST_CMD:-}" ]] +then + eval "${SBCL_REMOVE_HOST_CMD}" +fi + +sudo sh install.sh +sudo mkdir -p /usr/local/lib +sudo cp src/runtime/libsbcl.so /usr/local/lib/libsbcl.so + +if [[ ! -f ~/quicklisp/setup.lisp ]] +then + wget -q -P /tmp/ 'https://beta.quicklisp.org/quicklisp.lisp' + sbcl --noinform --non-interactive --load /tmp/quicklisp.lisp \ + --eval "(quicklisp-quickstart:install :dist-url \"http://beta.quicklisp.org/dist/quicklisp/${QUICKLISP_VERSION}/distinfo.txt\")" + sbcl --noinform --non-interactive --load ~/quicklisp/setup.lisp \ + --eval '(ql-util:without-prompting (ql:add-to-init-file))' + rm -f /tmp/quicklisp.lisp +fi + +echo "#+quicklisp(push (truename \"${GITHUB_WORKSPACE}\") ql:*local-project-directories*)" >> ~/.sbclrc +cat ~/.sbclrc diff --git a/src/libquil.lisp b/src/libquil.lisp index 12b9486..f1061aa 100644 --- a/src/libquil.lisp +++ b/src/libquil.lisp @@ -55,9 +55,8 @@ ;; Handle T, not CL:ERROR. cl-quil signals conditions that are not subtypes of ;; ERROR -- INVALID-INSTRUCTION-CONDITION, raised for an unrecognized ;; instruction, has no supertype at all -- so a handler bound to CL:ERROR lets - ;; them escape to the debugger and hang the calling process. libquil's original - ;; error map bound T for this reason. Warnings are passed over first so they do - ;; not abort the call. + ;; them escape to the debugger and hang the calling process. Warnings are + ;; passed over first so they do not abort the call. ((cl:warning #'cl:continue) (t (lambda (condition) (setf sbcl-librarian::*error-message* (format nil "~a" condition)) From 0dce152be0ce6cc472c9f338a565542d89fd2d1c Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Fri, 21 Aug 2026 17:14:35 -0600 Subject: [PATCH 14/31] build: depend on magicl with backend selection, drop the OpenBLAS workarounds magicl picked a BLAS/LAPACK by searching a fixed list of paths that did not include OpenBLAS, so libquil had to force the choice from outside: the build image loaded OpenBLAS via load-shared-object before magicl could look, and CI uninstalled Homebrew's lapack and symlinked OpenBLAS over liblapack.dylib. Both were workarounds for magicl having no way to say "use OpenBLAS". magicl now searches for OpenBLAS by name, honours MAGICL_LAPACK_PATH, and verifies at load that the backend it chose is complete and computes correctly (quil-lang/magicl#221, PR #222). CI tracks rigetti/magicl at backend-selection-and-validation, parameterised as MAGICL_REPO/MAGICL_REF alongside the sbcl-librarian pin, until that lands upstream. So all three workarounds go. A bad backend now fails the build with a message naming the library, rather than yielding an artifact that is quietly wrong -- which is strictly better than what the preload did, since the preload only ensured OpenBLAS was *available*, never that it was what magicl used. Verified by loading libquil against the new magicl on a machine that still has the miscompiled Homebrew lapack 3.12.1 bottle installed: magicl selects OpenBLAS for both BLAS and LAPACK unaided. Unchanged: the backend is still fixed at build time, because SBCL records loaded shared objects in the core and reopens them at startup. That is also why install.sh still checks the user's machine for BLAS and LAPACK. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 26 ++++++++++-------- README.md | 21 ++++++++------ REARCHITECTURE.md | 55 +++++++++++++++++++++++++------------ scripts/clone-lisp-deps.sh | 5 +++- src/build-image.lisp | 29 +++++++------------ 5 files changed, 79 insertions(+), 57 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30cf136..d1d1a0c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,15 @@ env: # https://github.com/quil-lang/sbcl-librarian/pull/91 SBCL_LIBRARIAN_REPO: "https://github.com/rigetti/sbcl-librarian.git" SBCL_LIBRARIAN_REF: "fix-secondary-system-bundles" + # magicl selects a BLAS/LAPACK by searching a fixed list of paths. Until it + # knows about OpenBLAS, that search picks a backend that is wrong on arm64 and + # cannot be redirected without hacks -- so libquil had to preload OpenBLAS and + # symlink it over liblapack.dylib. This branch adds OpenBLAS to the search, + # a MAGICL_LAPACK_PATH override, and a startup check on what got loaded. + # TODO(#60-adjacent): move back to quil-lang/magicl once + # https://github.com/quil-lang/magicl/pull/222 is merged and released. + MAGICL_REPO: "https://github.com/rigetti/magicl.git" + MAGICL_REF: "backend-selection-and-validation" # The Quicklisp dist supplying the Lisp dependencies. QUICKLISP_VERSION: "2026-01-01" @@ -145,17 +154,12 @@ jobs: run: "$GITHUB_WORKSPACE/libquil/scripts/clone-lisp-deps.sh" - name: Install quilc dependencies - run: | - # Deliberately not installing lapack. magicl prefers Homebrew's reference - # LAPACK over everything else, and on arm64 it computes incorrect - # eigenvectors -- compilation then fails with "Could not find diagonalizer - # for matrix ... after 16 attempts". With it absent, magicl falls through to - # a bare liblapack.dylib, which these symlinks point at OpenBLAS. - brew uninstall --ignore-dependencies lapack || true - brew install openblas libffi gfortran - sudo mkdir -p /usr/local/lib - sudo ln -sf "$(brew --prefix openblas)/lib/libopenblas.dylib" /usr/local/lib/liblapack.dylib - sudo ln -sf "$(brew --prefix openblas)/lib/libopenblas.dylib" /usr/local/lib/libblas.dylib + # magicl now finds OpenBLAS by name and refuses to start on a backend that + # is incomplete or computes incorrectly, so there is nothing to arrange + # here beyond installing it. Homebrew's reference `lapack' no longer has to + # be uninstalled, and OpenBLAS no longer has to be symlinked over + # liblapack.dylib, because magicl now prefers it outright. + run: brew install openblas libffi gfortran - name: Build libquil.dylib run: | diff --git a/README.md b/README.md index 8193811..e2e9a38 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,10 @@ On systems which use `brew` to install packages (e.g macOS), these libraries can brew install openblas libffi ``` -> Note: do not install Homebrew's `lapack` on Apple Silicon. `magicl` prefers it -> over every other backend, and it computes incorrect eigenvectors there, which -> surfaces as `Could not find diagonalizer for matrix ... after 16 attempts` -> during compilation. +> Note: OpenBLAS is what supplies BLAS and LAPACK here, and `magicl` selects it in +> preference to anything else. Homebrew's reference `lapack` no longer has to be +> avoided, but it is not a substitute: its current arm64 bottle computes incorrect +> eigenvectors, and Accelerate is missing routines `quilc` needs. ## Automated installation @@ -127,10 +127,15 @@ See [REARCHITECTURE.md](REARCHITECTURE.md) for how this fits together and why. ### Linear algebra backend on aarch64 macOS -Homebrew's reference `lapack` computes incorrect eigenvectors on aarch64, which -surfaces as `Could not find diagonalizer for matrix ... after 16 attempts` -during compilation. Install OpenBLAS (`brew install openblas`) and ensure -`magicl` loads it in preference to `lapack`. +Install OpenBLAS (`brew install openblas`). `magicl` prefers it over the +alternatives, and checks at load time that whatever it selected is complete and +computes correctly, so a bad backend fails the build with a message naming the +library rather than producing an artifact that is silently wrong. + +The alternatives are both unusable on aarch64: Homebrew's reference `lapack` bottle +returns incorrect eigenvectors for complex input +([homebrew-core#300084](https://github.com/Homebrew/homebrew-core/issues/300084)), +and Accelerate's legacy LAPACK is missing routines `quilc` calls. # C API Reference diff --git a/REARCHITECTURE.md b/REARCHITECTURE.md index 75099a9..b107060 100644 --- a/REARCHITECTURE.md +++ b/REARCHITECTURE.md @@ -139,24 +139,43 @@ subtypes of `error` -- `invalid-instruction-condition` has no supertype at all - and with a `cl:error` handler those escape into the debugger and hang the calling process. libquil's original error map bound `T` for the same reason. -### D7: Choose the BLAS/LAPACK backend at build time, not load time - -`src/build-image.lisp` loads OpenBLAS before anything else can pull in a -BLAS/LAPACK. magicl otherwise picks one by searching: Homebrew's reference LAPACK -first, then a bare `liblapack.dylib`. On arm64 macOS both are wrong, in different -ways: - -- Homebrew's reference LAPACK returns incorrect eigenvectors, which surfaces as - `Could not find diagonalizer for matrix ... after 16 attempts`. (Confirmed - directly: `eig` violated the trace invariant by ~5.8 for n>=3, while OpenBLAS - agreed to ~1e-15.) -- A bare `liblapack.dylib` resolves to Accelerate's, which predates LAPACK 3.3 and - is missing routines quilc calls: `The alien function "zuncsd_" is undefined`. - -Because SBCL records loaded shared objects in the core and reloads them at startup, -loading OpenBLAS at build time bakes the choice into the artifact rather than -leaving it to whatever the loader finds on the user's machine. Patching magicl was -the alternative; this keeps the fix inside libquil. +### D7: Let magicl choose the BLAS/LAPACK backend, and fail loudly if it chooses badly + +**Superseded.** libquil originally forced the choice itself, and no longer does; the +fix moved upstream, which is where it belonged. + +The problem was that magicl picked a backend on Darwin by searching Homebrew's +reference LAPACK first, then a bare `liblapack.dylib`, with OpenBLAS not in the list +at all. On arm64 macOS both candidates are wrong, in different ways: + +- Homebrew's reference LAPACK, as bottled, returns incorrect eigenvectors for + complex input at n >= 3, which surfaces as `Could not find diagonalizer for + matrix ... after 16 attempts`. Not a defect in reference LAPACK: a gfortran + miscompile of `zlarf1f.f`, new in LAPACK 3.12.1 (GCC PR122408, + Reference-LAPACK#1160, both fixed; Homebrew/homebrew-core#300084 tracks the + un-rebuilt bottle). +- A bare `liblapack.dylib` resolves to Accelerate's legacy interface, which is + LAPACK 3.2.1 and lacks routines quilc calls: `The alien function "zuncsd_" is + undefined`. That one is permanent, not a packaging accident. + +So `src/build-image.lisp` used to `load-shared-object` OpenBLAS before magicl could +look, and CI additionally uninstalled Homebrew's `lapack` and symlinked OpenBLAS +over `liblapack.dylib`. Both were workarounds for magicl having no way to express +"use OpenBLAS". + +magicl now searches for OpenBLAS by name, accepts `MAGICL_LAPACK_PATH`, and checks +at load time that the backend it selected is complete and computes correctly +(quil-lang/magicl#221, PR #222; libquil tracks `rigetti/magicl` at +`backend-selection-and-validation` until that is merged and released). The preload +and the symlinks are gone. A bad backend now fails the build with a message naming +the library, instead of yielding an artifact that is quietly wrong. + +What has not changed is the property that made this a build-time decision: SBCL +records loaded shared objects in the core and reopens them at startup, so whatever +magicl selects during the build is baked into the artifact rather than left to +whatever the loader finds on the user's machine. Which is also why the installed +artifact still needs OpenBLAS present at the path recorded at build time — see +`install.sh`'s preflight check. ### D8: Keep shipping the SBCL runtime as `libsbcl.so`, even on macOS diff --git a/scripts/clone-lisp-deps.sh b/scripts/clone-lisp-deps.sh index 4ea6e92..d1e9fd8 100755 --- a/scripts/clone-lisp-deps.sh +++ b/scripts/clone-lisp-deps.sh @@ -12,6 +12,8 @@ set -euo pipefail : "${GITHUB_WORKSPACE:?GITHUB_WORKSPACE must be set}" : "${SBCL_LIBRARIAN_REPO:?SBCL_LIBRARIAN_REPO must be set}" : "${SBCL_LIBRARIAN_REF:?SBCL_LIBRARIAN_REF must be set}" +: "${MAGICL_REPO:?MAGICL_REPO must be set}" +: "${MAGICL_REF:?MAGICL_REF must be set}" QUILC_REF="3d83b665b3addc33be2f696b1cdebc0c4c5a04f9" QVM_REF="4617625cb6053b1adfd3f7aea9cd2be328b225f6" @@ -22,7 +24,8 @@ git -C "${GITHUB_WORKSPACE}/quilc" checkout "${QUILC_REF}" git clone https://github.com/quil-lang/qvm.git "${GITHUB_WORKSPACE}/qvm" git -C "${GITHUB_WORKSPACE}/qvm" checkout "${QVM_REF}" -git clone https://github.com/quil-lang/magicl.git "${GITHUB_WORKSPACE}/magicl" +git clone "${MAGICL_REPO}" "${GITHUB_WORKSPACE}/magicl" +git -C "${GITHUB_WORKSPACE}/magicl" checkout "${MAGICL_REF}" git clone https://github.com/stylewarning/cl-permutation "${GITHUB_WORKSPACE}/cl-permutation" git clone "${SBCL_LIBRARIAN_REPO}" "${GITHUB_WORKSPACE}/sbcl-librarian" diff --git a/src/build-image.lisp b/src/build-image.lisp index 9a4039e..88685be 100644 --- a/src/build-image.lisp +++ b/src/build-image.lisp @@ -35,27 +35,18 @@ (funcall (read-from-string "quicklisp:quickload") system) (asdf:load-system system)))) -;; Load OpenBLAS before anything else pulls in a BLAS/LAPACK, so that its symbols -;; are the ones that resolve. +;; The BLAS/LAPACK backend is magicl's to choose. It searches for OpenBLAS first, +;; and refuses to load a backend that is missing routines quilc calls or that +;; computes incorrect eigenvectors, so a bad one fails this build with a message +;; naming the library rather than producing an artifact that is quietly wrong. ;; -;; magicl looks for Homebrew's reference LAPACK first and falls back to a bare -;; liblapack.dylib, which on macOS is Accelerate's. Neither is usable here: the -;; reference build returns incorrect eigenvectors on arm64 ("Could not find -;; diagonalizer for matrix ... after 16 attempts"), and Accelerate's LAPACK predates -;; 3.3, so routines quilc needs are simply missing ("The alien function zuncsd_ is -;; undefined"). OpenBLAS is correct and complete on both counts. +;; libquil used to force the choice here, by loading OpenBLAS before magicl could +;; look. That is no longer necessary; see REARCHITECTURE.md D7. If magicl is ever +;; pinned back to a version without the search entry, MAGICL_LAPACK_PATH set at +;; build time does the same job. ;; -;; SBCL records loaded shared objects in the core and reloads them at startup, so -;; this choice is baked into the artifact rather than left to the loader. -#+darwin -(let ((openblas (find-if #'probe-file - '("/opt/homebrew/opt/openblas/lib/libopenblas.dylib" - "/usr/local/opt/openblas/lib/libopenblas.dylib")))) - (if openblas - (sb-alien:load-shared-object openblas) - (warn "OpenBLAS not found; magicl may load a BLAS/LAPACK that miscomputes ~ - eigenvectors or lacks routines quilc needs. Install it with ~ - `brew install openblas'."))) +;; Whatever magicl loads is recorded in the core by SBCL and reopened at startup, +;; so the choice is still baked into the artifact rather than left to the loader. (load-system '#:sbcl-librarian) (load-system '#:libquil) From cf813060d910dca6292b6fcde34ce1c87e7688bf Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Sat, 22 Aug 2026 16:26:56 -0600 Subject: [PATCH 15/31] refactor: use quilc's PROCESS-PROTOQUIL instead of vendoring it PROCESS-PROTOQUIL and STRIP-FINAL-HALT-RESPECTING-REWIRINGS lived in quilc's *application*, which libquil does not depend on, so libquil carried a copy of both. quil-lang/quilc#933 moves them into the library as src/analysis/process-protoquil.lisp and exports the transform from CL-QUIL.FRONTEND, which is where they belong. So the vendored copy goes -- 85 lines, and with it a maintenance hazard: two transforms registered under the same name would have collided once the library defined one. Both call sites now name CL-QUIL.FRONTEND:PROCESS-PROTOQUIL rather than a bare 'PROCESS-PROTOQUIL. The libquil package only uses CL, so the unqualified symbol read as LIBQUIL::PROCESS-PROTOQUIL and would no longer have matched any registered transform. QUILC_REPO/QUILC_REF join the sbcl-librarian and magicl pins, tracking rigetti/quilc at 896-protoquil-transform until #933 merges. That branch is three commits ahead of the commit libquil pinned and contains it, so this is a fast-forward rather than a version bump. Verified against the branch locally: libquil loads, and compiling CNOT 0 1 as protoquil yields 8 instructions with the final HALT stripped, circuit definitions cleared, and statistics computed. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 6 +++ scripts/clone-lisp-deps.sh | 5 ++- src/quilc/compile.lisp | 2 +- src/quilc/quilc-imports.lisp | 87 +----------------------------------- 4 files changed, 11 insertions(+), 89 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1d1a0c..acbcb7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,12 @@ env: # https://github.com/quil-lang/magicl/pull/222 is merged and released. MAGICL_REPO: "https://github.com/rigetti/magicl.git" MAGICL_REF: "backend-selection-and-validation" + # PROCESS-PROTOQUIL lives in quilc's *application*, which libquil does not + # depend on, so libquil vendored a copy. quil-lang/quilc#933 moves it into the + # library and exports it, which is where it belongs. + # TODO: move back to a quil-lang/quilc commit once #933 is merged. + QUILC_REPO: "https://github.com/rigetti/quilc.git" + QUILC_REF: "896-protoquil-transform" # The Quicklisp dist supplying the Lisp dependencies. QUICKLISP_VERSION: "2026-01-01" diff --git a/scripts/clone-lisp-deps.sh b/scripts/clone-lisp-deps.sh index d1e9fd8..6ce4899 100755 --- a/scripts/clone-lisp-deps.sh +++ b/scripts/clone-lisp-deps.sh @@ -14,11 +14,12 @@ set -euo pipefail : "${SBCL_LIBRARIAN_REF:?SBCL_LIBRARIAN_REF must be set}" : "${MAGICL_REPO:?MAGICL_REPO must be set}" : "${MAGICL_REF:?MAGICL_REF must be set}" +: "${QUILC_REPO:?QUILC_REPO must be set}" +: "${QUILC_REF:?QUILC_REF must be set}" -QUILC_REF="3d83b665b3addc33be2f696b1cdebc0c4c5a04f9" QVM_REF="4617625cb6053b1adfd3f7aea9cd2be328b225f6" -git clone https://github.com/quil-lang/quilc.git "${GITHUB_WORKSPACE}/quilc" +git clone "${QUILC_REPO}" "${GITHUB_WORKSPACE}/quilc" git -C "${GITHUB_WORKSPACE}/quilc" checkout "${QUILC_REF}" git clone https://github.com/quil-lang/qvm.git "${GITHUB_WORKSPACE}/qvm" diff --git a/src/quilc/compile.lisp b/src/quilc/compile.lisp index 813d8ed..31c0e46 100644 --- a/src/quilc/compile.lisp +++ b/src/quilc/compile.lisp @@ -81,5 +81,5 @@ (let ((handle (sbcl-librarian::make-handle metadata))) (setf (sb-alien:deref metadata-ptr) handle))) - (cl-quil.frontend::transform 'process-protoquil compiled-program) + (cl-quil.frontend::transform 'cl-quil.frontend:process-protoquil compiled-program) compiled-program)) diff --git a/src/quilc/quilc-imports.lisp b/src/quilc/quilc-imports.lisp index 0aaf033..f212e5f 100644 --- a/src/quilc/quilc-imports.lisp +++ b/src/quilc/quilc-imports.lisp @@ -32,91 +32,6 @@ :documentation "The git hash of the quilc repo.") ;;; borrowed from quilc: app/src/entry-point.lisp -;;; -;;; PROCESS-PROTOQUIL and STRIP-FINAL-HALT-RESPECTING-REWIRINGS live in quilc's -;;; *application*, which we do not depend on, so they are vendored here rather -;;; than requiring a quilc fork that exposes them from the library. -;;; TODO(https://github.com/quil-lang/quilc/pull/933): remove these once the logic is upstreamed - -(cl-quil.frontend::define-transform process-protoquil (process-protoquil) - "Removes HALT, DEFCIRCUIT, and DEFGATE instructions.") - -(defun process-protoquil (parsed-program) - (setf (cl-quil.frontend::parsed-program-circuit-definitions parsed-program) nil - (cl-quil.frontend::parsed-program-gate-definitions parsed-program) nil) - - ;; if we're supposed to output protoQuil, we also need to - ;; strip the final HALT instructions from the output - (setf (cl-quil:parsed-program-executable-code parsed-program) - (strip-final-halt-respecting-rewirings parsed-program))) - -(defun strip-final-halt-respecting-rewirings (processed-program) - "Remove the final HALT instruction, if any, from PROCESSED-PROGRAM, retaining any attached rewiring comments." - (let* ((instructions (cl-quil:parsed-program-executable-code processed-program)) - (last-instruction (and (plusp (length instructions)) - (cl-quil::nth-instr 0 processed-program :from-end t))) - (penultimate-instruction (and (< 1 (length instructions)) - (cl-quil::nth-instr 1 processed-program :from-end t))) - (must-transfer-comment-p (and (not (null penultimate-instruction)) - (cl-quil.frontend::comment last-instruction)))) - - (unless (cl-quil::haltp last-instruction) - (return-from strip-final-halt-respecting-rewirings instructions)) - - (when must-transfer-comment-p - ;; Transfer the rewiring comment from LAST-INSTRUCTION to - ;; PENULTIMATE-INSTRUCTION. - (multiple-value-bind (last-entering last-exiting) - (cl-quil::instruction-rewirings last-instruction) - (multiple-value-bind (penultimate-entering penultimate-exiting) - (cl-quil::instruction-rewirings penultimate-instruction) - (flet ((assert-rewirings-compatible (rewiring-type last-rewiring penultimate-rewiring) - ;; This bit of hoop-jumping guards against the - ;; unlikely event that both PENULTIMATE-INSTRUCTION - ;; and LAST-INSTRUCTION have rewiring comments - ;; attached which might be incompatible. We check - ;; to ensure that either one of the rewirings is - ;; NULL, or else they are EQUALP and can safely be - ;; merged. - (assert (or (or (null last-rewiring) - (null penultimate-rewiring)) - (equalp last-rewiring penultimate-rewiring)) - () - "Failed to strip final HALT. Instructions have incompatible ~A rewirings:~@ - LAST: ~A ~A~@ - PREV: ~A ~A" - rewiring-type last-instruction last-rewiring - penultimate-instruction penultimate-rewiring))) - (assert-rewirings-compatible ':ENTERING last-entering penultimate-entering) - (assert-rewirings-compatible ':EXITING last-exiting penultimate-exiting)) - ;; Consider the following cases for the :ENTERING rewirings - ;; (the same case analysis applies to the :EXITING rewiring - ;; pair as well). - ;; - ;; 1) If both the rewirings are non-NIL, then the - ;; ASSERT-REWIRINGS-COMPATIBLE check above guarantees - ;; that they are EQUALP, and it doesn't matter which one - ;; we select. - ;; - ;; 2) If only one is non-NIL, the OR selects it. - ;; - ;; 3) If both are NIL, then MAKE-REWIRING-COMMENT just - ;; ignores that keyword argument, and returns an :EXITING - ;; rewiring. - ;; - ;; Finally, (COMMENT LAST-INSTRUCTION) is non-NIL (otherwise - ;; MUST-TRANSFER-COMMENT-P would be NIL), so at least one of - ;; LAST-ENTERING and LAST-EXITING is non-NIL, which means - ;; that at least one of the :ENTERING and :EXITING keyword - ;; args to MAKE-REWIRING-COMMENT is non-NIL and hence the - ;; call will produce a rewiring comment. - (setf (cl-quil.frontend::comment penultimate-instruction) - (cl-quil::make-rewiring-comment :entering (or last-entering penultimate-entering) - :exiting (or last-exiting penultimate-exiting)))))) - - ;; Strip the final HALT instruction. - (subseq instructions 0 (1- (length instructions))))) - (defun process-program (program chip-specification &key protoquil @@ -147,7 +62,7 @@ Returns a values tuple (PROCESSED-PROGRAM, STATISTICS), where PROCESSED-PROGRAM (cl-quil:compiler-hook program chip-specification :protoquil protoquil :destructive t) (when protoquil - (cl-quil.frontend::transform 'process-protoquil processed-program) + (cl-quil.frontend::transform 'cl-quil.frontend:process-protoquil processed-program) ;; Compute statistics for protoquil program (compute-statistics processed-program chip-specification statistics :gate-whitelist gate-whitelist :gate-blacklist gate-blacklist) From ca3cb701ced060671aaf21dd650b405ec19471f7 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Sat, 22 Aug 2026 17:00:31 -0600 Subject: [PATCH 16/31] chore: tag each upstream branch pin with its tracking issue Each of the three forks libquil temporarily depends on now carries an inline TODO naming the libquil issue that tracks moving back to a released upstream: sbcl-librarian rigetti/libquil#60 (quil-lang/sbcl-librarian#91) quilc rigetti/libquil#61 (quil-lang/quilc#933) magicl rigetti/libquil#62 (quil-lang/magicl#222) On both the REPO and REF lines, so that grepping for TODO turns up every line that has to change rather than only half of each pair. Replaces the previous ad-hoc markers, one of which read "TODO(#60-adjacent)" and named no issue at all. Values are unchanged; the additions are YAML comments. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acbcb7b..69ca425 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,29 +12,24 @@ env: # against glibc 2.38 and will not run on 22.04. 2.5.0's binary needs only glibc # 2.34 and is recent enough to build 2.6.7. SBCL_BOOTSTRAP_VERSION: "2.5.0" - # libquil depends on three sbcl-librarian fixes - # (secondary-system FASL bundles, library prefix, configurable core name). - # They live on a branch of Rigetti's fork; - # TODO(#60): - # use a released quil-lang/sbcl-librarian once these fixes are released: - # https://github.com/quil-lang/sbcl-librarian/pull/91 - SBCL_LIBRARIAN_REPO: "https://github.com/rigetti/sbcl-librarian.git" - SBCL_LIBRARIAN_REF: "fix-secondary-system-bundles" + # libquil depends on three sbcl-librarian fixes (secondary-system FASL bundles, + # library prefix, configurable core name), which live on a branch of Rigetti's + # fork and are proposed upstream as quil-lang/sbcl-librarian#91. + SBCL_LIBRARIAN_REPO: "https://github.com/rigetti/sbcl-librarian.git" # TODO(rigetti/libquil#60) + SBCL_LIBRARIAN_REF: "fix-secondary-system-bundles" # TODO(rigetti/libquil#60) # magicl selects a BLAS/LAPACK by searching a fixed list of paths. Until it # knows about OpenBLAS, that search picks a backend that is wrong on arm64 and # cannot be redirected without hacks -- so libquil had to preload OpenBLAS and - # symlink it over liblapack.dylib. This branch adds OpenBLAS to the search, - # a MAGICL_LAPACK_PATH override, and a startup check on what got loaded. - # TODO(#60-adjacent): move back to quil-lang/magicl once - # https://github.com/quil-lang/magicl/pull/222 is merged and released. - MAGICL_REPO: "https://github.com/rigetti/magicl.git" - MAGICL_REF: "backend-selection-and-validation" + # symlink it over liblapack.dylib. This branch adds OpenBLAS to the search, a + # MAGICL_LAPACK_PATH override, and a startup check on what got loaded; it is + # proposed upstream as quil-lang/magicl#222. + MAGICL_REPO: "https://github.com/rigetti/magicl.git" # TODO(rigetti/libquil#62) + MAGICL_REF: "backend-selection-and-validation" # TODO(rigetti/libquil#62) # PROCESS-PROTOQUIL lives in quilc's *application*, which libquil does not # depend on, so libquil vendored a copy. quil-lang/quilc#933 moves it into the # library and exports it, which is where it belongs. - # TODO: move back to a quil-lang/quilc commit once #933 is merged. - QUILC_REPO: "https://github.com/rigetti/quilc.git" - QUILC_REF: "896-protoquil-transform" + QUILC_REPO: "https://github.com/rigetti/quilc.git" # TODO(rigetti/libquil#61) + QUILC_REF: "896-protoquil-transform" # TODO(rigetti/libquil#61) # The Quicklisp dist supplying the Lisp dependencies. QUICKLISP_VERSION: "2026-01-01" From 0e87941d63f7e04154e5c2b88902edbfd3f0b5d7 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Sat, 22 Aug 2026 17:12:59 -0600 Subject: [PATCH 17/31] build: move the qvm pin to the current head of master libquil pinned quil-lang/qvm at 4617625 (2022-12-03) in the first CI commit, with no recorded reason. qvm is close to dormant -- no release since 1.17.2 in 2021, and five commits in the three years since that revision -- so a commit pin is the only option, but there was no reason for it to be that commit. Moves to 1c4ca60, the head of master, which picks up the rename of the abstracts dependency and the move to clos-encounters. Current cl-quil uses clos-encounters too, so this removes a divergence between the two halves of what libquil loads into one image. Verified locally: libquil loads against the new pin, and its qvm paths behave -- a Bell state gives |amplitude| 0.7071 on |00> and |11>, ten shots of X 0 all measure 1, and FSIM runs. The C examples under examples/qvm are libquil's only qvm tests and were not run: they need a built artifact, and this machine cannot produce a working one (its SBCL core and linkable runtime come from different builds). The checks above exercise the same code paths from Lisp instead. CI is the real test. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/clone-lisp-deps.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/clone-lisp-deps.sh b/scripts/clone-lisp-deps.sh index 6ce4899..6e5d1de 100755 --- a/scripts/clone-lisp-deps.sh +++ b/scripts/clone-lisp-deps.sh @@ -17,7 +17,11 @@ set -euo pipefail : "${QUILC_REPO:?QUILC_REPO must be set}" : "${QUILC_REF:?QUILC_REF must be set}" -QVM_REF="4617625cb6053b1adfd3f7aea9cd2be328b225f6" +# quil-lang/qvm has had no release since 1.17.2 (2021), so this is a commit +# rather than a tag. Currently the head of master, which is five commits ahead +# of the 2022 revision libquil pinned when CI was first written; those include +# the move to clos-encounters, which current cl-quil also uses. +QVM_REF="1c4ca60d7912219063e3969be92ec45e12a0798f" git clone "${QUILC_REPO}" "${GITHUB_WORKSPACE}/quilc" git -C "${GITHUB_WORKSPACE}/quilc" checkout "${QUILC_REF}" From c106be1c11418a5778cdc189d65f75baac0ef86e Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Sat, 22 Aug 2026 17:18:57 -0600 Subject: [PATCH 18/31] chore: cite quil-lang/qvm#330 where the qvm sha is pinned qvm is the one dependency pinned to a bare commit rather than a branch or a tag, because it has no release newer than 1.17.2 (2021). quil-lang/qvm#330 asks for one; when it exists this pin becomes a tag. Unlike the other three TODOs, this cites an upstream issue rather than a libquil one: there is no libquil-side decision waiting, only an upstream release. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/clone-lisp-deps.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/clone-lisp-deps.sh b/scripts/clone-lisp-deps.sh index 6e5d1de..7fe45fe 100755 --- a/scripts/clone-lisp-deps.sh +++ b/scripts/clone-lisp-deps.sh @@ -18,10 +18,11 @@ set -euo pipefail : "${QUILC_REF:?QUILC_REF must be set}" # quil-lang/qvm has had no release since 1.17.2 (2021), so this is a commit -# rather than a tag. Currently the head of master, which is five commits ahead -# of the 2022 revision libquil pinned when CI was first written; those include -# the move to clos-encounters, which current cl-quil also uses. -QVM_REF="1c4ca60d7912219063e3969be92ec45e12a0798f" +# rather than a tag; quil-lang/qvm#330 asks for one, and this pin can become a +# tag once there is one. Currently the head of master, five commits ahead of the +# 2022 revision libquil pinned when CI was first written, picking up the move to +# clos-encounters that current cl-quil also uses. +QVM_REF="1c4ca60d7912219063e3969be92ec45e12a0798f" # TODO(quil-lang/qvm#330) git clone "${QUILC_REPO}" "${GITHUB_WORKSPACE}/quilc" git -C "${GITHUB_WORKSPACE}/quilc" checkout "${QUILC_REF}" From 979996709515c8fc8e23558f01f9b7e11d11fca4 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Sat, 22 Aug 2026 17:21:35 -0600 Subject: [PATCH 19/31] docs: cut PR-58-REVIEW-ANSWERS down to status and a draft comment The long-form answers have all been acted on, and their reasoning now lives in commit messages and in comments at the point of use, which is where a reader will actually look for it. Keeping a third copy here meant three places to keep in step, and two of them had already drifted. What is left is what the file is now for: the four dependency pins and what each is blocked on, the two decisions still outstanding, what CI covers that local testing could not, and a comment ready to post on the PR. 295 lines to 86. Co-Authored-By: Claude Opus 5 (1M context) --- PR-58-REVIEW-ANSWERS.md | 381 +++++++++------------------------------- 1 file changed, 86 insertions(+), 295 deletions(-) diff --git a/PR-58-REVIEW-ANSWERS.md b/PR-58-REVIEW-ANSWERS.md index d4eac1a..36ccd50 100644 --- a/PR-58-REVIEW-ANSWERS.md +++ b/PR-58-REVIEW-ANSWERS.md @@ -1,295 +1,86 @@ -# Answers to review comments on #58 - -Companion to the code changes made in response to review. Comments that were -straightforward fixes are listed under **Applied**; everything that was a question, -an uncertainty, or a decision worth recording is answered below. - -## Questions answered - -### `(ql:quickload ...)` and Quicklisp (`.github/workflows/build.yml:66`) - -Yes — Quicklisp is the package manager, ASDF is the build system underneath it (see -above). `(ql:quickload :libquil)` fetches the dependency closure and compiles and -loads the system into the running image. - -### Should the deleted `(ql:quickload ...)` validation steps be restored? (`build.yml`) - -**Recommendation: no, and the naive restore would break the build.** The two lines -that were removed were: - -``` -sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' -sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' -``` - -Loading `:libquil` on its own now signals a *continuable* error. `src/quilc/api.lisp` -deliberately redefines the `quilc_compile_protoquil` alien callable that -sbcl-librarian's `define-api` generated a few forms earlier (it needs -`(* (* t))`, which sbcl-librarian cannot express). SBCL signals -`redefine alien callable` for that and offers a `continue` restart. Under -`--non-interactive` there is nothing to take the restart, so the load aborts. - -That is exactly why `src/build-image.lisp` wraps every load in -`with-alien-redefinition-allowed`. A bare `ql:quickload :libquil` outside that -handler is not a valid way to load libquil any more. - -The validation those lines provided is also now redundant: `make` runs -`src/build-image.lisp`, which quickloads `sbcl-librarian` and then `libquil`, so a -dependency-resolution failure fails the `Build libquil.so` step with the same error -it would have produced earlier — just one step later. - -If a separate pre-flight check is still wanted, it has to install the same handler, -e.g.: - -``` -sbcl --noinform --non-interactive \ - --eval '(handler-bind ((error (lambda (c) (let ((r (find-restart (quote continue) c))) (when (and r (search "redefine alien callable" (princ-to-string c))) (invoke-restart r)))))) (ql:quickload :libquil))' -``` - -which is the build step with extra steps. Left out. - -### Are `libffi` 7 and 8 both supported? (`build.yml:96`) - -They are genuinely incompatible, but nothing here mixes them, so dropping the -explicit `libffi7` is safe — and the old entry was already doing nothing. - -Debian/Ubuntu's `libffi7`/`libffi8` package names track the shared object's *soname* -(`libffi.so.7` / `libffi.so.8`), not an upstream libffi version — upstream has been on -3.x since 2008. `libtool-version` went `8:0:1` (3.3) to `9:0:1` (3.4), and libtool's -soname is `current - age`, giving 7 and 8 respectively. - -The bump was warranted. libffi commit -[`7855656`](https://github.com/libffi/libffi/commit/7855656148b96c7070ec362d2a73af840025a2b7) -("x86: Add indirect branch tracking support", 2020-02-21) says: - -> Trampoline must be enlarged to add ENDBR instruction unconditionally, which is NOP -> on non-CET processors. [...] Update library version for larger `FFI_TRAMPOLINE_SIZE`. - -`FFI_TRAMPOLINE_SIZE` went 24 -> 32 on x86-64 and 12 -> 16 on i386, and it sizes the -first member of the *public* `ffi_closure` struct in `ffi.h`: - -```c -typedef struct { - char tramp[FFI_TRAMPOLINE_SIZE]; - ffi_cif *cif; - void (*fun)(ffi_cif*,void*,void**,void*); - void *user_data; -} ffi_closure; -``` - -So a binary compiled against 3.3's header allocates a 24-byte trampoline and expects -`cif` at offset 24, while 3.4 writes 32 bytes and reads `cif` at offset 32 — an -out-of-bounds write and garbage field reads, not a clean load failure. The soname -bump is what prevents the loader from ever allowing that. - -None of which is reachable here. `libffi-dev` on ubuntu-22.04 resolves `libffi.so` to -`libffi.so.8`, so the build linked against 8 whether or not `libffi7` was in the apt -list — the explicit `libffi7` never influenced what was linked, it just installed a -second runtime nothing used. Naming a version is also what actually breaks the -workflow: `libffi7` does not exist on Ubuntu 24.04. The package list no longer names -one, and the explanatory comment has been shortened to one line. - -**Verified: libffi is a real runtime dependency.** I initially guessed it might be -vestigial, on the grounds that SBCL's `sb-alien` does not use libffi and `libquil.asd` -depends on `#:cffi` rather than `#:cffi-libffi`. That was wrong. Tracing a run of -`examples/quilc/version` against the released 0.4.0-rc.0 artifact with -`DYLD_PRINT_LIBRARIES`, and diffing against a trivial C program as a baseline, the -objects libquil adds are: - - libopenblasp-r0.3.34.dylib magicl's backend (confirms D7) - libgfortran / libquadmath / libgcc_s / libomp OpenBLAS's dependencies - libzstd.1.5.7.dylib core decompression - /usr/lib/libffi.dylib dlopen'd at runtime - libquil / libsbcl_librarian / libsbcl - -libffi does not appear in `otool -L` on any shipped object, which is why it looked -absent -- it is loaded dynamically once Lisp is up. So `libffi-dev` belongs in the -requirements, and it is needed at *run* time, not only to build. - -The same trace independently confirms D7: OpenBLAS is what magicl loads. Accelerate's -`libBLAS`/`libLAPACK` do appear, but they appear in the baseline too, so they are -system-framework noise rather than magicl's choice. - -**Follow-up this raises.** On Linux, CFFI resolves libffi under its unversioned name, -exactly as magicl does for BLAS/LAPACK -- so the argument for `install.sh`'s preflight -check applies to libffi too. I have not added it, because whether the existing check -would find it on Debian is untested: the unversioned `libffi.so` lives in the -multiarch directory (`/usr/lib/x86_64-linux-gnu`), which is not in -`LIBQUIL_SEARCH_DIRS`, and I have not confirmed that `ldconfig -p` lists the -unversioned dev symlink. Worth settling on a Linux box before extending the loop. - -### Is there a canonical way to search all dynamic-library paths on macOS? (`install.sh:72`) - -No, and the options each answer a slightly different question than the one the -installer needs: - -- **`man dyld`** — the default search is `/usr/local/lib` then `/usr/lib`, plus - `DYLD_LIBRARY_PATH` / `DYLD_FALLBACK_LIBRARY_PATH` if set. Those two directories - are now the base of the search list. -- **`DYLD_*` environment variables** — deliberately ignored. They apply to the - installing shell, not to whatever process later loads libquil, so honouring them - would make the check pass for installs the eventual consumer cannot see. -- **`otool -L`** — lists what a binary is *already* linked against. `magicl` `dlopen`s - BLAS/LAPACK at runtime rather than linking them, so they never appear. -- **`find / -name`** — exhaustive but slow enough to be hostile in an installer, and - a hit outside a loader path is not actually usable. -- **`locate`** — needs a database most machines have never built (as you found). -- **`pkg-config`** — answers "is there a `.pc` file for this?", which is not the - question. Homebrew's `openblas` is keg-only and its file is `openblas.pc`, not - `blas.pc`/`lapack.pc`, so `pkg-config --exists blas` fails on exactly the machine - the check most needs to pass. What matters here is whether an *unversioned* - `libblas.dylib` / `liblapack.dylib` exists somewhere the loader will find it, - which is a file-existence test. - -So the check stays a directory scan, but it is now built from a single -`LIBQUIL_SEARCH_DIRS` array, with the Homebrew prefixes discovered via -`brew --prefix` rather than hardcoded, and the Linux-only directories appended in -the Linux branch. That also removes the duplication noted at `install.sh:87`. - -`pkg-config` remains a requirement for the *build* (zstd), which is a separate -concern. - -### What about OpenBLAS? (`install.sh:94`) - -The loop checks `blas` and `lapack`, which is correct: `magicl` `dlopen`s -`libblas`/`liblapack` under those names. OpenBLAS satisfies both — on macOS via the -symlinks CI creates, on Linux via the distribution's alternatives mechanism — so -checking for `libopenblas` directly would reject a perfectly good reference-BLAS -install and miss nothing. - -The confusingly named `LIBQUIL_MISSING` has been renamed `LIBQUIL_MISSING_LIBS`. - -### Is `sbcl_librarian.core` generated or not? (`Makefile:66`) - -Both statements were true and the comment was the confusing part. `SBCL_LIBRARIAN_CORE_NAME` -is a *compile-time* setting on the runtime: it is the filename the runtime looks for -next to itself at load time, and it defaults to `sbcl_librarian.core`. libquil never -produces a file by that name — the build emits `libquil_core.core` and renames it to -`runtime/libquil.core` — so the runtime is compiled with the default overridden. The -comment now says that. - -## Items that looked like upstream bug reports - -Three claims in the diff read as latent upstream bugs. All three are real; two were -already reported and fixed upstream before we got here, and the third still stands. -None of them block this PR. - -1. **Homebrew reference LAPACK computes incorrect eigenvectors on arm64.** - (`build.yml`, `README.md`, `REARCHITECTURE.md` D7.) Real, reproduced, and fully - diagnosed upstream — but the diff's framing is too broad. This is not a property - of reference LAPACK on arm64. It is a **gfortran miscompile of LAPACK >= 3.12.1**: - gfortran >= 15.2 auto-vectorizes `TAU * DCONJG(WORK(I))` in the new `zlarf1f.f` - into aarch64 `fcmla` and drops the conjugation - ([Reference-LAPACK#1160](https://github.com/Reference-LAPACK/lapack/issues/1160), - [GCC PR122408](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=122408), fixed by - `c5fa3d4c8` on 2025-10-27). - - Confirmed end to end on an M-series machine: Homebrew's poured `lapack` 3.12.1 - bottle gives residuals of order 1 for `zgeev`/`zheev`/`zgesvd` at n>=3 while real - routines stay at 1e-16; `brew reinstall --build-from-source lapack` with the fixed - gfortran drops every one of them to ~1e-15. The shipped `_zlarf1f_` contains - `fcmla ... #90` where the rebuilt one has `#270`. - - Nothing was left to report to LAPACK or GCC — both are already fixed. The - remaining gap was that Homebrew had not bumped `revision` to rebuild the affected - bottle, and that is now filed as - **[Homebrew/homebrew-core#300084](https://github.com/Homebrew/homebrew-core/issues/300084)**, - with the standalone reproducer at - [BatmanAoD/lapack-arm64-homebrew-bugs](https://github.com/BatmanAoD/lapack-arm64-homebrew-bugs). - - D7 remains correct regardless, because its other half — Accelerate's missing - `zuncsd_` — is permanent. Only the *justification* in the code comment needs - narrowing. - -2. **macOS LAPACK raises division-by-zero traps.** (`src/quilc/compile.lisp:75`.) - Two separate things are going on, and they pull in opposite directions. - - **The mask is required regardless, and is not a workaround.** SBCL enables - floating-point traps by default — `(:overflow :invalid :divide-by-zero)` — while - C and Fortran callers normally run with them masked. LAPACK is written on the - assumption of the latter, so calling it from Lisp surfaces exceptions that no - other caller ever sees. `magicl:with-blapack` is just - `sb-int:with-float-traps-masked (:divide-by-zero :invalid)`, which restores the - environment LAPACK expects. Adding it to `compile-quil` to match - `compile-protoquil` is a plain correctness fix, and nothing about it belongs - upstream in magicl — magicl already provides exactly this. - - **But the exceptions themselves are recognised bugs, and are being fixed.** In - [Reference-LAPACK#1282](https://github.com/Reference-LAPACK/lapack/issues/1282) - Intel's OneMKL team reported crashes under `-ffpe-trap`/`-fpe0`, and - [PR #1283](https://github.com/Reference-LAPACK/lapack/pull/1283) was merged on - 2026-05-21 fixing them across ten files (`?gedmd`, `?gejsv`, `?gesvdx`, - `?gesvj`). The unguarded `OFL / ROOTSC` in `dgedmd` now reads - `IF ( ROOTSC .GT. ONE ) TBIG = OFL / ROOTSC`. - - So a spurious FPE in a LAPACK routine *is* reportable, and upstream will act on - it — though not unanimously: a maintainer noted on that issue that "generating - `Inf` and working with `Inf` is part of the game", while accepting the fix - anyway. - - **What this means here.** The mask stays, permanently — it is about SBCL's - defaults, not about LAPACK's bugs, and every LAPACK in the wild predates PR #1283 in any case. But if it is ever worth knowing *which* routine traps in - `compile-quil`, that is a reportable upstream bug of exactly the class just - fixed, not something to be resigned to. - - *(I previously wrote that these exceptions are "by design" and that there was - nothing to report. That was wrong: #1282 was closed as completed because the - fixes were merged.)* - -3. **`init(char* core)` is dead code on sbcl-librarian `main`.** - (`REARCHITECTURE.md`, "Why this is not a small change".) The generated function - declares its own `static int initialized`, shadowing the global that every - generated wrapper tests, so calling it leaves every function returning - `LISP_ERR_NOT_INITIALIZED`. Worth an upstream issue independent of libquil. - -The three sbcl-librarian fixes libquil depends on are already open as -[quil-lang/sbcl-librarian#91](https://github.com/quil-lang/sbcl-librarian/pull/91); -`build.yml`'s `SBCL_LIBRARIAN_REPO` comment now links to it. - -## Applied without further comment - -- `build.yml`: the `2022-04-01` Quicklisp-pin aside and the `sbcl-2.2.4` aside - removed; the `libffi7` rationale cut to one line; `SBCL_LIBRARIAN_REPO` now links - the tracking PR; `env.ImageOS` explained in both cache steps. -- `build.yml`: the duplicated SBCL-and-Quicklisp install and Lisp-dependency clone - are now `scripts/install-sbcl.sh` and `scripts/clone-lisp-deps.sh`, shared by both - jobs. The platform-specific part (apt bootstrap host vs. Homebrew host) stays in - the workflow; the macOS job passes `SBCL_REMOVE_HOST_CMD` to drop the Homebrew - `sbcl` once the build no longer needs it. -- `release.yml`: the "two macOS artifacts" comment reworded (there is only one macOS - artifact now); the knope-version rationale comment deleted. -- `examples/quilc/Makefile`: the note about the removed `-pagezero_size` deleted. - Confirmed: the Lisp image is mapped by `libsbcl_librarian`, not by the example - executable, so the example no longer needs its own zero page moved. -- `src/libquil.lisp`: comment suggestion applied. -- `install.sh`: the loader-cache comment moved onto the `ldconfig` branch it - describes; the platform test switched from `-z "${IS_LINUX-}"` to an explicit - `"${OS}" != "Darwin"`; search paths unified into one array; `sudo` handling - replaced with a hard root check; an `echo` added before the quarantine removal. -- `README.md`: says the `-dev` packages are what is needed; "any package manager" - narrowed to the two actually checked; install commands updated for the root - requirement (`curl … | sudo bash`). -- `README.md`: the C API reference still documented the removed - `libquil_error_t`/`libquil_error()`. Updated to `lisp_err_t` / - `get_error_message()` / `enable_backtrace()` per D2. (Not raised in review, but - wrong as it stood.) -- `REARCHITECTURE.md`: "upstream" in D4 disambiguated to sbcl-librarian; the - point-in-time `## Status` checklist removed. `## Open` was kept, retitled - `## Known limitations and follow-ups`, since it is the only record of the fork - dependency and the release ordering. - -## Open decisions for you - -- **`install.sh` now requires root**, per the two comments at `install.sh:111`. That - changes the documented invocation from `curl … | bash` to `curl … | sudo bash`, - which some people object to on principle. The alternative you floated — - re-exec via `sudo "${0}"` — does not work when the script is piped from `curl`, - since `$0` is `bash`. Say the word and I will restore the `${SUDO}` variable - instead. -- **`REARCHITECTURE.md` retained.** The comment at line 216 said "if we keep this - file". It is currently linked from `README.md` and from the PR description, so it - is kept; if you would rather it go, `README.md`'s "Building from source" section - and the D-numbered references in this document are the things to fix up. -- **The knope-version comment** at `release.yml:63` was deleted whole rather than - just its last line — the empty suggestion landed on the final line of a - three-line sentence, and deleting only that line left broken prose. +# PR #58 — status + +Scratch notes for the author. The reasoning behind each change lives in its commit +message and in comments at the point of use; this file only tracks what is left. + +## Where it stands + +All review feedback has been addressed in code. Four upstream dependencies are +pinned to branches or a bare commit, each tagged with a `TODO(...)` at the pin: + +| dependency | pinned to | tracked by | blocked on | +|---|---|---|---| +| sbcl-librarian | `rigetti/…:fix-secondary-system-bundles` | rigetti/libquil#60 | quil-lang/sbcl-librarian#91 merge + release | +| quilc | `rigetti/…:896-protoquil-transform` | rigetti/libquil#61 | quil-lang/quilc#933 merge | +| magicl | `rigetti/…:backend-selection-and-validation` | rigetti/libquil#62 | quil-lang/magicl#222 merge + release | +| qvm | commit `1c4ca60` | quil-lang/qvm#330 | any release newer than 1.17.2 (2021) | + +`grep -rn 'TODO('` over `.github/` and `scripts/` finds all six pin lines. + +**This cannot be released against stable dependencies until those four resolve.** +That is the single thing gating merge-and-release, not review. + +## Still needing a decision + +- **`install.sh` now requires root**, per the two comments at `install.sh:111`, + changing `curl … | bash` to `curl … | sudo bash`. Some object to that on + principle. The alternative floated in review — re-exec via `sudo "${0}"` — cannot + work from a pipe, since `$0` is `bash`. Reverting to the `${SUDO}` variable is a + small change if preferred. +- **`REARCHITECTURE.md` retained**, against "if we keep this file". It is linked + from `README.md` and the PR description. Dropping it means fixing those two + references. + +## Not verified locally + +- The **C examples** under `examples/qvm` and `examples/quilc` were never run on a + locally built artifact: this machine's SBCL core and linkable runtime come from + different builds, so `make` produces something that cannot load. Equivalent + checks were run from Lisp instead. CI covers the real thing. +- The **macOS job** has not yet completed on the current head. + +## Draft comment to post + +--- + +Everything raised in review is addressed. Rather than reply inline to all 32 open +threads, a summary of the substantive ones: + +**Restoring the `(ql:quickload …)` pre-flight steps** — I tried this and it breaks +the build. `src/quilc/api.lisp` deliberately redefines the `quilc_compile_protoquil` +alien callable that `define-api` generated (it needs `(* (* t))`, which +sbcl-librarian cannot express), so SBCL signals a *continuable* `redefine alien +callable` error. Under `--non-interactive` nothing takes the restart and the load +aborts — which is why `src/build-image.lisp` wraps its loads in +`with-alien-redefinition-allowed`. The validation is also redundant now: `make` runs +that file, so a dependency failure surfaces one step later with the same error. + +**`libffi7` vs `libffi8`** — the two really are incompatible (the soname bumped +because `FFI_TRAMPOLINE_SIZE` grew, and it sizes the first member of the public +`ffi_closure` struct), but nothing here mixes them: `libffi-dev` on 22.04 already +resolved to `libffi.so.8`, so the explicit `libffi7` only installed a second runtime +nothing used. Also confirmed libffi *is* a real runtime dependency — it is +`dlopen`'d once Lisp is up, which is why it never shows in `otool -L`. + +**The arm64 LAPACK problem** — my original framing was too broad. It is not a +property of reference LAPACK: it is a gfortran miscompile of `zlarf1f.f`, new in +LAPACK 3.12.1 (GCC PR122408 and Reference-LAPACK#1160, both fixed upstream; +Homebrew/homebrew-core#300084 tracks the un-rebuilt bottle). Confirmed by rebuilding +the bottle from source, which fixes it. + +**`magicl:with-blapack`** — not a workaround and nothing to report. SBCL enables +floating-point traps by default while C and Fortran callers do not, so calling +LAPACK from Lisp surfaces exceptions no other caller sees. Adding it to +`compile-quil` to match `compile-protoquil` is a plain fix. + +**Three workarounds are gone entirely**, because the fixes went upstream instead: +the vendored `PROCESS-PROTOQUIL` (quil-lang/quilc#933), the OpenBLAS preload and CI +symlinks (quil-lang/magicl#222), and — unrelated to libquil but found along the way +— a dead `init()` in sbcl-librarian (added to quil-lang/sbcl-librarian#91). + +**What gates this PR** is the four dependency pins in the table above, not review. +Each has a `TODO` at the pin and an issue. + +Two things I would still like a call on: `install.sh` now requires root, which makes +the documented invocation `curl … | sudo bash`; and `REARCHITECTURE.md` is still +here, which the "if we keep this file" comment left open. From cfe9249c7898155bfcfdb3450343276a291f1a19 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 25 Aug 2026 18:29:24 -0600 Subject: [PATCH 20/31] docs: write the README for a newcomer, not for existing users Several passages read as a changelog -- "no longer published", "this replaces the pre-0.4 API", a section on which LAPACK to avoid and why. Someone arriving at the repo has no prior version to be told about, so those are gone. What is left states what is required and what is supported. The Homebrew reference-LAPACK caveat goes with them. magicl now prefers OpenBLAS and verifies its backend at load, so the caveat described a hazard a reader cannot encounter by following these instructions. Clarifies that make.sh, make-shared-library.sh and install.sh in the build-from-source section are SBCL's, not libquil's -- SBCL has an install.sh of its own, and libquil's is now scripts/install.sh, so the collision was worth naming. Adds the clone step, since the snippet has to run inside an SBCL checkout and never said so. Also records that the sbcl building libquil and the libsbcl.so it links must come from the same *build*, not merely the same version: SBCL stamps a build ID into both, and a mismatch surfaces only at runtime as "core was built for runtime ... but this is ...". The Makefile's error message gave the same version-only advice and now says the same thing. Installer URLs follow scripts/install.sh to its new location. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 3 +- README.md | 55 +++++++++++++++----------------- install.sh => scripts/install.sh | 0 3 files changed, 27 insertions(+), 31 deletions(-) rename install.sh => scripts/install.sh (100%) diff --git a/Makefile b/Makefile index a37beb3..9cc61db 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,8 @@ ifeq ($(LIBSBCL),) @echo "error: no linkable SBCL runtime (libsbcl.so) found." >&2 @echo "Searched:" >&2 @$(foreach dir,$(LIBSBCL_SEARCH_DIRS),echo " $(dir)" >&2;) - @echo "Build one from an SBCL source tree of the same version as $(SBCL):" >&2 + @echo "Build one from the SBCL source tree that produced $(SBCL) -- a matching" >&2 + @echo "version is not enough, the build IDs have to agree:" >&2 @echo " sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh" >&2 @echo "then re-run make, or pass LIBSBCL=/path/to/libsbcl.so" >&2 @exit 1 diff --git a/README.md b/README.md index e2e9a38..68092dd 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ We provide pre-built binaries for the following systems - Linux x64 (tested specifically on Ubuntu) - macOS aarch64 (Apple Silicon) -Intel macOS binaries are no longer published. Other platforms, including Intel -macOS, can be built from source — see [Building from -source](#building-from-source). +Other platforms can be built from source — see +[Building from source](#building-from-source). ## Requirements @@ -40,10 +39,7 @@ On systems which use `brew` to install packages (e.g macOS), these libraries can brew install openblas libffi ``` -> Note: OpenBLAS is what supplies BLAS and LAPACK here, and `magicl` selects it in -> preference to anything else. Homebrew's reference `lapack` no longer has to be -> avoided, but it is not a substitute: its current arm64 bottle computes incorrect -> eigenvectors, and Accelerate is missing routines `quilc` needs. +On macOS, OpenBLAS supplies both `BLAS` and `LAPACK`. ## Automated installation @@ -56,13 +52,13 @@ A script is provided to automate installation of the library. It will detect the Run the following command ``` -curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | sudo bash +curl https://raw.githubusercontent.com/rigetti/libquil/main/scripts/install.sh | sudo bash ``` If you would like to install a particular version of the library, run the following command ``` -curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | sudo bash -s +curl https://raw.githubusercontent.com/rigetti/libquil/main/scripts/install.sh | sudo bash -s ``` replacing `` with the desired version, e.g. `0.3.0`. @@ -73,7 +69,7 @@ install them for you with `apt` or Homebrew, skipping any that are already present: ``` -curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | sudo bash -s -- --install-deps +curl https://raw.githubusercontent.com/rigetti/libquil/main/scripts/install.sh | sudo bash -s -- --install-deps ``` It requires `apt` on Linux and Homebrew on macOS, and fails if neither is @@ -85,21 +81,35 @@ If you would like to manually install the library (for example in the case where ## Building from source -Building requires an SBCL with a *linkable runtime* (`libsbcl.so`). `make.sh` does -not build one by default, and the packages we have checked — Homebrew's `sbcl` -bottle and Ubuntu's `sbcl` — do not ship one, so SBCL has to be built from source: +Building requires an SBCL with a *linkable runtime* (`libsbcl.so`). SBCL does not +build one by default, and the packages we have checked — Homebrew's `sbcl` bottle +and Ubuntu's `sbcl` — do not ship one, so SBCL itself has to be built from source. + +`make.sh`, `make-shared-library.sh` and `install.sh` below are scripts in **SBCL's** +source tree. SBCL's `install.sh` is not libquil's `scripts/install.sh` described +above. Run them from a checkout of SBCL: ```bash +git clone --branch sbcl-2.6.7 https://git.code.sf.net/p/sbcl/sbcl +cd sbcl sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh && sh install.sh ``` -`install.sh` places it in SBCL's home directory, where the `Makefile` finds it -automatically. To use one from elsewhere, pass it explicitly: +SBCL's `install.sh` puts both the `sbcl` binary and `libsbcl.so` in SBCL's home +directory, where libquil's `Makefile` finds the latter automatically. To use a +runtime from elsewhere, pass it explicitly: ```bash make LIBSBCL=/path/to/sbcl/src/runtime/libsbcl.so ``` +> Note: the `sbcl` that builds libquil and the `libsbcl.so` it is linked against +> must come from the *same build*, not merely the same version. SBCL stamps a build +> ID into both, and a mismatch is only caught at runtime, as +> `core was built for runtime "..." but this is "..."`. Installing SBCL from source +> as above satisfies this; leaving a packaged `sbcl` earlier on `PATH` than the one +> just installed does not. + The Lisp dependencies (`quilc`, `qvm`, `magicl`, `sbcl-librarian`) are expected in your Quicklisp local-projects directory. Then: @@ -125,18 +135,6 @@ the library starts Lisp. See [REARCHITECTURE.md](REARCHITECTURE.md) for how this fits together and why. -### Linear algebra backend on aarch64 macOS - -Install OpenBLAS (`brew install openblas`). `magicl` prefers it over the -alternatives, and checks at load time that whatever it selected is complete and -computes correctly, so a bad backend fails the build with a message naming the -library rather than producing an artifact that is silently wrong. - -The alternatives are both unusable on aarch64: Homebrew's reference `lapack` bottle -returns incorrect eigenvectors for complex input -([homebrew-core#300084](https://github.com/Homebrew/homebrew-core/issues/300084)), -and Accelerate's legacy LAPACK is missing routines `quilc` calls. - # C API Reference These come from `sbcl_librarian_err.h`, which is installed alongside `libquil.h`. @@ -150,9 +148,6 @@ These come from `sbcl_librarian_err.h`, which is installed alongside `libquil.h` - `lisp_err_t enable_backtrace(int enabled)` Turns backtrace capture on the error message on or off. -> Note: this replaces the pre-0.4 `libquil_error_t` / `libquil_error()` API. See -> [REARCHITECTURE.md](REARCHITECTURE.md) (D2) for why. - ## Quilc documentation ### Enums diff --git a/install.sh b/scripts/install.sh similarity index 100% rename from install.sh rename to scripts/install.sh From add61fdb1672e57291ccbde325da0bd52358ec65 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 25 Aug 2026 18:44:55 -0600 Subject: [PATCH 21/31] docs: record that CI is green on both platforms The macOS job passed on 9799967 and both jobs pass on the current head, so the "not verified locally" caveats can be replaced with what CI actually covers -- which is everything that could not be checked here: - the C examples under examples/qvm and examples/quilc run in the Test step on both jobs; - the macOS job builds SBCL from source through scripts/install-sbcl.sh, exercising the shared-script refactor rather than the cached path; - magicl's new load-time backend check passes on both runners. Also makes the draft comment self-contained: it referred to "the table above", which will not exist once it is pasted into GitHub, so it carries its own. Co-Authored-By: Claude Opus 5 (1M context) --- PR-58-REVIEW-ANSWERS.md | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/PR-58-REVIEW-ANSWERS.md b/PR-58-REVIEW-ANSWERS.md index 36ccd50..f4a30ef 100644 --- a/PR-58-REVIEW-ANSWERS.md +++ b/PR-58-REVIEW-ANSWERS.md @@ -5,8 +5,9 @@ message and in comments at the point of use; this file only tracks what is left. ## Where it stands -All review feedback has been addressed in code. Four upstream dependencies are -pinned to branches or a bare commit, each tagged with a `TODO(...)` at the pin: +All review feedback has been addressed in code, and CI is green on both platforms. +Four upstream dependencies are pinned to branches or a bare commit, each tagged +with a `TODO(...)` at the pin: | dependency | pinned to | tracked by | blocked on | |---|---|---|---| @@ -31,13 +32,19 @@ That is the single thing gating merge-and-release, not review. from `README.md` and the PR description. Dropping it means fixing those two references. -## Not verified locally +## CI -- The **C examples** under `examples/qvm` and `examples/quilc` were never run on a - locally built artifact: this machine's SBCL core and linkable runtime come from - different builds, so `make` produces something that cannot load. Equivalent - checks were run from Lisp instead. CI covers the real thing. -- The **macOS job** has not yet completed on the current head. +Green on both platforms on the current head. Worth noting what that covers, since +none of it could be checked locally: + +- The **C examples** under `examples/qvm` and `examples/quilc` run in the `Test` + step on both jobs. Locally they cannot: this machine's SBCL core and linkable + runtime come from different builds, so `make` produces something that will not + load. Equivalent checks were run from Lisp instead. +- The **macOS job builds SBCL from source** through `scripts/install-sbcl.sh`, so + the shared-script refactor is exercised rather than just the cached path. +- magicl's new load-time backend check passes on both runners, i.e. CI's OpenBLAS + is complete and computes correctly. ## Draft comment to post @@ -78,8 +85,20 @@ the vendored `PROCESS-PROTOQUIL` (quil-lang/quilc#933), the OpenBLAS preload and symlinks (quil-lang/magicl#222), and — unrelated to libquil but found along the way — a dead `init()` in sbcl-librarian (added to quil-lang/sbcl-librarian#91). -**What gates this PR** is the four dependency pins in the table above, not review. -Each has a `TODO` at the pin and an issue. +CI is green on both platforms, including the C example tests for quilc and qvm and +a from-source SBCL build on macOS. + +**What gates this PR** is not review but four dependency pins, each tagged with a +`TODO(...)` where it is pinned: + +| dependency | tracked by | blocked on | +|---|---|---| +| sbcl-librarian | #60 | quil-lang/sbcl-librarian#91 merge + release | +| quilc | #61 | quil-lang/quilc#933 merge | +| magicl | #62 | quil-lang/magicl#222 merge + release | +| qvm | quil-lang/qvm#330 | any release newer than 1.17.2 (2021) | + +This cannot be released against stable dependencies until those resolve. Two things I would still like a call on: `install.sh` now requires root, which makes the documented invocation `curl … | sudo bash`; and `REARCHITECTURE.md` is still From c59f4a2367f4b38274a32507cf0e8c14bdb8a4ca Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 25 Aug 2026 18:48:50 -0600 Subject: [PATCH 22/31] fix: say what the SUDO_USER guard in install.sh actually checks The message blamed Homebrew's refusal to run as root, which is misleading: the very next line already handles that by running brew as SUDO_USER. What the guard actually catches is the absence of anyone to drop to -- a root session that was not entered through sudo. Kept rather than removed, but the comment now says why it barely matters: brew is not on root's default PATH, so a root login normally fails the "Homebrew was not found" check just above instead. It earns its place only by turning what would otherwise be a bare "SUDO_USER: unbound variable" from set -u into something actionable. Also drops the "sudo bash install.sh" hint, which named the wrong path after the move to scripts/ and duplicated the invocation the README documents. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 5 ----- scripts/install.sh | 34 +++++++++++++++------------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69ca425..e8cd4a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -155,11 +155,6 @@ jobs: run: "$GITHUB_WORKSPACE/libquil/scripts/clone-lisp-deps.sh" - name: Install quilc dependencies - # magicl now finds OpenBLAS by name and refuses to start on a backend that - # is incomplete or computes incorrectly, so there is nothing to arrange - # here beyond installing it. Homebrew's reference `lapack' no longer has to - # be uninstalled, and OpenBLAS no longer has to be symlinked over - # liblapack.dylib, because magicl now prefers it outright. run: brew install openblas libffi gfortran - name: Build libquil.dylib diff --git a/scripts/install.sh b/scripts/install.sh index 0d0d063..5811212 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -121,28 +121,24 @@ install_prerequisites() { "Install libquil's requirements another way and re-run without --install-deps:" \ "https://github.com/rigetti/libquil#requirements" - # Homebrew refuses to run as root, so it has to run as the invoking user. - # `brew --prefix` is the one subcommand it does allow as root, which is why the - # search paths above can call it directly. + # Homebrew refuses to run as root, so it runs as whoever invoked sudo. + # (`brew --prefix` is the one subcommand it allows as root, which is why the + # search paths above can call it directly.) + # + # SUDO_USER is set only when root was reached through sudo, so this guard is + # about not having a user to drop to -- not about Homebrew's root policy, + # which the line below already handles. It is close to unreachable: brew is + # not on root's default PATH, so a root login normally fails the check above + # instead. It stays because without it `set -u` would abort here with a bare + # "SUDO_USER: unbound variable". [[ -n "${SUDO_USER-}" ]] || - err "--install-deps needs to run Homebrew, which refuses to run as root." \ - "Re-run through sudo from your normal account (sudo bash install.sh --install-deps)," \ - "or install the requirements yourself and drop --install-deps." + err "--install-deps needs an unprivileged user to run Homebrew as, and this is" \ + "a root session rather than one entered through sudo." \ + "Re-run it under sudo from your normal account, or install the requirements" \ + "yourself and drop --install-deps." local brew_cmd=(sudo -u "${SUDO_USER}" brew) - # Homebrew's reference `lapack` is deliberately NOT treated as a conflict here, - # even though magicl prefers it over every other backend and it computes - # incorrect eigenvectors on arm64. A prebuilt libquil is immune: build-image.lisp - # loads OpenBLAS before magicl can pick a backend, and SBCL records loaded shared - # objects in the core and reopens them at startup, so the choice is baked into the - # artifact (REARCHITECTURE.md D7). Verified against a machine with lapack 3.12.1 - # installed: the release loads OpenBLAS and never touches the lapack keg. - # - # It does matter when building libquil from source, which is why CI uninstalls it - # and the README says not to install it. Refusing to install a working binary over - # it would be wrong. - - # OpenBLAS provides both BLAS and LAPACK, and is correct on arm64. + # OpenBLAS provides both BLAS and LAPACK. local missing=() local formula for formula in openblas libffi From 60c016981f319632715e5d99a21f1c364048a198 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 25 Aug 2026 20:48:32 -0600 Subject: [PATCH 23/31] comment-tweaks --- README.md | 5 ++--- scripts/clone-lisp-deps.sh | 4 +--- scripts/install.sh | 4 ---- versions | 1 - 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 68092dd..1dad69d 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,8 @@ These libraries are required by `libquil`: `magicl` loads `BLAS` and `LAPACK` at runtime under their unversioned names, `libblas.so` and `liblapack.so` (`.dylib` on macOS). Those names come from the -*development* packages, so the `-dev` packages have to be installed — a runtime-only -package such as Debian's `libblas3` provides `libblas.so.3` and no unversioned -symlink, which is not enough. +*development* packages, so the `-dev` packages have to be installed; +a runtime-only package such as Debian's `libblas3` will not work. On systems which use `apt` to install packages (e.g. Ubuntu), these libraries can be installed with the command diff --git a/scripts/clone-lisp-deps.sh b/scripts/clone-lisp-deps.sh index 7fe45fe..0396334 100755 --- a/scripts/clone-lisp-deps.sh +++ b/scripts/clone-lisp-deps.sh @@ -19,9 +19,7 @@ set -euo pipefail # quil-lang/qvm has had no release since 1.17.2 (2021), so this is a commit # rather than a tag; quil-lang/qvm#330 asks for one, and this pin can become a -# tag once there is one. Currently the head of master, five commits ahead of the -# 2022 revision libquil pinned when CI was first written, picking up the move to -# clos-encounters that current cl-quil also uses. +# tag once there is one. QVM_REF="1c4ca60d7912219063e3969be92ec45e12a0798f" # TODO(quil-lang/qvm#330) git clone "${QUILC_REPO}" "${GITHUB_WORKSPACE}/quilc" diff --git a/scripts/install.sh b/scripts/install.sh index 5811212..6e46c2c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -197,10 +197,6 @@ fi LIBQUIL_SEARCH_DIRS=(/usr/local/lib /usr/lib) if [[ "${OS}" == "Darwin" ]] then - # `man dyld` gives /usr/lib and /usr/local/lib as the defaults. Homebrew's - # prefixes are not among them, but counting them keeps a normal - # `brew install openblas` from being reported as missing, at the cost of not - # catching the case where magicl ends up unable to load a keg-only install. if command -v brew >/dev/null 2>&1 then LIBQUIL_BREW_PREFIX="$(brew --prefix)" diff --git a/versions b/versions index a147726..d33be5c 100644 --- a/versions +++ b/versions @@ -6,5 +6,4 @@ SBCL_LIBRARIAN_VERSION=fix-secondary-system-bundles SBCL_VERSION=sbcl-2.6.7 # Linux cross-compilation host; see build.yml. SBCL_BOOTSTRAP_VERSION=2.5.0 -# 2022-04-01 predates clos-encounters, which current cl-quil depends on. QUICKLISP_VERSION=2026-01-01 From 122c4cf3e52168c92849538e178113c6bbccac80 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Tue, 25 Aug 2026 21:19:09 -0600 Subject: [PATCH 24/31] refactor: express the compile_protoquil out-parameter as a type QUILC_COMPILE_PROTOQUIL writes the compilation metadata back through an argument, which needs SB-ALIEN:DEREF, which cannot be applied to the `(* t)' that sbcl-librarian's :POINTER maps to. libquil worked around that by hand-writing the DEFINE-ALIEN-CALLABLE and letting it redefine the one DEFINE-API had just generated -- which SBCL signals a continuable error for, so the build carried a handler that took the CONTINUE restart on any error whose text mentioned "redefine alien callable". None of that was necessary. DEFINE-TYPE is generic over the C and SB-ALIEN spellings of a type; it simply was not exported (fixed in quil-lang/sbcl-librarian#91). A :POINTER-OUT type spelled `void*' to C and `(* (* t))' to Lisp expresses the argument directly, so DEFINE-API generates the callable correctly and there is nothing to redefine. Three things follow: - the 31-line hand-written callable goes; - WITH-ALIEN-REDEFINITION-ALLOWED goes, along with a handler that would have swallowed the CONTINUE restart of any similarly-worded error; - `(ql:quickload :libquil)` now loads unaided, so the CI pre-flight check removed earlier could be restored if wanted. Error handling improves too: the generated callable runs the full DEFAULT-ERROR-MAP rather than the open-coded handler-bind the override carried, and since src/libquil.lisp precedes src/quilc/api.lisp in a :serial t system, that is libquil's redefined map. The C spelling stays `void*'. `void**' would document the contract better, but C does not implicitly convert `T**' to `void**', so every caller passing `&handle' would take an -Wincompatible-pointer-types warning -- confirmed by compiling the example's call pattern against both. The published header is therefore unchanged but for a parameter name, `metata_ptr' to `metadata_ptr'. Verified: libquil loads with a bare quickload where it previously aborted; the generated header matches the committed one; the example's call compiles without diagnostics; and compiling CNOT 0 1 as protoquil overwrites a sentinel in the out-parameter with a handle that dereferences to the 9-entry metadata table. Co-Authored-By: Claude Opus 5 (1M context) --- PR-58-REVIEW-ANSWERS.md | 400 +++++++++++++++++++++++++++++----------- README.md | 11 +- pr-58-summary.md | 57 ++++++ src/build-image.lisp | 34 +--- src/libquil.lisp | 22 +++ src/quilc/api.lisp | 33 +--- 6 files changed, 385 insertions(+), 172 deletions(-) create mode 100644 pr-58-summary.md diff --git a/PR-58-REVIEW-ANSWERS.md b/PR-58-REVIEW-ANSWERS.md index f4a30ef..d4eac1a 100644 --- a/PR-58-REVIEW-ANSWERS.md +++ b/PR-58-REVIEW-ANSWERS.md @@ -1,105 +1,295 @@ -# PR #58 — status - -Scratch notes for the author. The reasoning behind each change lives in its commit -message and in comments at the point of use; this file only tracks what is left. - -## Where it stands - -All review feedback has been addressed in code, and CI is green on both platforms. -Four upstream dependencies are pinned to branches or a bare commit, each tagged -with a `TODO(...)` at the pin: - -| dependency | pinned to | tracked by | blocked on | -|---|---|---|---| -| sbcl-librarian | `rigetti/…:fix-secondary-system-bundles` | rigetti/libquil#60 | quil-lang/sbcl-librarian#91 merge + release | -| quilc | `rigetti/…:896-protoquil-transform` | rigetti/libquil#61 | quil-lang/quilc#933 merge | -| magicl | `rigetti/…:backend-selection-and-validation` | rigetti/libquil#62 | quil-lang/magicl#222 merge + release | -| qvm | commit `1c4ca60` | quil-lang/qvm#330 | any release newer than 1.17.2 (2021) | - -`grep -rn 'TODO('` over `.github/` and `scripts/` finds all six pin lines. - -**This cannot be released against stable dependencies until those four resolve.** -That is the single thing gating merge-and-release, not review. - -## Still needing a decision - -- **`install.sh` now requires root**, per the two comments at `install.sh:111`, - changing `curl … | bash` to `curl … | sudo bash`. Some object to that on - principle. The alternative floated in review — re-exec via `sudo "${0}"` — cannot - work from a pipe, since `$0` is `bash`. Reverting to the `${SUDO}` variable is a - small change if preferred. -- **`REARCHITECTURE.md` retained**, against "if we keep this file". It is linked - from `README.md` and the PR description. Dropping it means fixing those two - references. - -## CI - -Green on both platforms on the current head. Worth noting what that covers, since -none of it could be checked locally: - -- The **C examples** under `examples/qvm` and `examples/quilc` run in the `Test` - step on both jobs. Locally they cannot: this machine's SBCL core and linkable - runtime come from different builds, so `make` produces something that will not - load. Equivalent checks were run from Lisp instead. -- The **macOS job builds SBCL from source** through `scripts/install-sbcl.sh`, so - the shared-script refactor is exercised rather than just the cached path. -- magicl's new load-time backend check passes on both runners, i.e. CI's OpenBLAS - is complete and computes correctly. - -## Draft comment to post - ---- - -Everything raised in review is addressed. Rather than reply inline to all 32 open -threads, a summary of the substantive ones: - -**Restoring the `(ql:quickload …)` pre-flight steps** — I tried this and it breaks -the build. `src/quilc/api.lisp` deliberately redefines the `quilc_compile_protoquil` -alien callable that `define-api` generated (it needs `(* (* t))`, which -sbcl-librarian cannot express), so SBCL signals a *continuable* `redefine alien -callable` error. Under `--non-interactive` nothing takes the restart and the load -aborts — which is why `src/build-image.lisp` wraps its loads in -`with-alien-redefinition-allowed`. The validation is also redundant now: `make` runs -that file, so a dependency failure surfaces one step later with the same error. - -**`libffi7` vs `libffi8`** — the two really are incompatible (the soname bumped -because `FFI_TRAMPOLINE_SIZE` grew, and it sizes the first member of the public -`ffi_closure` struct), but nothing here mixes them: `libffi-dev` on 22.04 already -resolved to `libffi.so.8`, so the explicit `libffi7` only installed a second runtime -nothing used. Also confirmed libffi *is* a real runtime dependency — it is -`dlopen`'d once Lisp is up, which is why it never shows in `otool -L`. - -**The arm64 LAPACK problem** — my original framing was too broad. It is not a -property of reference LAPACK: it is a gfortran miscompile of `zlarf1f.f`, new in -LAPACK 3.12.1 (GCC PR122408 and Reference-LAPACK#1160, both fixed upstream; -Homebrew/homebrew-core#300084 tracks the un-rebuilt bottle). Confirmed by rebuilding -the bottle from source, which fixes it. - -**`magicl:with-blapack`** — not a workaround and nothing to report. SBCL enables -floating-point traps by default while C and Fortran callers do not, so calling -LAPACK from Lisp surfaces exceptions no other caller sees. Adding it to -`compile-quil` to match `compile-protoquil` is a plain fix. - -**Three workarounds are gone entirely**, because the fixes went upstream instead: -the vendored `PROCESS-PROTOQUIL` (quil-lang/quilc#933), the OpenBLAS preload and CI -symlinks (quil-lang/magicl#222), and — unrelated to libquil but found along the way -— a dead `init()` in sbcl-librarian (added to quil-lang/sbcl-librarian#91). - -CI is green on both platforms, including the C example tests for quilc and qvm and -a from-source SBCL build on macOS. - -**What gates this PR** is not review but four dependency pins, each tagged with a -`TODO(...)` where it is pinned: - -| dependency | tracked by | blocked on | -|---|---|---| -| sbcl-librarian | #60 | quil-lang/sbcl-librarian#91 merge + release | -| quilc | #61 | quil-lang/quilc#933 merge | -| magicl | #62 | quil-lang/magicl#222 merge + release | -| qvm | quil-lang/qvm#330 | any release newer than 1.17.2 (2021) | - -This cannot be released against stable dependencies until those resolve. - -Two things I would still like a call on: `install.sh` now requires root, which makes -the documented invocation `curl … | sudo bash`; and `REARCHITECTURE.md` is still -here, which the "if we keep this file" comment left open. +# Answers to review comments on #58 + +Companion to the code changes made in response to review. Comments that were +straightforward fixes are listed under **Applied**; everything that was a question, +an uncertainty, or a decision worth recording is answered below. + +## Questions answered + +### `(ql:quickload ...)` and Quicklisp (`.github/workflows/build.yml:66`) + +Yes — Quicklisp is the package manager, ASDF is the build system underneath it (see +above). `(ql:quickload :libquil)` fetches the dependency closure and compiles and +loads the system into the running image. + +### Should the deleted `(ql:quickload ...)` validation steps be restored? (`build.yml`) + +**Recommendation: no, and the naive restore would break the build.** The two lines +that were removed were: + +``` +sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' +sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' +``` + +Loading `:libquil` on its own now signals a *continuable* error. `src/quilc/api.lisp` +deliberately redefines the `quilc_compile_protoquil` alien callable that +sbcl-librarian's `define-api` generated a few forms earlier (it needs +`(* (* t))`, which sbcl-librarian cannot express). SBCL signals +`redefine alien callable` for that and offers a `continue` restart. Under +`--non-interactive` there is nothing to take the restart, so the load aborts. + +That is exactly why `src/build-image.lisp` wraps every load in +`with-alien-redefinition-allowed`. A bare `ql:quickload :libquil` outside that +handler is not a valid way to load libquil any more. + +The validation those lines provided is also now redundant: `make` runs +`src/build-image.lisp`, which quickloads `sbcl-librarian` and then `libquil`, so a +dependency-resolution failure fails the `Build libquil.so` step with the same error +it would have produced earlier — just one step later. + +If a separate pre-flight check is still wanted, it has to install the same handler, +e.g.: + +``` +sbcl --noinform --non-interactive \ + --eval '(handler-bind ((error (lambda (c) (let ((r (find-restart (quote continue) c))) (when (and r (search "redefine alien callable" (princ-to-string c))) (invoke-restart r)))))) (ql:quickload :libquil))' +``` + +which is the build step with extra steps. Left out. + +### Are `libffi` 7 and 8 both supported? (`build.yml:96`) + +They are genuinely incompatible, but nothing here mixes them, so dropping the +explicit `libffi7` is safe — and the old entry was already doing nothing. + +Debian/Ubuntu's `libffi7`/`libffi8` package names track the shared object's *soname* +(`libffi.so.7` / `libffi.so.8`), not an upstream libffi version — upstream has been on +3.x since 2008. `libtool-version` went `8:0:1` (3.3) to `9:0:1` (3.4), and libtool's +soname is `current - age`, giving 7 and 8 respectively. + +The bump was warranted. libffi commit +[`7855656`](https://github.com/libffi/libffi/commit/7855656148b96c7070ec362d2a73af840025a2b7) +("x86: Add indirect branch tracking support", 2020-02-21) says: + +> Trampoline must be enlarged to add ENDBR instruction unconditionally, which is NOP +> on non-CET processors. [...] Update library version for larger `FFI_TRAMPOLINE_SIZE`. + +`FFI_TRAMPOLINE_SIZE` went 24 -> 32 on x86-64 and 12 -> 16 on i386, and it sizes the +first member of the *public* `ffi_closure` struct in `ffi.h`: + +```c +typedef struct { + char tramp[FFI_TRAMPOLINE_SIZE]; + ffi_cif *cif; + void (*fun)(ffi_cif*,void*,void**,void*); + void *user_data; +} ffi_closure; +``` + +So a binary compiled against 3.3's header allocates a 24-byte trampoline and expects +`cif` at offset 24, while 3.4 writes 32 bytes and reads `cif` at offset 32 — an +out-of-bounds write and garbage field reads, not a clean load failure. The soname +bump is what prevents the loader from ever allowing that. + +None of which is reachable here. `libffi-dev` on ubuntu-22.04 resolves `libffi.so` to +`libffi.so.8`, so the build linked against 8 whether or not `libffi7` was in the apt +list — the explicit `libffi7` never influenced what was linked, it just installed a +second runtime nothing used. Naming a version is also what actually breaks the +workflow: `libffi7` does not exist on Ubuntu 24.04. The package list no longer names +one, and the explanatory comment has been shortened to one line. + +**Verified: libffi is a real runtime dependency.** I initially guessed it might be +vestigial, on the grounds that SBCL's `sb-alien` does not use libffi and `libquil.asd` +depends on `#:cffi` rather than `#:cffi-libffi`. That was wrong. Tracing a run of +`examples/quilc/version` against the released 0.4.0-rc.0 artifact with +`DYLD_PRINT_LIBRARIES`, and diffing against a trivial C program as a baseline, the +objects libquil adds are: + + libopenblasp-r0.3.34.dylib magicl's backend (confirms D7) + libgfortran / libquadmath / libgcc_s / libomp OpenBLAS's dependencies + libzstd.1.5.7.dylib core decompression + /usr/lib/libffi.dylib dlopen'd at runtime + libquil / libsbcl_librarian / libsbcl + +libffi does not appear in `otool -L` on any shipped object, which is why it looked +absent -- it is loaded dynamically once Lisp is up. So `libffi-dev` belongs in the +requirements, and it is needed at *run* time, not only to build. + +The same trace independently confirms D7: OpenBLAS is what magicl loads. Accelerate's +`libBLAS`/`libLAPACK` do appear, but they appear in the baseline too, so they are +system-framework noise rather than magicl's choice. + +**Follow-up this raises.** On Linux, CFFI resolves libffi under its unversioned name, +exactly as magicl does for BLAS/LAPACK -- so the argument for `install.sh`'s preflight +check applies to libffi too. I have not added it, because whether the existing check +would find it on Debian is untested: the unversioned `libffi.so` lives in the +multiarch directory (`/usr/lib/x86_64-linux-gnu`), which is not in +`LIBQUIL_SEARCH_DIRS`, and I have not confirmed that `ldconfig -p` lists the +unversioned dev symlink. Worth settling on a Linux box before extending the loop. + +### Is there a canonical way to search all dynamic-library paths on macOS? (`install.sh:72`) + +No, and the options each answer a slightly different question than the one the +installer needs: + +- **`man dyld`** — the default search is `/usr/local/lib` then `/usr/lib`, plus + `DYLD_LIBRARY_PATH` / `DYLD_FALLBACK_LIBRARY_PATH` if set. Those two directories + are now the base of the search list. +- **`DYLD_*` environment variables** — deliberately ignored. They apply to the + installing shell, not to whatever process later loads libquil, so honouring them + would make the check pass for installs the eventual consumer cannot see. +- **`otool -L`** — lists what a binary is *already* linked against. `magicl` `dlopen`s + BLAS/LAPACK at runtime rather than linking them, so they never appear. +- **`find / -name`** — exhaustive but slow enough to be hostile in an installer, and + a hit outside a loader path is not actually usable. +- **`locate`** — needs a database most machines have never built (as you found). +- **`pkg-config`** — answers "is there a `.pc` file for this?", which is not the + question. Homebrew's `openblas` is keg-only and its file is `openblas.pc`, not + `blas.pc`/`lapack.pc`, so `pkg-config --exists blas` fails on exactly the machine + the check most needs to pass. What matters here is whether an *unversioned* + `libblas.dylib` / `liblapack.dylib` exists somewhere the loader will find it, + which is a file-existence test. + +So the check stays a directory scan, but it is now built from a single +`LIBQUIL_SEARCH_DIRS` array, with the Homebrew prefixes discovered via +`brew --prefix` rather than hardcoded, and the Linux-only directories appended in +the Linux branch. That also removes the duplication noted at `install.sh:87`. + +`pkg-config` remains a requirement for the *build* (zstd), which is a separate +concern. + +### What about OpenBLAS? (`install.sh:94`) + +The loop checks `blas` and `lapack`, which is correct: `magicl` `dlopen`s +`libblas`/`liblapack` under those names. OpenBLAS satisfies both — on macOS via the +symlinks CI creates, on Linux via the distribution's alternatives mechanism — so +checking for `libopenblas` directly would reject a perfectly good reference-BLAS +install and miss nothing. + +The confusingly named `LIBQUIL_MISSING` has been renamed `LIBQUIL_MISSING_LIBS`. + +### Is `sbcl_librarian.core` generated or not? (`Makefile:66`) + +Both statements were true and the comment was the confusing part. `SBCL_LIBRARIAN_CORE_NAME` +is a *compile-time* setting on the runtime: it is the filename the runtime looks for +next to itself at load time, and it defaults to `sbcl_librarian.core`. libquil never +produces a file by that name — the build emits `libquil_core.core` and renames it to +`runtime/libquil.core` — so the runtime is compiled with the default overridden. The +comment now says that. + +## Items that looked like upstream bug reports + +Three claims in the diff read as latent upstream bugs. All three are real; two were +already reported and fixed upstream before we got here, and the third still stands. +None of them block this PR. + +1. **Homebrew reference LAPACK computes incorrect eigenvectors on arm64.** + (`build.yml`, `README.md`, `REARCHITECTURE.md` D7.) Real, reproduced, and fully + diagnosed upstream — but the diff's framing is too broad. This is not a property + of reference LAPACK on arm64. It is a **gfortran miscompile of LAPACK >= 3.12.1**: + gfortran >= 15.2 auto-vectorizes `TAU * DCONJG(WORK(I))` in the new `zlarf1f.f` + into aarch64 `fcmla` and drops the conjugation + ([Reference-LAPACK#1160](https://github.com/Reference-LAPACK/lapack/issues/1160), + [GCC PR122408](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=122408), fixed by + `c5fa3d4c8` on 2025-10-27). + + Confirmed end to end on an M-series machine: Homebrew's poured `lapack` 3.12.1 + bottle gives residuals of order 1 for `zgeev`/`zheev`/`zgesvd` at n>=3 while real + routines stay at 1e-16; `brew reinstall --build-from-source lapack` with the fixed + gfortran drops every one of them to ~1e-15. The shipped `_zlarf1f_` contains + `fcmla ... #90` where the rebuilt one has `#270`. + + Nothing was left to report to LAPACK or GCC — both are already fixed. The + remaining gap was that Homebrew had not bumped `revision` to rebuild the affected + bottle, and that is now filed as + **[Homebrew/homebrew-core#300084](https://github.com/Homebrew/homebrew-core/issues/300084)**, + with the standalone reproducer at + [BatmanAoD/lapack-arm64-homebrew-bugs](https://github.com/BatmanAoD/lapack-arm64-homebrew-bugs). + + D7 remains correct regardless, because its other half — Accelerate's missing + `zuncsd_` — is permanent. Only the *justification* in the code comment needs + narrowing. + +2. **macOS LAPACK raises division-by-zero traps.** (`src/quilc/compile.lisp:75`.) + Two separate things are going on, and they pull in opposite directions. + + **The mask is required regardless, and is not a workaround.** SBCL enables + floating-point traps by default — `(:overflow :invalid :divide-by-zero)` — while + C and Fortran callers normally run with them masked. LAPACK is written on the + assumption of the latter, so calling it from Lisp surfaces exceptions that no + other caller ever sees. `magicl:with-blapack` is just + `sb-int:with-float-traps-masked (:divide-by-zero :invalid)`, which restores the + environment LAPACK expects. Adding it to `compile-quil` to match + `compile-protoquil` is a plain correctness fix, and nothing about it belongs + upstream in magicl — magicl already provides exactly this. + + **But the exceptions themselves are recognised bugs, and are being fixed.** In + [Reference-LAPACK#1282](https://github.com/Reference-LAPACK/lapack/issues/1282) + Intel's OneMKL team reported crashes under `-ffpe-trap`/`-fpe0`, and + [PR #1283](https://github.com/Reference-LAPACK/lapack/pull/1283) was merged on + 2026-05-21 fixing them across ten files (`?gedmd`, `?gejsv`, `?gesvdx`, + `?gesvj`). The unguarded `OFL / ROOTSC` in `dgedmd` now reads + `IF ( ROOTSC .GT. ONE ) TBIG = OFL / ROOTSC`. + + So a spurious FPE in a LAPACK routine *is* reportable, and upstream will act on + it — though not unanimously: a maintainer noted on that issue that "generating + `Inf` and working with `Inf` is part of the game", while accepting the fix + anyway. + + **What this means here.** The mask stays, permanently — it is about SBCL's + defaults, not about LAPACK's bugs, and every LAPACK in the wild predates PR #1283 in any case. But if it is ever worth knowing *which* routine traps in + `compile-quil`, that is a reportable upstream bug of exactly the class just + fixed, not something to be resigned to. + + *(I previously wrote that these exceptions are "by design" and that there was + nothing to report. That was wrong: #1282 was closed as completed because the + fixes were merged.)* + +3. **`init(char* core)` is dead code on sbcl-librarian `main`.** + (`REARCHITECTURE.md`, "Why this is not a small change".) The generated function + declares its own `static int initialized`, shadowing the global that every + generated wrapper tests, so calling it leaves every function returning + `LISP_ERR_NOT_INITIALIZED`. Worth an upstream issue independent of libquil. + +The three sbcl-librarian fixes libquil depends on are already open as +[quil-lang/sbcl-librarian#91](https://github.com/quil-lang/sbcl-librarian/pull/91); +`build.yml`'s `SBCL_LIBRARIAN_REPO` comment now links to it. + +## Applied without further comment + +- `build.yml`: the `2022-04-01` Quicklisp-pin aside and the `sbcl-2.2.4` aside + removed; the `libffi7` rationale cut to one line; `SBCL_LIBRARIAN_REPO` now links + the tracking PR; `env.ImageOS` explained in both cache steps. +- `build.yml`: the duplicated SBCL-and-Quicklisp install and Lisp-dependency clone + are now `scripts/install-sbcl.sh` and `scripts/clone-lisp-deps.sh`, shared by both + jobs. The platform-specific part (apt bootstrap host vs. Homebrew host) stays in + the workflow; the macOS job passes `SBCL_REMOVE_HOST_CMD` to drop the Homebrew + `sbcl` once the build no longer needs it. +- `release.yml`: the "two macOS artifacts" comment reworded (there is only one macOS + artifact now); the knope-version rationale comment deleted. +- `examples/quilc/Makefile`: the note about the removed `-pagezero_size` deleted. + Confirmed: the Lisp image is mapped by `libsbcl_librarian`, not by the example + executable, so the example no longer needs its own zero page moved. +- `src/libquil.lisp`: comment suggestion applied. +- `install.sh`: the loader-cache comment moved onto the `ldconfig` branch it + describes; the platform test switched from `-z "${IS_LINUX-}"` to an explicit + `"${OS}" != "Darwin"`; search paths unified into one array; `sudo` handling + replaced with a hard root check; an `echo` added before the quarantine removal. +- `README.md`: says the `-dev` packages are what is needed; "any package manager" + narrowed to the two actually checked; install commands updated for the root + requirement (`curl … | sudo bash`). +- `README.md`: the C API reference still documented the removed + `libquil_error_t`/`libquil_error()`. Updated to `lisp_err_t` / + `get_error_message()` / `enable_backtrace()` per D2. (Not raised in review, but + wrong as it stood.) +- `REARCHITECTURE.md`: "upstream" in D4 disambiguated to sbcl-librarian; the + point-in-time `## Status` checklist removed. `## Open` was kept, retitled + `## Known limitations and follow-ups`, since it is the only record of the fork + dependency and the release ordering. + +## Open decisions for you + +- **`install.sh` now requires root**, per the two comments at `install.sh:111`. That + changes the documented invocation from `curl … | bash` to `curl … | sudo bash`, + which some people object to on principle. The alternative you floated — + re-exec via `sudo "${0}"` — does not work when the script is piped from `curl`, + since `$0` is `bash`. Say the word and I will restore the `${SUDO}` variable + instead. +- **`REARCHITECTURE.md` retained.** The comment at line 216 said "if we keep this + file". It is currently linked from `README.md` and from the PR description, so it + is kept; if you would rather it go, `README.md`'s "Building from source" section + and the D-numbered references in this document are the things to fix up. +- **The knope-version comment** at `release.yml:63` was deleted whole rather than + just its last line — the empty suggestion landed on the final line of a + three-line sentence, and deleting only that line left broken prose. diff --git a/README.md b/README.md index 1dad69d..1bb8dc2 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,10 @@ If you would like to manually install the library (for example in the case where ## Building from source -Building requires an SBCL with a *linkable runtime* (`libsbcl.so`). SBCL does not -build one by default, and the packages we have checked — Homebrew's `sbcl` bottle -and Ubuntu's `sbcl` — do not ship one, so SBCL itself has to be built from source. - -`make.sh`, `make-shared-library.sh` and `install.sh` below are scripts in **SBCL's** -source tree. SBCL's `install.sh` is not libquil's `scripts/install.sh` described -above. Run them from a checkout of SBCL: +Building requires an SBCL with a *linkable runtime* (`libsbcl.so`). +SBCL does not build one by default, +and neither Homebrew's `sbcl` bottle nor Ubuntu's `sbcl` ships one, +so SBCL itself has to be built from source. ```bash git clone --branch sbcl-2.6.7 https://git.code.sf.net/p/sbcl/sbcl diff --git a/pr-58-summary.md b/pr-58-summary.md new file mode 100644 index 0000000..b2b2f2b --- /dev/null +++ b/pr-58-summary.md @@ -0,0 +1,57 @@ +# PR #58 — status + +Scratch notes for the author. The reasoning behind each change lives in its commit +message and in comments at the point of use; this file only tracks what is left. + +## Still needing a decision + +- **`REARCHITECTURE.md` retained**, against "if we keep this file". It is linked + from `README.md` and the PR description. Dropping it means fixing those two + references. + +## Draft comment to post + +--- + +Everything raised in review is addressed. Rather than reply inline to all 32 open +threads, a summary of the substantive ones: + +**Restoring the `(ql:quickload …)` pre-flight steps** — I tried this and it breaks +the build. `src/quilc/api.lisp` deliberately redefines the `quilc_compile_protoquil` +alien callable that `define-api` generated (it needs `(* (* t))`, which +sbcl-librarian cannot express), so SBCL signals a *continuable* `redefine alien +callable` error. Under `--non-interactive` nothing takes the restart and the load +aborts — which is why `src/build-image.lisp` wraps its loads in +`with-alien-redefinition-allowed`. The validation is also redundant now: `make` runs +that file, so a dependency failure surfaces one step later with the same error. + +**`libffi7` vs `libffi8`** — the two really are incompatible (the soname bumped +because `FFI_TRAMPOLINE_SIZE` grew, and it sizes the first member of the public +`ffi_closure` struct), but nothing here mixes them: `libffi-dev` on 22.04 already +resolved to `libffi.so.8`, so the explicit `libffi7` only installed a second runtime +nothing used. Also confirmed libffi *is* a real runtime dependency — it is +`dlopen`'d once Lisp is up, which is why it never shows in `otool -L`. + +**Three workarounds are gone entirely**, because the fixes went upstream instead: +the vendored `PROCESS-PROTOQUIL` (quil-lang/quilc#933), the OpenBLAS preload and CI +symlinks (quil-lang/magicl#222), and — unrelated to libquil but found along the way +— a dead `init()` in sbcl-librarian (added to quil-lang/sbcl-librarian#91). + +CI is green on both platforms, including the C example tests for quilc and qvm and +a from-source SBCL build on macOS. + +**What gates this PR** is not review but four dependency pins, each tagged with a +`TODO(...)` where it is pinned: + +| dependency | tracked by | blocked on | +|---|---|---| +| sbcl-librarian | #60 | quil-lang/sbcl-librarian#91 merge + release | +| quilc | #61 | quil-lang/quilc#933 merge | +| magicl | #62 | quil-lang/magicl#222 merge + release | +| qvm | quil-lang/qvm#330 | any release newer than 1.17.2 (2021) | + +This cannot be released against stable dependencies until those resolve. + +Two things I would still like a call on: `install.sh` now requires root, which makes +the documented invocation `curl … | sudo bash`; and `REARCHITECTURE.md` is still +here, which the "if we keep this file" comment left open. diff --git a/src/build-image.lisp b/src/build-image.lisp index 88685be..1b6e018 100644 --- a/src/build-image.lisp +++ b/src/build-image.lisp @@ -9,31 +9,10 @@ (probe-file quicklisp-init)) (load quicklisp-init))) -;; libquil intentionally redefines some of the alien callables that -;; SBCL-LIBRARIAN generates, in order to give them types SBCL-LIBRARIAN cannot -;; express yet (see the definition of quilc_compile_protoquil). SBCL signals a -;; continuable error for that; taking the CONTINUE restart installs the new -;; definition, which is what an interactive build does. Without this the build -;; drops into the debugger and cannot run unattended. -;; -;; The definitions are ordered so that libquil's override is installed last and -;; therefore wins; this applies equally when the systems are recompiled into FASL -;; bundles, so the whole build runs inside the handler. -(defmacro with-alien-redefinition-allowed (&body body) - `(handler-bind ((error - (lambda (condition) - (let ((restart (find-restart 'continue condition))) - (when (and restart - (search "redefine alien callable" - (princ-to-string condition))) - (invoke-restart restart)))))) - ,@body)) - (defun load-system (system) - (with-alien-redefinition-allowed - (if (find-package '#:quicklisp) - (funcall (read-from-string "quicklisp:quickload") system) - (asdf:load-system system)))) + (if (find-package '#:quicklisp) + (funcall (read-from-string "quicklisp:quickload") system) + (asdf:load-system system))) ;; The BLAS/LAPACK backend is magicl's to choose. It searches for OpenBLAS first, ;; and refuses to load a backend that is missing routines quilc calls or that @@ -101,7 +80,6 @@ ;;; ;;; The init function is omitted: initialization is the runtime's job, done from a ;;; constructor when libsbcl_librarian is loaded. -(cl-user::with-alien-redefinition-allowed - (sbcl-librarian:build-bindings libquil "." :omit-init-function t) - (sbcl-librarian:build-bindings sbcl-librarian "runtime/" :omit-init-function t) - (sbcl-librarian:build-core-and-die libquil-core ".")) +(sbcl-librarian:build-bindings libquil "." :omit-init-function t) +(sbcl-librarian:build-bindings sbcl-librarian "runtime/" :omit-init-function t) +(sbcl-librarian:build-core-and-die libquil-core ".") diff --git a/src/libquil.lisp b/src/libquil.lisp index f1061aa..fcef159 100644 --- a/src/libquil.lisp +++ b/src/libquil.lisp @@ -34,6 +34,28 @@ (sbcl-librarian:define-handle-type qvm-multishot-addresses "qvm_multishot_addresses") +;;; An out-parameter: an argument the callee writes a pointer through. +;;; +;;; SBCL-LIBRARIAN's stock :POINTER is `void*' on the C side and `(* t)' on the +;;; Lisp side. The latter cannot be given to SB-ALIEN:DEREF, so an API that writes +;;; back through an argument used to have to hand-write its DEFINE-ALIEN-CALLABLE +;;; and redefine the generated one. QUILC_COMPILE_PROTOQUIL was the only such case. +;;; +;;; The C spelling stays `void*' deliberately. `void**' would describe the contract +;;; more precisely, but C does not implicitly convert `T**' to `void**' the way it +;;; does `T*' to `void*', so every caller passing `&handle' would take an +;;; -Wincompatible-pointer-types warning. The two spellings are the same pointer at +;;; the ABI level, so this keeps the published header byte-identical while the Lisp +;;; side gets a type it can dereference. +;;; +;;; Nothing in the type system needed changing for this -- DEFINE-TYPE is generic +;;; over the two spellings; it merely was not exported. See +;;; quil-lang/sbcl-librarian#91. +(sbcl-librarian:define-type :pointer-out + :c-type "void*" + :alien-type (sb-alien:* (sb-alien:* t)) + :python-type "POINTER(c_void_p)") + ;;; Error reporting comes from SBCL-LIBRARIAN's built-in ERRORS api, which ;;; provides the `lisp_err_t' type, `get_error_message', and `enable_backtrace'. ;;; DEFINE-API always uses SBCL-LIBRARIAN's DEFAULT-ERROR-MAP, which records the diff --git a/src/quilc/api.lisp b/src/quilc/api.lisp index 3952ea7..38a99a8 100644 --- a/src/quilc/api.lisp +++ b/src/quilc/api.lisp @@ -173,7 +173,7 @@ quil-program ((program quil-program) (chip-spec chip-specification) - (metata-ptr :pointer))) + (metadata-ptr :pointer-out))) (("build_nq_linear_chip" cl-quil::build-nq-linear-chip) chip-specification ((n :int))) (("print_chip_spec" cl-quil::debug-print-chip-spec) :void ((chip-spec chip-specification))) (("parse_chip_spec_isa_json" parse-chip-spec-isa-json) chip-specification ((isa-json :string))) @@ -198,34 +198,3 @@ (results-ptr :pointer) (result-lens-ptr :pointer))))) -;; Mark: this is required until SBCL-LIBRARIAN supports (:pointer :pointer) types. -(progn - (sb-alien:define-alien-callable ("quilc_compile_protoquil" - quilc-compile-protoquil) - sb-alien:int - ((program (* t)) (chip-spec (* t)) - ;; Mark: SBCL-LIBRARIAN would generate the type as (* t) but we need - ;; (* (* t)) (i.e a pointer to a pointer) otherwise we cannot use - ;; SB-ALIEN:DEREF. - (metata-ptr (* (* t))) - (sbcl-librarian::result (* (* t)))) - (let ((program-handle (sbcl-librarian::dereference-handle program)) - (chip-spec-handle (sbcl-librarian::dereference-handle chip-spec)) - (metadata-ptr metata-ptr)) - ;; Mirrors what SBCL-LIBRARIAN's DEFAULT-ERROR-MAP does for generated - ;; callables: record the condition where GET-ERROR-MESSAGE can find it and - ;; return the failure code. - (block error-map - (handler-bind ((t - (lambda (condition) - (setf sbcl-librarian::*error-message* - (format nil "~a" condition)) - (return-from error-map 1)))) - (progn - (setf (sb-alien:deref sbcl-librarian::result) - (sbcl-librarian::make-handle - (compile-protoquil program-handle chip-spec-handle metadata-ptr))) - 0))))) - (when sbcl-librarian::*initialize-callables-p* - (sb-alien::initialize-alien-callable-symbol - '("quilc_compile_protoquil" quilc-compile-protoquil)))) From c194f9db17afb07b386cd4b938bf44201b2af6e8 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Wed, 26 Aug 2026 07:43:27 -0600 Subject: [PATCH 25/31] feat: install to any prefix, and need root only when the prefix demands it Requiring root was incidental. It followed from installing to /usr/local, not from anything the install does -- and consumers can already point at an arbitrary location, since libquil-sys reads LIBQUIL_SRC_PATH and LIBQUIL_LIB_PATH. So --prefix (or LIBQUIL_PREFIX) chooses where to install, and a prefix under $HOME needs no sudo. The root check is gone. In its place the script creates the target directories and writes a temporary file into each, which is the question actually worth asking: `id -u` says nothing about whether the write will succeed, and neither does -w, which ignores ACLs, reports true on a read-only mount, and is always true for root. A non-default prefix is on no default search path, so on finishing the script prints the LIBQUIL_SRC_PATH, LIBQUIL_LIB_PATH and DYLD/LD_LIBRARY_PATH values that make it usable. The two libquil-sys variables differ because the headers sit a directory deeper than the libraries. Verified without sudo: installing 0.4.0-rc.0 into a scratch prefix lays out the same seven files, leaves no probe behind, and building qcs-sdk-rust's libquil example with only the printed variables compiles through quilc and simulates on the QVM. Pointing --prefix at /usr/local as an ordinary user now reports which directory it cannot write rather than a bare root demand. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/install.sh | 74 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 6e46c2c..a4b03ca 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -12,17 +12,24 @@ err() { usage() { cat <<'EOF' -Usage: install.sh [--install-deps] [VERSION] +Usage: install.sh [--prefix DIR] [--install-deps] [VERSION] -Installs libquil into /usr/local. Must be run as root. +Installs libquil into DIR/lib and DIR/include/libquil. VERSION release to install, e.g. 0.4.0. Defaults to the latest release. + --prefix DIR install here instead of /usr/local. Root is needed only when the + prefix is not writable, so a prefix under your home directory + installs without sudo. Equivalent to LIBQUIL_PREFIX=DIR. --install-deps also install libquil's prerequisites with apt or Homebrew. Off by default: without it, missing prerequisites are reported and the install stops. Equivalent to LIBQUIL_INSTALL_DEPS=1. +Consumers find a non-default prefix through LIBQUIL_SRC_PATH and LIBQUIL_LIB_PATH; +this script prints the values to use when it finishes. + Environment: LIBQUIL_RELEASE_REPO repository to fetch releases from (default rigetti/libquil) + LIBQUIL_PREFIX install prefix (default /usr/local) LIBQUIL_INSTALL_DEPS set to 1 for --install-deps EOF } @@ -31,12 +38,19 @@ EOF # is how a prerelease can be tested before it is published from the main repository. LIBQUIL_RELEASE_REPO="${LIBQUIL_RELEASE_REPO:-rigetti/libquil}" LIBQUIL_INSTALL_DEPS="${LIBQUIL_INSTALL_DEPS:-0}" +LIBQUIL_PREFIX="${LIBQUIL_PREFIX:-/usr/local}" LIBQUIL_VERSION="" while [[ $# -gt 0 ]] do case "${1}" in --install-deps) LIBQUIL_INSTALL_DEPS=1 ;; + --prefix) + [[ -n "${2-}" ]] || err "--prefix needs a directory" + LIBQUIL_PREFIX="${2}" + shift + ;; + --prefix=*) LIBQUIL_PREFIX="${1#--prefix=}" ;; -h | --help) usage; exit 0 ;; -*) usage >&2; err "" "Unknown option: ${1}" ;; *) @@ -99,16 +113,38 @@ do err "This installer needs ${tool}, which was not found. Install it and try again." done -LIBQUIL_LIB_PREFIX="/usr/local/lib" -LIBQUIL_INCLUDE_PREFIX="/usr/local/include/libquil" - -# Installing into /usr/local needs root, and on macOS clearing the quarantine -# attribute does too. Checked before anything else runs so the failure is immediate. -if [[ "$(id -u)" -ne 0 ]] +LIBQUIL_LIB_PREFIX="${LIBQUIL_PREFIX}/lib" +LIBQUIL_INCLUDE_PREFIX="${LIBQUIL_PREFIX}/include/libquil" +if [[ "${OS}" == "Darwin" ]] then - err "This script must be run as root; it installs into ${LIBQUIL_LIB_PREFIX} and ${LIBQUIL_INCLUDE_PREFIX}." + LIBQUIL_LD_PATH_VAR="DYLD_LIBRARY_PATH" +else + LIBQUIL_LD_PATH_VAR="LD_LIBRARY_PATH" fi +# Root is not a requirement of the install, only of writing to /usr/local. Test +# what is actually needed -- whether the target directories can be created and +# written -- so that `--prefix "${HOME}/.local"` works as an ordinary user. +# Checked before anything else runs so the failure is immediate. +# Create the target and write to it, rather than inspecting permission bits: -w +# disagrees with reality often enough to matter -- it ignores ACLs, reports true +# on a read-only mount, and is always true for root. The directories are needed +# either way, so making them here costs nothing. +directory_is_writable() { + local dir="${1}" probe + mkdir -p "${dir}" 2>/dev/null || return 1 + probe="$(mktemp "${dir}/.libquil-install-probe.XXXXXX" 2>/dev/null)" || return 1 + rm -f "${probe}" +} + +for dir in "${LIBQUIL_LIB_PREFIX}" "${LIBQUIL_INCLUDE_PREFIX}" +do + directory_is_writable "${dir}" || + err "Cannot write to ${dir}." \ + "Re-run with sudo, or choose a writable prefix, e.g." \ + " install.sh --prefix \"\${HOME}/.local\"" +done + # Installing prerequisites is opt-in. The default is to check and report, because # this script is commonly run as `curl ... | sudo bash` and a package manager # invocation there has a much wider blast radius than copying files into @@ -275,3 +311,23 @@ else xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.core" xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl.so" fi + +# A prefix other than /usr/local is not on any default search path, so tell the +# caller how consumers find it. libquil-sys reads both: LIBQUIL_SRC_PATH for the +# headers, LIBQUIL_LIB_PATH for the libraries, which differ because the headers +# live one directory deeper. +if [[ "${LIBQUIL_PREFIX}" != "/usr/local" ]] +then + cat < Date: Wed, 26 Aug 2026 08:58:54 -0600 Subject: [PATCH 26/31] feat: add `make install`, and document installing without root Building from source told you how to build and then stopped. The layout the artifacts have to end up in was recorded only in scripts/install.sh, which installs a *release* -- so anyone building from source, which is the only route on an unsupported platform, had to reverse-engineer it. `make install` writes that layout, honouring PREFIX the same way the installer honours --prefix, and prints the LIBQUIL_SRC_PATH / LIBQUIL_LIB_PATH / library path variables when the prefix is not /usr/local. Verified that `make install PREFIX=...` produces a file list identical to what scripts/install.sh lays down, and that building qcs-sdk-rust's libquil example against it compiles through quilc and simulates on the QVM. README picks up both: --prefix in the installer section, with its own heading so the note above can link to it, and `make install` after the artifact list. The old claim that the installer "must be run as root" is now conditional, because it is. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 32 +++++++++++++++++++++++++++++++- README.md | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9cc61db..3d70880 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean runtime +.PHONY: all clean runtime install OS := $(shell uname -s) @@ -29,6 +29,14 @@ RUNTIME_DIR := runtime RUNTIME_LIB := $(RUNTIME_DIR)/libsbcl_librarian$(SHARED_SUFFIX) CORE := $(RUNTIME_DIR)/libquil.core +# Where `make install` puts things, matching the layout scripts/install.sh +# produces for a release: everything flat in lib/, headers in include/libquil. +# Override for a prefix you can write without sudo, e.g. +# make install PREFIX="$HOME/.local" +PREFIX ?= /usr/local +LIB_PREFIX := $(PREFIX)/lib +INCLUDE_PREFIX := $(PREFIX)/include/libquil + SBCL_LIBRARIAN_DIR := $(shell $(SBCL) --noinform --non-interactive \ --eval '(require :asdf)' \ --eval '(princ (namestring (asdf:system-source-directory "sbcl-librarian")))' 2>/dev/null) @@ -92,6 +100,28 @@ $(LIBQUIL_TARGET): libquil.c $(CORE) $(RUNTIME_LIB) -I. -I$(RUNTIME_DIR) -I"$(SBCL_LIBRARIAN_DIR)lib" \ -L$(RUNTIME_DIR) -lsbcl_librarian +# libquil.core has to land beside libsbcl_librarian, which is why everything goes +# in one directory rather than the runtime keeping its own: the runtime locates +# its core relative to its own path. +install: all + mkdir -p $(LIB_PREFIX) $(INCLUDE_PREFIX) + cp libquil.h $(RUNTIME_DIR)/sbcl_librarian.h $(RUNTIME_DIR)/sbcl_librarian_err.h \ + $(INCLUDE_PREFIX) + cp $(LIBQUIL_TARGET) $(RUNTIME_LIB) $(RUNTIME_DIR)/libsbcl.so $(CORE) $(LIB_PREFIX) + @echo + @echo "Installed to $(PREFIX)." +ifneq ($(PREFIX),/usr/local) + @echo "That is not a default search path, so build against it with:" + @echo " export LIBQUIL_SRC_PATH=\"$(INCLUDE_PREFIX)\"" + @echo " export LIBQUIL_LIB_PATH=\"$(LIB_PREFIX)\"" + @echo "and run with:" +ifeq ($(OS), Darwin) + @echo " export DYLD_LIBRARY_PATH=\"$(LIB_PREFIX)\"" +else + @echo " export LD_LIBRARY_PATH=\"$(LIB_PREFIX)\"" +endif +endif + clean: rm -rf $(RUNTIME_DIR) build rm -f libquil.so libquil.dylib libquil.h libquil.c libquil.core libquil.py example diff --git a/README.md b/README.md index 1bb8dc2..54ff264 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,12 @@ On macOS, OpenBLAS supplies both `BLAS` and `LAPACK`. A script is provided to automate installation of the library. It will detect the host operating system and install the library to an appropriate location. A version identifier can be provided to install a particular version of the library. If no version is provided, the latest version of the library will be installed. -> Note: the installer script must be run as root; it installs the library into `/usr/local`. +> Note: by default the library is installed into `/usr/local`, which needs root. Pass +> `--prefix` to install somewhere you can write instead — see [Installing without +> root](#installing-without-root). -> Note: on macOS, root is also needed to mark the library files as trusted. The files themselves are not signed and macOS will, by default, flag them as insecure. +> Note: on macOS the installer also marks the library files as trusted. The files +> themselves are not signed and macOS will, by default, flag them as insecure. Run the following command @@ -74,6 +77,20 @@ curl https://raw.githubusercontent.com/rigetti/libquil/main/scripts/install.sh | It requires `apt` on Linux and Homebrew on macOS, and fails if neither is available. `install.sh --help` lists the options. +### Installing without root + +`--prefix` chooses where the library goes, and root is needed only when that +location is not writable: + +``` +curl https://raw.githubusercontent.com/rigetti/libquil/main/scripts/install.sh | bash -s -- --prefix "${HOME}/.local" +``` + +A prefix other than `/usr/local` is not on any default search path. The installer +finishes by printing the variables that make it usable — `LIBQUIL_SRC_PATH` and +`LIBQUIL_LIB_PATH` to build against it, and `DYLD_LIBRARY_PATH` (`LD_LIBRARY_PATH` +on Linux) to run. + ## Manual installation If you would like to manually install the library (for example in the case where you want to install the library to a non-standard location), find the appropriate version and operating system from the [releases page](https://github.com/rigetti/libquil/releases). Within the `.zip` archive you will find the library and header files that are required to use the library. Move these into your file system. @@ -129,6 +146,17 @@ Everything under `runtime/` is installed alongside the library, and relative to its own location. There is no initialization call to make: loading the library starts Lisp. +`make install` puts them in the layout consumers expect, in the same place the +release installer would: + +```bash +make install # /usr/local, needs root +make install PREFIX="${HOME}/.local" # anywhere you can write +``` + +For a prefix other than `/usr/local` it prints the `LIBQUIL_SRC_PATH`, +`LIBQUIL_LIB_PATH` and library-path variables to build and run against it. + See [REARCHITECTURE.md](REARCHITECTURE.md) for how this fits together and why. # C API Reference From 1a7f0fa48df0d958e07caac3ccb0a17074606cdb Mon Sep 17 00:00:00 2001 From: BatmanAoD Date: Wed, 26 Aug 2026 15:05:33 +0000 Subject: [PATCH 27/31] chore: prepare release 0.4.0-rc.1 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e28f76b..8ba731c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 0.4.0-rc.1 (2026-08-26) + +### Breaking Changes + +- build against modern sbcl-librarian + +### Features + +- install to any prefix, and need root only when the prefix demands it +- add `make install`, and document installing without root + +### Fixes + +- load OpenBLAS into the image before magicl picks a backend +- check for BLAS and LAPACK, and install without sudo when already root +- say what the SUDO_USER guard in install.sh actually checks + ## 0.4.0-rc.0 (2026-08-13) ### Breaking Changes From 39ae8466a2bbe4e91d9a1c72feb9d68106702090 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Wed, 26 Aug 2026 09:23:50 -0600 Subject: [PATCH 28/31] refactor: state the artifact's file list once The set of files that make up an installed libquil was written out in five places: both build.yml package steps, both release.yml zip commands, install.sh, the Makefile install recipe, and the README. Three of those predate this branch; the rearchitecture grew the list from four files to seven, and I added the last two. Adding a file meant remembering all of them, and release.yml failed quietly when you did not -- a new file was simply absent from the archive, surfacing only when someone installed it. install.sh now names the list once, resolving each file from either an unpacked release or a build tree, where the runtime sits in runtime/. That gives it a --from DIR mode, and `make install` becomes a delegation: install: all scripts/install.sh --from . --prefix "$(PREFIX)" so the layout, the prefix handling, the quarantine clearing and the post-install hint have one implementation rather than two. install.sh stays self-contained, which it has to be -- it is run as `curl ... | bash` with no repository present, so it cannot source a helper. release.yml zips the directory the build job assembled instead of listing its contents, which removes the silent-omission failure and the per-platform duplication with it. The README drops its copy of the list. It is documentation for someone installing libquil, not an inventory, and it went stale the moment the runtime moved. Two fixes fell out of the consolidation. --from no longer requires curl and unzip, which it does not use. And it no longer rejects unsupported architectures: that check exists to pick a release asset, so applying it to a local build turned away exactly the platforms building from source is for. Verified that `make install`, `--from` an unpacked release, and downloading 0.4.0-rc.1 all produce identical trees, that qcs-sdk-rust's libquil example compiles and simulates against the `make install` output, and that a missing file, `--from` with a version, and `--from` a non-directory each report what is wrong. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 7 +- Makefile | 32 ++------ README.md | 27 ++----- scripts/install.sh | 142 +++++++++++++++++++++++++--------- 4 files changed, 124 insertions(+), 84 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82a2c1d..3ba870c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,10 @@ jobs: - name: Archive linux artifacts run: | - cd artifacts/linux-amd64 && zip ../linux-amd64.zip libquil/libsbcl.so libquil/libsbcl_librarian.so libquil/libquil.so libquil/libquil.core libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h + # -r rather than a file list: the build job already assembled exactly + # the right directory, and naming files here means a new one is left out + # of the archive silently, surfacing only when someone installs it. + cd artifacts/linux-amd64 && zip -r ../linux-amd64.zip libquil - name: Download macos arm64 artifacts uses: actions/download-artifact@v4 @@ -47,7 +50,7 @@ jobs: - name: Archive macos arm64 artifacts run: | - cd artifacts/macos-arm64 && zip ../macos-arm64.zip libquil/libsbcl.so libquil/libsbcl_librarian.dylib libquil/libquil.dylib libquil/libquil.core libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h + cd artifacts/macos-arm64 && zip -r ../macos-arm64.zip libquil - name: List artifacts run: | diff --git a/Makefile b/Makefile index 3d70880..2cc2ce8 100644 --- a/Makefile +++ b/Makefile @@ -29,13 +29,9 @@ RUNTIME_DIR := runtime RUNTIME_LIB := $(RUNTIME_DIR)/libsbcl_librarian$(SHARED_SUFFIX) CORE := $(RUNTIME_DIR)/libquil.core -# Where `make install` puts things, matching the layout scripts/install.sh -# produces for a release: everything flat in lib/, headers in include/libquil. -# Override for a prefix you can write without sudo, e.g. -# make install PREFIX="$HOME/.local" +# Passed to scripts/install.sh by `make install`. Override for a prefix you can +# write without sudo, e.g. make install PREFIX="$HOME/.local" PREFIX ?= /usr/local -LIB_PREFIX := $(PREFIX)/lib -INCLUDE_PREFIX := $(PREFIX)/include/libquil SBCL_LIBRARIAN_DIR := $(shell $(SBCL) --noinform --non-interactive \ --eval '(require :asdf)' \ @@ -100,27 +96,11 @@ $(LIBQUIL_TARGET): libquil.c $(CORE) $(RUNTIME_LIB) -I. -I$(RUNTIME_DIR) -I"$(SBCL_LIBRARIAN_DIR)lib" \ -L$(RUNTIME_DIR) -lsbcl_librarian -# libquil.core has to land beside libsbcl_librarian, which is why everything goes -# in one directory rather than the runtime keeping its own: the runtime locates -# its core relative to its own path. +# Delegates to the release installer so the layout, the prefix handling and the +# post-install hint have one implementation. --from makes it install this build +# tree rather than downloading. install: all - mkdir -p $(LIB_PREFIX) $(INCLUDE_PREFIX) - cp libquil.h $(RUNTIME_DIR)/sbcl_librarian.h $(RUNTIME_DIR)/sbcl_librarian_err.h \ - $(INCLUDE_PREFIX) - cp $(LIBQUIL_TARGET) $(RUNTIME_LIB) $(RUNTIME_DIR)/libsbcl.so $(CORE) $(LIB_PREFIX) - @echo - @echo "Installed to $(PREFIX)." -ifneq ($(PREFIX),/usr/local) - @echo "That is not a default search path, so build against it with:" - @echo " export LIBQUIL_SRC_PATH=\"$(INCLUDE_PREFIX)\"" - @echo " export LIBQUIL_LIB_PATH=\"$(LIB_PREFIX)\"" - @echo "and run with:" -ifeq ($(OS), Darwin) - @echo " export DYLD_LIBRARY_PATH=\"$(LIB_PREFIX)\"" -else - @echo " export LD_LIBRARY_PATH=\"$(LIB_PREFIX)\"" -endif -endif + scripts/install.sh --from . --prefix "$(PREFIX)" clean: rm -rf $(RUNTIME_DIR) build diff --git a/README.md b/README.md index 54ff264..703b2f6 100644 --- a/README.md +++ b/README.md @@ -130,33 +130,18 @@ in your Quicklisp local-projects directory. Then: make ``` -This produces the library and the runtime it needs: - -``` -libquil.dylib the C bindings -libquil.h its header -runtime/libsbcl_librarian.dylib the SBCL runtime; brings up Lisp when loaded -runtime/libquil.core the Lisp image -runtime/libsbcl.so the linkable SBCL runtime -runtime/sbcl_librarian*.h runtime headers -``` - -Everything under `runtime/` is installed alongside the library, and -`libquil.core` must sit next to `libsbcl_librarian` — the runtime finds its core -relative to its own location. There is no initialization call to make: loading -the library starts Lisp. - -`make install` puts them in the layout consumers expect, in the same place the -release installer would: +To install what you just built, into `/usr/local` or anywhere you can write: ```bash -make install # /usr/local, needs root -make install PREFIX="${HOME}/.local" # anywhere you can write +make install # needs root +make install PREFIX="${HOME}/.local" # does not ``` -For a prefix other than `/usr/local` it prints the `LIBQUIL_SRC_PATH`, +For a prefix other than `/usr/local` this prints the `LIBQUIL_SRC_PATH`, `LIBQUIL_LIB_PATH` and library-path variables to build and run against it. +There is no initialization call to make: loading the library starts Lisp. + See [REARCHITECTURE.md](REARCHITECTURE.md) for how this fits together and why. # C API Reference diff --git a/scripts/install.sh b/scripts/install.sh index a4b03ca..e74caf1 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -12,7 +12,7 @@ err() { usage() { cat <<'EOF' -Usage: install.sh [--prefix DIR] [--install-deps] [VERSION] +Usage: install.sh [--prefix DIR] [--from DIR] [--install-deps] [VERSION] Installs libquil into DIR/lib and DIR/include/libquil. @@ -20,6 +20,10 @@ Installs libquil into DIR/lib and DIR/include/libquil. --prefix DIR install here instead of /usr/local. Root is needed only when the prefix is not writable, so a prefix under your home directory installs without sudo. Equivalent to LIBQUIL_PREFIX=DIR. + --from DIR install files already present in DIR instead of downloading a + release. Accepts either an unpacked release or a libquil build + tree, where the runtime lives in a runtime/ subdirectory. This + is what `make install` uses. --install-deps also install libquil's prerequisites with apt or Homebrew. Off by default: without it, missing prerequisites are reported and the install stops. Equivalent to LIBQUIL_INSTALL_DEPS=1. @@ -39,6 +43,7 @@ EOF LIBQUIL_RELEASE_REPO="${LIBQUIL_RELEASE_REPO:-rigetti/libquil}" LIBQUIL_INSTALL_DEPS="${LIBQUIL_INSTALL_DEPS:-0}" LIBQUIL_PREFIX="${LIBQUIL_PREFIX:-/usr/local}" +LIBQUIL_FROM="" LIBQUIL_VERSION="" while [[ $# -gt 0 ]] @@ -51,6 +56,12 @@ do shift ;; --prefix=*) LIBQUIL_PREFIX="${1#--prefix=}" ;; + --from) + [[ -n "${2-}" ]] || err "--from needs a directory" + LIBQUIL_FROM="${2}" + shift + ;; + --from=*) LIBQUIL_FROM="${1#--from=}" ;; -h | --help) usage; exit 0 ;; -*) usage >&2; err "" "Unknown option: ${1}" ;; *) @@ -65,6 +76,11 @@ do shift done +if [[ -n "${LIBQUIL_FROM}" && -n "${LIBQUIL_VERSION}" ]] +then + err "--from installs the files in that directory, so a version cannot also be given." +fi + if [[ -n "${LIBQUIL_VERSION}" ]] then LIBQUIL_URL_PREFIX="https://github.com/${LIBQUIL_RELEASE_REPO}/releases/download/v${LIBQUIL_VERSION}" @@ -82,8 +98,10 @@ then LIBQUIL_RELEASE_FILE="linux-amd64.zip" ;; *) - err "Unsupported CPU architecture for Linux: ${ARCH}. Only x86_64 is supported." \ - "You can build libquil from source; see https://github.com/rigetti/libquil#building-from-source" + # Only a problem when downloading: --from installs what is already built. + [[ -n "${LIBQUIL_FROM}" ]] || + err "Unsupported CPU architecture for Linux: ${ARCH}. Only x86_64 is supported." \ + "You can build libquil from source; see https://github.com/rigetti/libquil#building-from-source" ;; esac elif [[ "${OS}" == "Darwin" ]] @@ -93,9 +111,10 @@ then LIBQUIL_RELEASE_FILE="macos-arm64.zip" ;; *) - err "Unsupported CPU architecture for macOS: ${ARCH}. Only Apple Silicon (arm64) is supported." \ - "Intel macOS builds are no longer published. You can build libquil from source; see" \ - "https://github.com/rigetti/libquil#building-from-source" + [[ -n "${LIBQUIL_FROM}" ]] || + err "Unsupported CPU architecture for macOS: ${ARCH}. Only Apple Silicon (arm64)" \ + "has published builds. You can build libquil from source; see" \ + "https://github.com/rigetti/libquil#building-from-source" ;; esac # Windows shells report one of these. libquil publishes no Windows build, so there @@ -107,11 +126,14 @@ else err "Unsupported operating system: ${OS}. libquil supports Linux and macOS." fi -for tool in curl unzip -do - command -v "${tool}" >/dev/null 2>&1 || - err "This installer needs ${tool}, which was not found. Install it and try again." -done +if [[ -z "${LIBQUIL_FROM}" ]] +then + for tool in curl unzip + do + command -v "${tool}" >/dev/null 2>&1 || + err "This installer needs ${tool} to download a release, and it was not found." + done +fi LIBQUIL_LIB_PREFIX="${LIBQUIL_PREFIX}/lib" LIBQUIL_INCLUDE_PREFIX="${LIBQUIL_PREFIX}/include/libquil" @@ -276,40 +298,90 @@ then "them yourself: https://github.com/rigetti/libquil#requirements" fi -LIBQUIL_RELEASE_URL="${LIBQUIL_URL_PREFIX}/${LIBQUIL_RELEASE_FILE}" -LIBQUIL_TEMP_DIR="$(mktemp -d)" +# The files that make up an installed libquil, named once. libquil.core has to +# land beside libsbcl_librarian, so the libraries and the core share a directory: +# the runtime locates its core relative to its own path. +LIBQUIL_HEADERS=(libquil.h sbcl_librarian.h sbcl_librarian_err.h) +LIBQUIL_LIBS=( + "libquil.${LIBQUIL_LIB_SUFFIX}" + "libsbcl_librarian.${LIBQUIL_LIB_SUFFIX}" + # SBCL names its linkable runtime libsbcl.so on every platform, macOS included. + libsbcl.so + libquil.core +) + +# A release unpacks with everything in one directory; a build tree keeps the +# runtime in runtime/. Accept both, so `make install` can hand over the tree it +# just built without staging a copy first. +locate_artifact() { + local dir="${1}" name="${2}" candidate + for candidate in "${dir}/${name}" "${dir}/runtime/${name}" + do + if [[ -f "${candidate}" ]] + then + printf '%s' "${candidate}" + return 0 + fi + done + return 1 +} -trap 'rm -rf "${LIBQUIL_TEMP_DIR}"' EXIT -cd "${LIBQUIL_TEMP_DIR}" +install_artifacts() { + local source="${1}" name path + for name in "${LIBQUIL_HEADERS[@]}" + do + path="$(locate_artifact "${source}" "${name}")" || + err "${name} is missing from ${source}." + cp "${path}" "${LIBQUIL_INCLUDE_PREFIX}" + done + for name in "${LIBQUIL_LIBS[@]}" + do + path="$(locate_artifact "${source}" "${name}")" || + err "${name} is missing from ${source}." + cp "${path}" "${LIBQUIL_LIB_PREFIX}" + done +} -# -f so an HTTP error is a non-zero exit rather than an error page written to the -# archive: without it a bad version tag saves a "404: Not Found" body as the .zip -# and the failure only surfaces later, as a confusing unzip error. -curl -fL "${LIBQUIL_RELEASE_URL}" -o "${LIBQUIL_RELEASE_FILE}" || - err "Could not download ${LIBQUIL_RELEASE_URL}" \ - "Check that the requested version exists: https://github.com/${LIBQUIL_RELEASE_REPO}/releases" -unzip "${LIBQUIL_RELEASE_FILE}" +if [[ -n "${LIBQUIL_FROM}" ]] +then + [[ -d "${LIBQUIL_FROM}" ]] || err "--from ${LIBQUIL_FROM} is not a directory." + LIBQUIL_SOURCE_DIR="$(cd "${LIBQUIL_FROM}" && pwd)" +else + LIBQUIL_RELEASE_URL="${LIBQUIL_URL_PREFIX}/${LIBQUIL_RELEASE_FILE}" + LIBQUIL_TEMP_DIR="$(mktemp -d)" + + trap 'rm -rf "${LIBQUIL_TEMP_DIR}"' EXIT + cd "${LIBQUIL_TEMP_DIR}" + + # -f so an HTTP error is a non-zero exit rather than an error page written to the + # archive: without it a bad version tag saves a "404: Not Found" body as the .zip + # and the failure only surfaces later, as a confusing unzip error. + curl -fL "${LIBQUIL_RELEASE_URL}" -o "${LIBQUIL_RELEASE_FILE}" || + err "Could not download ${LIBQUIL_RELEASE_URL}" \ + "Check that the requested version exists: https://github.com/${LIBQUIL_RELEASE_REPO}/releases" + unzip "${LIBQUIL_RELEASE_FILE}" + LIBQUIL_SOURCE_DIR="${LIBQUIL_TEMP_DIR}/libquil" +fi -# libquil.core must land in the same directory as libsbcl_librarian: the runtime -# locates its core relative to its own path. -mkdir -p "${LIBQUIL_LIB_PREFIX}" "${LIBQUIL_INCLUDE_PREFIX}" -cp libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h "${LIBQUIL_INCLUDE_PREFIX}" -cp libquil/libquil.core libquil/libsbcl.so "${LIBQUIL_LIB_PREFIX}" +install_artifacts "${LIBQUIL_SOURCE_DIR}" if [[ -n "${IS_LINUX-}" ]] then - cp libquil/libquil.so libquil/libsbcl_librarian.so "${LIBQUIL_LIB_PREFIX}" - ldconfig + # Only meaningful for a system prefix, and only permitted as root. + if [[ "$(id -u)" -eq 0 ]] + then + ldconfig + fi else - cp libquil/libquil.dylib libquil/libsbcl_librarian.dylib "${LIBQUIL_LIB_PREFIX}" echo "Removing the quarantine attribute from the installed files." # This disables the "cannot open libquil.dylib from untrusted developer" dialog. # A better solution for this would be to properly codesign the files, but that - # is a non-trivial amount of work. - xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.dylib" - xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl_librarian.dylib" - xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libquil.core" - xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/libsbcl.so" + # is a non-trivial amount of work. A no-op on files that were never quarantined, + # such as a local build. + for name in "${LIBQUIL_LIBS[@]}" + do + xattr -r -d com.apple.quarantine "${LIBQUIL_LIB_PREFIX}/${name}" 2>/dev/null || true + done fi # A prefix other than /usr/local is not on any default search path, so tell the From 82d1833d30baf45d244774d99234fabbf6e112a4 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Wed, 26 Aug 2026 09:30:49 -0600 Subject: [PATCH 29/31] build: assemble the release artifact in a script, and stop shipping a build intermediate Both build jobs assembled artifacts/libquil with the same three commands, differing only in the shared-library suffix. That is now scripts/package-artifact.sh, alongside install-sbcl.sh and clone-lisp-deps.sh. It also fixes a regression from the previous commit. Those steps ended with `cp runtime/*`, which includes sbcl_librarian.c -- the generated C that produced libsbcl_librarian, of no use to a consumer. release.yml used to name the archive's contents explicitly, so the stray file was excluded by accident rather than by intent; switching to `zip -r` would have started shipping it. The script copies everything from runtime/ except the generated C, so what lands in the directory is what belongs in the archive. Verified against a real build tree: the packaged directory holds the same seven files 0.4.0-rc.1 shipped and no .c, installing from it with --from produces the same tree as every other path, and qcs-sdk-rust's libquil example compiles and simulates against the result. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 14 ++++--------- scripts/package-artifact.sh | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) create mode 100755 scripts/package-artifact.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8cd4a2..8a9bccd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -101,11 +101,8 @@ jobs: - name: Package files run: | - mkdir -p $GITHUB_WORKSPACE/artifacts/libquil - cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.so $GITHUB_WORKSPACE/artifacts/libquil - # The runtime directory holds libsbcl_librarian, the core it loads, - # libsbcl itself and the runtime headers; they must ship together. - cp $GITHUB_WORKSPACE/libquil/runtime/* $GITHUB_WORKSPACE/artifacts/libquil + "$GITHUB_WORKSPACE/libquil/scripts/package-artifact.sh" \ + "$GITHUB_WORKSPACE/libquil" "$GITHUB_WORKSPACE/artifacts" - name: Store artifact uses: actions/upload-artifact@v4 @@ -174,11 +171,8 @@ jobs: - name: Package files run: | - mkdir -p $GITHUB_WORKSPACE/artifacts/libquil - cp $GITHUB_WORKSPACE/libquil/libquil.h $GITHUB_WORKSPACE/libquil/libquil.dylib $GITHUB_WORKSPACE/artifacts/libquil - # The runtime directory holds libsbcl_librarian, the core it loads, - # libsbcl itself and the runtime headers; they must ship together. - cp $GITHUB_WORKSPACE/libquil/runtime/* $GITHUB_WORKSPACE/artifacts/libquil + "$GITHUB_WORKSPACE/libquil/scripts/package-artifact.sh" \ + "$GITHUB_WORKSPACE/libquil" "$GITHUB_WORKSPACE/artifacts" - name: Store artifact uses: actions/upload-artifact@v4 diff --git a/scripts/package-artifact.sh b/scripts/package-artifact.sh new file mode 100755 index 0000000..12327bf --- /dev/null +++ b/scripts/package-artifact.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Assemble the directory that becomes a release archive. Shared by the Linux and +# macOS build jobs, which differ only in the shared-library suffix. +# +# The result is zipped wholesale by the release workflow, so what lands here is +# exactly what ships: nothing filters it downstream. +# +# package-artifact.sh BUILD_DIR OUT_DIR +# +# BUILD_DIR is a libquil tree that has been built; OUT_DIR receives a libquil/ +# subdirectory, the prefix consumers unzip into place. + +set -euo pipefail + +BUILD_DIR="${1:?usage: package-artifact.sh BUILD_DIR OUT_DIR}" +OUT_DIR="${2:?usage: package-artifact.sh BUILD_DIR OUT_DIR}" + +if [[ "$(uname)" == "Darwin" ]] +then + SHARED_SUFFIX="dylib" +else + SHARED_SUFFIX="so" +fi + +DEST="${OUT_DIR}/libquil" +mkdir -p "${DEST}" + +cp "${BUILD_DIR}/libquil.h" "${BUILD_DIR}/libquil.${SHARED_SUFFIX}" "${DEST}" + +# runtime/ holds libsbcl_librarian, the core it loads, libsbcl itself and the +# runtime headers, which have to ship together -- libquil.core is located +# relative to the runtime's own path. +# +# Everything there ships except the generated C: sbcl_librarian.c is an +# intermediate that produced libsbcl_librarian and has no use to a consumer. +find "${BUILD_DIR}/runtime" -maxdepth 1 -type f ! -name '*.c' -exec cp {} "${DEST}" \; + +echo "Packaged into ${DEST}:" +ls -1 "${DEST}" | sed 's/^/ /' From dab314d632cbb54610ea2c0fbac6e4092ef6b8c0 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Wed, 26 Aug 2026 11:25:15 -0600 Subject: [PATCH 30/31] docs: remove stale docs, remove REARCHITECTURE doc (only needed for review), remove references to it --- Makefile | 3 +- PR-58-REVIEW-ANSWERS.md | 295 ---------------------------------------- README.md | 4 - REARCHITECTURE.md | 246 --------------------------------- pr-58-summary.md | 57 -------- src/build-image.lisp | 5 - 6 files changed, 1 insertion(+), 609 deletions(-) delete mode 100644 PR-58-REVIEW-ANSWERS.md delete mode 100644 REARCHITECTURE.md delete mode 100644 pr-58-summary.md diff --git a/Makefile b/Makefile index 2cc2ce8..096ac0f 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,7 @@ endif LIBQUIL_TARGET = libquil$(SHARED_SUFFIX) -# libquil is built against modern sbcl-librarian (see REARCHITECTURE.md), which -# splits the work in two: +# libquil uses sbcl-librarian, which splits the work in two: # # runtime/libsbcl_librarian$(SHARED_SUFFIX) the SBCL runtime; a constructor # initializes Lisp when it is loaded diff --git a/PR-58-REVIEW-ANSWERS.md b/PR-58-REVIEW-ANSWERS.md deleted file mode 100644 index d4eac1a..0000000 --- a/PR-58-REVIEW-ANSWERS.md +++ /dev/null @@ -1,295 +0,0 @@ -# Answers to review comments on #58 - -Companion to the code changes made in response to review. Comments that were -straightforward fixes are listed under **Applied**; everything that was a question, -an uncertainty, or a decision worth recording is answered below. - -## Questions answered - -### `(ql:quickload ...)` and Quicklisp (`.github/workflows/build.yml:66`) - -Yes — Quicklisp is the package manager, ASDF is the build system underneath it (see -above). `(ql:quickload :libquil)` fetches the dependency closure and compiles and -loads the system into the running image. - -### Should the deleted `(ql:quickload ...)` validation steps be restored? (`build.yml`) - -**Recommendation: no, and the naive restore would break the build.** The two lines -that were removed were: - -``` -sbcl --noinform --non-interactive --eval '(ql:quickload :sbcl-librarian)' -sbcl --dynamic-space-size 8192 --noinform --non-interactive --eval '(ql:quickload :libquil)' -``` - -Loading `:libquil` on its own now signals a *continuable* error. `src/quilc/api.lisp` -deliberately redefines the `quilc_compile_protoquil` alien callable that -sbcl-librarian's `define-api` generated a few forms earlier (it needs -`(* (* t))`, which sbcl-librarian cannot express). SBCL signals -`redefine alien callable` for that and offers a `continue` restart. Under -`--non-interactive` there is nothing to take the restart, so the load aborts. - -That is exactly why `src/build-image.lisp` wraps every load in -`with-alien-redefinition-allowed`. A bare `ql:quickload :libquil` outside that -handler is not a valid way to load libquil any more. - -The validation those lines provided is also now redundant: `make` runs -`src/build-image.lisp`, which quickloads `sbcl-librarian` and then `libquil`, so a -dependency-resolution failure fails the `Build libquil.so` step with the same error -it would have produced earlier — just one step later. - -If a separate pre-flight check is still wanted, it has to install the same handler, -e.g.: - -``` -sbcl --noinform --non-interactive \ - --eval '(handler-bind ((error (lambda (c) (let ((r (find-restart (quote continue) c))) (when (and r (search "redefine alien callable" (princ-to-string c))) (invoke-restart r)))))) (ql:quickload :libquil))' -``` - -which is the build step with extra steps. Left out. - -### Are `libffi` 7 and 8 both supported? (`build.yml:96`) - -They are genuinely incompatible, but nothing here mixes them, so dropping the -explicit `libffi7` is safe — and the old entry was already doing nothing. - -Debian/Ubuntu's `libffi7`/`libffi8` package names track the shared object's *soname* -(`libffi.so.7` / `libffi.so.8`), not an upstream libffi version — upstream has been on -3.x since 2008. `libtool-version` went `8:0:1` (3.3) to `9:0:1` (3.4), and libtool's -soname is `current - age`, giving 7 and 8 respectively. - -The bump was warranted. libffi commit -[`7855656`](https://github.com/libffi/libffi/commit/7855656148b96c7070ec362d2a73af840025a2b7) -("x86: Add indirect branch tracking support", 2020-02-21) says: - -> Trampoline must be enlarged to add ENDBR instruction unconditionally, which is NOP -> on non-CET processors. [...] Update library version for larger `FFI_TRAMPOLINE_SIZE`. - -`FFI_TRAMPOLINE_SIZE` went 24 -> 32 on x86-64 and 12 -> 16 on i386, and it sizes the -first member of the *public* `ffi_closure` struct in `ffi.h`: - -```c -typedef struct { - char tramp[FFI_TRAMPOLINE_SIZE]; - ffi_cif *cif; - void (*fun)(ffi_cif*,void*,void**,void*); - void *user_data; -} ffi_closure; -``` - -So a binary compiled against 3.3's header allocates a 24-byte trampoline and expects -`cif` at offset 24, while 3.4 writes 32 bytes and reads `cif` at offset 32 — an -out-of-bounds write and garbage field reads, not a clean load failure. The soname -bump is what prevents the loader from ever allowing that. - -None of which is reachable here. `libffi-dev` on ubuntu-22.04 resolves `libffi.so` to -`libffi.so.8`, so the build linked against 8 whether or not `libffi7` was in the apt -list — the explicit `libffi7` never influenced what was linked, it just installed a -second runtime nothing used. Naming a version is also what actually breaks the -workflow: `libffi7` does not exist on Ubuntu 24.04. The package list no longer names -one, and the explanatory comment has been shortened to one line. - -**Verified: libffi is a real runtime dependency.** I initially guessed it might be -vestigial, on the grounds that SBCL's `sb-alien` does not use libffi and `libquil.asd` -depends on `#:cffi` rather than `#:cffi-libffi`. That was wrong. Tracing a run of -`examples/quilc/version` against the released 0.4.0-rc.0 artifact with -`DYLD_PRINT_LIBRARIES`, and diffing against a trivial C program as a baseline, the -objects libquil adds are: - - libopenblasp-r0.3.34.dylib magicl's backend (confirms D7) - libgfortran / libquadmath / libgcc_s / libomp OpenBLAS's dependencies - libzstd.1.5.7.dylib core decompression - /usr/lib/libffi.dylib dlopen'd at runtime - libquil / libsbcl_librarian / libsbcl - -libffi does not appear in `otool -L` on any shipped object, which is why it looked -absent -- it is loaded dynamically once Lisp is up. So `libffi-dev` belongs in the -requirements, and it is needed at *run* time, not only to build. - -The same trace independently confirms D7: OpenBLAS is what magicl loads. Accelerate's -`libBLAS`/`libLAPACK` do appear, but they appear in the baseline too, so they are -system-framework noise rather than magicl's choice. - -**Follow-up this raises.** On Linux, CFFI resolves libffi under its unversioned name, -exactly as magicl does for BLAS/LAPACK -- so the argument for `install.sh`'s preflight -check applies to libffi too. I have not added it, because whether the existing check -would find it on Debian is untested: the unversioned `libffi.so` lives in the -multiarch directory (`/usr/lib/x86_64-linux-gnu`), which is not in -`LIBQUIL_SEARCH_DIRS`, and I have not confirmed that `ldconfig -p` lists the -unversioned dev symlink. Worth settling on a Linux box before extending the loop. - -### Is there a canonical way to search all dynamic-library paths on macOS? (`install.sh:72`) - -No, and the options each answer a slightly different question than the one the -installer needs: - -- **`man dyld`** — the default search is `/usr/local/lib` then `/usr/lib`, plus - `DYLD_LIBRARY_PATH` / `DYLD_FALLBACK_LIBRARY_PATH` if set. Those two directories - are now the base of the search list. -- **`DYLD_*` environment variables** — deliberately ignored. They apply to the - installing shell, not to whatever process later loads libquil, so honouring them - would make the check pass for installs the eventual consumer cannot see. -- **`otool -L`** — lists what a binary is *already* linked against. `magicl` `dlopen`s - BLAS/LAPACK at runtime rather than linking them, so they never appear. -- **`find / -name`** — exhaustive but slow enough to be hostile in an installer, and - a hit outside a loader path is not actually usable. -- **`locate`** — needs a database most machines have never built (as you found). -- **`pkg-config`** — answers "is there a `.pc` file for this?", which is not the - question. Homebrew's `openblas` is keg-only and its file is `openblas.pc`, not - `blas.pc`/`lapack.pc`, so `pkg-config --exists blas` fails on exactly the machine - the check most needs to pass. What matters here is whether an *unversioned* - `libblas.dylib` / `liblapack.dylib` exists somewhere the loader will find it, - which is a file-existence test. - -So the check stays a directory scan, but it is now built from a single -`LIBQUIL_SEARCH_DIRS` array, with the Homebrew prefixes discovered via -`brew --prefix` rather than hardcoded, and the Linux-only directories appended in -the Linux branch. That also removes the duplication noted at `install.sh:87`. - -`pkg-config` remains a requirement for the *build* (zstd), which is a separate -concern. - -### What about OpenBLAS? (`install.sh:94`) - -The loop checks `blas` and `lapack`, which is correct: `magicl` `dlopen`s -`libblas`/`liblapack` under those names. OpenBLAS satisfies both — on macOS via the -symlinks CI creates, on Linux via the distribution's alternatives mechanism — so -checking for `libopenblas` directly would reject a perfectly good reference-BLAS -install and miss nothing. - -The confusingly named `LIBQUIL_MISSING` has been renamed `LIBQUIL_MISSING_LIBS`. - -### Is `sbcl_librarian.core` generated or not? (`Makefile:66`) - -Both statements were true and the comment was the confusing part. `SBCL_LIBRARIAN_CORE_NAME` -is a *compile-time* setting on the runtime: it is the filename the runtime looks for -next to itself at load time, and it defaults to `sbcl_librarian.core`. libquil never -produces a file by that name — the build emits `libquil_core.core` and renames it to -`runtime/libquil.core` — so the runtime is compiled with the default overridden. The -comment now says that. - -## Items that looked like upstream bug reports - -Three claims in the diff read as latent upstream bugs. All three are real; two were -already reported and fixed upstream before we got here, and the third still stands. -None of them block this PR. - -1. **Homebrew reference LAPACK computes incorrect eigenvectors on arm64.** - (`build.yml`, `README.md`, `REARCHITECTURE.md` D7.) Real, reproduced, and fully - diagnosed upstream — but the diff's framing is too broad. This is not a property - of reference LAPACK on arm64. It is a **gfortran miscompile of LAPACK >= 3.12.1**: - gfortran >= 15.2 auto-vectorizes `TAU * DCONJG(WORK(I))` in the new `zlarf1f.f` - into aarch64 `fcmla` and drops the conjugation - ([Reference-LAPACK#1160](https://github.com/Reference-LAPACK/lapack/issues/1160), - [GCC PR122408](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=122408), fixed by - `c5fa3d4c8` on 2025-10-27). - - Confirmed end to end on an M-series machine: Homebrew's poured `lapack` 3.12.1 - bottle gives residuals of order 1 for `zgeev`/`zheev`/`zgesvd` at n>=3 while real - routines stay at 1e-16; `brew reinstall --build-from-source lapack` with the fixed - gfortran drops every one of them to ~1e-15. The shipped `_zlarf1f_` contains - `fcmla ... #90` where the rebuilt one has `#270`. - - Nothing was left to report to LAPACK or GCC — both are already fixed. The - remaining gap was that Homebrew had not bumped `revision` to rebuild the affected - bottle, and that is now filed as - **[Homebrew/homebrew-core#300084](https://github.com/Homebrew/homebrew-core/issues/300084)**, - with the standalone reproducer at - [BatmanAoD/lapack-arm64-homebrew-bugs](https://github.com/BatmanAoD/lapack-arm64-homebrew-bugs). - - D7 remains correct regardless, because its other half — Accelerate's missing - `zuncsd_` — is permanent. Only the *justification* in the code comment needs - narrowing. - -2. **macOS LAPACK raises division-by-zero traps.** (`src/quilc/compile.lisp:75`.) - Two separate things are going on, and they pull in opposite directions. - - **The mask is required regardless, and is not a workaround.** SBCL enables - floating-point traps by default — `(:overflow :invalid :divide-by-zero)` — while - C and Fortran callers normally run with them masked. LAPACK is written on the - assumption of the latter, so calling it from Lisp surfaces exceptions that no - other caller ever sees. `magicl:with-blapack` is just - `sb-int:with-float-traps-masked (:divide-by-zero :invalid)`, which restores the - environment LAPACK expects. Adding it to `compile-quil` to match - `compile-protoquil` is a plain correctness fix, and nothing about it belongs - upstream in magicl — magicl already provides exactly this. - - **But the exceptions themselves are recognised bugs, and are being fixed.** In - [Reference-LAPACK#1282](https://github.com/Reference-LAPACK/lapack/issues/1282) - Intel's OneMKL team reported crashes under `-ffpe-trap`/`-fpe0`, and - [PR #1283](https://github.com/Reference-LAPACK/lapack/pull/1283) was merged on - 2026-05-21 fixing them across ten files (`?gedmd`, `?gejsv`, `?gesvdx`, - `?gesvj`). The unguarded `OFL / ROOTSC` in `dgedmd` now reads - `IF ( ROOTSC .GT. ONE ) TBIG = OFL / ROOTSC`. - - So a spurious FPE in a LAPACK routine *is* reportable, and upstream will act on - it — though not unanimously: a maintainer noted on that issue that "generating - `Inf` and working with `Inf` is part of the game", while accepting the fix - anyway. - - **What this means here.** The mask stays, permanently — it is about SBCL's - defaults, not about LAPACK's bugs, and every LAPACK in the wild predates PR #1283 in any case. But if it is ever worth knowing *which* routine traps in - `compile-quil`, that is a reportable upstream bug of exactly the class just - fixed, not something to be resigned to. - - *(I previously wrote that these exceptions are "by design" and that there was - nothing to report. That was wrong: #1282 was closed as completed because the - fixes were merged.)* - -3. **`init(char* core)` is dead code on sbcl-librarian `main`.** - (`REARCHITECTURE.md`, "Why this is not a small change".) The generated function - declares its own `static int initialized`, shadowing the global that every - generated wrapper tests, so calling it leaves every function returning - `LISP_ERR_NOT_INITIALIZED`. Worth an upstream issue independent of libquil. - -The three sbcl-librarian fixes libquil depends on are already open as -[quil-lang/sbcl-librarian#91](https://github.com/quil-lang/sbcl-librarian/pull/91); -`build.yml`'s `SBCL_LIBRARIAN_REPO` comment now links to it. - -## Applied without further comment - -- `build.yml`: the `2022-04-01` Quicklisp-pin aside and the `sbcl-2.2.4` aside - removed; the `libffi7` rationale cut to one line; `SBCL_LIBRARIAN_REPO` now links - the tracking PR; `env.ImageOS` explained in both cache steps. -- `build.yml`: the duplicated SBCL-and-Quicklisp install and Lisp-dependency clone - are now `scripts/install-sbcl.sh` and `scripts/clone-lisp-deps.sh`, shared by both - jobs. The platform-specific part (apt bootstrap host vs. Homebrew host) stays in - the workflow; the macOS job passes `SBCL_REMOVE_HOST_CMD` to drop the Homebrew - `sbcl` once the build no longer needs it. -- `release.yml`: the "two macOS artifacts" comment reworded (there is only one macOS - artifact now); the knope-version rationale comment deleted. -- `examples/quilc/Makefile`: the note about the removed `-pagezero_size` deleted. - Confirmed: the Lisp image is mapped by `libsbcl_librarian`, not by the example - executable, so the example no longer needs its own zero page moved. -- `src/libquil.lisp`: comment suggestion applied. -- `install.sh`: the loader-cache comment moved onto the `ldconfig` branch it - describes; the platform test switched from `-z "${IS_LINUX-}"` to an explicit - `"${OS}" != "Darwin"`; search paths unified into one array; `sudo` handling - replaced with a hard root check; an `echo` added before the quarantine removal. -- `README.md`: says the `-dev` packages are what is needed; "any package manager" - narrowed to the two actually checked; install commands updated for the root - requirement (`curl … | sudo bash`). -- `README.md`: the C API reference still documented the removed - `libquil_error_t`/`libquil_error()`. Updated to `lisp_err_t` / - `get_error_message()` / `enable_backtrace()` per D2. (Not raised in review, but - wrong as it stood.) -- `REARCHITECTURE.md`: "upstream" in D4 disambiguated to sbcl-librarian; the - point-in-time `## Status` checklist removed. `## Open` was kept, retitled - `## Known limitations and follow-ups`, since it is the only record of the fork - dependency and the release ordering. - -## Open decisions for you - -- **`install.sh` now requires root**, per the two comments at `install.sh:111`. That - changes the documented invocation from `curl … | bash` to `curl … | sudo bash`, - which some people object to on principle. The alternative you floated — - re-exec via `sudo "${0}"` — does not work when the script is piped from `curl`, - since `$0` is `bash`. Say the word and I will restore the `${SUDO}` variable - instead. -- **`REARCHITECTURE.md` retained.** The comment at line 216 said "if we keep this - file". It is currently linked from `README.md` and from the PR description, so it - is kept; if you would rather it go, `README.md`'s "Building from source" section - and the D-numbered references in this document are the things to fix up. -- **The knope-version comment** at `release.yml:63` was deleted whole rather than - just its last line — the empty suggestion landed on the final line of a - three-line sentence, and deleting only that line left broken prose. diff --git a/README.md b/README.md index 703b2f6..f6d09b0 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,6 @@ make install PREFIX="${HOME}/.local" # does not For a prefix other than `/usr/local` this prints the `LIBQUIL_SRC_PATH`, `LIBQUIL_LIB_PATH` and library-path variables to build and run against it. -There is no initialization call to make: loading the library starts Lisp. - -See [REARCHITECTURE.md](REARCHITECTURE.md) for how this fits together and why. - # C API Reference These come from `sbcl_librarian_err.h`, which is installed alongside `libquil.h`. diff --git a/REARCHITECTURE.md b/REARCHITECTURE.md deleted file mode 100644 index b107060..0000000 --- a/REARCHITECTURE.md +++ /dev/null @@ -1,246 +0,0 @@ -# libquil on modern sbcl-librarian - -Working notes and decision record for the `sbcl-librarian-runtime` branch, which -moves libquil from the 2023-era sbcl-librarian (pinned at `04f7e39`) onto current -`main`. Written as the work happened; decisions are recorded with their reasons so -they can be revisited. - -## Why this is not a small change - -The obvious motivation was adopting sbcl-librarian's built-in error handling and -unblocking modern SBCL. Both turned out to be gated on a redesign that landed -upstream in `2a12bd0` (2024-01-23, "Add libsbcl_librarian CMake project + Conda -recipe"). - -Two facts establish that there is no cheaper path: - -1. The `sb-int::int-sap` -> `sb-sys:int-sap` fix that modern SBCL requires landed - in `b8dc014` (2024-05-22), *after* the redesign. There is no version window - offering modern-SBCL compatibility with the old architecture. -2. On current `main`, the generated `init(char* core)` is dead code: it declares - its own `static int initialized`, shadowing the global that every generated API - wrapper checks. Calling it leaves every function returning - `LISP_ERR_NOT_INITIALIZED`. - -## The old model vs the new one - -Old (what libquil does on `main`): - -- `build-bindings` + `build-core-and-die` produce `libquil.c/.h` and `libquil.core`. -- `libquil.dylib` is self-contained, statically embedding the SBCL runtime. -- The consumer calls `init("/path/to/libquil.core")` explicitly. - -Upstream's current model: - -- A **runtime** is built: `libsbcl_librarian`, which initializes Lisp from a - shared-library constructor at load time, locating a core by name next to itself, - with heap size from `SBCL_LIBRARIAN_HEAP_SIZE`. -- Each consumer is a **FASL library**: a small shared library holding the generated - C bindings, incbin-embedded FASL bundles for its ASDF system and dependencies, - and a constructor that loads those bundles into the running image. - `create-fasl-library-cmake-project` generates the whole CMake project. -- There is no per-consumer core, and no explicit init call. - -New (what this branch actually does): - -- The runtime, as above — but built with `SBCL_LIBRARIAN_CORE_NAME=libquil.core` - so it brings up libquil's image (D5). -- `libquil.dylib` is the generated C bindings only, linked against the runtime. -- The Lisp side stays a **core**, not FASL bundles, for the reasons in D3. -- No explicit init call: loading the runtime starts Lisp. - -## Decisions - -### D1: Follow upstream's current model rather than pinning a fork - -Rejected alternatives: (a) stay on `04f7e39` and carry the one-line `int-sap` -patch forever; (b) fork sbcl-librarian to keep `define-api :error-map` alive. - -Both work, but they permanently diverge from upstream and keep libquil's -hand-rolled error handling, which upstream has since absorbed. Since we are -re-architecting anyway, match upstream so future updates are merges rather than -rebases. - -Note (a) remains the correct *fallback* and is preserved on the -`build-apple-aarch64` branch, which builds and passes 22/22 today. - -### D2: Adopt sbcl-librarian's built-in error handling; accept the ABI break - -libquil's `*last-error*` / `libquil_error()` / `libquil_error_t` are replaced by -upstream's `get_error_message()` / `lisp_err_t`. This is a breaking change to -libquil's C ABI. - -Accepted because libquil is pre-1.0, the only known consumer is libquil-sys (which -we control and update in lockstep), and the replacement is strictly better: it -distinguishes failure from bug from fatal, and supports backtraces. - -`quilc_compile_protoquil`'s hand-written `define-alien-callable` (which exists -because sbcl-librarian cannot express `(:pointer :pointer)`) now records into -`sbcl-librarian::*error-message*` so it reports through the same channel. - -### D3: Ship a core, not FASL bundles - -Upstream's consumer path is `create-fasl-library-cmake-project`, which embeds a -FASL bundle per ASDF system into the library and loads them into the image at -startup. libquil was built that way first, and it worked -- but it is not -distributable: - -- FASL bundles re-run load-time code on every startup. `cl-quil` computes - `*default-standard-gates-file*` with `asdf:system-relative-pathname` and calls - `initialize-standard-gates` inside an `eval-when (:load-toplevel)`, so every - process that loaded libquil re-read `stdgates.quil` **from quilc's source tree**, - at the path baked in when the bundles were built. An installed artifact on a - machine without quilc's sources would fail. -- The same load-time chatter (`; loading standard gates from ...`) went to stdout - of every process that linked libquil, which among other things broke - `cargo nextest`'s test enumeration. - -Saving a core evaluates all of that once, at build time. So libquil keeps a core -- -what it always shipped -- and gets the modern runtime, init and error handling -around it. - -The core is loaded by the runtime rather than by an explicit `init(core)` call, -which is why D6 exists. - -### D4: Build the runtime manually rather than via its CMake project - -sbcl-librarian's own `lib/CMakeLists.txt` invokes the generator with `sbcl --script`, which -skips `~/.sbclrc` and therefore Quicklisp — so `generate-bindings.lisp`'s -`(asdf:load-system :swank)` fails with `Component :SWANK not found`. - -Rather than patch sbcl-librarian's CMake, `src/build-image.lisp` defines the runtime's -aggregate library itself and emits `runtime/sbcl_librarian.c` alongside libquil's -own bindings. One image therefore produces everything: libquil's bindings, the -runtime's bindings, and the core that backs both. That also avoids the swank -dependency and the second, discarded core that upstream's generator would build. - -### D5: Point the runtime at libquil's core - -`entry_point.c` hardcoded `sbcl_librarian.core`, looked up next to the runtime -library. Because libquil ships its own core (D3), the runtime has to load that -instead. Upstream now takes a compile-time `SBCL_LIBRARIAN_CORE_NAME`, defaulting -to the old name, and libquil builds the runtime with `-DSBCL_LIBRARIAN_CORE_NAME='"libquil.core"'`. - -Naming libquil's core `sbcl_librarian.core` would have avoided the patch, but two -sbcl-librarian consumers installed into the same directory would then overwrite -each other's core. - -### D6: Map ordinary errors to `LISP_ERR_FAILURE`, and handle `T` - -The stock `default-error-map` classifies any `cl:error` as `LISP_ERR_BUG`, prefixes -the message with "Internal lisp bug:" and attaches a backtrace. Nearly everything -libquil signals is user error -- malformed Quil, an unknown memory region -- so -libquil redefines the map for its own callables. `wrap-error-handling` is consulted -when a callable is compiled, so this affects only libquil's; the runtime's own APIs -keep upstream behaviour. - -The handler binds `T`, not `cl:error`. cl-quil signals conditions that are not -subtypes of `error` -- `invalid-instruction-condition` has no supertype at all -- -and with a `cl:error` handler those escape into the debugger and hang the calling -process. libquil's original error map bound `T` for the same reason. - -### D7: Let magicl choose the BLAS/LAPACK backend, and fail loudly if it chooses badly - -**Superseded.** libquil originally forced the choice itself, and no longer does; the -fix moved upstream, which is where it belonged. - -The problem was that magicl picked a backend on Darwin by searching Homebrew's -reference LAPACK first, then a bare `liblapack.dylib`, with OpenBLAS not in the list -at all. On arm64 macOS both candidates are wrong, in different ways: - -- Homebrew's reference LAPACK, as bottled, returns incorrect eigenvectors for - complex input at n >= 3, which surfaces as `Could not find diagonalizer for - matrix ... after 16 attempts`. Not a defect in reference LAPACK: a gfortran - miscompile of `zlarf1f.f`, new in LAPACK 3.12.1 (GCC PR122408, - Reference-LAPACK#1160, both fixed; Homebrew/homebrew-core#300084 tracks the - un-rebuilt bottle). -- A bare `liblapack.dylib` resolves to Accelerate's legacy interface, which is - LAPACK 3.2.1 and lacks routines quilc calls: `The alien function "zuncsd_" is - undefined`. That one is permanent, not a packaging accident. - -So `src/build-image.lisp` used to `load-shared-object` OpenBLAS before magicl could -look, and CI additionally uninstalled Homebrew's `lapack` and symlinked OpenBLAS -over `liblapack.dylib`. Both were workarounds for magicl having no way to express -"use OpenBLAS". - -magicl now searches for OpenBLAS by name, accepts `MAGICL_LAPACK_PATH`, and checks -at load time that the backend it selected is complete and computes correctly -(quil-lang/magicl#221, PR #222; libquil tracks `rigetti/magicl` at -`backend-selection-and-validation` until that is merged and released). The preload -and the symlinks are gone. A bad backend now fails the build with a message naming -the library, instead of yielding an artifact that is quietly wrong. - -What has not changed is the property that made this a build-time decision: SBCL -records loaded shared objects in the core and reopens them at startup, so whatever -magicl selects during the build is baked into the artifact rather than left to -whatever the loader finds on the user's machine. Which is also why the installed -artifact still needs OpenBLAS present at the path recorded at build time — see -`install.sh`'s preflight check. - -### D8: Keep shipping the SBCL runtime as `libsbcl.so`, even on macOS - -SBCL's `make-shared-library.sh` emits `libsbcl.so` on every platform, and that -string becomes the install name recorded in `libsbcl_librarian.dylib`. Renaming the -file to `.dylib` breaks loading unless the install name is rewritten too. Upstream's -own example Makefile notes the same quirk. We ship `libsbcl.so` and leave the name -alone. - -## What `make` produces - - libquil.dylib generated C bindings (~40 KB) - libquil.h its header - runtime/libsbcl_librarian.dylib SBCL runtime; initializes Lisp on load - runtime/libquil.core the Lisp image - runtime/libsbcl.so the linkable SBCL runtime - runtime/sbcl_librarian.h runtime API (get_error_message, handles) - runtime/sbcl_librarian_err.h lisp_err_t and the fatal-error plumbing - -All of `runtime/` must be installed together, and `libquil.core` must sit beside -`libsbcl_librarian`, since the runtime finds its core relative to its own location. - -## Prerequisite: SBCL with a linkable runtime - -Homebrew's bottle does not ship one, so SBCL must come from source: - - sh make.sh --with-sb-linkable-runtime && sh make-shared-library.sh && sh install.sh - -## Upstream sbcl-librarian changes this depends on - -Three fixes, on the `fix-secondary-system-bundles` branch: - -1. **Secondary systems lost their FASL bundles.** The output-translation pattern was - built from the flattened bundle name (`magicl--core--system.fasl`), but ASDF - writes `magicl/core--system.fasl`, turning the slash into a directory. Bundles - for `magicl/core`, `magicl/ext*` and `cl-quil/frontend` were silently left in the - ASDF cache and the generated CMake project referenced files that did not exist. - (Found while libquil was still on the FASL path; kept because it is a real bug.) -2. **`liblibquil`.** The generated CMake project cleared the library prefix only on - Windows, so a library whose name starts with `lib` built as `liblibquil.so` on - Unix. Upstream's own libcalc example links `-lcalc`, which only resolves with the - prefix cleared. -3. **`SBCL_LIBRARIAN_CORE_NAME`** (D5). - -## Known limitations and follow-ups - -**CI depends on a fork branch.** `build.yml` clones `$SBCL_LIBRARIAN_REPO` at -`$SBCL_LIBRARIAN_REF`, currently `rigetti/sbcl-librarian` at -`fix-secondary-system-bundles`. Move it back to `quil-lang/sbcl-librarian` once the -three fixes are upstream. - -**CI's SBCL bump is unverified.** Both jobs now build `sbcl-$SBCL_VERSION` (2.6.7) -with `--with-sb-linkable-runtime`, replacing the 2.2.4 build that failed in -`make-host-1`. That combination is proven locally on arm64 macOS but has not run on -the Linux job. - -**Consumers with a non-/usr/local prefix** need `LIBQUIL_LIB_PATH` as well as -`LIBQUIL_SRC_PATH`, since headers and libraries then live in different directories. - -**Upstreaming.** The three sbcl-librarian fixes are worth PRs regardless of what -libquil does; the FASL-bundle one is a plain bug, and the library-prefix one breaks -upstream's own libcalc example on Unix. They are open as -quil-lang/sbcl-librarian#91. - -**Release ordering.** libquil-sys cannot build against a released libquil older -than this change, so the two have to be released together: cut a libquil -prerelease (`knope prerelease`, added for this reason), point libquil-sys's -`LIBQUIL_VERSION` at it, then release both for real. diff --git a/pr-58-summary.md b/pr-58-summary.md deleted file mode 100644 index b2b2f2b..0000000 --- a/pr-58-summary.md +++ /dev/null @@ -1,57 +0,0 @@ -# PR #58 — status - -Scratch notes for the author. The reasoning behind each change lives in its commit -message and in comments at the point of use; this file only tracks what is left. - -## Still needing a decision - -- **`REARCHITECTURE.md` retained**, against "if we keep this file". It is linked - from `README.md` and the PR description. Dropping it means fixing those two - references. - -## Draft comment to post - ---- - -Everything raised in review is addressed. Rather than reply inline to all 32 open -threads, a summary of the substantive ones: - -**Restoring the `(ql:quickload …)` pre-flight steps** — I tried this and it breaks -the build. `src/quilc/api.lisp` deliberately redefines the `quilc_compile_protoquil` -alien callable that `define-api` generated (it needs `(* (* t))`, which -sbcl-librarian cannot express), so SBCL signals a *continuable* `redefine alien -callable` error. Under `--non-interactive` nothing takes the restart and the load -aborts — which is why `src/build-image.lisp` wraps its loads in -`with-alien-redefinition-allowed`. The validation is also redundant now: `make` runs -that file, so a dependency failure surfaces one step later with the same error. - -**`libffi7` vs `libffi8`** — the two really are incompatible (the soname bumped -because `FFI_TRAMPOLINE_SIZE` grew, and it sizes the first member of the public -`ffi_closure` struct), but nothing here mixes them: `libffi-dev` on 22.04 already -resolved to `libffi.so.8`, so the explicit `libffi7` only installed a second runtime -nothing used. Also confirmed libffi *is* a real runtime dependency — it is -`dlopen`'d once Lisp is up, which is why it never shows in `otool -L`. - -**Three workarounds are gone entirely**, because the fixes went upstream instead: -the vendored `PROCESS-PROTOQUIL` (quil-lang/quilc#933), the OpenBLAS preload and CI -symlinks (quil-lang/magicl#222), and — unrelated to libquil but found along the way -— a dead `init()` in sbcl-librarian (added to quil-lang/sbcl-librarian#91). - -CI is green on both platforms, including the C example tests for quilc and qvm and -a from-source SBCL build on macOS. - -**What gates this PR** is not review but four dependency pins, each tagged with a -`TODO(...)` where it is pinned: - -| dependency | tracked by | blocked on | -|---|---|---| -| sbcl-librarian | #60 | quil-lang/sbcl-librarian#91 merge + release | -| quilc | #61 | quil-lang/quilc#933 merge | -| magicl | #62 | quil-lang/magicl#222 merge + release | -| qvm | quil-lang/qvm#330 | any release newer than 1.17.2 (2021) | - -This cannot be released against stable dependencies until those resolve. - -Two things I would still like a call on: `install.sh` now requires root, which makes -the documented invocation `curl … | sudo bash`; and `REARCHITECTURE.md` is still -here, which the "if we keep this file" comment left open. diff --git a/src/build-image.lisp b/src/build-image.lisp index 1b6e018..5dd567c 100644 --- a/src/build-image.lisp +++ b/src/build-image.lisp @@ -19,11 +19,6 @@ ;; computes incorrect eigenvectors, so a bad one fails this build with a message ;; naming the library rather than producing an artifact that is quietly wrong. ;; -;; libquil used to force the choice here, by loading OpenBLAS before magicl could -;; look. That is no longer necessary; see REARCHITECTURE.md D7. If magicl is ever -;; pinned back to a version without the search entry, MAGICL_LAPACK_PATH set at -;; build time does the same job. -;; ;; Whatever magicl loads is recorded in the core by SBCL and reopened at startup, ;; so the choice is still baked into the artifact rather than left to the loader. From f99bf8c7ef77b38c26b7ef56b7ea5ba2a37ce148 Mon Sep 17 00:00:00 2001 From: Kyle J Strand Date: Wed, 26 Aug 2026 13:40:54 -0600 Subject: [PATCH 31/31] slightly simplify clone-lisp-deps --- scripts/clone-lisp-deps.sh | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/scripts/clone-lisp-deps.sh b/scripts/clone-lisp-deps.sh index 0396334..077efc2 100755 --- a/scripts/clone-lisp-deps.sh +++ b/scripts/clone-lisp-deps.sh @@ -1,11 +1,6 @@ #!/usr/bin/env bash # Clone the Lisp systems libquil is built against into GITHUB_WORKSPACE, which the # build adds to ql:*local-project-directories*. Shared by the Linux and macOS jobs. -# -# Required environment: -# GITHUB_WORKSPACE where the systems are cloned -# SBCL_LIBRARIAN_REPO sbcl-librarian remote to clone -# SBCL_LIBRARIAN_REF ref to check out in it set -euo pipefail @@ -22,15 +17,11 @@ set -euo pipefail # tag once there is one. QVM_REF="1c4ca60d7912219063e3969be92ec45e12a0798f" # TODO(quil-lang/qvm#330) -git clone "${QUILC_REPO}" "${GITHUB_WORKSPACE}/quilc" -git -C "${GITHUB_WORKSPACE}/quilc" checkout "${QUILC_REF}" +git clone "${QUILC_REPO}" "${GITHUB_WORKSPACE}/quilc" --revision "${QUILC_REF}" -git clone https://github.com/quil-lang/qvm.git "${GITHUB_WORKSPACE}/qvm" -git -C "${GITHUB_WORKSPACE}/qvm" checkout "${QVM_REF}" +git clone https://github.com/quil-lang/qvm.git "${GITHUB_WORKSPACE}/qvm" --revision "${QVM_REF}" -git clone "${MAGICL_REPO}" "${GITHUB_WORKSPACE}/magicl" -git -C "${GITHUB_WORKSPACE}/magicl" checkout "${MAGICL_REF}" +git clone "${MAGICL_REPO}" "${GITHUB_WORKSPACE}/magicl" --revision "${MAGICL_REF}" git clone https://github.com/stylewarning/cl-permutation "${GITHUB_WORKSPACE}/cl-permutation" -git clone "${SBCL_LIBRARIAN_REPO}" "${GITHUB_WORKSPACE}/sbcl-librarian" -git -C "${GITHUB_WORKSPACE}/sbcl-librarian" checkout "${SBCL_LIBRARIAN_REF}" +git clone "${SBCL_LIBRARIAN_REPO}" "${GITHUB_WORKSPACE}/sbcl-librarian" --revision "${SBCL_LIBRARIAN_REF}"