diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 714e388..bb3ed08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,22 +4,43 @@ on: pull_request: workflow_call: +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. + 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: 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 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 @@ -27,73 +48,93 @@ 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 - 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 - sudo apt remove -y sbcl - sudo sh install.sh + 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 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/2022-04-01/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 - 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 + git clone $SBCL_LIBRARIAN_REPO $GITHUB_WORKSPACE/sbcl-librarian && cd $GITHUB_WORKSPACE/sbcl-librarian && git checkout $SBCL_LIBRARIAN_REF - 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: | 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@v3 + uses: actions/upload-artifact@v4 with: name: linux-amd64 - 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: - name: Build libquil.dylib + build-macos-arm: + name: Build Apple-Silicon libquil.dylib runs-on: macos-latest 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 +142,7 @@ jobs: /usr/local/bin/sbcl ~/quicklisp ~/.sbclrc - key: ${{ runner.os }}-build-${{ hashFiles('**/macos-versions' )}} + key: ${{ env.ImageOS }}-${{ runner.arch }}-build-${{ hashFiles('**/versions') }} - uses: Homebrew/actions/setup-homebrew@master name: Setup homebrew @@ -110,37 +151,52 @@ jobs: 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 + # 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 - 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))' + 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 - 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 + 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: | 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 @@ -149,15 +205,22 @@ 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@v3 + uses: actions/upload-artifact@v4 with: - name: macos - path: ${{ github.workspace }}/artifacts/* + name: macos-arm64 + # 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 1dbdde4..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: @@ -25,40 +20,61 @@ 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 + # 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. - name: Download linux artifacts - uses: actions/download-artifact@v3 + 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/libsbcl_librarian.so libquil/libquil.so libquil/libquil.core libquil/libquil.h libquil/sbcl_librarian.h libquil/sbcl_librarian_err.h - - name: Download macos artifacts - uses: actions/download-artifact@v3 + - 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/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: | 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 + 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/.gitignore b/.gitignore index 9e5309c..9fad6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ libquil.c libquil.core libquil.h libquil.so +libquil.dylib +libquil.py +build/ +runtime/ diff --git a/Makefile b/Makefile index c2d9223..a66d69c 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,94 @@ -.PHONY: all clean +.PHONY: all clean runtime + +OS := $(shell uname -s) + +SBCL ?= sbcl +CC ?= cc -OS:=$(shell uname -s) 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 +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 +# +# 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) $(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 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) + +# 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 (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 " 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 + 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) -fPIC -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.core libquil.c - gcc $(CCFLAGS) -o $@ libquil.c -lsbcl +$(LIBQUIL_TARGET): libquil.c $(CORE) $(RUNTIME_LIB) + $(CC) $(SHARED_FLAGS) -fPIC -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/README.md b/README.md index 703950c..2a58c10 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ We provide pre-built binaries for the following systems - Linux x64 (tested specifically on Ubuntu) -- macOS x64 +- macOS aarch64 (Apple Silicon) -Note that ARM is not yet supported on any of the above. +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 @@ -19,20 +21,29 @@ 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 libffi7 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 ``` -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. @@ -59,6 +70,55 @@ 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 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 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.so +``` + +The Lisp dependencies (`quilc`, `qvm`, `magicl`, `sbcl-librarian`) are expected +in your Quicklisp local-projects directory. Then: + +```bash +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 +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/REARCHITECTURE.md b/REARCHITECTURE.md new file mode 100644 index 0000000..f0e8e30 --- /dev/null +++ b/REARCHITECTURE.md @@ -0,0 +1,240 @@ +# 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 + +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: 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 +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 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/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 9012dcf..d26af0c 100755 --- a/install.sh +++ b/install.sh @@ -7,52 +7,140 @@ err() { exit 1 } -if [[ "$(uname -p)" =~ "arm" ]]; then - err "Unsupported CPU architecture: $(uname -p)." -fi +# 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)" +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 +# 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}" + 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 ldconfig + ${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 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" diff --git a/src/build-image.lisp b/src/build-image.lisp index 95833a5..9a4039e 100644 --- a/src/build-image.lisp +++ b/src/build-image.lisp @@ -1,15 +1,116 @@ (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))) + +;; 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)))) + +;; 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) (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 0941969..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 */") @@ -135,7 +134,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 @@ -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/quilc/compile.lisp b/src/quilc/compile.lisp index 76f1196..813d8ed 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)) @@ -75,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) 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 */") 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 diff --git a/versions b/versions index 17a62fd..a147726 100644 --- a/versions +++ b/versions @@ -1,5 +1,10 @@ +# 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 +# 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