Језичка лабораторија у Ракету (3. део)

Аутор: Лука Хаџи-Ђокић

Да се подсетимо: у претходном делу овог серијала, позабавили смо се лексичком и синтаксном анализом. Већи део посла под хаубом урадио је brag пакет, у облику lexer-srcloc функције коју смо користили у изради лексера и у облику #lang brag језика за изражавање граматике и аутоматско генерисање абстрактног синтаксног стабла на основу ње.

Оно што сада имамо јесу три датотеке или модула нашег преводиоца. У фасцикли libre-lang налазе се lexer.rkt, tokenizer.rkt и parser.rkt. У њима смо дефинисали одређене функције, али, иако смо прошли кроз неколико примера, те функције још увек нисмо покренули и испробали. Време је да направимо тестове.

Тестирање лексера

У директоријумu tests, можемо направити датотеку lexer-test.rkt и почети са радом.

#lang racket
(require libre-lang/lexer brag/support rackunit)

(define (lex str)
   (apply-lexer libre-lexer str))

Опет програмирамо у ракету, па то назначавамо са #lang racket и увозимо неколико потребних пакета. Лексер који смо већ направили, brag/support, који нам је познат, и rackunit – пакет за јединично тестирање (енг. unit testing).

Онда дефинишемо помоћну функцију lex која примењује наш лексер на ниску коју јој проследимо.

Остатак програма је праволинијски.

(check-equal? (lex "") empty)

(check-equal?
   (lex "// komentar\n")
   (list (srcloc-token (token 'COM "// komentar")
                            (srcloc 'string 1 0 1 10))
          (srcloc-token (token "\n" #:skip? #t)
                            (srcloc 'string 1 10 11 1))))

(check-equal?
   (lex "print")
   (list (srcloc-token (token "print" "print")
                            (srcloc 'string 1 0 1 5))))

(check-equal?
   (lex "42")
   (list (srcloc-token (token 'INTEGER 42)
                            (srcloc 'string 1 0 1 2))))

(check-equal?
   (lex "\"Zdravo svete!\"")
   (list (srcloc-token (token 'STRING "Zdravo svete!")
                            (srcloc 'string 1 0 1 15))))

(check-equal?
   (lex "identifikator")
   (list (srcloc-token (token 'ID 'identifikator)
                            (srcloc 'string 1 0 1 10))))

Састоји се из неколико провера функцијом check-equal?, којима упоређујемо резултат функције lex над неком ниском са листом токена коју хоћемо да добијемо. Токени су упаковани у структуру srcloc-token, која чува и локацију лексеме. Локација нам за сада није битна, али касније ћемо моћи да је искористимо како бисмо правилно означили грешке у програмима на нашем језику.

Сада можемо покренути ове тестове командом:

raco test tests

Тестови за парсер и тестови за експандер, који ћемо правити у овом делу, конструишу се на сличан начин, и можете их наћи у Гитхаб репозиторијуму пројекта.

Експандер

Прођимо кроз пример са краја претходног дела.

[init foo 42+7*7]

лексером и парсером претвара се у следећи С-израз:

(libre-prog (libre-init foo (libre-sum 
                                   (libre-sum (libre-prod 42)) "+" (libre-prod 
                                                                    (libre-prod (libre-prod 7) "*" 7) "*" 7))))

Тај С-израз је, по својој синтакси, поптуно валидан израз у Ракету. Међутим, у Ракету нису унапред дефинисане функције libre-prog, libre-init, libre-sum i libre-prod. У експандеру ћемо урадити баш то.

Док радимо на овом делу преводиоца, од велике користи биће нам Ракетов макро (енг. macro) систем. Макро служи да се (у време компилације) један С-израз замени другим С-изразом. На пример, функцију сабирање, можемо дефинисати овако:

(define (sabiranje x y)
   (+ x y))

Када покренемо програм, дефинише се функција или процедура sabiranje, која прима 2 аргумента и сабира их. Можемо је и испробати у РЕПЛ-у.

> (sabiranje 5 6)
11

Међутим, такође можемо sabiranje уз помоћ макроа претворити у валидан Ракет израз.

(define-syntax sabiranje
   (syntax-rules ()
      [(sabiranje) 0]
      [(sabiranje x y) (+ x y)]
      [(sabiranje x ...) (+ x ...)]))

Сада се сваки израз који одговара неком од шаблона које смо дефинисали у оквиру макроа сабирање претвара у одговарајући Ракет израз. Предност макроа лежи у чињеници да се макрои „примењују” у време превођења, уместо када покренемо програм. То нам у општем случају може убрзати извршавање програма (јер смо део извршавања урадили у току превођења), али нам и, због информација које су доступне у време превођења, отварају могућности које у време извршавања немамо.

У нашем експандеру користићемо мало моћнији конструктор макроа – syntax-case.

Погледајмо, на пример, макро за libre-sum, дефинисан у датотеци expander.rkt:

(define-syntax (libre-sum caller-stx)
   (syntax-case caller-stx ()
      [(libre-sum x) (syntax-protect (syntax/loc caller-stx x))]
      [(libre-sum x "+" y) (syntax-protect (syntax/loc caller-stx (+ x y)))]
      [(libre-sum x "-" y) (syntax-protect (syntax/loc caller-stx (- x y)))]
      [else
        (syntax-protect (raise-syntax-error
          'sum-error
         (format "wrong operator in: ~a" caller-stx)))]))

Са caller-stx означавамо синтаксни објекат који анализирамо. Ако је он облика (libre-sum x), претварамо га (уз syntax-protect i syntax-loc којима опет чувамо податке о локацији израза) у x. Ако је облика (libre-sum x “+” y), претварамо га у (+ x y) и слично у случају одузимања. Ако је облика који се разликује од неког од ова три, враћамо синтаксну грешку функцијом raise-syntax-error.

На сличан начин дефинишемо и libre-prod, за множење и дељење, као и libre-bool, за операције поређења.

За исписивање на стандардни излаз уз помоћ libre-print, макро изгледа овако:

(define-syntax (libre-print caller-stx)
   (syntax-case caller-stx ()
      [(libre-print) (syntax-protect (syntax/loc caller-stx (void)))]
      [(libre-print x y ...) (syntax-protect (syntax/loc caller-stx
                                   (begin
                                     (displayln x)
                                     (libre-print y ...))))]
      [else
        (syntax-protect (raise-syntax-error
          'print-error
          (format "wrong calling pattern in: ~a" caller-stx)))]))

Уколико позовемо libre-print без аргумената, не исписује се ништа, што постижемо враћањем израза (void). Уколико имамо више од једног аргумента, прво штампамо први уз помоћ displayln, па онда позивамо libre-print на остатак аргумената. Тиме добијамо валидну рекурзивну дефиницију макроа.

Како бисмо имплементирали while петљу, морамо је „превести” на рекурзивни облик.

(define-syntax (libre-while caller-stx)
   (syntax-case caller-stx ()
       [(libre-while test stmts ...) (syntax-protect (syntax/loc caller-stx
                                                           (let loop ()
                                                             (when test
                                                               stmts ...
                                                               (loop)))))]
       [else
         (syntax-protect (raise-syntax-error
          'while-error
          (format "wrong calling pattern in: ~a" caller-stx)))]))

Привремено смо дефинисали функцију loop, коју позивамо изнова док год се test израчунава на тачно.

libre-if је у односу на libre-while мало једноставније, јер у Ракету имамо функцију if.

(define-syntax (libre-if caller-stx)
   (syntax-case caller-stx ()
      [(libre-if test s1) (syntax-protect (syntax/loc caller-stx
                                                (if test s1 (void))))]
      [(libre-if test s1 s2) (syntax-protect (syntax/loc caller-stx
                                   (if test s1 s2)))]
      [else
        (syntax-protect (raise-syntax-error
          'if-error
        (format "wrong calling pattern in: ~a" caller-stx)))]))

Преостале су нам још две ствари – libre-init и libre-prog. Прво морамо да одлучимо како да чувамо информације о променљивим вредностима. [init foo 42] уписује 42 у променљиву foo. Постоји неколико решења за овај проблем. Међутим, у оквиру Ракета, он је већ решен. Можемо ово да искористимо и наше променљиве просто дефинишемо као Ракет променљиве. Јасно, користићемо постојећу Ракет функцију define, али нам то оставља још један нерешен проблем. За разлику од define, нашом init наредбом можемо да „иницијализујемо” или дефинишемо променљиву, али и да променимо њену вредност. У Ракету се за мењање вредности претходно дефинисане променљиве користи функција set!. Зато ћемо init преводити у set!, али нам је потребно да све променљиве које користимо унапред дефинишемо. То ћемо урадити уз помоћ функције begin-for-syntax, која се извршава пре него што макро систем почне са радом.

(begin-for-syntax
    (require racket/list)
    (define (syntax-flatten stx)
       (let* ([stx-unwrapped (syntax-e stx)]
                [maybe-pair (and (pair? stx-unwrapped) (flatten stx-unwrapped))])
         (if maybe-pair
               (append-map syntax-flatten maybe-pair)
               (list stx))))

   (define (find-unique-ids stmts)
      (remove-duplicates
        (for/list ([stx (in-list (syntax-flatten stmts))]
                       #:when (syntax-property stx 'libre-id))
           stx)
        #:key syntax->datum)))

Дефинишемо помоћну функцију syntax-flatten, која „поравнава” синтаксни објекат и враћа обичну листу. Над таквом структуром лако можемо да дефинишемо функцију find-unique-ids, која пролази кроз листу и тражи све елементе са синтаксним својством libre-id. Када нам је та функција дефинисана, можемо направити посебан макро libre-module-begin на следећи начин:

(define-syntax (libre-module-begin caller-stx)
    (syntax-case caller-stx ()
       [(libre-module-begin (libre-prog stmts ...))
         (with-syntax ([(ids ...) (find-unique-ids #'(stmts ...))])
         #'(#%module-begin
           (define ids 0) ...
           (begin stmts ...)))]))

#%module-begin је почетна тачка програма, па ми дефинишемо своју, libre-module-begin, која прво проналази све јединствене идентификаторе, а затим претвара (libre-prog stmts …) у

(define ids 0)
(begin stmts ...)

 

Сада можемо лако направити и макро за libre-init.

(define-syntax (libre-init caller-stx)
   (syntax-case caller-stx ()
       [(libre-init id ",s") (syntax-protect (syntax/loc caller-stx
                                 (set! id (read-line (current-input-port) 'any))))]
       [(libre-init id ",i") (syntax-protect (syntax/loc caller-stx
                                (set! id (inexact->exact
                                               (string->number
                                                (read-line (current-input-port) 'any))))))]
       [(libre-init id expo) (syntax-protect (syntax/loc caller-stx
                                    (set! id expo)))]
       [else
         (syntax-protect (raise-syntax-error
           'init-error
            (format "wrong calling patern in: ~a" caller-stx)))]))

Уколико је други аргумент ,s, учитавамо стринг са стандардног улаза. Уколико је други аргумент ,i, учитавамо број са стандардног улаза. Уколико је неки израз, вредност променљиве мењамо у вредност тог израза.

Сада када смо све потребне макрое дефинисали, остало је да омогућимо њихово коришћење ван датотеке expander.rkt.

(provide libre-init
            libre-print
            libre-while
            libre-if
            libre-sum
            libre-prod
            libre-bool
            #%top-interaction
            #%app
            #%datum
            (rename-out [libre-module-begin #%module-begin]))

Како бисмо могли коначно да почнемо да програмирамо у нашем језику, треба да у датотеци main.rkt повежемо све различите делове нашег преводиоца и направимо модул reader, који ће Ракет преводилац препознати као читач нашег језика.

#lang racket
(require "parser.rkt" "tokenizer.rkt" syntax/strip-context)

(define (read-syntax path port)
    (define parse-tree (parse path (make-tokenizer port path)))
    (strip-context
     #//(module libre-lang-mod libre-lang/expander
           #,parse-tree)))

(module+ reader
   (provide read-syntax))

 

Тиме смо написали и последњу линију кода за овај компајлер. Сада можемо да инсталирамо наш пројекат командом

raco pkg install

у терминалу. Испробајмо онај програм за рачунање факторијала из првог дела:

#lang libre-lang
[print "Upisite broj:"] // Ovaj program izracunava faktorijal broja sa standardnog ulaza
[init foo ,i]
[init bar foo-1]
[if foo > 0 [while bar > 0
                       [init foo foo*bar]
                       [init bar bar-1]]]
[if foo > 0 [print foo] [print "Broj je manji od 0"]]

Наравно, ни сам језик, ни преводилац који смо сада направили, није савршен. Фале неке врло корисне наредбе као што је break за излазак из петље, операције над нискама, други типови података, логички оператори and, or и not, функције, и многи други конструкти који се узимају здраво за готово у „правим” програмским језицима. Нека од ових побољшања је теже имплементирати од осталих, али се углавном своде на иста три корака – додавање правила у лексер, додавање правила у парсер и додавање макроа у експандер.

Ако желите да погледате сав изворни код, можете наћи репозиторијум на Гитхабу. Остаје на вама да експериментишете, а ако се у неком тренутку заглавите, документација Ракет језика је доступна на следећем линку. Постоји и књига о прављењу различитих језика у Ракету која је послужила као одличан референтни материјал и инспирација за овај серијал.

Претходни део

Оставите одговор

Ваша адреса е-поште неће бити објављена. Неопходна поља су означена *

Time limit is exhausted. Please reload CAPTCHA.