データ型
| Type | Example | Description |
|---|---|---|
| Integer | 42, -5, 1,000, #xff, #o777, #b1010 | 64ビット符号付き整数。オーバーフロー時には自動的に多倍長整数へ昇格し、どのバックエンドでも任意の大きさで正確です。#x/#o/#b は16進/8進/2進リテラルとして読み込まれます |
| Ratio | 1/3, -2/5 | 正確な有理数(Common Lispのratio)。常に正規化され、3つのバックエンドすべてでサポートされます |
| Double | 3.14, -0.5, 3,000.50, 1d0, 6.02e23 | 64ビット浮動小数点数 |
| String | "hello" | 文字列リテラル |
| Character | #\a, #\Space, #\Newline | 文字リテラル(#\ の後ろに字形または標準名: Space, Newline, Tab, Return, Page, Backspace, Nul, Rubout)。WASMバックエンドは文字列をバイト単位でインデックスするため、非ASCII文字は対象外です |
| Symbol | x, foo | 識別子 |
| Keyword | :foo, :bar | : で始まる自己評価シンボル |
| Nil | nil | 偽 / 空リスト |
| T | t | 真 |
| Pi | pi | 定数 π。double値 3.141592653589793 として読み込まれます |
| Fixnum 範囲 | most-positive-fixnum、most-negative-fixnum | pi と同様に自己評価する整数として読み込まれます。値はバックエンド依存です (WASM の fixnum はアンボックスの 31 ビット参照、インタープリターと JVM バックエンドは 64 ビット long) |
| その他の限界値 | char-code-limit、array-total-size-limit、array-dimension-limit | Fixnum 範囲と同様に自己評価する整数として読み込まれます。char-code-limit はすべてのバックエンドで 1114112 (Unicode コードポイント全域)、配列の限界値はバックエンド依存です |
| 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)) | 任意の階数の固定サイズ配列(階数1はベクタ)。#(...) と #nA(...) は自己評価される配列リテラルです |
| 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+ と同様に)数値ではなくシンボルとして読み込まれます。
どのバックエンドでも、整数演算が暗黙のうちにラップすることはありません。演算(+,
-, *, /, 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)。
バックエンドごとに、各成分は整数表現に従います。インタプリタおよびJVMコンパイラは多倍長整数を使用し(巨大な分子/分母のratioも正確なまま)、WASMコンパイラはratioの成分をオーバーフロー昇格なしで31ビットのfixnum範囲に保ちます(通常の整数は上限なく昇格し、2整数の truncate/floor/ceiling/round/mod/rem は任意の大きさで正確に割り切ります。制限があるのは分数として保持される除算のみです: 31ビットを超える成分は畳み込まれ、割り切れない / にリム表現の多倍長整数が渡るとトラップします)。コンパイルされた read/load のために発行されるランタイムリーダはratioリテラルをパースしません(実行時に読まれる
1/3 トークンはシンボルになります)。また mod、evenp/oddp、gcd/lcm、isqrt
は整数専用のままです。
コメント・フィーチャー条件・*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
のビルドはリアクタでもあるので、両方が立ちます)。
*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 を使います(システムガイドを参照)。
注意点:
- 読み取りはフロントエンドで一度だけ行われます。インタプリタは
:rontolisp-interpreterで読み、.class/.wasmへのコンパイルは:rontolisp-jvm/:rontolisp-wasmで読むため、コンパイル済みプログラムの#+条件が解決されるセットはコンパイル時に固定されます。コンパイル時のload/require/asdf:load-systemインクルードで取り込まれるファイルも、同じターゲットフィーチャーで読まれます。実行時の*features*のリストも、その同じセットから始まります。 - 不成立の
#+/#-ガードでスキップされるフォームは、パースされずに生の文字レベルでスキップされるため、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 や
array-dimension-limit の置換と同じ仕組みで、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つのバックエンドすべてで機能します。
階数1以上の任意の階数の配列がサポートされます。dimensions引数は整数(階数1)または空でない整数のリストで、:initial-element
はすべてのセルを設定します(デフォルトはnil)。要素は行優先で格納され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、...)。同じ深さのリストはすべて同じ長さでなければならず、
不揃いな内容は読み込みエラーになります。配列はすべてのバックエンドで同じ可読構文で印字され、prin1
は文字列要素を引用符で囲み、princ は囲みません:
パックド浮動小数点配列(#d / #f)
#d(...) と #f(...) はパックド浮動小数点配列を表します。これは要素をアン
ボックスで格納する浮動小数点型の配列です。#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)
で構築できます。
スカラは double のままです。要素を読むと double に拡張され(single-float 要素は
f32 -> f64 に拡張)、格納するときは配列の幅に丸められます(single-float 配列では
f64 -> f32)。非実数を格納すると型エラーです(一般配列は任意の値を保持します)。
それ以外のすべての操作 -- 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 のダンプはこうして読み込みます。
ハッシュテーブル
make-hash-table、gethash、(setf (gethash ...))、remhash、clrhash、hash-table-count、hash-table-p、maphash
は3つのバックエンドすべてで機能します。キーは構造的に(equal のように)比較されます。(list r c)
のようなリストキーは等しいリストにマッチし、数値・シンボル・文字・文字列は値でマッチします。:test
は馴染みのために受け付けられますがこの挙動を変えません -- eql
テーブルでも構造的に等しい集約キーにマッチします。反復順序(maphash)はバックエンド間で保証されないため、ポータブルなコードはこれに依存すべきではありません。テーブル自体はSBCLの印字形式から末尾の同一性ハッシュを除いた
#<HASH-TABLE :TEST EQUAL :COUNT n> として印字されます -- どのバックエンドでも同じテキストで、エントリの内容は含まれません。:TEST
は作成時の :test に関わらず常に 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
でのカウント: