(rontolisp) docs

データ型

TypeExampleDescription
Integer42, -5, 1,000, #xff, #o777, #b101064ビット符号付き整数。オーバーフロー時には自動的に多倍長整数へ昇格し、どのバックエンドでも任意の大きさで正確です。#x/#o/#b は16進/8進/2進リテラルとして読み込まれます
Ratio1/3, -2/5正確な有理数(Common Lispのratio)。常に正規化され、3つのバックエンドすべてでサポートされます
Double3.14, -0.5, 3,000.50, 1d0, 6.02e2364ビット浮動小数点数
String"hello"文字列リテラル
Character#\a, #\Space, #\Newline文字リテラル(#\ の後ろに字形または標準名: Space, Newline, Tab, Return, Page, Backspace, Nul, Rubout)。WASMバックエンドは文字列をバイト単位でインデックスするため、非ASCII文字は対象外です
Symbolx, foo識別子
Keyword:foo, :bar: で始まる自己評価シンボル
Nilnil偽 / 空リスト
Tt真
Pipi定数 π。double値 3.141592653589793 を保持するグローバル変数として束縛されます。クォートされた pi はシンボルのままです
Fixnum 範囲most-positive-fixnum、most-negative-fixnumpi と同様にグローバル変数として束縛されます。値はバックエンド依存です (WASM の fixnum はアンボックスの 31 ビット参照、インタープリターと JVM バックエンドは 64 ビット long)
その他の限界値char-code-limit、array-total-size-limit、array-dimension-limit、array-rank-limit、call-arguments-limit、lambda-parameters-limit、multiple-values-limitFixnum 範囲と同様にグローバル変数として束縛されます。char-code-limit はすべてのバックエンドで 1114112 (Unicode コードポイント全域)、配列の限界値はバックエンド依存です
浮動小数点の範囲most-positive-double-float、least-positive-normalized-single-float、double-float-epsilon標準の浮動小数点範囲定数。pi と同様に double を保持するグローバル変数として束縛されます。short-float は single-float、long-float は double-float と同一で、本処理系の浮動小数点はすべて double のため、single-float の境界値はその binary32 値をそのまま double にした値になります
Cons(1 2 3), (a . 1)consセルで構築された連結リスト。(a . b) は単一セルを表すドット対記法
Function#'car, (lambda (x) x)#'/function/lambda で得られる関数オブジェクト
Array#(1 2 3), #2A((1 2) (3 4)), #0A5任意の階数の固定サイズ配列(階数1はベクタ、階数0はスカラーの箱)。#(...)、#nA(...)、#0A<datum> は自己評価される配列リテラルです
Hash table(make-hash-table)構造的(equal)なキーを持つ可変のキー/値テーブル
構造体#S(POINT :X 1 :Y 2)defstruct 型のインスタンス。#S(...) はインスタンスの印字形式であると同時に、インスタンスとして読み取られる自己評価リテラルでもあります。defstruct はそれより前のトップレベルフォームに現れている必要があります

数値リテラルでは、整数部の桁の間に区切り文字として , を使用できます。たとえば 1,000 は 1000 として読み込まれ、(+ 1,000 100) は 1100 に評価されます。カンマが区切り文字として扱われるのは2つの数字の間にある場合のみで、パース前に除去され、3つのバックエンドすべてに適用されます。これは , がunquote文字であるCommon Lisp(ここではサポートされません)とは異なります。

浮動小数点リテラルにはCommon Lispの指数マーカーを付けられます。仮数部の後ろに e, s, f, d, l のいずれか(大文字小文字を区別しません)、省略可能な符号、そして指数を続けます。例: 1d0, 1e0, 1.5d3(1500.0)、-2e-3, 6.02e23。これは3つのバックエンドすべてで機能します(リーダレベルの機能です)。Common Lispとは異なり、rontolispは浮動小数点型を1種類しか持たないため、すべてのマーカーは同じ 64ビットdoubleとして読み込まれます -- single/short/long-floatの区別(1d0 対 1e0 対 1f0)は保持されず、*read-default-float-format* もありません。指数の数字が後続しないマーカーは浮動小数点ではありません。1d と 1d0x は(1+ と同様に)数値ではなくシンボルとして読み込まれます。

小数点の両側に数字は必要ありません。.4 は 0.4 です(したがって (a .5) は 2 要素のリスト (A 0.5) です。ドット対は (a . 5) のようにドットを単独で書きます)。1.e5 も浮動小数点です。指数を伴わない末尾のドットは 10 進整数のマーカーです。1. は整数 1 です。

どのバックエンドでも、整数演算が暗黙のうちにラップすることはありません。演算(+, -, *, /, 1+, 1-, abs, ...)が固定幅表現をオーバーフローする場合、結果は自動的に任意精度の多倍長整数へ昇格し、任意の大きさの整数リテラルが正確に読み込まれます。より狭い表現に再び収まる多倍長整数の結果は再度降格されるため、値は単一の正規表現を保ちます。たとえば (defun fact (n) (if (= n 0) 1 (* n (fact (- n 1))))) の場合、(fact 32) はどのバックエンドでも正確な値 263130836933693530167218012160000000 を返します。(WASMコンパイラは2段階で昇格します -- アンボックスの31ビットfixnumがまず符号付き64ビット値にボックス化され、次にリム表現の多倍長整数になります -- がプログラムからは見えません。)

3つのバックエンドすべてがCommon Lispのratio(正確な有理数)をサポートします。1/3 はratioリテラルとして読み込まれ、割り切れない整数除算は切り捨てる代わりにratioを返します:

CL-USER> 1/3
1/3
CL-USER> (/ 1 2)
1/2
CL-USER> (+ 1/2 1/3)
5/6
CL-USER> (/ 1 2.0)
0.5
CL-USER> (float 1/2)
0.5

ratioの結果は常に正規化されます。gcdで約分され符号は分子に付き(2/4 は 1/2 として読み込まれます)、分母が1に約分される場合は整数へ降格されます((/ 10 2) は 5、(+ 1/2 1/2) は 1)。算術、比較(= < > <= >=)、eq/eql、abs/min/max/1+/1-/signum、述語(numberp, rationalp, zerop, plusp, minusp)、truncate/floor/ceiling/round、整数の指数を持つ expt((expt 2 -1) は 1/2)、numerator/denominator はすべてratioを扱います。浮動小数点が混ざると浮動小数点への伝播に切り替わります。単項の (/ x) は逆数です((/ 2) は 1/2)。

ratioの分子と分母は他の整数と同じ整数なので、どのバックエンドでもratioは任意の大きさで正確です。インタプリタおよびJVMコンパイラは分子と分母を多倍長整数として、WASMコンパイラは他のすべての整数と同じ昇格する整数表現として保持するため、(/ 3000000000 7) はどこでも 3000000000/7 です。ratioの float は4つのバックエンドすべてで最も近いdouble(偶数丸め)になります((float 30000000000000004/100000000000000000) は 0.30000000000000004)。コンパイルされた read/load のために発行されるランタイムリーダは、フロントエンドと同じくratioのトークンをパースします((read-from-string "1/3") は 1/3)。mod/rem はratioを含む任意の実数を受け付け、evenp/oddp、gcd/lcm、isqrt はCommon Lispと同じく整数だけを受け付けます。

コメント・フィーチャー条件・*features*

; の行コメントに加えて、リーダは Common Lisp の #| ... |# ブロックコメント(標準どおりネスト可)と #+/#- フィーチャー条件をサポートします。#+expr form はフィーチャー式が成立するときだけ form を残し、#-expr form は成立しないときだけ残します。フィーチャー式はフィーチャー名、または (and ...)/(or ...)/(not ...) の組み合わせです(裸のシンボルでもキーワードでもよく、大文字小文字を区別しません)。アクティブなフィーチャーはすべてのバックエンドで :rontolisp、加えてバックエンドを識別するフィーチャー — :rontolisp-interpreter、 :rontolisp-jvm、:rontolisp-wasm — が1つで、1つのソースファイルでバックエンドごとのコードを選択できます。さらに :unicode もアクティブです。これは「文字は Unicode コードポイントである」というポータブルな綴りで、すべてのバックエンドで真なので、これで分岐するライブラリは UTF-8 のパスを選びます。インタプリタと JVM には加えて :thread-support があります(本当にスレッドを生成できます — rontolisp:make-thread を参照)。また、リアクタモードの WASM コンパイル(--no-wasi または --no-gc)には :rontolisp-reactor が加わります — モジュールのエントリポイントはホストが呼ぶエクスポートである、という意味で、Clack のハンドラバックエンドがトランスポートを選ぶ手掛かりです(Clack ガイドを参照)。--component のコンパイルにはさらに :rontolisp-component が加わります。これはバックエンドではなくバウンダリの名前です。コンポーネントのホスト関数は canonical ABI を通るので rontolisp:wasm-import はそこでは拒否され、これを宣言するソースは #-rontolisp-component で囲みます(--component --no-wasi のビルドはリアクタでもあるので、両方が立ちます)。 --native の出力にはさらに :rontolisp-native が加わります。中身のモジュールはこれまでどおりの Preview 1 ビルドのままですが、それを包むランナーは Preview 1 単体にはない輸入(rontolisp:fetch、および macos-aarch64 での objc:/appkit:/metal:/scene:)に応えるので、native では fetch し、それ以外の Preview 1 では代替する、というソースは #+rontolisp-native で囲みます。

*features* はそのリストを保持する普通のスペシャル変数で、これはすべてのバックエンドで同じです。push でき、setq でき、他のスペシャル変数と同様に let で束縛できます。

ソースは自分自身についてフィーチャーを宣言することもできます。トップレベルの (pushnew :my-feature *features*)(裸でも eval-when/progn の中でもかまいません)はリーダに読まれるので、同じファイル内の #+my-feature がそれを見ます。これは本物の Common Lisp がファイルを1フォームずつロードすることで得ている定石で、ここでは4つのバックエンドすべてで同じように振る舞います。数えられるのはリテラルのキーワードの push だけです。値をプログラムが計算する push ((pushnew (intern name :keyword) *features*))は実行時の push としては本物ですが、リーダからは見えません。それを見るにはプログラムをどう読むかを決めるためにプログラムを実行することになるからです。.asd が、自分が定義するシステムのファイルに対してフィーチャーを宣言したい場合は、代わりに :rontolisp-features を使います(システムガイドを参照)。

さらに、利用者がコマンドラインでフィーチャーを宣言できます。--feature NAME (カンマ区切り、繰り返し指定可)はその実行で読まれるすべてのソース — エントリファイル、それが load するファイル、その下でロードされる ASDF システム — のフィーチャーセットを広げ、*features* にも同じ名前を入れます。したがって (member :sbcl *features*) とその隣の #+sbcl が食い違うことはありません。

rontolisp app.lisp --feature sbcl
rontolisp app.lisp -o app.jar --feature sbcl,clisp

これは、#+sbcl / #+clisp / #-(or ...) の連鎖が rontolisp の存在以前に書かれている移植性重視のライブラリのためのものです。その連鎖はこちらのフィーチャーを1つも名指ししないので、sbcl ブランチが使うプリミティブがすべて SBCL と完全に同じ答えを返す場合でも、呼び出しはすべて else ブランチ — たいていは (error "not implemented") — に落ちます。rontolisp が :sbcl を自分から宣言することはありません。それを求めていないすべてのプログラムに対して嘘をつくことになるからです。このフラグは、それをあなたが読んだライブラリについて、これから始める実行に対して行う宣言にします。結果もあなたのものです。同じ宣言は、その実装の内部を実際に呼ぶブランチも選びます。rontolisp 自身のフィーチャーは拒否されます。:rontolisp、バックエンドフィーチャー、ターゲットを表すもの(:rontolisp-component、:rontolisp-reactor など)は出力が何であるかを表しており、出力が何であるかを決めるのは -o とその隣のフラグだからです。

注意点:

  • 読み取りはフロントエンドで一度だけ行われます。インタプリタは :rontolisp-interpreter で読み、.class/.wasm へのコンパイルは :rontolisp-jvm/:rontolisp-wasm で読むため、コンパイル済みプログラムの #+ 条件が解決されるセットはコンパイル時に固定されます。コンパイル時の load/require/asdf:load-system インクルードで取り込まれるファイルも、同じターゲットフィーチャーで読まれます。実行時の *features* のリストも、その同じセットから始まります。
  • --feature が届くのはあなたが持ち込んだソース — エントリファイル、それがロードするすべて、その下でロードされる ASDF システム — です。rontolisp 自身が同梱するソース(プレリュード、組み込みライブラリ、システムのシム)には決して届かず、そちらは常にバックエンド自身のセットで読まれます。サードパーティのライブラリについてあなたが行う宣言が、その下にあるこちらの条件式を書き換えてはならないからです。
  • 不成立の #+/#- ガードでスキップされるフォームは、パースされずに生の文字レベルでスキップされるため、rontolisp がサポートしない構文を使っていても構いません(それがガードの目的です)。
  • #. の read 時評価はサポート されます: 各 #. データはそのトップレベルフォームが実行される直前に評価され — インタープリタではグローバル環境に対して、JVM / WASM コンパイルパスではコンパイル時(マクロ時)評価器に対して — 値がフォームに置換されます。.asd ファイル内では代わりに #. フォームは警告付きでスキップされます(システムガイドを参照)。ブラウザプレイグラウンドの Compile ボタンは #. をサポートしません。
  • コンパイル済みプログラムのランタイムリーダ(read、read-from-string、実行時の load)は、backquote と同様に、ブロックコメントとフィーチャー条件を知りません — コンパイル済み read/load の制限を参照してください。
  • :common-lisp は意図的に *features* に 含まれません: rontolisp はサブセットであり、準拠実装ではないからです。

ソース位置リテラル(rontolisp:current-file、rontolisp:current-line)

リーダが、そのシンボルが置かれている位置に置換する2つのシンボルです。pi や限界値定数がバックエンドごとのグローバルに束縛されるシンボルになった今、読み取り時置換として残るのはこの2つだけです(上の表を参照)。 rontolisp:current-file は元ファイル名の文字列 (存在しない場合 — REPL の1行や read-from-string — は nil)に、 rontolisp:current-line はそのシンボル自身が乗っている1始まりの行番号になります。置換後はただのリテラルなので実行時コストはゼロで、インタープリタと全コンパイルバックエンドで同じ値になります。

load / require / asdf:load-system で取り込まれたファイルは、スプライス先のエントリファイルではなく 自分自身 を名乗ります。これが要点です: 多数のファイルから組み上げられたプログラムでも、メッセージが本当の出所を言えます。ファイル名はフロントエンドが見たままの綴り(コマンドラインで与えたパス、または load が解決したパス)で、read エラーの綴り方と同じです。

$ cat lib.lisp
(defun where ()
  (list rontolisp:current-file rontolisp:current-line))
$ cat main.lisp
(load "lib.lisp")
(print (where))
(print (list rontolisp:current-file rontolisp:current-line))
$ rontolisp main.lisp
("lib.lisp" 2)
("main.lisp" 3)

注意点:

  • 置換は read 時 に行われるため、defmacro のテンプレート内ではマクロの呼び出し側ではなくマクロ自身の定義位置を指します。したがってロギングマクロは、C で __FILE__ / __LINE__ を渡すのと同じように、呼び出し側で引数として渡します:

    (defmacro log-at (file line msg)
      `(format t "~a:~a: ~a~%" ,file ,line ,msg))
    
    (log-at rontolisp:current-file rontolisp:current-line "started")
    ; app.lisp:12: started のように出力される
    
  • 認識されるのは修飾付きの綴りのみです(rontolisp:current-file、 rontolisp::current-file、rl:current-file)。rontolisp パッケージの他のシンボルと異なり、(in-package rontolisp) 後の非修飾での使用は できません: read は in-package ディレクティブの解釈より前に行われるためです。

  • read 時であるため、クォートされたデータ内も含めて出現箇所すべてが置換されます — 'rontolisp:current-line はシンボルではなく数値です。#+/#- や #. と同じ規則です。

ドット対・連想リスト・属性リスト

リーダはCommon Lispのドット対記法をサポートします。(a . b) は car が a、cdr が b の単一のconsセルを表し、(a b . c) は最後の cdr が nil ではなく c になるリストです。連想リスト(alist)のリテラルはこの記法で書きます:

ドット末尾はバッククォートテンプレートでも機能し(`(a . ,x) は cons の連鎖に展開されます)、コンパイルされたプログラムのランタイムリーダも同じ記法をパースするため、"(a . 1)" の read/read-from-string はすべてのバックエンドで同一に動作します。リストの外に単独で現れる . はCommon Lispと同様に読み取りエラーです。またバッククォートテンプレート内では ,@ とドット末尾を組み合わせることはできません。呼び出し位置のドット末尾((+ 1 . 2) など)は3つのバックエンドすべてでエラーになります -- ドット対はデータとしてのみ意味を持ちます。

連想リスト関数群 -- assoc、assoc-if、rassoc、acons、pairlis、copy-alist -- は3つのバックエンドすべてで機能します。assoc と rassoc は既定では eql で比較し、member と同様に省略可能な :test/:key キーワード(:test は関数指定子。 文字列キーには #'equal など。:key は比較の前に各ペアの car/cdr へ適用される セレクタ)を受け付けます:

属性リスト(plist) -- (:a 1 :b 2) のようにインジケータと値が交互に並ぶ平坦なリスト -- は、キーワードを使うalistの親戚です。getf はインジケータに対応する値を読み取り(2引数のみ: &optional default はありません)、remf マクロは変数などの setf プレースに保持されたplistからインジケータと値の組を取り除きます。ラムダリストの &key パラメータも同じ形から解析されます。(setf (getf ...)) はサポートされるプレースではなく、シンボルの属性リスト(get/symbol-plist)もありません。エントリの追加や更新にはリストを作り直します(list* で先頭に追加するなど):

配列

make-array、aref、(setf (aref ...)) は3つのバックエンドすべてで機能します。 階数0以上の任意の階数の配列がサポートされます。dimensions引数は整数(階数1)または整数のリストで、:initial-element はすべてのセルを設定します(デフォルトはnil)。空リスト(nil)は階数0の配列になります。これは Common Lisp における「スカラーを配列として見た箱」であり、要素を1つだけ保持し、aref と (setf (aref ...)) は添字なしでその要素に到達します。要素は行優先で格納されO(1)でアクセスでき(階数に依存しないフラットなアクセスは row-major-aref / array-row-major-index)、配列は同一性(eq)で比較されるため、異なる2つの配列が equal になることはありません。length はベクタ(階数1の配列)の要素数を返します。多次元配列はシーケンスではないため、length はエラーを通知します。ハッシュテーブル演算子とは異なり、配列演算子は第一級の関数値として公開されないため、#'aref と #'make-array は利用できません(直接呼び出してください)。 ベクタは vector で構築し、svref で読み取ることもできます。配列の形状は array-dimensions / array-rank / array-total-size で調べられ、 coerce はリスト・ベクタ・文字列を相互に変換します。 配列の上で numpy スタイルのベクトル・行列演算を行うには linalg パッケージを参照してください。 ネストしたループでインデックス参照する2次元配列:

#(...) リーダ構文は、要素がデータとして(評価されずに)読み込まれる自己評価される階数1のベクタリテラルを表します。例: #(1 2 3) や #(a "b")。階数nの配列は、内容を深さnのネストしたリストとして #nA((...) ...) と書きます(行列は #2A、階数3の配列は #3A、...)。同じ深さのリストはすべて同じ長さでなければならず、 不揃いな内容は読み込みエラーになります。階数0の配列は括弧をまったく伴わない #0A<datum> と書くので、#0A5 は数 5 を、#0A(1 2) はリスト (1 2) を保持します。配列はすべてのバックエンドで同じ可読構文で印字され、prin1 は文字列要素を引用符で囲み、princ は囲みません:

配列リテラルは定数ではなくコンストラクタです。評価するたびに新しい、独立に変更可能な配列を 構築するので、関数本体に書いたリテラルはその配列を簡潔に綴る手段になり、結果に書き込んでも次の 呼び出しには届きません。これはすべての配列構文(#(...)、#nA(...)、#*1011、#d(...)、 #f(...))について、すべてのバックエンドで成り立ちます。Common Lisp はリテラルを共有したままに し、その変更を未定義とするため、下の (eq (f) (f)) は準拠処理系では T を返しますが、ここでは NIL になります:

同じ構文でも quote の下では定数であり、Common Lisp と同じです。1 つの quote 箇所を 評価するたびに、どのバックエンドでも同じ共有オブジェクトが返ります。quote されたリストでも 配列でも同様で、(defun f () '(1 2 3)) の (eq (f) (f)) は T になり、結果への破壊的な 書き込みは次の呼び出しに届きます。quote されたデータは、他の Common Lisp と同様に読み取り 専用として扱ってください。

構造体リテラルとパス名リテラルは quote の外でも定数です。#S(...) と #P"..." は 自己評価的で、1 つの箇所を評価するたびに、どのバックエンドでも同じ共有オブジェクトが返ります。 そのため (defun f () #P"a/b.txt") の (eq (f) (f)) は T になります。上のコンストラクタ 規則に従わない唯一のリテラル構文です。

パックド浮動小数点配列(#d / #f / #bf16)

#d(...)・#f(...)・#bf16(...) はパックド浮動小数点配列を表します。これは要素をアン ボックスで格納する浮動小数点型の配列です。#d(...) は double-float(f64)、 #f(...) は single-float(f32 -- メモリは半分、SIMD レーン数は 2 倍)です。 #(...) と同じ記法で読み込まれますが、各要素は配列の浮動小数点型に強制変換される ため、#d(1 2 3) と #d(1.0 2.0 3.0) は同じベクトルであり、 (array-element-type #d(1.0)) は double-float(#f なら single-float)になり ます。高階のリテラルはネストしたリストで書き(#d((1.0 2.0) (3.0 4.0)) は行列)、 実行時には (make-array n :element-type 'double-float)(または 'single-float) で構築できます。

#bf16(...) は 3 つめの幅、bfloat16 です。IEEE binary32 の上位 16 ビット -- 符号 1 ビット、f32 と同じ指数 8 ビット、仮数 7 ビット -- であり、f32 の全範囲を #d の 1/4 のメモリと約 3 桁の 10 進精度で保持します。公開された機械学習チェック ポイントがこの形式で重みを格納しているのはそのためです。これは計算用ではなく 格納用の幅です。重みを保持するために使うものであって、行列式を求めるためのもの ではありません。実行時には (make-array n :element-type 'bfloat16) で構築でき、 (array-element-type #bf16(1.0)) は bfloat16 を返し、印字結果は他の 2 つと同様に #bf16(...) として読み戻せます。型名としては subtypep の束で float の下に位置し、この型に属するスカラはありません。インタプリタと JVM のみです -- WASM バックエンド に bfloat16 配列は存在せず、それを要求した呼び出しの位置で幅を名指しして拒否します (bfloat16 arrays are supported on the interpreter and the JVM only)。

スカラは double のままです。要素を読むと double に拡張され(single-float 要素は f32 -> f64 に拡張)、格納するときは配列の幅に丸められます(single-float 配列では f64 -> f32)。bfloat16 のスカラも存在しないため、#bf16 の要素を読むとその 16 ビット が表す double がそのまま返り(bfloat16 パターンの拡張は何も失いません)、格納時は 最近接偶数丸めで丸められます -- rontolisp:bfloat16-bits と同じ丸めです。非実数を格納すると型エラーです(一般配列は任意の値を保持します)。 それ以外のすべての操作 -- aref・(setf (aref ...))・length・row-major-aref・ array-rank・array-dimensions・coerce -- は同じ数値の一般配列と同じように機能し ますが、印字だけは独自の #d(...) / #f(...) リーダ構文を用いるため、その印字結果を 読み戻すと(アンボックス表現を保ったまま)同じ幅のパックド配列になり、一般配列に劣化 しません。数値カーネルが用いる、アンボックスで浮動小数点に特化した表現に過ぎないため、 フィルポインタ・可変長(adjustable)・ずらし配列(displaced)は利用できません(それ らには一般配列が必要です)。double-float の幅が既定で、linalg が生成するのもこの 幅です。パックド配列上の高速なベクトルカーネル(および任意のハードウェアアクセラレー ション)については vec パッケージを参照してくだ さい。パックド配列はバイナリ I/O のバッファでもあります。read-sequence / write-sequence はその要素を生のリトルエンディアン IEEE-754 として 一括転送します(任意ランク、行優先順)。重みファイルや numpy のダンプはこうして読み込みます。 #bf16 配列が転送するのは格納されているパターンそのもの、1 要素あたりリトルエンディアン 2 バイトです。これは BF16 の safetensors や GGUF のテンソルが保持しているものそのままなので、 そうしたテンソルは一切の変換なしに読み込め、書き戻せばファイルをバイト単位で再現します。

ハッシュテーブル

make-hash-table、gethash、(setf (gethash ...))、remhash、clrhash、hash-table-count、hash-table-p、maphash は3つのバックエンドすべてで機能します。キーは構造的に(equal のように)比較されます。(list r c) のようなリストキーは等しいリストにマッチし、数値・シンボル・文字・文字列は値でマッチします。:test 'equalp はこれを広げます。このテーブルはキーを配置する前に畳み込むため、"CS"・"Cs"・"cs" は1つのキーになり、#\a と #\A も1つ、浮動小数点数とそれに等しい有理数も1つ(1 と 1.0、1/2 と 0.5)で、それらのリストは要素ごとに畳み込まれます。配列は意図的に畳み込みません (ベクタのキーは同一性で比較されるため、畳み込んだコピーは自分自身を見つけられません)。またコンパイル系バックエンドでは make-hash-table の引数が評価されないため、:test はリテラルで書く必要があります。畳み込みはキーの配置にだけ使われ、maphash は各キーを最初に格納したときの形で返します。"cs" で書き込んだ後に "CS" で書き込んだエントリでは "cs" が、0.5 で書き込んだエントリでは 0.5 が返ります。テーブルにすでにあるキーで格納すると、どのテストでも値だけが置き換わりキーはそのまま残ります。eql・eq テーブルは集約(コンス・ベクタ・文字列・インスタンス)を同一性で、それ以外の値を値でキーにします。反復順序(maphash)はバックエンド間で保証されないため、ポータブルなコードはこれに依存すべきではありません。テーブル自体はSBCLの印字形式から末尾の同一性ハッシュを除いた #<HASH-TABLE :TEST EQUAL :COUNT n> として印字されます -- どのバックエンドでも同じテキストで、エントリの内容は含まれません。:TEST は実際に検索が実装しているテスト、すなわち畳み込むテーブルなら EQUALP、それ以外なら EQUAL です(hash-table-test が返す答えと同じです)。:COUNT は現在のエントリ数で、hash-table-count が返す値と同じです。キーは深さ上限付きの構造ハッシュで配置され、その後 equal で判定されるため、検索コストはキーの印字形の大きさに依存せず、構造が循環しているキーも使えます(同じオブジェクトで格納・取得する限り):

これらは3つのバックエンドすべてで第一級の関数値としても使用できます(#'gethash、#'remhash、#'clrhash、#'hash-table-count、#'hash-table-p、#'maphash、#'make-hash-table) -- 固定アリティのラッパー経由で渡されるため、gethash の省略可能なデフォルトは関数値経由では利用できません。#'make-hash-table はキーワード引数を受け取りますが無視し、常にデフォルトのテーブルを作ります。典型的な使い方 -- placeに対する incf でのカウント: