typelisp 0.3.0

A statically typed Lisp with an interpreter and a compiler to native executables (LLVM)
docs.rs failed to build typelisp-0.3.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

TypeLisp

English | 日本語 | 简体中文 | 繁體中文 | 한국어 | Español | Português (Brasil) | Deutsch | Français | Русский | العربية | Italiano | हिन्दी | Türkçe | Tiếng Việt | Bahasa Indonesia | Polski | ภาษาไทย | Українська | Nederlands | Svenska

TypeLisp は静的型付きの Lisp です。 文法などは主にCommon Lispを参考にしています。

特徴

関数定義

関数を定義するときには、引数の型と戻り値の型を指定します。 具体的には、以下のようになります。

(defun fun-name ((arg1 type1) (arg2 type2) ...) return-type
    body ...)

メソッド

(defstruct Point (x i32) (y i32))

(impl Point
  (show (self) ()
    (println "Point: (x ~d y ~d)" self::x self::y)))

(show (make-instance 'Point :x 1 :y 2))

例

factorial

(defun factorial ((n int)) int
  (if (<= n 1)
    1
    (* n (factorial (- n 1))) ))

(let ((num (factorial 10)))
  (println "10! = ~d" num) )

wc

(defun is-whitespace ((b int)) bool
  (case b
    ((#x20 #x09 #x0a #x0b #x0c #x0d) true)
    (else false)))

(defun main () ()
  (let ((args (command-line-args)))
    (when (< (len args) 2)
      (println "usage: wc file")
      (exit 0))

    (let ((path (get args 1))
          (char-count 0)
          (word-count 0)
          (line-count 0)
          (in-word false))
      (match (open-binary-input path)
        ((ok file)
         (progn
           (loop (match (read-byte file)
                   ((some b)
                    (progn
                      (incf char-count)
                      (when (= b #x0a)
                        (incf line-count))
                      (if in-word
                        (when (is-whitespace b)
                          (setf in-word false))
                        (unless (is-whitespace b)
                          (setf in-word true)
                          (incf word-count)))))
                   ((none) (break))))
           (close file)
           (println "~10d ~10d ~10d ~a" line-count word-count char-count path)))
        ((err e)
         (progn
           (println "wc: ~a" (message e))
           (exit 1)))))))

(main)

Trait

(deftrait Animal ()
  (name ((self Self)) string)
  (sound ((self Self)) string)
  (speak ((self Self)) string
    (format false "~a says ~a" (name self) (sound self))))

(defstruct Dog (nick string))
(defstruct Cat (nick string))

(impl Animal Dog
  (name ((self Self)) string self::nick)
  (sound ((self Self)) string "Woof"))

(impl Animal Cat
  (name ((self Self)) string self::nick)
  (sound ((self Self)) string "Meow"))

(defun main () ()
  (let ((animals (the Vector<:dyn Animal> (Vector::new))))
    (push animals (make-instance 'Dog :nick "Pochi"))
    (push animals (make-instance 'Cat :nick "Tama"))
    (doiter (a (iter animals))
      (println "~a" (speak a)))))

(main)

Task

(defun worker ((name string) (jobs Chan<int>) (results Chan<string>)) ()
  (doiter (n jobs)
    (sleep 0.1)
    (send results (format false "~a: ~d * ~d = ~d" name n n (* n n)))))

(defun main () ()
  (let ((jobs (the Chan<int> (Chan::new 0)))
        (results (the Chan<string> (Chan::new 10))))
    (task (worker "A" jobs results))
    (task (worker "B" jobs results))
    (dotimes (i 4)
      (send jobs (+ i 1)))
    (close jobs)
    (dotimes (i 4)
      (println "~a" (unwrap (recv results))))))

(main)

nil の代わりに Option と Result

nil はありません。値が無いかもしれないときは Option<T>、失敗するかもしれないときは Result<T,E> を返し、match で中身を取り出します。

(defun find-user ((id int)) Option<string>
  (case id
    (1 (Option::some "alice"))
    (2 (Option::some "bob"))
    (else (Option::none))))

(defun parse-port ((s string)) Result<int,string>
  (match (parse-int s)
    ((ok n) (if (<= 1 n 65535)
                (Result::ok n)
                (Result::err (format false "port out of range: ~a" n))))
    ((err e) (Result::err (message e)))))

(match (find-user 3)
  ((some name) (println "user: ~a" name))
  ((none) (println "not found")))        ; not found

(match (parse-port "99999")
  ((ok p) (println "port ~a" p))
  ((err m) (println "error: ~a" m)))     ; error: port out of range: 99999

Option<string> を string として使ったり、match で none の場合を書き忘れたりすると、 実行する前に型エラーになります。

型検査されるマクロ

マクロは Common Lisp と同じ defmacro、準クオート、gensym で書けます。展開した結果も 普通のコードと同じように型検査されます。

(defmacro swap (a b)
  (let ((tmp (gensym "tmp")))
    `(let ((,tmp ,a))
       (setf ,a ,b)
       (setf ,b ,tmp))))

(let ((x 1) (y 2))
  (swap x y)
  (println "x=~a y=~a" x y))             ; x=2 y=1

展開した結果が型に合わなければ、マクロを呼んだ場所でエラーになります。

(defmacro add-a (x) `(+ ,x "a"))
(add-a 1)
;; error: type error: type mismatch: expected `int`, found `string`

そのほかの例

examples/ にあるプログラムは、ファイル名を指定して実行できます。上の wc、Trait、Task の例も examples/wc.typl、examples/animals.typl、examples/tasks.typl にあります。

typl examples/fizzbuzz.typl
typl examples/wc.typl README_JP.md
ファイル 内容
bst.typl 二分探索木(defenum と match)
factorial.typl 階乗(63 ビットを超えると自動で多倍長になる)
fibonacci.typl フィボナッチ数列(ループ)
fizzbuzz.typl FizzBuzz
game_of_life.typl ライフゲーム
maze_bfs.typl 迷路の最短経路(幅優先探索)
primes.typl エラトステネスの篩
tasks.typl タスクとチャネルで仕事を分ける

examples/projects/ には、複数のファイルに分かれたプログラムがあります。 src/main.typl を指定して実行します(例: typl examples/projects/todo-cli/src/main.typl)。

プロジェクト 内容
echo-server 接続ごとにタスクを起動するエコーサーバ
expr-eval 四則演算の対話計算機(字句解析、構文解析、評価)
http HTTP/1.1 のサーバとクライアント(TLS 対応)
mini-lisp 小さな Lisp の REPL
shape-canvas 文字のキャンバスに図形を描く(:dyn とエラー型)
todo-cli ToDo を管理するコマンドラインツール

インストール

macOS(Intel / Apple Silicon)では、ビルド済みの typl と typl-lsp を次のコマンドで インストールできます。

curl -fsSL https://raw.githubusercontent.com/JunSuzukiJapan/typelisp/main/install.sh | sh

~/.typelisp/bin に入るので、表示に従って PATH に追加してください。typl -c で実行ファイルを 作るには Xcode Command Line Tools(xcode-select --install)が必要です。ビルド済みの typl は Apple Silicon では macOS 26 以降、Intel では macOS 15 以降で動きます。Apple Silicon で macOS 26 より 前の macOS では、起動時に警告が 1 行出ます。そのまま使えますが、typl が実行中にコンパイルした コードの中で panic・throw・unwind-protect を使うと、異常終了することがあります。

インストール先は環境変数 TYPELISP_HOME で、バージョンは TYPELISP_VERSION で指定できます (指定しなければ最新のリリース)。

curl -fsSL https://raw.githubusercontent.com/JunSuzukiJapan/typelisp/main/install.sh | TYPELISP_VERSION=0.3.0 sh

アンインストールするときは、typl が書き出したライブラリを削除してから、インストール先を 削除します。

typl --remove-lib --all
rm -rf ~/.typelisp

ソースからビルドする場合は、次の「ビルド」を参照してください。

ビルド

必要なもの

  • Rust(cargo)
  • LLVM 22
  • macOS では Xcode Command Line Tools、Linux では C コンパイラ(どちらもリンクに使います)

macOS では、LLVM 22 は Homebrew で入れられます。

brew install llvm@22

Linux では、ディストリビューションのパッケージで LLVM 22 の開発用ファイルを入れます。 Ubuntu 24.04、Debian 13、Fedora 44 でビルドとテストを確かめています。

  • Ubuntu / Debian:apt.llvm.org のリポジトリを追加して llvm-22-dev と libpolly-22-dev を入れます。ほかに build-essential、pkg-config、zlib1g-dev、 libzstd-dev、libxml2-dev が要ります。
  • Fedora:llvm-devel、llvm-static、gcc、gcc-c++、zlib-devel、libzstd-devel、 libxml2-devel、libffi-devel を入れます。

初回の設定

クローンした後に一度だけ、次のスクリプトを実行します。

scripts/setup-cargo-env.sh

このスクリプトは LLVM 22 の場所を調べ(macOS では brew --prefix llvm@22、Linux では llvm-config-22 か、LLVM 22 の llvm-config)、.cargo/config.toml を生成します。 このファイルはマシンごとに内容が違うので、Git の管理対象外です。macOS では、ビルドに使う最低 OS バージョン(MACOSX_DEPLOYMENT_TARGET)も書き込みます。Rust のツールチェーンを更新したときは、 もう一度実行してください。

スクリプトが最低 OS バージョンを読み取れなかった場合は、その旨を表示して止まります。そのときは 値を自分で指定します。

scripts/setup-cargo-env.sh --deployment-target 15.0

設定ファイルを生成したくない場合は、cargo のコマンドを scripts/with-llvm-env.sh 経由で実行します。 こちらは実行のたびに同じ値を環境変数に設定します。

scripts/with-llvm-env.sh cargo build

Homebrew を使わない場合は、LLVM_SYS_221_PREFIX に LLVM 22 のインストール先を設定してください。 macOS の最低 OS バージョン(MACOSX_DEPLOYMENT_TARGET)は、設定しなくてもビルドできます。設定 しなければ、Rust の標準ライブラリが対象とするバージョンを使います。

ビルドする

cargo build            # デバッグビルド(target/debug/)
cargo build --release  # リリースビルド(target/release/)

次の2つの実行ファイルができます。

実行ファイル 役割
typl 処理系本体(使い方は次の「実行方法」)
typl-lsp 言語サーバ(エディタ連携用)

cargo install でインストールする

typl と typl-lsp を ~/.cargo/bin に入れるには、クローンしたリポジトリで次を実行します。

cargo install --locked --path .

cargo install --path は、「初回の設定」で生成した .cargo/config.toml を読みます。設定ファイルを 生成していない場合は、scripts/with-llvm-env.sh cargo install --locked --path . を実行します。 Homebrew を使わない場合は、LLVM_SYS_221_PREFIX を設定してから cargo install --locked --path . を 実行します。

インストールした typl は、コンパイルに使う静的ライブラリを中に持っているので、インストール後に リポジトリを移動・削除しても -c や compile-file が使えます(次の「ファイルをコンパイルする」を 参照)。

実行方法

typl の起動のしかたは次の3通りです。以下の例では、typl にパスが通っているものとします (install.sh や cargo install で入れて PATH を通した場合、またはビルドした target/debug/・target/release/ にパスを通した場合)。

インタラクティブモード(REPL)

引数を付けずに起動すると、インタラクティブモード(REPL)になります。式を1つ入力するたびに、 その場で評価して結果を表示します。:quit または :exit で終了します。

typl

ファイルを実行する

ファイル名だけを指定すると、そのファイルを実行します。ファイル名より後ろに書いた引数は、 プログラムに渡されます((command-line-args) で受け取れます)。

typl foo.typl
typl foo.typl a b c

テストを実行する

#[test] を付けた関数(テスト関数)を走らせます。プロジェクトのフォルダ(typelisp.toml の あるフォルダか、その下)で実行すると、プロジェクトのすべてのファイルと tests/ フォルダの ファイルからテスト関数を集めます。ファイル名を指定すると、そのファイルのテスト関数だけを 走らせます。最後に文字列を書くと、名前にその文字列を含むテストだけを走らせます。

typl test
typl test parse
typl test foo.typl

書き方は構文リファレンスの「#[test] — テスト関数」を 参照してください。

ファイルをコンパイルする

-c または --compile を指定すると、そのファイルをコンパイルして実行ファイルを作ります。 -o を省くと、ファイル名から .typl を除いた名前の実行ファイル(この例では foo)ができます。

typl -c foo.typl
typl --compile foo.typl -o bar

--release を付けるとリリースビルドになり、調べるための出力 eprint/eprintln を取り除きます。 --release は typl 全体のオプションで、ファイルを実行するときやテストのときにも効きます。

typl --release -c foo.typl

-O0 〜 -O3 で最適化のレベルを指定します(既定は -O0 で、最適化しません)。これも typl 全体の オプションです。

typl -O2 -c foo.typl

コンパイルして作る実行ファイルには、静的ライブラリ libtypelisp_front.a をリンクします。 リリースビルドの typl(cargo install で入れたものを含む)は、このライブラリを中に持って いて、初めてコンパイルするときに ~/.typelisp/lib/<ビルドID>/ に書き出します。2回目からは 書き出したものを使います。書き出す場所は環境変数 TYPELISP_HOME で変えられます(TYPELISP_HOME を設定すると $TYPELISP_HOME/lib/<ビルドID>/ になります)。ビルドIDはライブラリの中身から 決まるので、別の版の typl を入れると、次のコンパイルで新しいディレクトリに書き出されます。

書き出したライブラリは、次のコマンドで削除できます。

typl --remove-lib           # この typl が書き出したものを削除する
typl --remove-lib --others  # 別の版の typl が書き出したものを削除する
typl --remove-lib --all     # すべての版のものを削除する

typl をアンインストールする前には typl --remove-lib を、入れ直した後には typl --remove-lib --others を実行すると、使われないライブラリが残りません。削除したものは、 typl が次にコンパイルするときに書き出し直します。削除するのは lib/ の下のビルドIDの ディレクトリだけで、それ以外のファイルには触れません。

デバッグビルドの typl は、ビルドしたリポジトリの target/debug/ にあるものを使い、何も 書き出しません。

別の場所に置いたライブラリを使うには、--lib-dir でそのフォルダを指定します。

cp target/debug/libtypelisp_front.a ~/lib/typelisp/
typl --lib-dir ~/lib/typelisp -c foo.typl

libtypelisp_front.a は、そのファイルと同時にビルドした typl でしか使えません。typl を ビルドし直したら、コピーも取り直してください。

その他のオプション

typl --help        # オプションの一覧を表示する
typl --version     # バージョンを表示する
typl --remove-lib  # 書き出したライブラリを削除する(--others / --all も使える)

文書

日本語の文書の一覧は docs/ja/README.md にあります。

チュートリアル

ガイド

リファレンス

エディタ連携

ライセンス

typelisp は、次の2つのライセンスのどちらかを選んで利用できます。

どちらを選んだ場合も、LICENSE-EXCEPTION の例外が加わります。typl -c や compile-file で作った実行ファイルには typelisp の一部が含まれますが、その部分については typelisp の著作権表示やライセンス文を添えずに配布できます。

typelisp への貢献は、特に断りのない限り、上と同じ条件(2つのライセンスのどちらか、および例外)で 提供されたものとして扱います。