3 ;;; ecmascript standard:
4 ;;; http://www.ecma-international.org/publications/standards/Ecma-262.htm
6 ;;; javascript name conversion
8 (defparameter *special-chars
*
18 (defun string-chars (string)
19 (coerce string
'list
))
21 (defun constant-string-p (string)
22 (let ((len (length string
))
23 (constant-chars '(#\
+ #\
*)))
25 (member (char string
0) constant-chars
)
26 (member (char string
(1- len
)) constant-chars
))))
28 (defun first-uppercase-p (string)
29 (and (> (length string
) 1)
30 (member (char string
0) '(#\
+ #\
*))))
32 (defun untouchable-string-p (string)
33 (and (> (length string
) 1)
34 (char= #\
: (char string
0))))
36 (defun symbol-to-js (symbol)
37 (when (symbolp symbol
)
38 (setf symbol
(symbol-name symbol
)))
39 (let ((symbols (string-split symbol
'(#\.
))))
40 (cond ((null symbols
) "")
41 ((= (length symbols
) 1)
46 (cond ((constant-string-p symbol
)
48 symbol
(subseq symbol
1 (1- (length symbol
)))))
49 ((first-uppercase-p symbol
)
51 symbol
(subseq symbol
1)))
52 ((untouchable-string-p symbol
)
54 symbol
(subseq symbol
1))))
58 ((and lowercase
(not all-uppercase
))
63 (dotimes (i (length symbol
))
64 (let ((c (char symbol i
)))
67 (setf lowercase
(not lowercase
)))
68 ((assoc c
*special-chars
*)
69 (dolist (i (coerce (cdr (assoc c
*special-chars
*)) 'list
))
72 (coerce (nreverse res
) 'string
)))
73 (t (string-join (mapcar #'symbol-to-js symbols
) ".")))))
77 (defmethod js-equal ((obj1 list
) (obj2 list
))
78 (and (= (length obj1
) (length obj2
))
79 (every #'js-equal obj1 obj2
)))
80 (defmethod js-equal ((obj1 t
) (obj2 t
))
83 (defmacro defjsclass
(name superclasses slots
&rest class-options
)
84 (let ((slot-names (mapcar #'(lambda (slot) (if (atom slot
) slot
(first slot
))) slots
)))
86 (defclass ,name
,superclasses
87 ,slots
,@class-options
)
88 (defmethod js-equal ((obj1 ,name
) (obj2 ,name
))
89 (every #'(lambda (slot)
90 (js-equal (slot-value obj1 slot
)
91 (slot-value obj2 slot
)))
94 (defjsclass statement
()
95 ((value :initarg
:value
:accessor value
:initform nil
)))
97 (defjsclass expression
(statement)
102 (defun special-append-to-last (form elt
)
103 (flet ((special-append (form elt
)
104 (let ((len (length form
)))
106 (string= (char form
(1- len
)) elt
))
108 (concatenate 'string form elt
)))))
109 (cond ((stringp form
)
110 (special-append form elt
))
112 (let ((last (last form
)))
113 (if (stringp (car last
))
114 (rplaca last
(special-append (car last
) elt
))
115 (append-to-last (car last
) elt
))
117 (t (error "unsupported form ~S" form
)))))
119 (defun dwim-join (value-string-lists max-length
124 (white-space (make-string (length start
) :initial-element
#\Space
))
126 (append-to-last #'append-to-last
)
129 (format t
"value-string-lists: ~S~%" value-string-lists
)
131 ;;; collect single value-string-lists until line full
133 (do* ((string-lists value-string-lists
(cdr string-lists
))
134 (string-list (car string-lists
) (car string-lists
))
143 (list (concatenate 'string start end
))
147 (funcall append-to-last
(first res
) end
)))
150 (format t
"string-list: ~S~%" string-list
)
153 (unless (null (cdr string-lists
))
154 (funcall append-to-last string-list join-after
)))
156 (if (and collect
(= (length string-list
) 1))
159 (format t
"cur-elt: ~S line-length ~D, max-length ~D, string: ~S~%"
161 (+ (length (first string-list
))
166 (< (+ (length (first string-list
))
167 (length cur-elt
)) max-length
))
169 (concatenate 'string cur-elt
170 (if (or is-first
(and cur-empty
(string= join-before
"")))
171 "" (concatenate 'string separator join-before
))
176 (setf cur-elt
(concatenate 'string white-space
177 join-before
(first string-list
))
183 (setf cur-elt white-space
185 (setf res
(nconc (nreverse
186 (cons (concatenate 'string
191 (mapcar #'(lambda (x) (concatenate 'string white-space x
))
194 (setf cur-elt white-space cur-empty t
)))))
196 (defmethod js-to-strings ((expression expression
) start-pos
)
197 (declare (ignore start-pos
))
198 (list (princ-to-string (value expression
))))
200 (defmethod js-to-statement-strings ((expression expression
) start-pos
)
201 (js-to-strings expression start-pos
))
203 (defmethod js-to-statement-strings ((statement statement
) start-pos
)
204 (declare (ignore start-pos
))
205 (list (princ-to-string (value statement
))))
209 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
210 (defvar *js-compiler-macros
* (make-hash-table :test
'equal
)
211 "*JS-COMPILER-MACROS* is a hash-table containing the functions corresponding
212 to javascript special forms, indexed by their name. Javascript special
213 forms are compiler macros for JS expressions.")
215 (defun undefine-js-compiler-macro (name)
216 (declare (type symbol name
))
217 (when (gethash (symbol-name name
) *js-compiler-macros
*)
218 (warn "Redefining compiler macro ~S" name
)
219 (remhash (symbol-name name
) *js-compiler-macros
*))))
221 (defmacro define-js-compiler-macro
(name lambda-list
&rest body
)
222 "Define a javascript compiler macro NAME. Arguments are destructured
223 according to LAMBDA-LIST. The resulting JS language types are appended
224 to the ongoing javascript compilation."
225 (let ((js-name (intern (concatenate 'string
"JS-" (symbol-name name
)) #.
*package
*)))
226 `(eval-when (:compile-toplevel
:load-toplevel
:execute
)
227 (defun ,js-name
,lambda-list
,@body
)
228 (setf (gethash ,(symbol-name name
) *js-compiler-macros
*) #',js-name
))))
230 (defun js-compiler-macro-form-p (form)
231 (when (and (symbolp (car form
))
232 (gethash (symbol-name (car form
)) *js-compiler-macros
*))
235 (defun js-get-compiler-macro (name)
237 (gethash (symbol-name name
) *js-compiler-macros
*)))
241 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
242 (defvar *js-macro-toplevel
* (make-hash-table :test
'equal
)
243 "Toplevel of macro expansion, holds all the toplevel javascript macros.")
244 (defvar *js-macro-env
* (list *js-macro-toplevel
*)
245 "Current macro environment."))
247 (defun lookup-macro (name)
248 "Lookup the macro NAME in the current macro expansion
249 environment. Returns the macro and the parent macro environment of
251 (unless (symbolp name
)
252 (return-from lookup-macro nil
))
253 (do ((env *js-macro-env
* (cdr env
)))
255 (let ((val (gethash (symbol-name name
) (car env
))))
257 (return-from lookup-macro
258 (values val
(or (cdr env
)
259 (list *js-macro-toplevel
*))))))))
261 (defmacro defjsmacro
(name args
&rest body
)
262 "Define a javascript macro, and store it in the toplevel macro environment."
263 (let ((lambda-list (gensym)))
264 (undefine-js-compiler-macro name
)
265 `(setf (gethash ,(symbol-name name
) *js-macro-toplevel
*)
266 #'(lambda (&rest
,lambda-list
)
267 (destructuring-bind ,args
,lambda-list
,@body
)))))
269 (defun import-macros-from-lisp (&rest names
)
270 "Import the named lisp macros into the js macro expansion"
273 (undefine-js-compiler-macro name
)
274 (setf (gethash (symbol-name name
) *js-macro-toplevel
*)
276 (macroexpand `(,name
,@args
)))))))
278 (defun js-expand-form (expr)
279 "Expand a javascript form."
281 (multiple-value-bind (js-macro macro-env
)
284 (js-expand-form (let ((*js-macro-env
* macro-env
))
288 ((js-compiler-macro-form-p expr
) expr
)
290 ((equal (first expr
) 'quote
) expr
)
292 (t (let ((js-macro (lookup-macro (car expr
))))
294 (js-expand-form (apply js-macro
(cdr expr
)))
297 (defvar *gen-js-name-counter
* 0)
299 (defun gen-js-name-string (&key
(prefix "_ps_"))
300 "Generates a unique valid javascript identifier ()"
302 prefix
(princ-to-string (incf *gen-js-name-counter
*))))
304 (defun gen-js-name (&key
(prefix "_ps_"))
305 "Generate a new javascript identifier."
306 (intern (gen-js-name-string :prefix prefix
)
309 (defmacro with-unique-js-names
(symbols &body body
)
310 "Evaluate BODY with the variables on SYMBOLS bound to new javascript identifiers.
312 Each element of SYMBOLS is either a symbol or a list of (symbol
314 `(let* ,(mapcar (lambda (symbol)
315 (destructuring-bind (symbol &optional prefix
)
320 `(,symbol
(gen-js-name :prefix
,prefix
))
321 `(,symbol
(gen-js-name)))))
325 (defjsmacro rebind
(variables expression
)
326 ;; Creates a new js lexical environment and copies the given
327 ;; variable(s) there. Executes the body in the new environment. This
328 ;; has the same effect as a new (let () ...) form in lisp but works on
329 ;; the js side for js closures."
331 (unless (listp variables
)
332 (setf variables
(list variables
)))
334 (let ((new-context (new *object
)))
335 ,@(loop for variable in variables
336 do
(setf variable
(symbol-to-js variable
))
337 collect
`(setf (slot-value new-context
,variable
) (slot-value this
,variable
)))
339 (return ,expression
))))))
341 (defvar *var-counter
* 0)
343 (defun js-gensym (&optional
(name "js"))
344 (intern (format nil
"tmp-~A-~A" name
(incf *var-counter
*)) #.
*package
*))
348 (defmacro defjsliteral
(name string
)
349 "Define a Javascript literal that will expand to STRING."
350 `(define-js-compiler-macro ,name
() (make-instance 'expression
:value
,string
)))
352 (defjsliteral this
"this")
353 (defjsliteral t
"true")
354 (defjsliteral nil
"null")
355 (defjsliteral false
"false")
356 (defjsliteral undefined
"undefined")
358 (defmacro defjskeyword
(name string
)
359 "Define a Javascript keyword that will expand to STRING."
360 `(define-js-compiler-macro ,name
() (make-instance 'statement
:value
,string
)))
362 (defjskeyword break
"break")
363 (defjskeyword continue
"continue")
367 (defjsclass array-literal
(expression)
368 ((values :initarg
:values
:accessor array-values
)))
370 (define-js-compiler-macro array
(&rest values
)
371 (make-instance 'array-literal
372 :values
(mapcar #'js-compile-to-expression values
)))
374 (defjsmacro list
(&rest values
)
377 (defmethod js-to-strings ((array array-literal
) start-pos
)
378 (let ((value-string-lists
379 (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
380 (array-values array
)))
381 (max-length (- 80 start-pos
2)))
382 (dwim-join value-string-lists max-length
383 :start
"[ " :end
" ]"
386 (defjsclass js-aref
(expression)
387 ((array :initarg
:array
388 :accessor aref-array
)
389 (index :initarg
:index
390 :accessor aref-index
)))
392 (define-js-compiler-macro aref
(array &rest coords
)
393 (make-instance 'js-aref
394 :array
(js-compile-to-expression array
)
395 :index
(mapcar #'js-compile-to-expression coords
)))
397 (defmethod js-to-strings ((aref js-aref
) start-pos
)
398 (dwim-join (cons (js-to-strings (aref-array aref
) start-pos
)
399 (mapcar #'(lambda (x) (dwim-join (list (js-to-strings x
(+ start-pos
2)))
401 :start
"[" :end
"]"))
403 (- 80 start-pos
2) :separator
""
406 (defjsmacro make-array
(&rest inits
)
407 `(new (*array
,@inits
)))
409 ;;; object literals (maps and hash-tables)
411 (defjsclass object-literal
(expression)
412 ((values :initarg
:values
:accessor object-values
)))
414 (define-js-compiler-macro {} (&rest values
)
415 (make-instance 'object-literal
417 for
(key value
) on values by
#'cddr
418 collect
(cons key
(js-compile-to-expression value
)))))
420 (defmethod js-to-strings ((obj object-literal
) start-pos
)
422 for
(key . value
) in
(object-values obj
)
424 (dwim-join (list (list (format nil
"~A:" (symbol-to-js key
)))
425 (js-to-strings value
(+ start-pos
2)))
427 :start
"" :end
"" :join-after
"")))
429 :start
"{ " :end
" }"
434 (defjsclass string-literal
(expression)
437 (defvar *js-quote-char
* #\'
438 "Specifies which character JS sholud use for delimiting strings.
440 This variable is usefull when have to embed some javascript code
441 in an html attribute delimited by #\\\" as opposed to #\\', or
444 (defmethod js-to-strings ((string string-literal
) start-pos
)
445 (declare (ignore start-pos
)
446 (inline lisp-special-char-to-js
))
447 (list (with-output-to-string (escaped)
448 (write-char *js-quote-char
* escaped
)
450 for char across
(value string
)
451 for code
= (char-code char
)
452 for special
= (lisp-special-char-to-js char
)
456 (write-char #\\ escaped
)
457 (write-char special escaped
))
458 ((or (<= code
#x1f
) (>= code
#x80
))
459 (format escaped
"\\u~4,'0x" code
))
460 (t (write-char char escaped
)))
461 finally
(write-char *js-quote-char
* escaped
)))))
463 (defparameter *js-lisp-escaped-chars
*
467 (#\f .
#.
(code-char 12))
472 (defun lisp-special-char-to-js(lisp-char)
473 (car (rassoc lisp-char
*js-lisp-escaped-chars
*)))
477 (defjsclass number-literal
(expression)
482 (defjsclass js-variable
(expression)
485 (defmethod js-to-strings ((v js-variable
) start-form
)
486 (declare (ignore start-form
))
487 (list (symbol-to-js (value v
))))
489 ;;; arithmetic operators
491 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
493 (defparameter *op-precedence-hash
* (make-hash-table))
495 (defparameter *op-precedences
*
512 (setf *= /= %
= += -
= <<= >>= >>>= \
&= ^
= \|
=)
515 ;;; generate the operator precedences from *OP-PRECEDENCES*
516 (let ((precedence 1))
517 (dolist (ops *op-precedences
*)
519 (setf (gethash op
*op-precedence-hash
*) precedence
))
522 (defun js-convert-op-name (op)
531 (defjsclass op-form
(expression)
532 ((operator :initarg
:operator
:accessor operator
)
533 (args :initarg
:args
:accessor op-args
)))
535 (defun op-form-p (form)
537 (not (js-compiler-macro-form-p form
))
538 (not (null (gethash (first form
) *op-precedence-hash
*)))))
540 (defun klammer (string-list)
541 (prepend-to-first string-list
"(")
542 (append-to-last string-list
")")
545 (defmethod expression-precedence ((expression expression
))
548 (defmethod expression-precedence ((form op-form
))
549 (gethash (operator form
) *op-precedence-hash
*))
551 (defmethod js-to-strings ((form op-form
) start-pos
)
552 (let* ((precedence (expression-precedence form
))
554 (mapcar #'(lambda (x)
555 (let ((string-list (js-to-strings x
(+ start-pos
2))))
556 (if (>= (expression-precedence x
) precedence
)
557 (klammer string-list
)
560 (max-length (- 80 start-pos
2))
561 (op-string (format nil
"~A " (operator form
))))
562 (dwim-join value-string-lists max-length
:join-before op-string
)
565 (defjsmacro 1-
(form)
568 (defjsmacro 1+ (form)
571 (defjsclass one-op
(expression)
572 ((pre-p :initarg
:pre-p
574 :accessor one-op-pre-p
)
578 (defmethod js-to-strings ((one-op one-op
) start-pos
)
579 (let* ((value (value one-op
))
580 (value-strings (js-to-strings value start-pos
)))
581 (when (typep value
'op-form
)
582 (setf value-strings
(klammer value-strings
)))
583 (if (one-op-pre-p one-op
)
584 (prepend-to-first value-strings
586 (append-to-last value-strings
589 (define-js-compiler-macro incf
(x)
590 (make-instance 'one-op
:pre-p t
:op
"++"
591 :value
(js-compile-to-expression x
)))
592 (define-js-compiler-macro ++ (x)
593 (make-instance 'one-op
:pre-p nil
:op
"++"
594 :value
(js-compile-to-expression x
)))
595 (define-js-compiler-macro decf
(x)
596 (make-instance 'one-op
:pre-p t
:op
"--"
597 :value
(js-compile-to-expression x
)))
598 (define-js-compiler-macro --
(x)
599 (make-instance 'one-op
:pre-p nil
:op
"--"
600 :value
(js-compile-to-expression x
)))
603 (define-js-compiler-macro not
(x)
604 (let ((value (js-compile-to-expression x
)))
605 (if (and (typep value
'op-form
)
606 (= (length (op-args value
)) 2))
607 (let ((new-op (case (operator value
)
618 (make-instance 'op-form
:operator new-op
619 :args
(op-args value
))
620 (make-instance 'one-op
:pre-p t
:op
"!"
622 (make-instance 'one-op
:pre-p t
:op
"!"
627 (defjsclass function-call
(expression)
628 ((function :initarg
:function
:accessor f-function
)
629 (args :initarg
:args
:accessor f-args
)))
631 (defun funcall-form-p (form)
633 (not (op-form-p form
))
634 (not (js-compiler-macro-form-p form
))))
636 (defmethod js-to-strings ((form function-call
) start-pos
)
637 (let* ((value-string-lists
638 (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
640 (max-length (- 80 start-pos
2))
641 (args (dwim-join value-string-lists max-length
642 :start
"(" :end
")" :join-after
",")))
643 (etypecase (f-function form
)
645 (dwim-join (list (append (dwim-join (list (js-to-strings (f-function form
) (+ start-pos
2)))
647 :start
"(" :end
")" :separator
"")
651 ((or js-variable js-aref js-slot-value
)
652 (dwim-join (list (js-to-strings (f-function form
) (+ start-pos
2))
657 ;; TODO it adds superfluous newlines after each ()
658 ;; and it's nearly the same as the js-lambda case above
659 (dwim-join (list (append (dwim-join (list (js-to-strings (f-function form
) (+ start-pos
2)))
660 max-length
:separator
"")
662 max-length
:separator
"")))))
664 (defjsclass method-call
(expression)
665 ((method :initarg
:method
:accessor m-method
)
666 (object :initarg
:object
:accessor m-object
)
667 (args :initarg
:args
:accessor m-args
)))
669 (defmethod js-to-strings ((form method-call
) start-pos
)
670 (let ((fname (dwim-join (list (js-to-strings (m-object form
) (+ start-pos
2))
671 (list (symbol-to-js (m-method form
))))
675 (let ((butlast (butlast fname
))
676 (last (car (last fname
))))
678 (dwim-join (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
685 (defun method-call-p (form)
686 (and (funcall-form-p form
)
687 (symbolp (first form
))
688 (eql (char (symbol-name (first form
)) 0) #\.
)))
692 (defjsclass js-body
(expression)
693 ((stmts :initarg
:stmts
:accessor b-stmts
)
694 (indent :initarg
:indent
:initform
"" :accessor b-indent
)))
696 (define-js-compiler-macro progn
(&rest body
)
697 (make-instance 'js-body
698 :stmts
(mapcar #'js-compile-to-statement body
)))
700 (defmethod initialize-instance :after
((body js-body
) &rest initargs
)
701 (declare (ignore initargs
))
702 (let* ((stmts (b-stmts body
))
704 (last-stmt (car last
)))
705 (when (typep last-stmt
'js-body
)
707 (nconc (butlast stmts
)
708 (b-stmts last-stmt
))))))
711 (defmethod js-to-statement-strings ((body js-body
) start-pos
)
712 (dwim-join (mapcar #'(lambda (x) (js-to-statement-strings x
(+ start-pos
2)))
716 :append-to-last
#'special-append-to-last
717 :start
(b-indent body
) :collect nil
720 (defmethod js-to-strings ((body js-body
) start-pos
)
721 (dwim-join (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
724 :append-to-last
#'special-append-to-last
726 :start
(b-indent body
)))
728 (defjsclass js-sub-body
(js-body)
731 (defmethod js-to-statement-strings ((body js-sub-body
) start-pos
)
732 (declare (ignore start-pos
))
733 (nconc (list "{") (call-next-method) (list "}")))
735 (defmethod expression-precedence ((body js-body
))
736 (if (= (length (b-stmts body
)) 1)
737 (expression-precedence (first (b-stmts body
)))
738 (gethash 'comma
*op-precedence-hash
*)))
740 ;;; function definition
742 (defjsclass js-lambda
(expression)
743 ((args :initarg
:args
:accessor lambda-args
)
744 (body :initarg
:body
:accessor lambda-body
)))
746 (define-js-compiler-macro lambda
(args &rest body
)
747 (make-instance 'js-lambda
748 :args
(mapcar #'js-compile-to-symbol args
)
749 :body
(make-instance 'js-body
751 :stmts
(mapcar #'js-compile-to-statement body
))))
753 (defmethod js-to-strings ((lambda js-lambda
) start-pos
)
754 (let ((fun-header (dwim-join (mapcar #'(lambda (x)
755 (list (symbol-to-js x
)))
756 (lambda-args lambda
))
758 :start
(function-start-string lambda
)
759 :end
") {" :join-after
","))
760 (fun-body (js-to-statement-strings (lambda-body lambda
) (+ start-pos
2))))
761 (nconc fun-header fun-body
(list "}"))))
763 (defmethod function-start-string ((lambda js-lambda
))
766 (defmethod js-to-statement-strings ((lambda js-lambda
) start-pos
)
767 (js-to-strings lambda start-pos
))
769 (defjsclass js-defun
(js-lambda)
770 ((name :initarg
:name
:accessor defun-name
)))
772 (define-js-compiler-macro defun
(name args
&rest body
)
773 (make-instance 'js-defun
774 :name
(js-compile-to-symbol name
)
775 :args
(mapcar #'js-compile-to-symbol args
)
776 :body
(make-instance 'js-body
778 :stmts
(mapcar #'js-compile-to-statement body
))))
780 (defmethod function-start-string ((defun js-defun))
781 (format nil
"function ~A(" (symbol-to-js (defun-name defun
))))
785 (defjsclass js-object
(expression)
786 ((slots :initarg
:slots
789 (define-js-compiler-macro create
(&rest args
)
790 (make-instance 'js-object
791 :slots
(loop for
(name val
) on args by
#'cddr
792 collect
(list (js-compile-to-symbol name
)
793 (js-compile-to-expression val
)))))
795 (defmethod js-to-strings ((object js-object
) start-pos
)
796 (let ((value-string-lists
797 (mapcar #'(lambda (slot)
798 (dwim-join (list (js-to-strings (second slot
) (+ start-pos
4)))
800 :start
(concatenate 'string
(symbol-to-js (first slot
)) " : ")
801 :white-space
" ")) (o-slots object
)))
802 (max-length (- 80 start-pos
2)))
803 (dwim-join value-string-lists max-length
810 (defjsclass js-slot-value
(expression)
811 ((object :initarg
:object
816 (define-js-compiler-macro slot-value
(obj slot
)
817 (make-instance 'js-slot-value
:object
(js-compile-to-expression obj
)
818 :slot
(js-compile slot
)))
820 (defmethod js-to-strings ((sv js-slot-value
) start-pos
)
821 (append-to-last (js-to-strings (sv-object sv
) start-pos
)
822 (if (symbolp (sv-slot sv
))
823 (format nil
".~A" (symbol-to-js (sv-slot sv
)))
824 (format nil
"[~A]" (first (js-to-strings (sv-slot sv
) 0))))))
826 (defjsmacro with-slots
(slots object
&rest body
)
827 `(symbol-macrolet ,(mapcar #'(lambda (slot)
828 `(,slot
'(slot-value ,object
',slot
)))
834 (define-js-compiler-macro macrolet
(macros &rest body
)
835 (let* ((macro-env (make-hash-table :test
'equal
))
836 (*js-macro-env
* (cons macro-env
*js-macro-env
*)))
837 (dolist (macro macros
)
838 (destructuring-bind (name arglist
&rest body
) macro
839 (setf (gethash (symbol-name name
) macro-env
)
840 (compile nil
`(lambda ,arglist
,@body
)))))
841 (js-compile `(progn ,@body
))))
843 (defjsmacro symbol-macrolet
(macros &rest body
)
844 `(macrolet ,(mapcar #'(lambda (macro)
845 `(,(first macro
) () ,@(rest macro
))) macros
)
850 (defjsmacro lisp
(&rest forms
)
851 (eval (cons 'progn forms
)))
855 (defjsclass js-if
(expression)
856 ((test :initarg
:test
863 (define-js-compiler-macro if
(test then
&optional else
)
864 (make-instance 'js-if
:test
(js-compile-to-expression test
)
865 :then
(js-compile-to-body then
:indent
" ")
867 (js-compile-to-body else
:indent
" "))))
869 (defmethod initialize-instance :after
((if js-if
) &rest initargs
)
870 (declare (ignore initargs
))
871 (when (and (if-then if
)
872 (typep (if-then if
) 'js-sub-body
))
873 (change-class (if-then if
) 'js-body
))
874 (when (and (if-else if
)
875 (typep (if-else if
) 'js-sub-body
))
876 (change-class (if-else if
) 'js-body
)))
878 (defmethod js-to-statement-strings ((if js-if
) start-pos
)
879 (let ((if-strings (dwim-join (list (js-to-strings (if-test if
) 0))
883 (then-strings (js-to-statement-strings (if-then if
) (+ start-pos
2)))
884 (else-strings (when (if-else if
)
885 (js-to-statement-strings (if-else if
)
887 (nconc if-strings then-strings
(if else-strings
888 (nconc (list "} else {") else-strings
(list "}"))
891 (defmethod expression-precedence ((if js-if
))
892 (gethash 'if
*op-precedence-hash
*))
894 (defmethod js-to-strings ((if js-if
) start-pos
)
895 (assert (typep (if-then if
) 'expression
))
897 (assert (typep (if-else if
) 'expression
)))
898 (dwim-join (list (append-to-last (js-to-strings (if-test if
) start-pos
) " ?")
899 (let* ((new-then (make-instance 'js-body
900 :stmts
(b-stmts (if-then if
))
902 (res (js-to-strings new-then start-pos
)))
903 (if (>= (expression-precedence (if-then if
))
904 (expression-precedence if
))
909 (let* ((new-else (make-instance 'js-body
910 :stmts
(b-stmts (if-else if
))
912 (res (js-to-strings new-else start-pos
)))
913 (if (>= (expression-precedence (if-else if
))
914 (expression-precedence if
))
921 (defjsmacro when
(test &rest body
)
922 `(if ,test
(progn ,@body
)))
924 (defjsmacro unless
(test &rest body
)
925 `(if (not ,test
) (progn ,@body
)))
927 ;;; single keyword expressions and statements
929 (defmacro define-js-single-op
(name &optional
(superclass 'expression
))
930 (let ((js-name (intern (concatenate 'string
"JS-" (symbol-name name
)) #.
*package
*)))
932 (defjsclass ,js-name
(,superclass
)
934 (define-js-compiler-macro ,name
(value)
935 (make-instance ',js-name
:value
(js-compile-to-expression value
)))
936 (defmethod ,(if (eql superclass
'expression
)
938 'js-to-statement-strings
) ((,name
,js-name
) start-pos
)
939 (dwim-join (list (js-to-strings (value ,name
) (+ start-pos
2)))
941 :start
,(concatenate 'string
(string-downcase (symbol-name name
)) " ")
942 :white-space
" ")))))
945 (define-js-single-op return statement
)
946 (define-js-single-op throw statement
)
947 (define-js-single-op delete
)
948 (define-js-single-op void
)
949 (define-js-single-op typeof
)
950 (define-js-single-op new
)
952 ;; TODO this may not be the best integrated implementation of
953 ;; instanceof into the rest of the code
954 (defjsclass js-instanceof
(expression)
956 (type :initarg
:type
)))
958 (define-js-compiler-macro instanceof
(value type
)
959 (make-instance 'js-instanceof
960 :value
(js-compile-to-expression value
)
961 :type
(js-compile-to-expression type
)))
963 (defmethod js-to-strings ((instanceof js-instanceof
) start-pos
)
965 (list (js-to-strings (value instanceof
) (+ start-pos
2))
967 (js-to-strings (slot-value instanceof
'type
) (+ start-pos
2)))
974 (defjsclass js-setf
(expression)
975 ((lhs :initarg
:lhs
:accessor setf-lhs
)
976 (rhsides :initarg
:rhsides
:accessor setf-rhsides
)))
978 (defun assignment-op (op)
994 (defun make-js-test (lhs rhs
)
995 (if (and (typep rhs
'op-form
)
996 (member lhs
(op-args rhs
) :test
#'js-equal
))
997 (let ((args-without (remove lhs
(op-args rhs
)
998 :count
1 :test
#'js-equal
))
999 (args-without-first (remove lhs
(op-args rhs
)
1002 (one (list (make-instance 'number-literal
:value
1))))
1004 (format t
"OPERATOR: ~S, ARGS-WITHOUT: ~S, ARGS-WITHOUT-FIRST ~S~%"
1008 (cond ((and (js-equal args-without one
)
1009 (eql (operator rhs
) '+))
1010 (make-instance 'one-op
:pre-p nil
:op
"++"
1012 ((and (js-equal args-without-first one
)
1013 (eql (operator rhs
) '-
))
1014 (make-instance 'one-op
:pre-p nil
:op
"--"
1016 ((and (assignment-op (operator rhs
))
1017 (member (operator rhs
)
1019 (make-instance 'op-form
1020 :operator
(assignment-op (operator rhs
))
1021 :args
(list lhs
(make-instance 'op-form
1022 :operator
(operator rhs
)
1023 :args args-without
))))
1024 ((and (assignment-op (operator rhs
))
1025 (js-equal (first (op-args rhs
)) lhs
))
1026 (make-instance 'op-form
1027 :operator
(assignment-op (operator rhs
))
1028 :args
(list lhs
(make-instance 'op-form
1029 :operator
(operator rhs
)
1030 :args
(cdr (op-args rhs
))))))
1031 (t (make-instance 'js-setf
:lhs lhs
:rhsides
(list rhs
)))))
1032 (make-instance 'js-setf
:lhs lhs
:rhsides
(list rhs
))))
1034 (define-js-compiler-macro setf
(&rest args
)
1035 (let ((assignments (loop for
(lhs rhs
) on args by
#'cddr
1036 for rexpr
= (js-compile-to-expression rhs
)
1037 for lexpr
= (js-compile-to-expression lhs
)
1038 collect
(make-js-test lexpr rexpr
))))
1039 (if (= (length assignments
) 1)
1041 (make-instance 'js-body
:indent
"" :stmts assignments
))))
1043 (defmethod js-to-strings ((setf js-setf
) start-pos
)
1044 (dwim-join (cons (js-to-strings (setf-lhs setf
) start-pos
)
1045 (mapcar #'(lambda (x) (js-to-strings x start-pos
)) (setf-rhsides setf
)))
1049 (defmethod expression-precedence ((setf js-setf
))
1050 (gethash '= *op-precedence-hash
*))
1054 (defjsclass js-defvar
(statement)
1055 ((names :initarg
:names
:accessor var-names
)
1056 (value :initarg
:value
:accessor var-value
)))
1058 (define-js-compiler-macro defvar
(name &optional value
)
1059 (make-instance 'js-defvar
:names
(list (js-compile-to-symbol name
))
1060 :value
(when value
(js-compile-to-expression value
))))
1062 (defmethod js-to-statement-strings ((defvar js-defvar
) start-pos
)
1063 (dwim-join (nconc (mapcar #'(lambda (x) (list (symbol-to-js x
))) (var-names defvar
))
1064 (when (var-value defvar
)
1065 (list (js-to-strings (var-value defvar
) start-pos
))))
1068 :start
"var " :end
";"))
1072 (define-js-compiler-macro let
(decls &rest body
)
1073 (let ((single-defvar (make-instance 'js-defvar
1074 :names
(mapcar #'js-compile-to-symbol
1075 (remove-if-not #'atom decls
))
1077 (defvars (mapcar #'(lambda (decl)
1078 (let ((name (first decl
))
1079 (value (second decl
)))
1080 (make-instance 'js-defvar
1081 :names
(list (js-compile-to-symbol name
))
1082 :value
(js-compile-to-expression value
))))
1083 (remove-if #'atom decls
))))
1084 (make-instance 'js-sub-body
1086 :stmts
(nconc (when (var-names single-defvar
) (list single-defvar
))
1088 (mapcar #'js-compile-to-statement body
)))))
1092 (defjsclass js-for
(statement)
1093 ((vars :initarg
:vars
:accessor for-vars
)
1094 (steps :initarg
:steps
:accessor for-steps
)
1095 (check :initarg
:check
:accessor for-check
)
1096 (body :initarg
:body
:accessor for-body
)))
1098 (defun make-for-vars (decls)
1099 (loop for decl in decls
1100 for var
= (if (atom decl
) decl
(first decl
))
1101 for init
= (if (atom decl
) nil
(second decl
))
1102 collect
(make-instance 'js-defvar
:names
(list (js-compile-to-symbol var
))
1103 :value
(js-compile-to-expression init
))))
1105 (defun make-for-steps (decls)
1106 (loop for decl in decls
1107 when
(= (length decl
) 3)
1108 collect
(js-compile-to-expression (third decl
))))
1110 (define-js-compiler-macro do
(decls termination
&rest body
)
1111 (let ((vars (make-for-vars decls
))
1112 (steps (make-for-steps decls
))
1113 (check (js-compile-to-expression (list 'not
(first termination
))))
1114 (body (js-compile-to-body (cons 'progn body
) :indent
" ")))
1115 (make-instance 'js-for
1121 (defjsmacro dotimes
(iter &rest body
)
1122 (let ((var (first iter
))
1123 (times (second iter
)))
1124 `(do ((,var
0 (1+ ,var
)))
1128 (defjsmacro dolist
(i-array &rest body
)
1129 (let ((var (first i-array
))
1130 (array (second i-array
))
1131 (arrvar (js-gensym "arr"))
1132 (idx (js-gensym "i")))
1133 `(let ((,arrvar
,array
))
1134 (do ((,idx
0 (1+ ,idx
)))
1135 ((>= ,idx
(slot-value ,arrvar
'length
)))
1136 (let ((,var
(aref ,arrvar
,idx
)))
1139 (defmethod js-to-statement-strings ((for js-for
) start-pos
)
1140 (let* ((init (dwim-join (mapcar #'(lambda (x)
1141 (dwim-join (list (list (symbol-to-js (first (var-names x
))))
1142 (js-to-strings (var-value x
)
1148 :start
"var " :join-after
","))
1149 (check (js-to-strings (for-check for
) (+ start-pos
2)))
1150 (steps (dwim-join (mapcar #'(lambda (x var
)
1152 (list (list (symbol-to-js (first (var-names var
))))
1153 (js-to-strings x
(- start-pos
2)))
1160 (header (dwim-join (list init check steps
)
1162 :start
"for (" :end
") {"
1164 (body (js-to-statement-strings (for-body for
) (+ start-pos
2))))
1165 (nconc header body
(list "}"))))
1167 (defjsclass for-each
(statement)
1168 ((name :initarg
:name
:accessor fe-name
)
1169 (value :initarg
:value
:accessor fe-value
)
1170 (body :initarg
:body
:accessor fe-body
)))
1172 (define-js-compiler-macro doeach
(decl &rest body
)
1173 (make-instance 'for-each
:name
(js-compile-to-symbol (first decl
))
1174 :value
(js-compile-to-expression (second decl
))
1175 :body
(js-compile-to-body (cons 'progn body
) :indent
" ")))
1177 (defmethod js-to-statement-strings ((fe for-each
) start-pos
)
1178 (let ((header (dwim-join (list (list (symbol-to-js (fe-name fe
)))
1180 (js-to-strings (fe-value fe
) (+ start-pos
2)))
1184 (body (js-to-statement-strings (fe-body fe
) (+ start-pos
2))))
1185 (nconc header body
(list "}"))))
1187 (defjsclass js-while
(statement)
1188 ((check :initarg
:check
:accessor while-check
)
1189 (body :initarg
:body
:accessor while-body
)))
1191 (define-js-compiler-macro while
(check &rest body
)
1192 (make-instance 'js-while
1193 :check
(js-compile-to-expression check
)
1194 :body
(js-compile-to-body (cons 'progn body
) :indent
" ")))
1196 (defmethod js-to-statement-strings ((while js-while
) start-pos
)
1197 (let ((header (dwim-join (list (js-to-strings (while-check while
) (+ start-pos
2)))
1201 (body (js-to-statement-strings (while-body while
) (+ start-pos
2))))
1202 (nconc header body
(list "}"))))
1206 (defjsclass js-with
(statement)
1207 ((obj :initarg
:obj
:accessor with-obj
)
1208 (body :initarg
:body
:accessor with-body
)))
1210 (define-js-compiler-macro with
(statement &rest body
)
1211 (make-instance 'js-with
1212 :obj
(js-compile-to-expression statement
)
1213 :body
(js-compile-to-body (cons 'progn body
) :indent
" ")))
1215 (defmethod js-to-statement-strings ((with js-with
) start-pos
)
1216 (nconc (dwim-join (list (js-to-strings (with-obj with
) (+ start-pos
2)))
1218 :start
"with (" :end
") {")
1219 (js-to-statement-strings (with-body with
) (+ start-pos
2))
1224 (defjsclass js-switch
(statement)
1225 ((value :initarg
:value
:accessor case-value
)
1226 (clauses :initarg
:clauses
:accessor case-clauses
)))
1228 (define-js-compiler-macro switch
(value &rest clauses
)
1229 (let ((clauses (mapcar #'(lambda (clause)
1230 (let ((val (first clause
))
1231 (body (cdr clause
)))
1232 (list (if (eql val
'default
)
1234 (js-compile-to-expression val
))
1235 (js-compile-to-body (cons 'progn body
) :indent
" "))))
1237 (check (js-compile-to-expression value
)))
1238 (make-instance 'js-switch
:value check
1241 (defmethod js-to-statement-strings ((case js-switch
) start-pos
)
1242 (let ((body (mapcan #'(lambda (clause)
1243 (let ((val (car clause
))
1244 (body (second clause
)))
1245 (dwim-join (list (if (eql val
'default
)
1247 (js-to-strings val
(+ start-pos
2)))
1248 (js-to-statement-strings body
(+ start-pos
2)))
1250 :start
(if (eql val
'default
) " default" " case ")
1252 :join-after
":"))) (case-clauses case
))))
1255 (format t
"body: ~S~%" body
)
1256 (nconc (dwim-join (list (js-to-strings (case-value case
) (+ start-pos
2)))
1258 :start
"switch (" :end
") {")
1262 (defjsmacro case
(value &rest clauses
)
1263 (labels ((make-clause (val body more
)
1265 (append (mapcar #'list
(butlast val
))
1266 (make-clause (first (last val
)) body more
)))
1267 ((member val
'(t otherwise
))
1268 (make-clause 'default body more
))
1269 (more `((,val
,@body break
)))
1270 (t `((,val
,@body
))))))
1271 `(switch ,value
,@(mapcon #'(lambda (x)
1272 (make-clause (car (first x
))
1279 (defjsclass js-try
(statement)
1280 ((body :initarg
:body
:accessor try-body
)
1281 (catch :initarg
:catch
:accessor try-catch
)
1282 (finally :initarg
:finally
:accessor try-finally
)))
1284 (define-js-compiler-macro try
(body &rest clauses
)
1285 (let ((body (js-compile-to-body body
:indent
" "))
1286 (catch (cdr (assoc :catch clauses
)))
1287 (finally (cdr (assoc :finally clauses
))))
1288 (make-instance 'js-try
1290 :catch
(when catch
(list (js-compile-to-symbol (caar catch
))
1291 (js-compile-to-body (cons 'progn
(cdr catch
))
1293 :finally
(when finally
(js-compile-to-body (cons 'progn finally
)
1296 (defmethod js-to-statement-strings ((try js-try
) start-pos
)
1297 (let* ((catch (try-catch try
))
1298 (finally (try-finally try
))
1299 (catch-list (when catch
1301 (dwim-join (list (list (symbol-to-js (first catch
))))
1305 (js-to-statement-strings (second catch
) (+ start-pos
2)))))
1306 (finally-list (when finally
1307 (nconc (list "} finally {")
1308 (js-to-statement-strings finally
(+ start-pos
2))))))
1309 (nconc (list "try {")
1310 (js-to-statement-strings (try-body try
) (+ start-pos
2))
1317 (defjsclass regex
(expression)
1320 (define-js-compiler-macro regex
(regex)
1321 (make-instance 'regex
:value
(string regex
)))
1323 (defun first-slash-p (string)
1324 (and (> (length string
) 0)
1325 (eq (char string
0) '#\
/)))
1327 (defmethod js-to-strings ((regex regex
) start-pos
)
1328 (declare (ignore start-pos
))
1329 (let ((slash (if (first-slash-p (value regex
)) nil
"/")))
1330 (list (format nil
(concatenate 'string slash
"~A" slash
) (value regex
)))))
1332 ;;; conditional compilation
1334 (defjsclass cc-if
()
1335 ((test :initarg
:test
:accessor cc-if-test
)
1336 (body :initarg
:body
:accessor cc-if-body
)))
1338 (defmethod js-to-statement-strings ((cc cc-if
) start-pos
)
1339 (nconc (list (format nil
"/*@if ~A" (cc-if-test cc
)))
1340 (mapcan #'(lambda (x) (js-to-strings x start-pos
)) (cc-if-body cc
))
1343 (define-js-compiler-macro cc-if
(test &rest body
)
1344 (make-instance 'cc-if
:test test
1345 :body
(mapcar #'js-compile body
)))
1349 (defun js-compile (form)
1350 (setf form
(js-expand-form form
))
1351 (cond ((stringp form
)
1352 (make-instance 'string-literal
:value form
))
1354 (make-instance 'string-literal
:value
(string form
)))
1356 (make-instance 'number-literal
:value form
))
1358 (let ((c-macro (js-get-compiler-macro form
)))
1361 (make-instance 'js-variable
:value form
))))
1363 (eql (first form
) 'quote
))
1366 (js-compile-list form
))
1367 (t (error "Unknown atomar expression ~S" form
))))
1369 (defun js-compile-list (form)
1370 (let* ((name (car form
))
1372 (js-form (js-get-compiler-macro name
)))
1374 (apply js-form args
))
1377 (make-instance 'op-form
1378 :operator
(js-convert-op-name (js-compile-to-symbol (first form
)))
1379 :args
(mapcar #'js-compile-to-expression
(rest form
))))
1381 ((method-call-p form
)
1382 (make-instance 'method-call
1383 :method
(js-compile-to-symbol (first form
))
1384 :object
(js-compile-to-expression (second form
))
1385 :args
(mapcar #'js-compile-to-expression
(cddr form
))))
1387 ((funcall-form-p form
)
1388 (make-instance 'function-call
1389 :function
(js-compile-to-expression (first form
))
1390 :args
(mapcar #'js-compile-to-expression
(rest form
))))
1392 (t (error "Unknown form ~S" form
)))))
1394 (defun js-compile-to-expression (form)
1395 (let ((res (js-compile form
)))
1396 (assert (typep res
'expression
))
1399 (defun js-compile-to-symbol (form)
1400 (let ((res (js-compile form
)))
1401 (when (typep res
'js-variable
)
1402 (setf res
(value res
)))
1403 (assert (symbolp res
))
1406 (defun js-compile-to-statement (form)
1407 (let ((res (js-compile form
)))
1408 (assert (typep res
'statement
))
1411 (defun js-compile-to-body (form &key
(indent ""))
1412 (let ((res (js-compile-to-statement form
)))
1413 (if (typep res
'js-body
)
1414 (progn (setf (b-indent res
) indent
)
1416 (make-instance 'js-body
1418 :stmts
(list res
)))))
1422 (defjsmacro floor
(expr)
1423 `(*Math.floor
,expr
))
1425 (defjsmacro random
()
1430 (define-js-compiler-macro js
(&rest body
)
1431 (make-instance 'string-literal
1432 :value
(string-join (js-to-statement-strings
1433 (js-compile (cons 'progn body
)) 0) " ")))
1435 (define-js-compiler-macro js-inline
(&rest body
)
1436 (make-instance 'string-literal
1440 (string-join (js-to-statement-strings
1441 (js-compile (cons 'progn body
)) 0) " "))))
1444 (defmacro js
(&rest body
)
1445 `(js* '(progn ,@body
)))
1447 (defmacro js
* (&rest body
)
1448 "Return the javascript string representing BODY.
1452 (js-to-statement-strings (js-compile (list 'progn
,@body
)) 0)
1453 (string #\Newline
)))
1455 (defun js-to-string (expr)
1457 (js-to-statement-strings (js-compile expr
) 0)
1458 (string #\Newline
)))
1460 (defun js-to-line (expr)
1462 (js-to-statement-strings (js-compile expr
) 0) " "))
1464 (defmacro js-file
(&rest body
)
1469 (defmacro js-script
(&rest body
)
1470 `((:script
:type
"text/javascript")
1471 (:princ
(format nil
"~%// <![CDATA[~%"))
1472 (:princ
(js ,@body
))
1473 (:princ
(format nil
"~%// ]]>~%"))))
1475 (defmacro js-inline
(&rest body
)
1476 `(js-inline* '(progn ,@body
)))
1478 (defmacro js-inline
* (&rest body
)
1479 "Just like JS-INLINE except that BODY is evaluated before being
1480 converted to javascript."
1481 `(concatenate 'string
"javascript:"
1482 (string-join (js-to-statement-strings (js-compile (list 'progn
,@body
)) 0) " ")))