Если, как вы говорите, выражения, которые вы пытаетесь проанализировать, могут иметь несбалансированные скобки, то эти выражения явно живут в некоторых неструктурированных данных, которые почти наверняка представляют собой последовательность символов какого-то типа. Поскольку синтаксис выражений, которые вы хотите прочитать, является подмножеством синтаксиса Scheme, вы можете просто использовать инструмент, который предоставляет вам язык: read
: вы определенно не хотите писать свои собственный синтаксический анализатор для этого, если вы не любите тратить много времени на получение правильных угловых случаев.
Обратите внимание, что любой синтаксический анализатор, который превращает последовательность символов в какой-либо объект, должен в какой-то момент запросить следующий символ из последовательности. В ответ на это могут произойти две вещи:
- он может получить символ, возможно, после ожидания чего-то на другом конце этой последовательности — возможно, человека — для создания персонажа;
- он может получить указание на то, что символов больше нет — своего рода индикатор конца файла.
Никакая магия, которую вы можете сделать, не может избежать этого. Таким образом, описанная вами проблема с читателем, ожидающим закрывающей скобки, не реальна: любой парсер, который вы пишете, будет иметь ту же проблему при чтении из интерактивного потока: ему просто нужно дождаться человеческого на другом конце этого потока, чтобы либо ввести что-то, либо указать, что поток завершен, и в этот момент он может решить, было ли то, что он видел, правильным или нет.
Таким образом, ответ на часть вашей проблемы, связанную с превращением последовательностей символов в объект, заключается в использовании read
, предоставляемого языком. Вот один из способов сделать это — поскольку вы указали, что используете Racket, для любого n используется Racket, а не схема RnRS. Почти весь этот код не понадобился бы, если бы вы не позаботились о некотором ограничении средства чтения (и я не уверен, что ограничил его достаточно). Остальная часть связана с обработкой исключений из модуля чтения и преобразованием их в несколько значений.
(define (read-thing source)
;; Read some object from a source port.
;; Return two values: either the object read and #t,
;; or some exception object and #f if something went wrong.
;;
;; This tries to defang the reader but I am not sure it does enough
(with-handlers ([exn? (λ (e) (values e #f))])
(call-with-default-reading-parameterization
(thunk
(parameterize ([read-accept-lang #f]
[read-accept-reader #f])
(values (read source) #t))))))
(define (read-thing-from-file f)
;; read a thing from a file
(call-with-input-file f read-thing))
(define (read-thing-from-string s)
;; read a thing from a string
(call-with-input-string s read-thing))
Итак, теперь мы можем попробовать это:
> (read-thing-from-string "foo")
'foo
#t
> (read-thing-from-string "(foo")
(exn:fail:read:eof
"read: expected a `)` to close `(`"
#<continuation-mark-set>
(list (srcloc 'string #f #f 1 1)))
#f
Как видите, второе значение сообщает вам, вызвало ли read
исключение или нет.
Теперь вы можете использовать этот считыватель вещей, чтобы накормить оценщика. Например, вы можете использовать такую функцию:
(define (read-and-evaluate reader source
evaluator environment)
;; Read something with a read-thing compatible reader from source
;; and hand it to evaluator with environment as a second argument
;; If read-thing indicates an error then simply raise it again.
(call-with-values
(thunk (reader source))
(λ (thing good)
(when (not good)
(raise thing))
(evaluator thing environment))))
Так что теперь проблема сводится к написанию evaluator
, чего я делать не собираюсь.
Вот пример использования этой функции с тривиальным оценщиком, который просто возвращает заданную форму.
> (read-and-evaluate
read-thing-from-string "(foo)"
(λ (form env) form) #f)
'(foo)
> (read-and-evaluate
read-thing-from-string "(foo"
(λ (form env) form) #f)
; string::1: read: expected a `)` to close `(` [,bt for context]
Конечно, вся эта штука с обработкой исключения в считывателе здесь не нужна, так как все, что я в конечном итоге делаю, это повторно поднимаю его, но это действительно показывает вам, как вы можете обрабатывать такую ошибку.
Примечание о написании оценщика. Заманчиво сказать, что (+ 1 2)
является допустимым выражением Scheme, а в Scheme есть eval
, поэтому просто используйте его. Это похоже на использование термоядерного устройства для сноса дома: оно хорошо сносит дом, но имеет нежелательные последствия, такие как гибель сотен тысяч людей. Учтите, например, что (system "rm -rf $HOME")
является также допустимым выражением Racket, но вы можете не захотеть запускать его.
Чтобы избежать такой атаки с внедрением кода, вы хотите максимально ограничить оценщик, чтобы он оценивал только интересующий вас язык. То же самое относится к считывателю: над полный читатель Racket может, я почти уверен, оценить произвольный код Racket во время чтения: я пытался (но, вероятно, потерпел неудачу) обезвредить его выше.
Racket имеет значительный набор инструментов, которые позволяют вам определить более ограниченный язык, для которого eval
будет безопасным. Однако для действительно простого языка, такого как вычисление арифметических выражений, почти наверняка проще просто написать собственный вычислитель. Это, безусловно, более познавательно.
person
Community
schedule
26.10.2020