s/js:funcall/js::funcall/
[clinton/parenscript.git] / src / printer.lisp
index 9ab4e28..0eaeef2 100644 (file)
@@ -1,22 +1,45 @@
-(in-package :parenscript)
-
-(defvar *ps-output-stream*)
-(defparameter *indent-level* 0)
-
-(defmethod parenscript-print (ps-form &optional *ps-output-stream*)
-  (setf *indent-level* 0)
-  (flet ((print-ps (form)
-           (if (and (listp form) (eql 'js-block (car form))) ; ignore top-level block
-               (loop for (statement . remaining) on (third form) do
-                     (ps-print statement) (psw ";") (when remaining (psw #\Newline)))
-               (ps-print form))))
-    (if *ps-output-stream*
-        (print-ps ps-form)
-        (with-output-to-string (*ps-output-stream*)
-          (print-ps ps-form)))))
-
-(defun psw (obj) ; parenscript-write
-  (princ obj *ps-output-stream*))    
+(in-package "PARENSCRIPT")
+
+(defvar *ps-print-pretty* t)
+(defvar *indent-num-spaces* 4)
+(defvar *js-string-delimiter* #\'
+  "Specifies which character should be used for delimiting strings.
+
+This variable is used when you want to embed the resulting JavaScript
+in an html attribute delimited by #\\\" as opposed to #\\', or
+vice-versa.")
+
+(defvar *indent-level*)
+
+(defvar *psw-stream*)
+
+(defun parenscript-print (form immediate?)
+  (declare (special immediate?))
+  (let ((*indent-level* 0)
+        (*psw-stream* (if immediate?
+                          *psw-stream*
+                          (make-string-output-stream)))
+        (%psw-accumulator ()))
+    (declare (special %psw-accumulator))
+    (if (and (listp form) (eq 'js:block (car form))) ; ignore top-level block
+        (loop for (statement . remaining) on (cdr form) do
+             (ps-print statement) (psw #\;) (when remaining (psw #\Newline)))
+        (ps-print form))
+    (unless immediate?
+      (reverse (cons (get-output-stream-string *psw-stream*) %psw-accumulator)))))
+
+(defun psw (obj)
+  (declare (special %psw-accumulator immediate?))
+  (typecase obj
+    (string (write-string obj *psw-stream*))
+    (character (write-char obj *psw-stream*))
+    (otherwise
+     (if immediate?
+         (write-string (eval obj) *psw-stream*)
+         (setf %psw-accumulator
+               (cons obj
+                     (cons (get-output-stream-string *psw-stream*)
+                           %psw-accumulator)))))))
 
 (defgeneric ps-print% (special-form-name special-form-args))
 
@@ -33,30 +56,20 @@ arguments, defines a printer for that form using the given body."
 
 (defgeneric ps-print (compiled-form))
 
-(defmethod ps-print ((form null)) ; don't print top-level nils (ex: result of defining macros, etc.)
-  )
+(defmethod ps-print ((form null))) ; don't print top-level nils (ex: result of defining macros, etc.)
+
+(defmethod ps-print ((s symbol))
+  (assert (keywordp s) nil "~S is not a symbol" s)
+  (ps-print (string-downcase s)))
 
 (defmethod ps-print ((compiled-form cons))
-  "Prints the given compiled ParenScript form starting at the given
-indent position."
   (ps-print% (car compiled-form) (cdr compiled-form)))
 
-;;; indentation
-(defvar *ps-print-pretty* t)
-(defvar *indent-num-spaces* 4)
-
 (defun newline-and-indent ()
-  (when (and (fresh-line *ps-output-stream*) *ps-print-pretty*)
-    (loop repeat (* *indent-level* *indent-num-spaces*)
-          do (psw #\Space))))
-
-;;; string literals
-(defvar *js-string-delimiter* #\'
-  "Specifies which character should be used for delimiting strings.
-
-This variable is used when you want to embed the resulting JavaScript
-in an html attribute delimited by #\\\" as opposed to #\\', or
-vice-versa.")
+  (if *ps-print-pretty*
+      (progn (psw #\Newline)
+             (loop repeat (* *indent-level* *indent-num-spaces*) do (psw #\Space)))
+      (psw #\Space)))
 
 (defparameter *js-lisp-escaped-chars*
   '((#\' . #\')
@@ -75,238 +88,191 @@ vice-versa.")
           for code = (char-code char)
           for special = (lisp-special-char-to-js char)
           do (cond (special (psw #\\) (psw special))
-                   ((or (<= code #x1f) (>= code #x80))
-                    (format *ps-output-stream* "\\u~4,'0x" code))
+                   ((or (<= code #x1f) (>= code #x80)) (format *psw-stream* "\\u~4,'0x" code))
                    (t (psw char))))
     (psw *js-string-delimiter*)))
 
 (defmethod ps-print ((number number))
-  (format *ps-output-stream* (if (integerp number) "~S" "~F") number))
+  (format *psw-stream* (if (integerp number) "~S" "~F") number))
 
 ;;; expression and operator precedence rules
 
 (defun expression-precedence (expr)
   (if (consp expr)
       (case (car expr)
-        (js-expression-if (op-precedence 'js-expression-if))
-        (js-assign (op-precedence '=))
+        ((js:slot-value js:aref) (op-precedence (car expr)))
+        (js:= (op-precedence 'js:=))
+        (js:? (op-precedence 'js:?))
+        (js:unary-operator (op-precedence (second expr)))
         (operator (op-precedence (second expr)))
-        (otherwise 0))
-      0))
-
-(eval-when (:compile-toplevel :load-toplevel :execute)
-  (defparameter *op-precedence-hash* (make-hash-table :test #'equal))
-
-  ;;; generate the operator precedences from *OP-PRECEDENCES*
-  (let ((precedence 1))
-    (dolist (ops '((js-aref)
-                   (js-slot-value)
-                   (! not ~)
-                   (* / %)
-                   (+ -)
-                   (<< >>)
-                   (>>>)
-                   (< > <= >=)
-                   (in js-expression-if)
-                   (eql == != =)
-                   (=== !==)
-                   (&)
-                   (^)
-                   (\|)
-                   (\&\& and)
-                   (\|\| or)
-                   (js-assign *= /= %= += -= <<= >>= >>>= \&= ^= \|=)
-                   (comma)))
-      (dolist (op ops)
-        (let ((op-name (symbol-name op)))
-          (setf (gethash op-name *op-precedence-hash*) precedence)))
-      (incf precedence)))
-
-  (defun op-precedence (op)
-    (gethash (if (symbolp op)
-                 (symbol-name op)
-                 op)
-             *op-precedence-hash*)))
-
-(defprinter ps-quote (val)
-  (if (null val)
-      (psw "null")
-      (error "Cannot translate quoted value ~S to javascript" val)))
-
-(defprinter js-literal (str)
+        (otherwise -1))
+      -1))
+
+(defprinter js:literal (str)
   (psw str))
 
 (defun print-comma-delimited-list (ps-forms)
   (loop for (form . remaining) on ps-forms do
         (ps-print form) (when remaining (psw ", "))))
 
-(defprinter array-literal (&rest initial-contents)
+(defprinter js:array (&rest initial-contents)
   (psw #\[) (print-comma-delimited-list initial-contents) (psw #\]))
 
-(defprinter js-aref (array indices)
-  (ps-print array)
+(defprinter js:aref (array indices)
+  (if (>= (expression-precedence array) (op-precedence 'js:aref))
+      (parenthesize-print array)
+      (ps-print array))
   (loop for idx in indices do
         (psw #\[) (ps-print idx) (psw #\])))
 
-(defprinter object-literal (&rest slot-definitions)
-  (psw #\{)
-  (loop for ((key . value) . remaining) on slot-definitions do
-        (format *ps-output-stream* "~A: " (js-translate-symbol key))
-        (ps-print value)
-        (when remaining (psw ", ")))
-  (psw " }"))
+(defvar *lexical-bindings* nil)
+
+(defun rename-js-variable (name)
+  (or (cdr (assoc name *lexical-bindings*))
+      name))
 
-(defprinter js-variable (var)
-  (psw (js-translate-symbol var)))
+(defprinter js:let (variables &body body)
+  (let ((*lexical-bindings*
+        (append (mapcar (lambda (var)
+                          (cons var (if (assoc var *lexical-bindings*)
+                                        (ps-gensym var)
+                                        var)))
+                        variables))))
+    (loop for (exp . remaining) on body do
+        (ps-print exp) (when remaining (psw ";") (newline-and-indent)))))
+
+(defprinter js:variable (var)
+  (psw (symbol-to-js-string (rename-js-variable var))))
 
 ;;; arithmetic operators
 (defun parenthesize-print (ps-form)
   (psw #\() (ps-print ps-form) (psw #\)))
 
-(defprinter operator (op args)
+(defprinter js:operator (op &rest args)
   (loop for (arg . remaining) on args
         with precedence = (op-precedence op) do
         (if (>= (expression-precedence arg) precedence)
             (parenthesize-print arg)
             (ps-print arg))
-        (when remaining (format *ps-output-stream* " ~(~A~) " op))))
-
-(defprinter unary-operator (op arg &key prefix)
-  (when prefix (psw op))
-  (if (and (listp arg) (eql 'operator (car arg)))
+        (when remaining (format *psw-stream* " ~(~A~) " op))))
+
+(defprinter js:unary-operator (op arg &key prefix space)
+  (when prefix (format *psw-stream* "~(~a~)~:[~; ~]" op space))
+  (if (> (expression-precedence arg)
+         (op-precedence (case op
+                          (+ 'unary+)
+                          (- 'unary-)
+                          (t op))))
       (parenthesize-print arg)
       (ps-print arg))
-  (unless prefix (psw op)))
-
-;;; function and method calls
-(defprinter js-funcall (fun-designator args)
-  (cond ((member (car fun-designator) '(js-variable js-aref js-slot-value))
-         (ps-print fun-designator))
-        ((eql 'js-lambda (car fun-designator))
-         (psw #\() (ps-print fun-designator) (psw #\)))
-        ((eql 'js-funcall (car fun-designator))
-         (ps-print fun-designator)))
-  (psw #\() (print-comma-delimited-list args) (psw #\)))
+  (unless prefix (format *psw-stream* "~(~a~)" op)))
 
-(defprinter js-method-call (method object args)
-  ;; TODO: this may not be the best way to add ()'s around lambdas
-  ;; probably there is or should be a more general solution working
-  ;; in other situations involving lambdas
-  (if (or (numberp object) (and (consp object) (member (car object) '(js-lambda js-object operator js-expression-if))))
-      (parenthesize-print object)
-      (ps-print object))
-  (psw (js-translate-symbol method))
+(defprinter js::funcall (fun-designator &rest args)
+  (funcall (if (member (car fun-designator) '(js:variable js:aref js:slot-value js::funcall))
+               #'ps-print
+               #'parenthesize-print)
+           fun-designator)
   (psw #\() (print-comma-delimited-list args) (psw #\)))
 
-(defprinter js-block (block-type statements)
-  (case block-type
-    (:statement
-     (psw #\{)
-     (incf *indent-level*)
-     (dolist (statement statements)
-       (newline-and-indent) (ps-print statement) (psw #\;))
-     (decf *indent-level*)
-     (newline-and-indent)
-     (psw #\}))
-    (:expression
-     (psw #\()
-     (loop for (statement . remaining) on statements do
-           (ps-print statement) (when remaining (psw ", ")))
-     (psw #\)))))
-
-(defprinter js-lambda (args body)
+(defprinter js:|,| (&rest expressions)
+  (psw #\()
+  (loop for (exp . remaining) on expressions do
+       (ps-print exp) (when remaining (psw ", ")))
+  (psw #\)))
+
+(defprinter js:block (&rest statements)
+  (psw #\{)
+  (incf *indent-level*)
+  (dolist (statement statements)
+    (newline-and-indent) (ps-print statement) (psw #\;))
+  (decf *indent-level*)
+  (newline-and-indent)
+  (psw #\}))
+
+(defprinter js:lambda (args body)
   (print-fun-def nil args body))
 
-(defprinter js-defun (name args body)
+(defprinter js:defun (name args body)
   (print-fun-def name args body))
 
 (defun print-fun-def (name args body-block)
-  (format *ps-output-stream* "function ~:[~;~A~](" name (js-translate-symbol name))
+  (format *psw-stream* "function ~:[~;~A~](" name (symbol-to-js-string name))
   (loop for (arg . remaining) on args do
-        (psw (js-translate-symbol arg)) (when remaining (psw ", ")))
+        (psw (symbol-to-js-string arg)) (when remaining (psw ", ")))
   (psw ") ")
   (ps-print body-block))
 
-;;; object literals
-(defprinter js-object (slot-defs)
+(defprinter js:object (&rest slot-defs)
   (psw "{ ")
-  (loop for ((slot-name slot-value) . remaining) on slot-defs do
-        (if (and (listp slot-name) (eql 'ps-quote (car slot-name)) (symbolp (second slot-name)))
-            (psw (js-translate-symbol (second slot-name)))
-            (ps-print slot-name))
-        (psw " : ")
-        (ps-print slot-value)
-        (when remaining (psw ", ")))
+  (loop for ((slot-name . slot-value) . remaining) on slot-defs do
+       (ps-print slot-name) (psw " : ") (ps-print slot-value)
+       (when remaining (psw ", ")))
   (psw " }"))
 
-(defprinter js-slot-value (obj slot)
-  (if (and (listp obj) (member (car obj) '(js-expression-if)))
+(defprinter js:slot-value (obj slot)
+  (if (or (> (expression-precedence obj) (op-precedence 'js:slot-value))
+          (numberp obj)
+          (and (listp obj) (member (car obj) '(js:lambda js:object))))
       (parenthesize-print obj)
       (ps-print obj))
-  (if (and (listp slot) (eql 'ps-quote (car slot)))
-      (progn (psw #\.)
-             (if (symbolp (second slot))
-                 (psw (js-translate-symbol (second slot)))
-                 (ps-print slot)))
+  (if (and (symbolp slot) (not (keywordp slot)))
+      (progn (psw #\.) (psw (symbol-to-js-string slot)))
       (progn (psw #\[) (ps-print slot) (psw #\]))))
 
-(defprinter js-cond-statement (clauses)
-  (loop for (test body-block) in clauses
-        for start = "if (" then " else if (" do
-        (if (equalp test "true")
-            (psw " else ")
-            (progn (psw start)
-                   (ps-print test)
-                   (psw ") ")))
-        (ps-print body-block)))
-
-(defprinter js-statement-if (test then-block else-block)
+(defprinter js:if (test consequent &rest clauses)
   (psw "if (") (ps-print test) (psw ") ")
-  (ps-print then-block)
-  (when else-block
-      (psw " else ")
-      (ps-print else-block)))
-
-(defprinter js-expression-if (test then else)
-  (ps-print test)
+  (ps-print consequent)
+  (loop while clauses do
+       (ecase (car clauses)
+         (:else-if (psw " else if (") (ps-print (cadr clauses)) (psw ") ")
+                   (ps-print (caddr clauses))
+                   (setf clauses (cdddr clauses)))
+         (:else (psw " else ")
+                (ps-print (cadr clauses))
+                (return)))))
+
+(defprinter js:? (test then else)
+  (if (>= (expression-precedence test) (op-precedence 'js:?))
+      (parenthesize-print test)
+      (ps-print test))
   (psw " ? ")
-  (if (>= (expression-precedence then) (op-precedence 'js-expression-if))
+  (if (>= (expression-precedence then) (op-precedence 'js:?))
       (parenthesize-print then)
       (ps-print then))
   (psw " : ")
-  (if (>= (expression-precedence else) (op-precedence 'js-expression-if))
+  (if (>= (expression-precedence else) (op-precedence 'js:?))
       (parenthesize-print else)
       (ps-print else)))
 
-(defprinter js-assign (lhs rhs)
+(defprinter js:= (lhs rhs)
   (ps-print lhs) (psw " = ") (ps-print rhs))
 
-(defprinter js-var (var-name &rest var-value)
+(defprinter js:var (var-name &rest var-value)
   (psw "var ")
-  (psw (js-translate-symbol var-name))
+  (ps-print var-name)
   (when var-value
     (psw " = ")
     (ps-print (car var-value))))
 
-(defprinter js-break (&optional label)
+(defprinter js:break (&optional label)
   (psw "break")
   (when label
     (psw " ")
-    (psw (js-translate-symbol label))))
+    (psw (symbol-to-js-string label))))
 
-(defprinter js-continue (&optional label)
+(defprinter js:continue (&optional label)
   (psw "continue")
   (when label
     (psw " ")
-    (psw (js-translate-symbol label))))
+    (psw (symbol-to-js-string label))))
 
 ;;; iteration
-(defprinter js-for (label vars tests steps body-block)
-  (when label (psw (js-translate-symbol label)) (psw ": ") (newline-and-indent))
+(defprinter js:for (label vars tests steps body-block)
+  (when label (psw (symbol-to-js-string label)) (psw ": ") (newline-and-indent))
   (psw "for (")
   (loop for ((var-name . var-init) . remaining) on vars
         for decl = "var " then "" do
-        (psw decl) (psw (js-translate-symbol var-name)) (psw " = ") (ps-print var-init) (when remaining (psw ", ")))
+        (psw decl) (psw (symbol-to-js-string var-name)) (psw " = ") (ps-print var-init) (when remaining (psw ", ")))
   (psw "; ")
   (loop for (test . remaining) on tests do
        (ps-print test) (when remaining (psw ", ")))
@@ -316,19 +282,23 @@ vice-versa.")
   (psw ") ")
   (ps-print body-block))
 
-(defprinter js-for-in (var object body-block)
-  (psw "for (") (ps-print var) (psw " in ") (ps-print object) (psw ") ")
+(defprinter js:for-in (var object body-block)
+  (psw "for (var ") (ps-print var) (psw " in ")
+  (if (> (expression-precedence object) (op-precedence 'in))
+      (parenthesize-print object)
+      (ps-print object))
+  (psw ") ")
   (ps-print body-block))
 
-(defprinter js-while (test body-block)
+(defprinter js:while (test body-block)
   (psw "while (") (ps-print test) (psw ") ")
   (ps-print body-block))
 
-(defprinter js-with (expression body-block)
+(defprinter js:with (expression body-block)
   (psw "with (") (ps-print expression) (psw ") ")
   (ps-print body-block))
 
-(defprinter js-switch (test clauses)
+(defprinter js:switch (test clauses)
   (flet ((print-body-statements (body-statements)
            (incf *indent-level*)
            (loop for statement in body-statements do
@@ -349,40 +319,50 @@ vice-versa.")
     (newline-and-indent)
     (psw #\})))
 
-(defprinter js-try (body-block &key catch finally)
+(defprinter js:try (body-block &key catch finally)
   (psw "try ")
   (ps-print body-block)
   (when catch
-    (psw " catch (") (psw (js-translate-symbol (first catch))) (psw ") ")
+    (psw " catch (") (psw (symbol-to-js-string (first catch))) (psw ") ")
     (ps-print (second catch)))
   (when finally
     (psw " finally ")
     (ps-print finally)))
 
 ;;; regex
-(defprinter js-regex (regex)
-  (flet ((first-slash-p (string)
-           (and (> (length string) 0) (char= (char string 0) #\/))))
-    (let ((slash (unless (first-slash-p regex) "/")))
-      (format *ps-output-stream* (concatenate 'string slash "~A" slash) regex))))
-
-(defprinter js-return (value)
-  (psw "return ") (ps-print value))
+(defprinter js:regex (regex)
+  (let ((slash (unless (and (> (length regex) 0) (char= (char regex 0) #\/)) "/")))
+    (psw (concatenate 'string slash regex slash))))
 
 ;;; conditional compilation
-(defprinter cc-if (test body-forms)
+(defprinter js:cc-if (test &rest body)
   (psw "/*@if ")
   (ps-print test)
   (incf *indent-level*)
-  (dolist (form body-forms)
+  (dolist (form body)
     (newline-and-indent) (ps-print form) (psw #\;))
   (decf *indent-level*)
   (newline-and-indent)
   (psw "@end @*/"))
 
-(defprinter js-instanceof (value type)
-  (psw #\() (ps-print value) (psw " instanceof ") (ps-print type) (psw #\)))
-
-(defprinter js-named-operator (op value)
-  (format *ps-output-stream* "~(~A~) " op)
-  (ps-print value))
+(defprinter js:instanceof (value type)
+  (psw #\()
+  (if (> (expression-precedence value) (op-precedence 'js:instanceof))
+      (parenthesize-print value)
+      (ps-print value))
+  (psw " instanceof ")
+  (if (> (expression-precedence type) (op-precedence 'js:instanceof))
+      (parenthesize-print type)
+      (ps-print type))
+  (psw #\)))
+
+(defprinter js:escape (literal-js)
+  ;; literal-js should be a form that evaluates to a string containing valid JavaScript
+  (psw literal-js))
+
+;;; named statements
+(defprinter js:throw (x)
+  (psw "throw ") (ps-print x))
+
+(defprinter js:return (x)
+  (psw "return ") (ps-print x))