Разработка языка программирования на Racket

УДК 004.42

Павлов Дмитрий Алексеевич

РАЗРАБОТКА ЯЗЫКА ПРОГРАММИРОВАНИЯ

НА RACKET

Аннотация

Статья подробно описывает процесс создания простейшего предметно-ориентированного языка на платформе Racket. Присутствуют лексический анализатор, синтаксический анализатор, интерактивная среда, компиляция, отладка, бектрейсы, подсветка синтаксиса. Созданный язык интегрируется в среду разработки DrRacket. Весь проект занимает менее 300 строк кода.

Ключевые слова: предметно-ориентированный язык, парсинг, компиляция, Racket.

Статья в 6-м выпуске журнала за 2011 г. [1] была посвящена пользе предметно-ориентированных языков и основным подходам к их созданию. В этой статье будет детально показан процесс создания предметно-ориентированного языка на основе Racket - одного из современных языков программирования, ведущего свою родословную от языка LISP.

Пара слов о Лиспе: будучи созданным 50 лет назад, он живёт и развивается до сих пор, причём последние 25 лет практически без финансирования. Это означает, что язык действительно хорош. Надо отметить, что Джон Маккарти (автор термина «искусственный интеллект», AI) изобрёл Лисп как подручное средство для создания этого самого AI. Маккарти умер, AI как не было так и нет (см. «AI Winter»), но идеи, заложенные в Лисп, в течение полувека находят применение в совершенно не связанных с AI областях. Это означает, что идеи действительно хороши.

Цель авторов Racket можно сформулировать так: дать разработчикам возможность создавать свои языки, не лишая их при этом прелестей Лиспа. «Лисп как основа и любой ваш каприз вдобавок» - звучит как предложение, от которого невозможно отказаться. В отличие от Common Lisp, где новые языки, как правило, создаются на базе грамматики S-выра-жений, в Racket для них допустима любая грамматика. Добавим к этому, что отладчик и редактор с подсветкой синтаксиса - достаются практически даром.

В качестве «подопытного кролика» для демонстрации возможностей Racket мы возьмём простейший язык, который назовём «калькулятор» (calc). Вот как выглядит программа на этом языке:

X = 2.8

Y = (X + 1) * 5 print X / Y

Кроме простого запуска программ, у нас будет пошаговое выполнение, бектрейсы и средство интерактивной разработки, так называемый read-eval-print loop, или REPL.

© Павлов Д.А., 2012

То, что относится к языкам, Racket желает видеть в одной из своих «системных» директорий, поэтому директорию, в которой мы ведём разработку (назовём её work), лучше с самого начала внести в соответствующий список. В среде DrRacket это делается следующим образом: меню Language  Choose Language  кнопка «Show Details»  раздел «Collection Paths»  Add. В директории work у нас будет директория calc, куда мы поместим файлы, относящиеся к новому языку.

Этап 1: базовый комплект

Начнём с лексического анализатора, который будет в файле calc/lexer.rkt:

#lang racket (require

; lex — стандартное средство для построения лексических анализаторов parser-tools/lex

; Библиотека регулярных выражений для lex. Символы, импортированные из неё, ; будут предваряться знаком двоеточия, в целях избежания конфликтов имён

; Указанные вещи из этого модуля идут на экспорт (provide value-tokens op-tokens

position-line position-col position-offset calc-lexer)

; Регулярные выражения для букв, цифр и всяких пробелов (define-lex-abbrevs

; Токены, имеющие значения — идентификатор

; Токены, не имеющие значений — арифметические операции, скобки, присваивание, ; операция печати (PRINT) и специальный токен EOF (define-empty-tokens op-tokens

; calc-lexer — это функция, полученная в результате вызова lexer-src-pos (define calc-lexer

; lexer-src-pos отличается от lexer тем, что выдаёт не просто токены,

; а position-token — токены с информацией об их положении в исходном файле

(lexer-src-pos

; пробелы и прочее пропускаем, рекурсивно запуская lexer дальше

; токены операций

; идентификатор = буква + [произвольный набор букв и цифр]

Теперь парсер, calc/parser.rkt:

#lang racket (require

; модуль с генератором парсеров parser-tools/yacc

; модуль, содержащий нужную нам функцию raise-read-error syntax/readerr

; наш лексический анализатор calc/lexer)

; Функция, которую мы будем вызывать при ошибке парсинга

(lambda (tok-ok? tok-name tok-value start-pos end-pos) ; генерирует исключение (raise-read-error

"Parser error" ; текст исключения source-name ; имя файла

(position-line start-pos) ; номер строки с ошибкой (position-col start-pos) ; номер колонки с ошибкой

; calc-parser — это функция, возвращающая функцию, полученную ; в результате вызова функции parser из модуля parser-tools/yacc (define (calc-parser source-name) (parser

(src-po s) ; это нужно для того, чтобы в функцию обработки ошибки

; передавалась позиция проблемного куска (start start) ; стартовый нетерминал у нас называется start (end EOF) ; конец файла у нас называется EOF

; Грамматика

; или команда + команды

; $1 и $2 обозначают соответственно первый и второй элементы

; сопоставления, то есть statement и statements

; или выражение плюс/минус токен.

; PLUS и MINUS здесь — терминалы, взятые из op-tokens из calc/lexer.

(assignment

((IDENTIFIER ASSIGN expression) (list 'assign $1 $3))) (printing

Теперь свяжем лексический и синтаксический анализаторы в одно целое с помощью функции, которая принимает некоторый входной поток (в Racket это называется «порт») и соответствующее ему имя файла и возвращает результат грамматического разбора. Правим дальше calc/parser.rkt:

; и передать в него функцию, которая должна выдавать токены один за другим

; calc-lexer как раз это и делает

Поскольку наш анализатор должен будет работать в среде Racket на тех же правах, на-которых работает «родной» анализатор, мы должны реализовать функции calc-read и calc-read-syntax, которые придут на замену стандартным read и read-syntax [2]. Окончание calc/parser.rkt:

Пояснение: в то время как calc-read возвращает прочитанные данные (datum) вида ((assign X (+ 1 2)), calc-read-syntax возвращает синтаксические объекты [3], которые суть те же самые данные, но с привязкой к участкам исходного файла и прочей информацией. В зависимости от режима работы, Racket пользуется либо read, либо read-syntax, и, во-избежание неожиданностей, лучше сделать их одинаковыми. Что и сделано: calc-read просто берёт синтаксические объекты из calc-read-syntax и превращает их в данные с помощью syntax->datum.

Настало время встроить наш анализатор в среду Racket. Это очень просто: нужно создать файл calc/lang/reader. rkt следующего содержания:

(module reader syntax/module-reader #:language ' racket ; об этом позже

#:read calc-read ; переопределение стандартного read

#:read-syntax calc-read-syntax ; переопределение стандартного read-syntax #:whole-body-readers? #t ; этот флаг устанавливается в том случае, когда наши

; read и read-syntax всегда читают входной поток до ; конца (calc-read и calc-read-syntax так и делают) (require calc/parser)) ; модуль, в котором лежат calc-read и calc-read-syntax

Racket, увидев #lang calc в начале файла, будет искать calc/lang/reader. rkt в системных директориях, включая work, где мы всё и делаем. Поэтому мы уже можем начинать писать код на calc в среде Racket. Создадим файл test. calc:

#lang calc X = 3 Y = X + 1 Print X * X

Y

pi = 3.1415926535 r = 12.2 l = 40

Print pi * r * (2

l)

File Edit View Language Racket insert Jabs Help

test.calc* (define Check Syntax C^ Debug Macro Stepper #'f Run J? Stop

1: lexer.rlct | 2: parser.rkt | 3: reader.rkt f 4: test,calc |

#lang calc

X = 3

Y = X + 1

Print |X * X + Y * Y

pi = 3.1415926535 =

r = 12.2

1 = 4G

Print pi * r * (2 + I) 3

© Parser error il

Interactions disabled. 1

Determine language from source* 4:6 ПА

Рис. 1

Запускать файл на выполнение ещё рано, так как реализации языка пока ещё нет. Но уже можно посмотреть на результат синтаксического анализа кода, для чего в Racket есть удобный инструмент под названием «Macro stepper». Нажимаем... и видим сообщение «Parser error» из нашего calc/parser.rkt (рис. 1). Ошибка состоит в том, что слово Print написано с большой буквы, тогда как лексический анализатор допускает только «print» без вариантов. Сделаем его нечувствительным к регистру (calc/lexer. rkt):

; вспомогательная функция, преобразующая строку в выражение, нечувствительное ; к регистру, например '' foo'' ->

; специализированный макрос для применения в лексическом анализаторе

(define-lex-trans lex-ci

(( id) ; здесь id — это строка, которую мы преобразуем, т.е. "print"

(with-syntax ((result (string->ci-pattern

# 'result)))))

(define calc-lexer

(lexer-src-pos

Запускаем Macro stepper вторично на test.calc и видим (рис. 2).

Парсер успешно отработал и выдал в качестве результата набор конструкций вида

(assign X 3) (assign Y

Это не является валидным кодом на Racket, что подтверждается сообщением «unbound identifier in module». Но по замыслу - полученный код и не олжен являться кодом на Racket! Для исполнения этого кода мы должны создать набор макросов, преобразующих его в код на Racket, а точнее - в синтаксические объекты. Декларация #:language в calc/lang/ reader, rkt - именно об этом. Мы написали #: language ' racket исключительно потому, что совсем ничего не писать система не позволяет. На самом деле, в этом месте нужно указать путь к модулю с макросами:

loua test.calc - DrRacket 0@0|

| File Edit View Language Racket insert Jabs Help

1 test.calc  (define.,,) Check Syntax Q^ Debug if Macro Stepper #',jf Run jf Stop|

| 1: lexer.rkt 1 2; parser,rkt 1 3; reader.rkt] 4: test.calc

#lang сale test.cafc - Macro stepper "^¡^¡рЩЩШ

X = 3 | File Edit Stepper Jabs

Y = X + 1 [ || Step j [ Step -> J [ End —> J

Print X * X + Y * Y

(module test racket Ц

pi = 3.1415926535 (#%inodule-begin

г = 12.2

1 = 40

Print pi * r * (2 + 1 (print

(plus

(assign r 12.2) i

(assign 1 40) 1

(print

(multiply

Error

expand: unbound identifier in module

assign 1

Macro hiding: | Standard | C ]

Welcome to DrRacket, version 5,1.3 [3m]. 1

Language: calc; memory limit: 128 MB.

© expand: unbound identifier in module in: assign > j

| Determine language from source* 1:0  1]

Рис. 2

(module reader syntax/module-reader #:language 'calc/language

#:read calc-read #:read-syntax calc-read-syntax #:whole-body-readers? #t

#:language-info '#

Этот модуль мы сейчас и создадим (calc/language.rkt): #lang racket (provide

; Системные вещи, на которых мы не будем заострять внимание. ; Достаточно знать, что #%module-begin и #%datum из racket ; годятся и для calc, поэтому мы переэкспортируем их без изменений #%module-begin #%datum

; Макросы для выполнения операций языка calc

assign plus minus divide multiply negate value-of print)

; Окружение = хеш-таблица со значениями переменных

; Присваивание переменной = запись в хеш-таблицу

; Обратите внимание на апостроф перед name: он делает из имени ; переменной, переданной в макрос, символ. Таким образом, из

; выдало бы ошибку

; Арифметические операции

; Получение значения переменной по имени

; Печать

Теперь можно наконец выполнить (Run) наш test. calc (рис. 3).

Запустив Macro stepper снова и выключив в нём опцию «macro hiding», мы можем наблюдать, во что в конечном итоге раскрылся наш код (рис. 4).

На этом этапе можно считать, что у нас есть парсер и компилятор. Всё это занимает 110 строк, не считая пустых строк и комментариев.

Этап 2: необходимые «примочки»

Если мы сейчас попробуем запустить отладчик (Debug) и поставить точки останова в код программы, нас ждёт неудача. Объясняется это просто: парсер в calc/parser.rkt возвращает не синтаксические объекты, а простые списки, то есть datum. Они преобразуются в синтаксические объекты позже, автоматически, но-дела это в принципе не меняет, так как информации о местоположении синтаксических единиц в исходном коде не появляется. Эта информация известна только пар-серу. Он её «забывает» везде, кроме функции on-error. И поставить точки останова невозможно, так как система просто не знает, какой синтаксический объект к какому месту исходного кода относится. Сейчас мы это исправим (calc/parser.rkt):

ггшцщ test.calc - Macro stepper « Li.SyJ

| File Edit Stepper Jabs j

1 <- Start J <- Step J Step -> || End -> j

»гМ^м'гй-йКяааа

File Edit View Language Racket insert Tabs Help

test.calc* (define

Check Syntax C^ Debug i^f Macro S

1: lexer.rkt 2: parser.rkt 3: reader.rkt 4: test,calc 5; language.rkr

#lang calc

X = 3 Y = X + 1

print X * X + Y * Y

pi = 3.1415926535 г = 12.2 I = 40

print pi r

Language: calc; memory limit: 128 MB. 25

1609.7520756534

Expansion finished (module test calc/language (#%module-begin (#%app

call-with-values

(lambda () (#%app hash-set! current-env 'X ' 3)) print-values) (#%app

call-with-values (lambda () (#%app hash-set! current-env ' Y

(#%app:35 + : .34-

(#%app:37 hash-ret:36 current-env:36 'X) 'I))) print-values) (#%app

call-with-values (lambda () (#%app print f '"-v\n" (#%app:38 + : 39

№app:40 * : 41

(#%app:42 hash-ref:43 cur rent-env:43 (#%app:44 hash-ref:45 cur rent-env:45 (#oapp:46 + : 47

X)

X))

Y)

Y)))))

(#%app:19

call-wit h-v a1ues:19

(#%app:21 hash-set! : 18 current-env: 18 'pi '3.1415926535;'; print-values:19) (#%app:23

call-with-values:23

(#%app:25 hash-set!:22 current-env:22 r '12.2)) print-values:23) (#%app:27

call-with-values:27

(lambda:27 () (#%app:29 hash-set! :26 current-env:26 'I '40;'; print-values:27) (#e.app:31

Macro hiding: Disable

Interactions disabled: calc/language does not support a REPL s

Determine language from sources

i

Рис. 4

Рис. 3

(define-syntax (build-so stx) (syntax-case stx () ((_ value start end) ; вытаскиваем из контекста (stx) $i-start-pos и $j-end-pos, где i и j — ; числа, переданные в макрос как start и end; а также source-name (with-syntax ((start-pos (datum->syntax

stx

(string->symbol (format ''$~a-start-pos''

(syntax->datum #'start))))) (end-pos (datum->syntax stx

(string->symbol (format ''$~a-end-pos''

(syntax->datum #'end))))) (source (datum->syntax stx

'source-name)))

(syntax

(datum->syntax #f

value

; конструируем уже знакомую по on-error пятёрку значений (list source

(grammar (start

; Числа 1 и 1, переданные в макрос build-so, раскрываются соответственно в ; $1-start-pos и $1-end-pos, каковые переменные доступны благодаря указанию ;

(expression

; Числа 1 и 3 раскрываются макросом build-so в $1-start-pos и $3 -end-pos

; соответственно, т.е. от начала первого нетерминала до конца третьего.

(term

(factor

(primary-expression

(assignment

(printing

И - о чудо!- теперь работает отладчик, можно ставить точки останова, ходить по шагам, даже стек вызовов имеется (рис. 5).

Racket позволяет отладчику заходить в реализацию вашего языка в лисповых функциях, если в момент отладки модули с этими функциями открыты. Но нашего calc/language. rkt это не касается, поскольку ни одной функции у нас нет - мы обошлись макросами. То есть ближайший уровень, в который может попасть отладчик после кода на calc - это реализация hash-set, + и прочих примитивных вызовов. На такой уровень нам нет необходимости опускаться.

Обратим теперь внимание на то, что для языка calc не работает REPL! Об этом честно сообщается после выполнения программы (Run) чёрным текстом на жёлтом фоне. REPL не работает, потому что в реализации (calc/language. rkt) не определён макрос #%top-interaction, который, собственно, и должен раскрывать полученные в REPL синтаксические объекты. Стандартный макрос из Racket не годится, так что мы напишем свой, очень простой:

.............'iimh-•a-waK.'Btall.'H.MiiM—i .

File Edit View Language Racket Insert Tabs Help

test.calc {define...)-' Check Syntax Debugs Macro 5tepper ajn y Stop

¡(pause Q[>Go £>| Step ^Over H^Out Stack

1: lexer.rkt j 2: parser.rkt j 3: reader;rkt j 4: test.calc | r Pi

#lang calc X = 3 Y = X + 1 Print X :> X + Y Y »1 = 3.1415926535 r = 12.2 1 = 40 Print pi * ^ * (2 + 1)| pi ... Print...

Variables

Welcome tD DrRacket. version 5.1.3 [3m], Language: calc; memory limit: 128 MB. 25

Determine language from source•* 9:22 | | jf

Рис. 5

#lang racket (provide

#%module-begin #%datum

; Переопределять #%top-interaction внутри модуля нельзя, поэтому мы ; сделаем макрос top-interaction и переименуем его при экспорте,

assign plus minus divide multiply negate value-of print)

; Макрос действительно такой — с троеточиями. Это часть синтаксиса,

Но этого недостаточно. Проблема в том, что декларации # : read и # : read-syntax в calc/reader. rkt по какой-то причине не распространяются на REPL. REPL в DrRacket принадлежит самой DrRacket и подлежит настройке отдельно, по строго определённым правилам. Нужно добавить декларацию #:language-info в calc/reader .rkt:

(module reader syntax/module-reader #:language 'calc/language #:read calc-read #:read-syntax calc-read-syntax #:whole-body-readers? #t

#:language-info '#

Затем нужно создать модуль calc/lang/lang-info. rkt такого содержания:

#lang racket/base

(define (get-info data) (lambda (key default) (case key

'(#(calc/lang/configure-runtime configure #f))) (else

default))))

Наконец, нужно создать модуль calc/lang/configure-runtime.rkt, содержащий функцию конфигурации рантайма:

#lang racket/base

; Конфигурация заключается в установке параметра current-read-interaction ;

(define (even-read source-name input-port) (beginO

(parse-calc-port input-port source-name) ; Почему-то нужно делать так, чтобы последующий вызов ; current-read-interaction вернул EOF. С этой целью производится

; нижеследующий трюк с заменой параметра на odd-read.

Вторая часть трюка заключается в замене параметра обратно на even-read. Среди разработчиков нет согласия по поводу того, правильно ли так делать; оставим этот вопрос на их совести, в любом случае все имеющиеся примеры ; работают именно так.

И вот у нас работает REPL (рис. 6).

Благодаря умной работе механизма модулей, два и более открытых одновременно окна с программами на calc не конфликтуют и не засоряют окружение друг друга (рис. 7).

Самое время заметить, что ошибки с необъявленными переменными возникают на этапе выполнения программы, тогда как во всех нормальных средах они должны отлавливаться на этапе компиляции. Полноценный компилятор мы писать не будем, ограничимся проверкой того, что все используемые переменные встретились перед использованием в левой части. Файл calc/compiler.rkt:

#lang racket

(provide compile-program

compile-statement)

Хеш-таблица имён переменных

Рис. 6

Рис. 7

(for-each check-syntax (syntax-e p)) ; вернуть исходную синтаксическую ; конструкцию как результат ; (приличный компилятор вернул бы новую,

; оптимизированную конструкцию) p)

(define (check-syntax s) ; Что за объект?

; Преобразуем в данные и посмотрим,

; Если нет — ошибка со ссылкой на соответствующий синтаксический объект (raise-syntax-error

#f "access to unassigned variable"

Попросим парсер вызывать наш «компилятор» перед возвращением результата

(calc/parser.lisp):

(require parser-tools/yacc

syntax/readerr

calc/lexer

calc/compiler)

(compile-program

Насладимся результатом ( рис. 8).

Попросим REPL делать то же самое (calc/lang/configure-runtime. rkt):

(require calc/parser

calc/compiler)

(define (even-read source-name input-port) (beginO

Ш

File Edit View Language Racket insert labs Help

test.calc»- (define ..,5 Check Syntax Debug ф Macro Stepper #'^¡1 Run jf 1: lexer,rkt | 2: parser,rkt | 3: reader.rkt 4: test.calc |

#lang сale X = 3 Y = X + 1

Print X * X + Y * Y

Print foo

pi = 3.1415926535 r = 12.2 1 = 40

Print pi * r *

Welcome to DrRacket. version 5,1,3 [3m].

Language: calc; memory limit: 128 MB.

Module Language: invalid module text

0 © foo: access to unassigned variable in: foo

Interactions disabled.

Determi ne I anguage from source-w 7:0

Di

Рис. 8

Насладимся результатом и здесь (рис. 9).

Отметим, что ошибки времени выполнения всё же случаются и снабжены бектрейсом (рис. 10).

На данный момент у нас есть всё необходимое, а написали мы всего 208 строк кода. Этап 3: элементы «роскоши»

Заметим небольшое неудобство: REPL не позволяет переводить строку. Нажатие Enter ведёт к немедленному отправлению строки на синтаксический анализ. Мы собирались дописать команду на следующей строке, но REPL безжалостен (рис. 11).

Хорошая новость: поведение REPL можно настроить. Открываем снова

calc/lang/lang-info.rkt:

(define (get-info data) (lambda (key default) (case key

'

(dynamic-require 'calc/tool/submit 'repl-submit?)) (else

default))))

и создаём модуль calc/tool/submit. rkt с функцией repl-submit?, которая возвращает #t, если перевод строки завершающий, и #f, если промежуточный. Определить это не так просто, учитывая, что пользователь может вводить в REPL всё, что ему заблагорассудится. Будем считать, что строка готова к синтаксическому анализу, если она не пустая, не завершается оператором (+, -, /, *, =, print) и если скобки в ней сбалансированы. Весьма уместно использовать уже готовый лексический анализатор для выполнения этой задачи:

#lang racket

(require calc/lexer

parser-tools/lex)

File Edit View Language Racket ¡nsert labs Help

test.calc-»- (defins ,,.)» Check Syntax ^ Debug yj Macro Stepper #'1 Run 4е

1: lexer.rkt j 2: parser.rkt j 3: reader.rkt | 4: test.calc |

#lang calc X = 3 Y = X + 1 Print X * X + Y * Y

pi = 3.1415926535 r = 12.2 1 = 40 Print pi * r *

Welcome to DrRacket, version 5.1.3 [3m]. Language: calc; memory limit; 128 MB. 25 1609.7520756534 > print X + foo Q © foo: access to unassigned variable in: foo > |

Determine language from source-» 7:2 [~~J jj

Рис. 9

©QU test.calc - DrRacket* |T][S][T]

File Edit View Language Racket Insert Jabs Help

test.calc-»- (define .,,)-»• Save H Check Syntax C^ Debugs Macro Stepper F

1; lexer.rkt | 2; parser.rkt |3; reader.rkt  4: test.calc |

#lang calc X = 3 Y = X + 1 F

an ППП File Edit labs Help

Print X * X + Y * Y S—4 / (Y - X - 1) + 8 /: division by zero /home/dpavlov/calc/test.calc: 6:4

pi = 3.1415926535 |Z = 4 / (Y - X - 1) + 8

r = 12.2 I = 40 /home/dpavlov/calc/test.calc: 6:4

Print pi * r * (2 + I) |Z = 4 / (Y - X - 1) + 8 -

/home/dpavlov/calc/test.calc: 6:0

|Z = 4 / (Y - X - 1) + 8 -

Welcome to DrRacket. version 5.1,

li_Ш

Language: calc; memory limit: 128 Mb. 25 У Ô /: division by zero >

Determine language from source-»'

Рис. 10

(define (repl-submit? ip has-white-space?) (let loop ( (blank? #t) ; строка пустая?

(pending-op? #f) ; строка завершается оператором? (paren-count 0)) ; баланс скобок ; Все ошибки лексического анализатора мы обязаны пропускать (with-handlers ((exn:fail:read?

(loop #f #f (- paren-count 1))) (else

Вроде работает (рис. 12).

Почти всё. Осталось только добавить подсветку синтаксиса. Ну и, например, комментарии, чтобы было что подсвечивать. Пусть комментарии будут в стиле bash - от символа «#» до конца строки. Обновляем calc/lexer.rkt, попутно экспортируя из него некоторые вещи, которые скоро понадобятся:

(provide value-tokens op-tokens

position-line position-col position-offset calc-lexer

lex:comment lex:identifier lex:number lex-ci)

File Edit View Language Racket insert Jabs Help

test.calc* (define Check Syntax C^ Debugs Macro Stepper #'|f Run Д

1: lexer.rkt | 2: parser.rkt j 3: reader.rkt | 4: test.calc j 5 submit.rkt 1 6: lang-info,rkt

#lang calc -

X = 3

Y = X + 1

Print X * X + Y Y

pi = 3.1415926535

r = 12.2

1 = 40

Print pi r *

Language: calc; memory limit: 1 28 MB *

25

1609.7520756534

> X =

^ & Parser error > 1 -

Determine language from source*

Рис. 11

plfl 1

File Edit View Language Racket insert labs Help

test.calc*- {define Check Syntax C^. Pebug ^ Macro Stepper #'4 Run

1: lexer.rkt | 2 parser.rkt j 3: reader.rkt | 4: test.calc submit.rkt j 6: lang-info,rkt

#lang calc

X = 3

Y = X + 1

Print X * X + Y * Y

pi = 3.1415926535

r = 12.2

1 = 40

Print pi r (.2+1) -

> print -

x*x -

Y + (

X + 1 -

7

Z =

X / Y

> 1

Determine language from source* 14:2

Рис. 12

(define-lex-abbrevs

(define calc-lexer (lexer-src-pos

; комментарии пропускаем так же, как и пробелы

'' _ ''

'' _ ''

'' * ''

'') ''

Информация о подсветке синтаксиса задаётся определённым образом в директиве #: info файла calc/lang/reader.rkt:

(module reader syntax/module-reader #:language 'calc/language #:read calc-read #:read-syntax calc-read-syntax #:whole-body-readers? #t

#:language-info '#(calc/lang/lang-info get-info #f) #:info (lambda (key defval default) (case key

Собственно подсветку осуществляет отдельный модуль calc/tool/syntax-color.rkt. В нём находится, по сути, ещё один лексический анализатор, но такой, который выдаёт не-токены, а особые «пятёрки» значений: лексема, её тип (комментарий/константа/ключевое слово/символ/ete), «скобочность», а также начало и конец лексемы в исходном файле:

#lang racket

(require parser-tools/lex

calc/lexer)

(syn-val lexeme 'whitespace #f start-pos end-pos)) (lex:comment

(syn-val lexeme 'comment #f start-pos end-pos)) (lex:number

(syn-val lexeme 'keyword #f start-pos end-pos)) ; Имена переменных у нас будут идентфикаторами (lex:identifier

Внимание: чтобы подсветка синтаксиса заработала, надо перезапустить Racket. Если вдуматься, это значит, что при выполнении всех предыщущих операций Racket не надо бышо перезапускать! И даже test.calc можно было не закрывать. При том что мы там «резали по живому» во многих местах. Racket истинно динамическая платформа. Вот что получится после перезапуска (рис. 13).

Подсветка синтаксиса в действии. В REPL, правда, она не работает (то есть работает, но не наша, а стандартная), но не будем придираться к мелочам. Racket -прекрасная вещь. Всё вышеописанное мы сделали, написав 268 строк кода. То, что получилось, можно скачать по ссылке [4]. Вообще-то можно ещё сделать так, чтобы язык calc включался без #lang calc, и ещё можно научить DrRacket смотреть переменные в отладчике, и ещё можно собрать DrRacket, кастомизированный под calc и готовый к распространению, но обо всём этом как-нибудь в другой раз.

Рис. 13

Литература

1. Павлов Д.А. Создание предметно-ориентированных языков // Компьютерные инструменты в образовании, 2011. № 6. С. 57-60.

2. http://docs.racket-lang.org/reference/Reading.html (дата обращения: 30.10.2012).

3. http://docs.racket-lang.org/reference/svntax-model.html (дата обращения: 30.10.2012).

4. https://github.com/kugelblitz/calc (дата обращения: 30.10.2012).

Abstract

The article gives a detailed explanation of how to create a simple domain-specific language (DSL) on the Racket development platform. There is a lexical analyzer, parser, REPL, compiler, debugger, backtraces, syntax highlighting. The new language is integrated into the DrRacket IDE. The whole project is less than 300 lines of code.

Keywords: domain-specific language, parsing, compilation, Racket.

© Наши авторы, 2012. Our authors, 2012.

Павлов Дмитрий Алексеевич, научный сотрудник Института прикладной астрономии РАН, dpavlov@ipa.nw.ru