🚀

ECLでCommon Lispに入門する2 - EmacsとSLYとテスト

に公開

はじめに

前回の続きです。

Emacs拡張のSLYを使うことで、コンパイルや実行、テストが簡単にできるようになるので、SLYを使った開発手順と、テストの方法についてまとめます。

プロジェクトの雛形を作る

プロジェクトの雛形を作るためのスクリプトを用意したので、これをベースに進めます。
以下を適当な名前(psetup.sh)で、~/.local/binなどに保存します。

#!/bin/sh

PROJECT="$1"

if [ -z "$PROJECT" ]; then
  echo "Usage: $0 <project-name>"
  exit 1
fi

if [ -e "$PROJECT" ]; then
  echo "Error: Directory '$PROJECT' already exists. Aborting."
  exit 1
fi

mkdir -p "$PROJECT"/{src,tests,.deps}

# ASDファイル
cat > "$PROJECT/$PROJECT.asd" <<EOF
(defsystem "$PROJECT"
           :version "0.1.0"
           :author "Your Name"
           :license "GPL"
           :description "A Common Lisp project."
           :depends-on ()
           :pathname "src"
           :serial t
           :components ((:file "package")
                        (:file "main"))
           :in-order-to ((test-op (test-op "$PROJECT/test"))))

(defsystem "$PROJECT/test"
           :depends-on ("$PROJECT" "fiveam")
           :pathname "tests"
           :serial t
           :components ((:file "main-test"))
           :perform (test-op (o s)
                             (uiop:symbol-call :fiveam '#:run!
                                               (uiop:find-symbol* '#:main-test-suite
                                                                  :main-tests))))
EOF

# パッケージファイル
cat > "$PROJECT/src/package.lisp" <<EOF
(defpackage :$PROJECT
  (:use :cl)
  (:export :main))
EOF

# メインファイル
cat > "$PROJECT/src/main.lisp" <<EOF
(in-package :$PROJECT)

(defun main ()
  (format t "Hello from $PROJECT!~%"))
EOF

# テストファイル
cat > "$PROJECT/tests/main-test.lisp" <<EOF
(defpackage :main-tests
            (:use :cl
                  :fiveam))
(in-package :main-tests)

(def-suite main-test-suite)
(in-suite main-test-suite)

(test test-fn
  ;; テストを書きます
  )
EOF

mkdir -p "$PROJECT/.deps/cache" # コンパイル済みキャッシュ

QL_DIR="$PROJECT/.deps/systems"
mkdir -p "$QL_DIR"

cd "$PROJECT"

echo "Downloading Quicklisp"
curl -O https://beta.quicklisp.org/quicklisp.lisp

echo "Installing Quicklisp to $QL_DIR"

LISP="ecl" # 処理系でここ変更する

"$LISP" --load quicklisp.lisp \
        --eval "(quicklisp-quickstart:install :path \".deps/systems\")" \
        --eval "(quit)"

rm quicklisp.lisp

echo "Quicklisp installed to $QL_DIR"

echo "Project '$PROJECT' created successfully."

続いて、Quicklisp経由の操作をシェルから簡易的に行うため、以下のシェルスクリプトをql.shのような名前で~/.local/binに保存します。

#!/bin/sh

COMMAND="$1"
SYSTEM="$2"

QUICKLISP_SETUP="$(pwd)/.deps/systems/setup.lisp"

if [ ! -f "$QUICKLISP_SETUP" ]; then
    echo "Quicklisp setup.lisp not found at $QUICKLISP_SETUP"
    exit 1
fi

if [ -z "$COMMAND" ] || [ -z "$SYSTEM" ]; then
    echo "Usage: $0 {install|uninstall|search} <system-name>"
    exit 1
fi

case "$COMMAND" in
    install)
        ACTION="(ql:quickload :$SYSTEM)"
        ;;
    uninstall)
        ACTION="(ql:uninstall :$SYSTEM)"
        ;;
    search)
        ACTION="(progn
      (format t \"~%Results for ~A:~%~%\" \"$SYSTEM\")
      (loop for name in (ql:system-apropos \"$SYSTEM\") do (format t \"~A~%\" name)))"
        ;;
    *)
        echo "Unknown command: $COMMAND"
        exit 1
        ;;
esac

ecl --eval "(load \"$QUICKLISP_SETUP\")" \
    --eval "$ACTION" \
    --eval "(quit)"

ここまで完了したら、

$ cd ~/Project
$ psetup.sh picopico

のような感じで実行すると、Quicklispのセットアップも行うため少し時間がかかりますが、プロジェクトディレクトリとプロジェクトが作成されます。プロジェクトは以下のような構成になります。

$ cd picopico
$ tree
.
├── picopico.asd
├── src
│   ├── main.lisp
│   └── package.lisp
└── tests
    └── main-test.lisp

3 directories, 4 files

picopico/.depsには、Quicklisp本体や、インストールしたシステム、コンパイル済みのキャッシュが格納されます。

EmacsでSLYを設定する

~/.config/emacs.d/init.elに以下のコードを記述します。

(use-package sly
  :straight t
  :init
  (setq inferior-lisp-program "ecl") ; 使用する処理系を指定
  :hook (lisp-mode . sly-editing-mode)
  :config
  (setq sly-contribs '(sly-mrepl))
  (setq sly-lisp-implementations
        '((ecl ("ecl"))
          ;; 複数処理系を切り替えるならここに追加
          )))

SLYについて

SLY User Manualにマニュアルがあります。Common Lispのインタラクティブな開発環境(IDE)をEmacsに追加する拡張です。

EmacsでSLYを起動し、プロジェクトを開く

作成したpicopicoというプロジェクトをEmacsで開きます。

$ cd ~/Project/picopico
$ emacs picopico.asd &

起動後、M-x sly を実行するとSLYが起動します。初回はSLY自体のコンパイルが行われるため、少し時間がかかります。コンパイルキャッシュはASDFの設定に基づくため、前回記載のASDFの設定を.eclrcに記述している場合は、picopico/.deps/cache以下に生成されます。
無事起動すると、開いたasdファイルに加え、SLYのREPL環境が分割されて表示されます。

*Ibuffer*を確認すると、以下のように複数のSLY関連のバッファを確認できます。

  • *sly-mrepl for ecl*
  • *sly-inferior-lisp for ecl*
  • *sly-events for ecl*

このうち、sly-inferior-lispは標準エラーなどの出力、sly-eventsはSLYとLISP間のイベント周りの出力となり、開発で常用するのはsly-mreplバッファになります。

ソースコードのコンパイル

全体のコンパイル

まずは今あるpackage.lispmain.lispをコンパイルします。asdf:load-systemをmreplで実行すると、プロジェクト全体でコンパイルが実行されます。mreplへの移動は、C-x oでフレームを移動しても良いですが、C-c C-zで移動できます。

CL-USER> (asdf:load-system :picopico)
;;;
;;; Compiling /home/hiro/Projects/picopico/src/package.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/package.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;
T

システム名と呼び出す関数を指定し、関数を実行できることを確認します。

CL-USER> (picopico:main)
Hello from picopico!
NIL

ファイル単位のコンパイル

次は、ファイル単位のコンパイルを試します。C-x C-fsrc/main.lispを開きます。

(in-package :picopico)

(defun main ()
  (format t "Hello from picopico!~%")
  (format t "Goodbye~%"))

Goodbyeという文字を表示する処理を追加し、C-c C-kを実行すると、そのファイルをコンパイルします。正常にコンパイルされると、以下のような表示がmrepl側に表示されます。

;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;

以下のように、わざと間違えた関数呼び出しを行った場合などはコンパイルエラーが発生します。

(defun main ()
  (format t "Hello from picopico!~%")
  (format "Goodbye~%"))

コンパイルエラーは、sly-compilationというバッファに以下のように出力されます。FORMAT関数の引数がおかしいということがエラー内容に表示されています。

cd /home/hiro/Projects/picopico/src/
3 compiler notes:

main.lisp:3:1:
  style-warning: 
    Warning:
      in file main.lisp, position 22
      at (DEFUN MAIN ...)
      ! Too few arguments for proclaimed function FORMAT
  style-warning: 
    Warning:
      in file main.lisp, position 22
      at (DEFUN MAIN ...)
      ! Too few arguments for proclaimed function FORMAT
  error: 
    Error:
      in file main.lisp, position 22
      at (DEFUN MAIN ...)
      * Wrong number of arguments for function FORMAT

Compilation failed.

sly-compilationにはエラーの他、コンパイル時の警告なども表示されます。C-c C-kでEmacsのステータスにWarningが表示された場合には、sly-compilationを確認してください。

Goodbyeを追加しコンパイルした後、先ほどと同様main関数を呼び出すと、以下のように結果が変わります。

CL-USER> (picopico:main)
Hello from picopico!
Goodbye
NIL

関数単位のコンパイル

関数単位のコンパイルを確認するため、add1add2の関数を作成し、その結果を確認できるようにします。

(in-package :picopico)

(defun add1()
  (+ 1 1))

(defun add2()
  (+ 1 2))

(defun main ()
  (format t "~D,~D~%" (add1) (add2))
  (format t "Hello from picopico!~%")
  (format t "Goodbye~%"))

最初にC-c C-kでファイル全体をコンパイルし、実行結果が以下のようになることを確認します。

CL-USER> (picopico:main)
2,3
Hello from picopico!
Goodbye
NIL

次に、add1とadd2の関数の結果が変わるように書き換えます。

(defun add1()
  (+ 1 10))

(defun add2()
  (+ 1 20))

このままC-c C-kを実行してしまうとファイル全体がコンパイルされますが、add2だけをコンパイルするためカーソルをadd2に移動させてC-c C-cを実行します。再度プログラムを実行すると、add1の変更はコンパイルされていないため、add1の結果は2、add2の結果は21になります。

CL-USER> (picopico:main)
2,21
Hello from picopico!
Goodbye
NIL

関数単位のコンパイルは、継続的に動作し続けるプログラムの動作中、一部の関数の動作を変更して確認したいと言ったときに、停止することなく関数の動作を変更できるという利点があります。

テスト

FiveAMというシステムを使うことでテストを行うことができます。ここでは、

CL-USER> (asdf:test-system :picopico)

と入力した際、picopicoのテストが実行されるようにします。

asdファイルの確認

picopico.asdpicopico/testというシステムはテストのためのものです。

(defsystem "picopico/test"
           :depends-on ("picopico" "fiveam")
           :pathname "tests"
           :serial t
           :components ((:file "main-test"))
           :perform (test-op (o s)
                             (uiop:symbol-call :fiveam '#:run!
                                               (uiop:find-symbol* '#:main-test-suite
                                                                  :main-tests))))
  • :depends-on 依存するシステム
  • :pathname テストコードのディレクトリ
  • :perform test-systemを実行する際、fiveam:run! に、main-tests:main-test-suiteを引数として渡して実行する設定

(asdf:test-system :picopico) を実行した際、picopico/testをテストとして実行させる設定は、以下のin-order-toの部分で記述します。

(defsystem "picopico"
           ;; 省略
           :in-order-to ((test-op (test-op "picopico/test"))))

依存システムのインストール

プロジェクトのトップで、picopico/testが依存しているシステムをインストールするため、以下のコマンドを実行します。

$ cd ~/Project/picopico
$ ql.sh install "picopico/test"

テスト対象の関数を作る

add1という関数を新しく作ります。src/package.lispexportにまずは関数を追加します。

(defpackage :picopico
  (:use :cl)
  (:export :main :add1))

src/main.lispに関数を追加します。

(in-package :picopico)

(defun add1(n)
  (+ n 1))

(defun main ()
  (format t "Hello from picopico!~%"))

テストを書く

tests/main-test.lispというファイルを開き、以下のようにadd1をテストするための、test-add1というテストを記述します。もし外部に公開していない関数をテストする場合にはpicopico::add1というようにコロンを2つ繋げて記述します。

(defpackage :main-tests
            (:use :cl
                  :fiveam))
(in-package :main-tests)

(def-suite main-test-suite)
(in-suite main-test-suite)

(test test-add1
      (is (= 2 (picopico:add1 1))))

(asdf:test-system :picopico)でテストが実行されるかを確認します。

CL-USER> (asdf:test-system :picopico)

;;;
;;; Compiling /home/hiro/Projects/picopico/src/package.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/package.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/src/main.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/src/main.lisp.
;;;
;;;
;;; Compiling /home/hiro/Projects/picopico/tests/main-test.lisp.
;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=3
;;;
;;; Finished compiling /home/hiro/Projects/picopico/tests/main-test.lisp.
;;;

Running test suite MAIN-TEST-SUITE
 Running test TEST-ADD1 .
 Did 1 check.
    Pass: 1 (100%)
    Skip: 0 ( 0%)
    Fail: 0 ( 0%)


T

参考

Discussion