HomeMAL' // MANUFACTORUM
datacrypt
Piton / src / lexer.rkt
src/lexer.rkt4.9 KB
#lang racket/base

(require parser-tools/lex
         (prefix-in : parser-tools/lex-sre)
         racket/string
         racket/list
         racket/match
         racket/function
         "errors.rkt")
(provide main-lexer
         keywords
         delimiters
         operators
         atoms)

;; Tokens
(define-empty-tokens delimiters
  (eof eol indent dedent col comma opar cpar obra cbra arrow))

(define-empty-tokens keywords
  (if else while def return))

(define-empty-tokens operators
  (assign-op land lor lnot add sub mul div eq neq gt lt gte lte))

(define-tokens atoms
  (num str const type name))

;; Regexp abbreviations
(define-lex-abbrev latin_
  (:or (char-range "a" "z")
       (char-range "A" "Z")
       "_"))

(define-lex-abbrev name
  (:: latin_ (:* (:or latin_ numeric))))

(define-lex-abbrev type
  (:or "int" "bool" "str"))

(define-lex-abbrev num
  (:: (:? "-") (:+ numeric)))

(define-lex-abbrev eol-and-indent
  (:: #\newline (:* (:or #\tab #\space))))

(define-lex-abbrev comment
  (:or (:: #\newline (:* (:or #\tab #\space)) "#" (:* (:~ #\newline))) ; full line
       (:: "#" (:* (:~ #\newline))))) ; line end

;; Main lexer
(define main-lexer
  (lexer-src-pos
   ; allow ending without trailing line, even when indented
   [(eof)          (append (handle-indent "\n")
                           (list (token-eof)))]
   [comment        (return-without-pos (main-lexer input-port))]

   ;; Delimiters
   [eol-and-indent (handle-indent lexeme)]
   [":"            (token-col)]
   [","            (token-comma)]
   ["("            (token-opar)]
   [")"            (token-cpar)]
   ["["            (token-obra)]
   ["]"            (token-cbra)]
   ["->"           (token-arrow)]
   ;; Keywords
   ["if"           (token-if)]
   ["else"         (token-else)]
   ["while"        (token-while)]
   ["def"          (token-def)]
   ["return"       (token-return)]
   ;; Operators
   ["and"          (token-land)]
   ["or"           (token-lor)]
   ["not"          (token-lnot)]
   ["="            (token-assign-op)]
   ["+"            (token-add)]
   ["-"            (token-sub)]
   ["*"            (token-mul)]
   ["/"            (token-div)]
   ["=="           (token-eq)]
   ["!="           (token-neq)]
   [">"            (token-gt)]
   ["<"            (token-lt)]
   [">="           (token-gte)]
   ["<="           (token-lte)]
   ;; Atoms
   [num            (token-num (string->number lexeme))]
   ["True"         (token-const 'true)]
   ["False"        (token-const 'false)]
   ; TODO None
   ["\""           (token-str (apply string-append (string-lexer input-port)))]
   [type           (token-type (string->symbol lexeme))]
   [name           (token-name lexeme)]

   [whitespace     (return-without-pos (main-lexer input-port))]
   [any-char       (raise-lexer-error start-pos lexeme)]))

;; String lexer
;; TODO multiline strings, single-quoted strings
(define string-lexer
  (lexer
   ; double quote
   ["\\\"" (cons "\"" (string-lexer input-port))]
   ; slash TODO not working properly
   ["\\\\" (cons "\\" (string-lexer input-port))]
   ; newline
   ["\\n"  (cons "\n" (string-lexer input-port))]
   ; tab
   ["\\t"  (cons "\t" (string-lexer input-port))]
   ; string end
   ["\""   (list)]
   [(eof)  (raise-unterminated-str-error start-pos)]
   [any-char (cons lexeme (string-lexer input-port))]))

;; Indentation
(define indent-stack (list))
;; <returns> tokens to emit (eol, indent,dedent)
;; and updates <indent-stack>
(define (handle-indent eol-and-indent)
  (define indent-string (string-trim eol-and-indent "\n"
                                     #:left? #t #:right? #f #:repeat? #t))
  (let loop ([string indent-string]
             [stack indent-stack])
    (match (cons (empty? stack) (equal? string ""))
      ;; stack and string are not empty
      [(cons #f #f)
       ; indentation string must start with
       ; next indentation piece found in stack
       (define piece (first stack))
       (when (not (string-prefix? string piece))
         (if (string-prefix? piece string)
             (raise-unindent-error)
             (raise-tab-error)))
       ; remove indentation piece from string and continue
       (let ([string (string-trim string piece
                                  #:left? #t #:right? #f #:repeat? #f)])
         (loop string (rest stack)))]

      ;; stack and string are both empty
      [(cons #t #t)
       ; indentation did not change
       (list (token-eol))]

      ;; stack is empty but string isn't yet
      [(cons #t #f)
       ; push one indentation level
       (set! indent-stack (append indent-stack (list string)))
       (list (token-eol)
             (token-indent))]

      ;; string is empty but stack isn't yet
      [(cons #f #t)
       ; pop one or several indentation levels
       (define dedent-count (length stack))
       (set! indent-stack (drop-right indent-stack dedent-count))
       (append (list (token-eol))
               ; emit as many dedent tokens as poped indentation levels
               (build-list dedent-count (const (token-dedent))))])))