週刊nmosh - 低レベル構文プリミティブを考える会

nmoshのexpanderは積極的には低レベルマクロをサポートしないようにしたいので、適当なプリミティブを用意する必要がある。
低レベルマクロをサポートしないのは、1) 低レベルマクロのエラーによって生成されたdiagが通常の人間には理解しづらいため 2) syntax-case処理系とexplicit-renaming処理系とdefine-macro処理系の全部を常識的なパフォーマンスでサポートする必要が有るため(= 標準ライブラリのポータビリティを確保するため) 3) 副作用の無い安全なライブラリ構文であることを保証するため。

シンボルのinject

syntax-rulesで記述できない代表的な操作としては識別子の生成が有る。このため、シンボルのconcat機能を持つマクロ define-inject-syntax を用意する。

  • aifの実装例
(define-syntax aif0/body
  (syntax-rules ()
    ((_ k sym (obj true false))
     (k obj (if sym true false)))))

(define-inject-syntax
  aif0
  ("it")
  aif0/body)

(define-syntax aif
  (syntax-rules ()
    ((_ obj true false)
     (aif0 (obj true false)))
    ((_ obj true)
     (aif obj true (values)))))

define-inject-syntaxは、ここでは中間マクロ aif0 を定義するのに使用している。aif0はシンボル it を定義した状態で、シンボル挿入用のマクロを生成して次のCPSマクロ aif0/body を呼ぶ。
aif0/body は、シンボル自体を表わす識別子 sym と、構文規則を曲げてコードを挿入するためのマクロ k を受け取り、コードを組み立てた上でマクロ k に渡す。マクロ k は渡されたコードから文脈情報を取り去り、そのまま挿入することで syntax-rules のような健全なマクロでは不可能な変換を行っている。
(もっともnmoshやyuniはaifを提供しない。HyperTalkみたいで面白いが役に立たない。)

  • R6RS(syntax-case)版

syntax-caseがこの手の低レベルマクロを一番書きにくい(個人の感想です)。https://github.com/okuoku/yuni/blob/3aac1febf4f99a95cc89c5a167a0ac325a75257e/lib-compat/r6rs-common-yuni/compat/macro/primitives0.sls

(library (r6rs-common-yuni compat macro primitives0)
         (export syntax-inject)
         (import (for (rnrs) run (meta -1)))

(define-syntax syntax-inject
  (lambda (stx)
    (define (conv lis)
      (map (lambda (s)
             (cond ((symbol? s) (symbol->string s))
                   ((string? s) s)
                   (syntax-violation
                     #f
                     "Invalid object as part of identifier" s )))
           lis))

    (syntax-case stx ()
      ((_ (x ...) k)
       (with-syntax ((total (datum->syntax
                              #'none
                              (string->symbol
                                (apply string-append
                                       (conv (syntax->datum #'(x ...))))))))
         #'(lambda (h)
             (syntax-case h ()
               ((out param)
                (with-syntax ((name (datum->syntax #'out 'total)))
                  #`(let-syntax ((b (lambda (y)
                                      (syntax-case y ()
                                        ;; Definition
                                        ((_ obj)
                                         (datum->syntax #'out
                                                        (syntax->datum #'obj)))
                                        ;; Binding
                                        ((_ binding obj)
                                         ;; Strip syntactic information
                                         (let ((bind (syntax->datum #'binding))
                                               (prog (syntax->datum #'obj)))
                                           (datum->syntax
                                             #'out
                                             `(letrec ((name ,bind))
                                                ,prog))))))))
                      (begin
                       (k b name param))))))))))))

)

(library (r6rs-common-yuni compat macro primitives)
         (export define-inject-syntax)
         (import
           (rnrs)
           (for (r6rs-common-yuni compat macro primitives0) run expand))

(define-syntax define-inject-syntax
  (syntax-rules ()
    ((_ name sym k)
     (define-syntax name (syntax-inject sym k)))))

)

R6RSでは、nmoshのようにexplicit phasingを採用している処理系のために2段階の実装が必要となる。まず、マクロ変換器を生成する syntax-inject 構文をメタレベル -1 にエクスポートし、本番の構文定義 define-inject-syntax ではメタレベル 1 (= expand)にインポートして使う。 ...通常の人間には難しい気がする。
syntax-caseでは、let-syntaxして生成した変換器を適用せずに出力できる。このため、chibi-schemeGaucheのように中間マクロを定義する必要は無く、syntax-injectをsyntax-rulesのような他の変換器と同様に使用できる。(ただし、syntax-injectは他のSchemeでは実装できないのでyuniのライブラリとしては採用していない。)

  • chibi-schemeのer-macro-transformer版
(library (chibi-yuni compat macro primitives)
         (export define-inject-syntax)
         (import (chibi) (chibi match))

(define-syntax define-inject-syntax
  (syntax-rules ()
    ((_ nam syms orig-k)
     (expand-syms syms orig-k nam))))

(define-syntax expand-syms
  (er-macro-transformer
    (lambda (stx here _0)
      (define (conv lis)
        (map (lambda (s)
               (cond ((symbol? s) (symbol->string s))
                     ((string? s) s)
                     (else
                       (error "Invalid object as part of identifier" s))))
             lis))
      (match stx
             ((_ (x ...) orig-k nam)
              (let ((sym (string->symbol (apply string-append (conv x))))
                    (k (here 'syntax-inject-entry)))
               `(,k ,sym ,orig-k ,nam)))))))

(define (strip sexp)
  (cond ((pair? sexp)
         (cons (strip (car sexp))
               (strip (cdr sexp))))
        ((vector? sexp)
         (vector-map strip sexp))
        ((identifier? sexp)
         (identifier->symbol sexp))
        (else sexp)))

(define-syntax syntax-inject-entry
  (syntax-rules ()
    ((_ sym k name)
     (begin
       (define-syntax b
         (er-macro-transformer
           (lambda (y here _2)
             (match y
                    ((_ obj)
                     (strip obj))
                    ((_ bind prog)
                     `(,(here 'letrec)
                        ((sym ,bind))
                        ,(strip prog)))))))
       (define-syntax name
         (syntax-rules ()
           ((_ param)
            (k b sym param))))))))
)

yuniでは、処理系毎のcompatライブラリであっても可能な限りyuni独自のR6RS-light形式のライブラリとして記述している。
chibi-schemeのer-macro-transformerでは、syntax-caseと異なりマクロの展開を遅延させる簡単な方法が無い(stripは本当に構文情報を全て取り去ってしまう)。このため、中間マクロ b を定義して挿入する方法を取っている。

実はまだ動くものが作れていない

(define-module yuni-runtime.gauche.macro-primitives
               (use gauche)
               (export define-inject-syntax))

(select-module yuni-runtime.gauche.macro-primitives)

(define (conv lis)
        (map (lambda (s)
               (cond ((symbol? s) (symbol->string s))
                     ((string? s) s)
                     (else
                       (error "Invalid object as part of identifier" s))))
             lis))

(define-macro (expand-syms k b syms param)
  (define sym-value (conv syms))
  (list k b sym-value param))

(define-syntax define-inject-syntax
  (syntax-rules ()
    ((_ nam syms k)
     (begin
       (define sym-value (conv 'syms))
       (define-macro b
         (case-lambda
           ((form)
            form)
           ((bind prog)
            `(letrec ((,sym-value ,bind)) ,prog))))
       (define-syntax nam
         (syntax-rules ()
           ((_ param)
            (expand-syms k b syms param))))))))

まず、Gaucheは今のところ define-syntax で不健全なマクロを定義する方法が無い。このため、define-macroを使用する必要が有り、そのため syntax-inject のように変換器を返すインターフェースを採用できない。(つまり、let-syntax等と組み合わせて使用することができない。)
より重要な問題は、これは伝統的マクロなので使用される環境に letrec 等の定義が無いと正しく実行できない。
このコードは動作しない。デバッグ中。

oku@spring ~/repos/yuni
$ run/gosh.sh check.sps
*** ERROR: Compile Error: Compile Error: Compile Error: symbol required, but got #<identifier yuni.base.shorten#^body>
"lib-stub/gauche/yuni/base/shorten.scm":1:(define-library (yuni base shorten)  ...

"lib-stub/gauche/yuni/scheme.scm":1:(define-library (yuni scheme) (expor ...

"./check.sps":1:(import (yuni scheme) (yuni compat m ...

Stack Trace:
_______________________________________

inject以外

inject以外は必要性が微妙なので後で考える。考えられるのは:

  • invalid-syntax

nmoshやyuniでは補助構文はboundであることがルールなので、補助構文を宣言するための専用構文が必要だと考えている。"xxxは単体で使用できません。yyy構文の中で使用してください。"のような多少親切なエラーメッセージを出すために必要。

  • キーワードオブジェクトを補助構文として使用できるsyntax-rules/keyword

これは以前書いた - http://d.hatena.ne.jp/mjt/20140315/p1

  • 文字列をキーワードとして扱う、グループ化に対応したsyntax-rules-group

通常のSchemeにはキーワードオブジェクトが存在しないため、補助構文ではなくキーワードを受けいれたいマクロや、ループを含む複雑なマクロでは文字列(や、数値)によるキーがよく使われる。(例えば、R7RSのparameterize等 - http://d.hatena.ne.jp/mjt/20130708/p1 )
キーワードに対応した処理系であれば、syntax-rulesのclause中の文字列をそのままキーワードに変換することで目的は(ほぼ)達成できる。そうでない処理系は、補助構文を生成してそれに置き換える必要が有る。

  • 最適化ヒント構文

define-inlineとか必要かもしれない。他に、format文字列のようなpre-parseの可能性が有るものを修飾する等。

先週やったこと / 今週やること

パーサの続き... アレ意外と難しくて。。

nmosh> #i#x1
1.0
nmosh> #x#i1
1.0
nmosh> #x#if
15.0
nmosh>

いわゆるexactness指定子が基数指定よりも後ろに来ても良いことに気付いていなかったので状態を切りなおす必要が有った。