(in-package txt2web) ;;;; Parses TXT source to an intermediate representation (eval-when (:compile-toplevel :load-toplevel :execute) (defparameter *max-args* 10)) ;;; 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)) "TXT2WEB.LIB") (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) "TXT2WEB.LIB") 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))))) (walker:deftransform parser-qspmod mod (&rest args) (list* 'qspmod (mapcar #'walker:walk-continue args))) (defun do-binop (list) (walker:walk 'parser-qspmod (destructuring-bind (left-op rest-ops) list (do-binop% left-op (mapcar #'binop-rest rest-ops))))) (defun maybe-text (list) "Leaves lists in place and applies esrap:text to everything else" (let ((parts nil) (part (list 'text))) (loop :for token :in list :do (cond ((listp token) (push (nreverse part) parts) (setf part (list 'text)) (push token parts)) (t (push token part)))) (push (nreverse part) parts) (remove "" (loop :for part :in (nreverse parts) :collect (case (first part) ('text (p:text (rest part))) (t part))) :test #'equal))) (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) (:error-report nil)) (p:defrule spaces? (* (or #\space #\tab line-continuation)) (:constant nil) (:error-report nil)) (p:defrule colon #\: (:constant nil)) (p:defrule equal #\= (: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-char (or squote-esc (not-quote character)) (:text t)) (p:defrule dstring-char (or dquote-esc (not-doublequote character)) (:text t)) ;;; Identifiers (defparameter *keywords* '(act addlib addobj addqst and arrcomp arrpos arrsize cla clear *clear close clr *clr cls cmdclear cmdclr copyarr countobj curact curacts curloc debug delact dellib delobj desc disablescroll disablesubex dynamic dyneval else elseif end exit loop freelib func getobj gosub goto gs gt if iif img *img inclib input instr isnum isplay jump killall killobj killqst killvar lcase len let loc local maintxt max menu mid min mod msecscount msg nl *nl no nosave obj opengame openqst or p *p pl *pl play qspver rand refint replace rgb rnd savegame selact selobj set settimer showacts showinput showobjs showstat stattxt step str strcomp strfind strpos to trim ucase unsel unselect user_text usrtxt val view wait while xgoto xgt)) (defun trim-$ (str) (if (char= #\$ (elt str 0)) (subseq str 1) str)) (defun qsp-keyword-p (id) (member (intern (trim-$ (string-upcase id))) *keywords*)) (defun not-qsp-keyword-p (id) (not (member (intern (trim-$ (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) (intern (string-upcase (p:text list)) "TXT2WEB.LIB"))) (p:defrule identifier (not-qsp-keyword-p identifier-raw)) ;;; Strings (p:defrule qsp-string (or normal-string brace-string)) (p:defrule brace-string (and #\{ before-statement block-body #\}) (:lambda (list) (list* 'lib:qspblock (third list)))) (p:defrule normal-string (or sstring dstring) (:lambda (str) (list* 'lib:str (or str (list ""))))) (p:defrule sstring (and #\' (* (or sstring-interpol sstring-exec sstring-char)) #\') (:lambda (list) (maybe-text (second list)))) (p:defrule dstring (and #\" (* (or dstring-interpol dstring-exec dstring-char)) #\") (:lambda (list) (maybe-text (second list)))) (defun parse-interpol (list) (p:parse 'expression (p:text (mapcar 'second (second list))))) (defun parse-exec (list) (list* 'lib:exec (p:parse 'exec-body (p:text (second list))))) (p:defrule sstring-interpol (and "<<" (+ (and (p:! ">>") sstring-char)) ">>") (:function parse-interpol)) (p:defrule dstring-interpol (and "<<" (+ (and (p:! ">>") dstring-char)) ">>") (:function parse-interpol)) (p:defrule sstring-exec (or (and (p:~ "\"exec:") (+ (and (p:& (not-doublequote character)) sstring-char)) #\") (and (p:~ "''exec:") (+ (not-quote character)) "''")) (:function parse-exec)) (p:defrule dstring-exec (or (and (p:~ "'exec:") (+ (and (p:& (not-quote character)) dstring-char)) #\') (and (p:~ "\"\"exec") (+ (not-doublequote character)) "\"\"")) (:function parse-exec)) ;;; Location (p:defrule txt2web-grammar (and (* (or spaces #\newline)) (* location)) (:lambda (list) `(lib:game ,@(second list)))) (p:defrule location (and location-header block-body location-end) (:destructure (header body end) (declare (ignore end)) `(lib: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 #\- (* not-newline) #\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? sum-expr))) (:function do-binop)) (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-raw) (let ((idx (case idx-raw ((nil) 0) (:last nil) (t idx-raw)))) (list 'lib:qspvar id idx)))) (p:defrule array-index (and #\[ spaces? (p:? expression) spaces? #\]) (:lambda (list) (or (third list) :last))) (p:defrule assignment (or kw-assignment plain-assignment op-assignment) (:destructure (qspvar eq expr) (declare (ignore eq)) (list 'lib:set qspvar expr))) (p:defrule kw-assignment (and (or (p:~ "let") (p:~ "set")) spaces? (or op-assignment plain-assignment)) (:function third)) (p:defrule op-assignment (and variable spaces? (or "+" "-" "*" "/") #\= spaces? expression) (:destructure (qspvar ws1 op eq ws2 expr) (declare (ignore ws1 ws2)) (list qspvar eq (intern-first (list op qspvar expr))))) (p:defrule plain-assignment (and variable spaces? #\= spaces? expression) (:function remove-nil)) ;;; 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))))