🦉

Common Lisp用コードフォーマッタ

2020/10/19に公開

Code formatter for Common Lisp

Meta note.

対象読者

  • Common Lispのコードフォーマッタを探している人。

Introdunction.

Common Lisp用のコードフォーマッタを作りました。

自作ライブラリをQuicklispに登録しようとしたら「お前のコードのフォーマット、コミュニティで一般的なフォーマットじゃねぇからそのへん直してから出直しな」(超意訳)と門前払いされました。
一人でほそぼそと書いてると変な癖がついていけませんね。

手動で直すのは骨なのでフォーマッタを作った次第。

ここではREADMEを翻訳しておきます。

TRIVIAL-FORMATTER 9.0.0

これは何か?

CommonLisp用のコードフォーマッタです。
trivial-formatterのソースコードを参照してください。
このファイルはtrivial-formatter自体によってフォーマットされています。

使用法

(trivial-formatter:fmt :your-system :supersede)

詳細については、specファイルを参照してください。

線幅。

デフォルトは実装によって異なります。
(おそらく80です。)
通常の一般的なLispプリティ印刷システムの方法で、つまり *PRINT-RIGHT-MARGIN*を使用して制御できます。

(let ((*print-right-margin* 100)) ; <--- 線幅を100で指定。
  (trivial-formatter:fmt :your-system :supersede))

厳密モード。

デバッガーを呼び出すと、一部の実装(eclなど)は:cl-userパッケージに入ります。
このような場合、loopマクロのバックトレースフォームは醜くなります。

(LOOP YOUR-PROJECT::FOR YOUR-PROJECT::I YOUR-PROJECT::UPFROM 0 ...)

キーワードシンボルをループマクロキーワードとして使用すると、このような醜い形式を回避できます。

(LOOP :FOR YOUR-PROJECT::I :UPFROM 0 ...)

strictモードでは、trivial-formatterは、ループマクロキーワードをキーワードシンボルにフォーマットします。
有効にするには、 *strict-loop-keyword-p*tにバインドします。

* (let ((trivial-formatter:*strict-loop-keyword-p* t))
    (trivial-formatter:fmt :your-system :supersede))

開発者から

Reader

ソースコードに特別なリーダーマクロがある場合、trivial-foramtterは不明なリーダーマクロに関するエラーを通知します。
このような場合、trivial-formatterはreadtable、特にNAMED-READTABLESに大きく依存するため、通常の一般的なLispの方法でリーダーを拡張できます。
これを拡張するには、リーダーマクロ関数が中間オブジェクトを返す必要があります。

(let ((*readtable* (named-readtables:copy-named-readtable 'as-code)))
  (set-macro-character #\! (lambda (stream char) `(,char ,(read stream))))
  (read-as-code))
!hoge
=> (#\! HOGE)

詳細については、clhs
およびnamed-readtablesを参照してください。

プリンター。

中間オブジェクトを作成するときは、そのためのプリティプリント関数を作成する必要があります。
Trivial-formatterはプリティプリントシステムに大きく依存しているため、通常の一般的なLispの方法で拡張できます。

(defun !-printer (stream exp)
  (format stream "~<~A~S~:>" exp))
(set-pprint-dispatch '(cons (eql #\!) (cons * null)) '!-printer)
(print-as-code '(#\! hoge))
=> !hoge

詳細については、clhsを参照してください。

外部フォーマッタをロードします。

trivial-formatterは、外部フォーマッターをロードできます。
外部フォーマッタとして拡張コードを書くことができます。

外部フォーマッターファイルには「formatters.lisp」という名前を付ける必要があります。
trivial-formatterは、 *foreign-formatters-directories*からファイルを検索します。
デフォルトは、quicklispのlocal-projectsディレクトリとroswellのlocal-projectsディレクトリです。

パッケージtrivial-formatter-user。

trivial-formatter-userでは、deformatter、pprint-fun-call、およびpprint-linear-eltを通常の一般的なlispシンボルとともに使用できます。

PPRINT-FUN-CALL

pprint-fun-callは、キーと値のペアを考慮します。

(pprint-fun-call nil '(asdf:component-pathname component :direction :output :if-does-not-exist :create :if-exists if-exists))
(ASDF:COMPONENT-PATHNAME COMPONENT
                         :DIRECTION :OUTPUT
                         :IF-DOES-NOT-EXIST :CREATE
                         :IF-EXISTS IF-EXISTS))
NIL

PPRINT-LINEAR-ELT

pprint-linear-eltは現在のインデントを設定し、すべてのcdr要素に改行を入れます。

(pprint-linear-elt nil '(asdf:component-pathname component :direction :output :if-does-not-exist :create :if-exists if-exists))
(ASDF:COMPONENT-PATHNAME COMPONENT
                         :DIRECTION
                         :OUTPUT
                         :IF-DOES-NOT-EXIST
                         :CREATE
                         :IF-EXISTS
                         IF-EXISTS))
NIL

DEFORMATTER

deformatterは、パッケージの存在とシンボルの競合、およびset-pprint-dispatchに注意を払います。

(macroexpand-1 '(deformatter package symbol (stream exp)
                  (format stream "~A" exp)))

(WHEN (FIND-PACKAGE "PACKAGE")
  (DEFUN #:PPRINT-SYMBOL3440 (STREAM EXP) (FORMAT STREAM "~A" EXP))
  (SET-PPRINT-DISPATCH
   `(CONS (MEMBER ,(UIOP/PACKAGE:FIND-SYMBOL* "SYMBOL" "PACKAGE")))
   '#:PPRINT-SYMBOL3440)
  '#:PPRINT-SYMBOL3440)

TRIVIAL-FORMATTER-USER:SET-PPRINT-DISPATCH

プリティプリント機能を一時的に設定したい場合は、 *print-pprint-dispatch*をバインドし、 cl:set-pprint-dispatchの代わりにtrivial-formatter-user:set-pprint-dispatchを使用する必要があります。そうでなければ、一時的機能は決して機能しません。

cl:set-pprint-dispatchはパッケージ:trivial-formatter-userでシャドウされています。

(deformatter sxql where (stream exp)
  (let ((*print-pprint-dispatch* (copy-pprint-dispatch)))
    (set-pprint-dispatch '(cons (member :and :or)) 'pprint-linear-elt)
    (pprint-fun-call stream exp)))

製品の目標

ライセンス

mit

開発

sbcl / 2.0.9

テスト済み

  • sbcl / 2.0.9
  • ccl / 1.12 ; cclがansi規格に違反しているために失敗しました。
  • ecl / 20.4.24

注意

trivial-formatterは、少なくとも上記の処理系で移植可能に機能します。
しかし、それは決して同じように機能することを意味するものではありません。
たとえば、「if」形式は異なります。
sbclは、要素が短い場合でも改行を出力しますが、他の処理系ではそうではない場合があります。

#+sbcl
(if a
    b
    c)

#+(or ecl ccl)
(if a b c)

既知の問題。

clisp

clispはサポートされていません。
clispの発言

The Lisp Pretty Printer implementation is not perfect yet.

ccl

現在、ansi規格に違反しているため、cclのサポートを一時的に停止しています。

? (pprint-dispatch t nil)
=> error

clhsによると

table---a pprint dispatch table, or nil.

幸い、この問題はすでに修正されています。
次のcclリリースを待つか、ソースから現在のcclをビルドしてください。

Reader。

リーダーマクロが競合する場合、そのようなリーダーマクロは黙って無視されます。
新しいリーダーマクロを追加することはできますが、既存のリーダーマクロを変更することはできません。

フォーマット制御

trivial-formatterは~newlineフォーマットコントロールのインデントを調整できません。

インストール

trivial-formatterをインストールするには、roswellをお勧めします。

$ ros install hyotang666/trivial-formatter

実行中のlisp環境にtrivial-formatterをロードするには、replで以下を評価します。

* (ql:quickload :trivial-formatter)

Discussion