Какова цель синтаксических объектов в схеме?

Я пытаюсь написать небольшой язык, похожий на схему, на python, чтобы попытаться лучше понять схему.

Проблема в том, что я застрял на объектах синтаксиса. Я не могу их реализовать, потому что не очень понимаю, для чего они и как они работают.

Чтобы попытаться понять их, я немного поигрался с объектами синтаксиса в DrRacket.

Из того, что я смог найти, оценил #'(+ 2 3) ничем не отличается от оценки '(+ 2 3), за исключением случая, когда есть лексический + переменная затенения в пространстве имен верхнего уровня, в этом случае (eval '(+ 2 3)) все еще возвращается 5, но (eval #'(+ 2 3)) просто выдает ошибку.

Например:

(define (top-sym)
  '(+ 2 3))
(define (top-stx)
  #'(+ 2 3))
(define (shadow-sym)
  (define + *)
  '(+ 2 3))
(define (shadow-stx)
  (define + *)
  #'(+ 2 3))

(eval (top-sym)), (eval (top-stx)), а также (eval (shadow-sym)) все возвращаются 5, в то время как (eval (shadow-stx)) выдает ошибку. Никто из них не вернулся 6,

Если бы я не знал лучше, я бы подумал, что единственная особенность синтаксических объектов (помимо тривиального факта, что они хранят расположение кода для лучшего сообщения об ошибках) - это то, что они выдают ошибку при определенных обстоятельствах, когда их аналоги символа вернули бы потенциально нежелательное значение.

Если бы история была такой простой, не было бы реального преимущества использования объектов синтаксиса по сравнению с обычными списками и символами.

Итак, мой вопрос: что мне не хватает в синтаксических объектах, которые делают их такими особенными?

1 ответ

Решение

Синтаксические объекты являются хранилищем лексического контекста для базового компилятора Racket. Конкретно, когда мы входим в программу, как:

#lang racket/base
(* 3 4)

Компилятор получает объект синтаксиса, представляющий весь контент этой программы. Вот пример, позволяющий нам увидеть, как выглядит этот синтаксический объект:

#lang racket/base

(define example-program 
  (open-input-string
   "
    #lang racket/base
    (* 3 4)
   "))

(read-accept-reader #t)
(define thingy (read-syntax 'the-test-program example-program))
(print thingy) (newline)
(syntax? thingy)

Обратите внимание, что * в программе имеет представление времени компиляции в качестве объекта синтаксиса в thingy, И на данный момент, * в thingy понятия не имеет, откуда он взялся: у него еще нет обязательной информации. Именно в процессе расширения, во время компиляции компилятор ассоциирует * в качестве ссылки на * из #lang racket/base,

Нам будет легче это увидеть, если мы будем взаимодействовать с вещами во время компиляции. (Примечание: я намеренно избегаю говорить о eval потому что я хочу избежать путаницы в обсуждении того, что происходит во время компиляции и во время выполнения.)

Вот пример, который позволит нам больше узнать о том, что делают эти объекты синтаксиса:

#lang racket/base
(require (for-syntax racket/base))

;; This macro is only meant to let us see what the compiler is dealing with
;; at compile time.

(define-syntax (at-compile-time stx)
  (syntax-case stx ()
    [(_ expr)
     (let ()
       (define the-expr #'expr)
       (printf "I see the expression is: ~s\n" the-expr)

       ;; Ultimately, as a macro, we must return back a rewrite of
       ;; the input.  Let's just return the expr:
       the-expr)]))


(at-compile-time (* 3 4))

Мы будем использовать макрос здесь, at-compile-time Позвольте нам проверить состояние вещей во время компиляции. Если вы запустите эту программу в DrRacket, вы увидите, что DrRacket сначала компилирует программу, а затем запускает ее. Как он компилирует программу, когда он видит использование at-compile-time, компилятор вызовет наш макрос.

Так что во время компиляции мы увидим что-то вроде:

I see the expression is: #<syntax:20:17 (* 3 4)>

Давайте немного пересмотрим программу и посмотрим, сможем ли мы проверить identifier-binding идентификаторов:

#lang racket/base
(require (for-syntax racket/base))

(define-syntax (at-compile-time stx)
  (syntax-case stx ()
    [(_ expr)
     (let ()
       (define the-expr #'expr)
       (printf "I see the expression is: ~s\n" the-expr)
       (when (identifier? the-expr)
         (printf "The identifier binding is: ~s\n" (identifier-binding the-expr)))

       the-expr)]))


((at-compile-time *) 3 4)

(let ([* +])
  ((at-compile-time *) 3 4))

Если мы запустим эту программу в DrRacket, мы увидим следующий вывод:

I see the expression is: #<syntax:21:18 *>
The identifier binding is: (#<module-path-index> * #<module-path-index> * 0 0 0)
I see the expression is: #<syntax:24:20 *>
The identifier binding is: lexical
12
7

(Кстати: почему мы видим выход из at-compile-time впереди? Потому что компиляция выполняется полностью до выполнения! Если мы предварительно скомпилируем программу и сохраним байт-код с помощью raco make, мы не увидим, как компилятор вызывается при запуске программы.)

К тому времени, когда компилятор доходит до использования at-compile-time он знает, как связать соответствующую информацию лексического связывания с идентификаторами. Когда мы проверяем identifier-binding в первом случае компилятор знает, что он связан с конкретным модулем (в этом случае #lang racket/base что то, что это module-path-index дело о). Но во втором случае он знает, что это лексическая привязка: компилятор уже прошел через (let ([* +]) ...) и поэтому он знает, что использует * вернуться к привязке, установленной let,

Компилятор Racket использует объекты синтаксиса для передачи такой информации о привязке клиентам, например, нашим макросам.


Пытаясь использовать eval проверка такого рода вещей чревата проблемами: информация о привязке в объектах синтаксиса может быть неактуальной, потому что к тому времени, когда мы оцениваем объекты синтаксиса, их привязки могут ссылаться на вещи, которых не существует! По сути, это причина, по которой вы видели ошибки в своих экспериментах.

Тем не менее, вот один пример, который показывает разницу между s-выражениями и объектами синтаксиса:

#lang racket/base

(module mod1 racket/base
  (provide x)
  (define x #'(* 3 4)))

(module mod2 racket/base
  (define * +) ;; Override!
  (provide x)
  (define x  #'(* 3 4)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;

(require (prefix-in m1: (submod "." mod1))
         (prefix-in m2: (submod "." mod2)))

(displayln m1:x)
(displayln (syntax->datum m1:x))
(eval m1:x)

(displayln m2:x)
(displayln (syntax->datum m2:x))
(eval m2:x)

Этот пример тщательно построен так, что содержимое объектов синтаксиса относится только к связанным с модулем вещам, которые будут существовать в то время, когда мы используем eval, Если бы мы немного изменили пример,

(module broken-mod2 racket/base
  (provide x)
  (define x  
    (let ([* +])
      #'(* 3 4))))

тогда вещи ужасно ломаются, когда мы пытаемся eval x это выходит из broken-mod2, поскольку объект синтаксиса ссылается на лексическую привязку, которая не существует к тому времени, когда мы eval, eval это сложный зверь.

Другие вопросы по тегам