(in-package sugar-qsp) ;;;; Parses TXT source to an intermediate representation ;;; Utility (defun remove-nth (list nth) (append (subseq list 0 nth) (subseq list (1+ nth)))) (defun not-quote (char) (not (eql #\' char))) (defun not-doublequote (char) (not (eql #\" char))) (defun not-brace (char) (not (eql #\} char))) (defun not-integer (string) (when (find-if-not #'digit-char-p string) t)) (defun not-newline (char) (not (eql #\newline char))) (defun id-any-char (char) (and (not (digit-char-p char)) (not (eql #\newline char)) (not (find char " !:&=<>+-*/,'\"()[]{}")))) (defun intern-first (list) (list* (intern (string-upcase (first list))) (rest list))) (eval-when (:compile-toplevel :load-toplevel :execute) (defun remove-nil (list) (remove nil list))) (defun binop-rest (list) (destructuring-bind (ws1 operator ws2 operand2) list (declare (ignore ws1 ws2)) (list (intern (string-upcase operator)) operand2))) (defun do-binop% (left-op other-ops) (if (null other-ops) left-op (destructuring-bind ((operator right-op) &rest rest-ops) other-ops (if (and (listp left-op) (eq (first left-op) operator)) (do-binop% (append left-op (list right-op)) rest-ops) (do-binop% (list operator left-op right-op) rest-ops))))) (defun do-binop (list) (destructuring-bind (left-op rest-ops) list (do-binop% left-op (mapcar #'binop-rest rest-ops)))) (p:defrule line-continuation (and #\_ #\newline) (:constant nil)) (p:defrule text-spaces (+ (or #\space #\tab line-continuation)) (:text t)) (p:defrule spaces (+ (or #\space #\tab line-continuation)) (:constant nil)) (p:defrule spaces? (* (or #\space #\tab line-continuation)) (:constant nil)) (p:defrule colon #\: (:constant nil)) (p:defrule alphanumeric (alphanumericp character)) (p:defrule not-newline (not-newline character)) (p:defrule squote-esc "''" (:lambda (list) (p:text (elt list 0)))) (p:defrule dquote-esc "\"\"" (:lambda (list) (p:text (elt list 0)))) (p:defrule sstring-chars (+ (and (and (p:! "<<") (p:! (p:~ "\"exec:"))) (or squote-esc (not-quote character)))) (:lambda (list) (p:text (mapcar #'second list)))) (p:defrule dstring-chars (+ (and (and (p:! "<<") (p:! (p:~ "'exec:"))) (or dquote-esc (not-doublequote character)))) (:lambda (list) (p:text (mapcar #'second list)))) ;;; Identifiers ;; From the official docs (defparameter *keywords* '(act addlib addobj addqst and arrcomp arrpos arrsize cla clear *clear close clr *clr cls cmdclear cmdclr copyarr $counter countobj $curacts curloc debug delact dellib delobj desc disablescroll disablesubex dynamic dyneval else elseif end exit fcolor $fname freelib fsize func getobj gosub goto gs gt if iif inclib input instr isnum isplay jump killall killobj killqst killvar lcase lcolor len let loc $maintxt max menu mid min mod msecscount msg nl *nl no nosave obj $onactsel $ongload $ongsave $onnewloc $onobjadd $onobjdel $onobjsel opengame openqst or p *p pl *pl play qspver rand refint replace rgb rnd savegame selact selobj set settimer showacts showinput showobjs showstat $stattxt str strcomp strfind strpos trim ucase unsel unselect $usercom user_text usrtxt val view wait xgoto xgt)) (defun qsp-keyword-p (id) (member (intern (string-upcase id)) *keywords*)) (defun not-qsp-keyword-p (id) (not (member (intern (string-upcase id)) *keywords*))) (p:defrule qsp-keyword (qsp-keyword-p identifier-raw)) (p:defrule id-first (id-any-char character)) (p:defrule id-next (or (id-any-char character) (digit-char-p character))) (p:defrule identifier-raw (and id-first (* id-next)) (:lambda (list) (let ((id (p:text list))) (when (member id *keywords*) (error "~A is a keyword" id)) (intern (string-upcase id))))) (p:defrule identifier (not-qsp-keyword-p identifier-raw)) ;;; Strings (p:defrule qsp-string (or normal-string brace-string)) (p:defrule normal-string (or sstring dstring) (:lambda (str) (list* 'str (or str (list ""))))) (p:defrule sstring (and #\' (* (or string-interpol sstring-exec sstring-chars)) #\') (:function second)) (p:defrule dstring (and #\" (* (or string-interpol dstring-exec dstring-chars)) #\") (:function second)) (p:defrule string-interpol (and "<<" expression ">>") (:function second)) (p:defrule sstring-exec-body (+ (or squote-esc (not-doublequote character))) (:text t)) (p:defrule dstring-exec-body (+ (or dquote-esc (not-quote character))) (:text t)) (p:defrule sstring-exec (and (p:~ "\"exec:") sstring-exec-body #\") (:lambda (list) (list* 'exec (p:parse 'exec-body (second list))))) (p:defrule dstring-exec (and (p:~ "'exec:") dstring-exec-body #\') (:lambda (list) (list* 'exec (p:parse 'exec-body (second list))))) (p:defrule brace-string (and #\{ before-statement block-body #\}) (:lambda (list) (list* 'qspblock (third list)))) ;;; Location (p:defrule sugar-qsp-grammar (and (* (or spaces #\newline)) (* location)) (:function second)) (p:defrule location (and location-header block-body location-end) (:destructure (header body end) (declare (ignore end)) `(location (,header) ,@body))) (p:defrule location-header (and #\# (+ not-newline) (and #\newline spaces? before-statement)) (:destructure (spaces1 name spaces2) (declare (ignore spaces1 spaces2)) (string-upcase (string-trim " " (p:text name))))) (p:defrule location-end (and #\- #\newline before-statement) (:constant nil)) ;;; Block body (p:defrule newline-block-body (and #\newline spaces? block-body) (:function third)) (p:defrule block-body (* statement) (:function remove-nil)) ;; Just for " "<=" ">=" "=<" "=>" #\= #\< #\> #\!) spaces? cat-expr))) (:function do-binop)) (p:defrule cat-expr (and sum-expr (* (and spaces? #\& spaces? (p:! expr-stopper) sum-expr))) (:lambda (list) (do-binop (list (first list) (mapcar (lambda (l) (remove-nth l 3)) (second list)))))) (p:defrule sum-expr (and mod-expr (* (and spaces? (or #\+ #\-) spaces? mod-expr))) (:function do-binop)) (p:defrule mod-expr (and mul-expr (* (and spaces? (p:~ "mod") spaces? mul-expr))) (:function do-binop)) (p:defrule mul-expr (and unary-expr (* (and spaces? (or #\* #\/) spaces? unary-expr))) (:function do-binop)) (p:defrule unary-expr (and (p:? (or #\+ #\-)) atom-expr) (:lambda (list) (let ((expr (remove-nil list))) (if (= 1 (length expr)) (first expr) (intern-first expr))))) (p:defrule atom-expr (and (or variable literal returning-intrinsic paren-expr) spaces?) (:function first)) (p:defrule paren-expr (and #\( spaces? expression spaces? #\)) (:function third)) (p:defrule or-op (p:~ "or") (:constant "or")) (p:defrule and-op (p:~ "and") (:constant "and")) ;;; Variables (p:defrule variable (and identifier (p:? array-index)) (:destructure (id idx) (list 'var id (or idx 0)))) (p:defrule array-index (and #\[ spaces? (p:? expression) spaces? #\]) (:lambda (list) (or (third list) :end))) (p:defrule assignment (or kw-assignment plain-assignment) (:destructure (var eq expr) (declare (ignore eq)) (list 'set var expr))) (p:defrule plain-assignment (and variable spaces? #\= spaces? expression) (:function remove-nil)) (p:defrule kw-assignment (and (or (p:~ "let") (p:~ "set")) spaces? plain-assignment) (:function third)) ;;; Non-string literals (p:defrule literal (or qsp-string brace-string number)) (p:defrule number (+ (or #\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)) (:lambda (list) (parse-integer (p:text list))))