eval-when special form
[clinton/parenscript.git] / src / parser.lisp
... / ...
CommitLineData
1(in-package :parenscript)
2
3;;;; The mechanisms for defining macros & parsing Parenscript.
4
5(defclass identifier ()
6 ((symbol :accessor id-symbol :initform nil :type symbol))
7 (:documentation ""))
8
9(defclass script-package ()
10 ;; configuration slots
11 ((name :accessor script-package-name :initform nil :initarg :name :type string
12 :documentation "Canonical name of the package (a String).")
13 (nicknames :accessor script-package-nicknames :initform nil :initarg :nicknames
14 :documentation "List of nicknames for the package (as strings).")
15 (lisp-package :accessor script-package-lisp-package :initform nil :initarg :lisp-package)
16 (secondary-lisp-packages :accessor script-package-secondary-lisp-packages :initform nil
17 :initarg :secondary-lisp-packages)
18 (exports :accessor script-package-exports :initform nil :initarg :exports
19 :documentation "List of exported identifiers.")
20 (used-packages :accessor script-package-used-packages :initform nil :initarg :used-packages
21 :documentation "")
22 (documentation :accessor script-package-documentation :initform nil :initarg :documentation)
23 (compilation-env :accessor script-package-comp-env :initform nil :initarg :comp-env)
24 (locked? :accessor script-package-locked? :initform nil :initarg :locked?
25 :documentation "t if redefinition of top-level symbols is disallowed.")
26 ;; internal use slots
27 (exclusive-lisp-package-p
28 :initform nil :initarg :exclusive-lisp-package?
29 :accessor script-package-exclusive-lisp-package-p
30 :documentation "t if the lisp package is an anonymous package created exclusively for
31 the script package.")
32; (toplevel-identifiers :accessor script-package-toplevel-ids :initarg :toplevel-ids
33; :initform nil)
34; (macro-table :accessor script-package-macro-table
35; :initform (make-hash-table :test #'eql)
36; :documentation "This package's macro environment, set up as a hash table
37; from symbols to macro functions")
38; (special-form-table :accessor script-package-special-form-table
39; :initform (make-hash-table :test #'equal)
40; :documentation "Holds special form macros for the package.
41; Probably not used except for built-in packages."))
42 )
43 (:documentation "A Parenscript package is a lisp object that holds information
44about a set of Suavescript code."))
45
46(defclass compilation-environment ()
47 ((script-packages :accessor comp-env-script-packages :initform nil :initarg :packages
48 :documentation "List of packages defined in this environment.")
49 (current-package :accessor comp-env-current-package :initform nil :initarg :current-package
50 :documentation "Current in-package.")
51
52 (lisp-to-script-package-table
53 :accessor comp-env-lisp-to-script-package-table :initform (make-hash-table)
54 :documentation "Maps a lisp package to a script package.")
55 (compiling-toplevel-p
56 :accessor comp-env-compiling-toplevel-p :initform nil :initarg :processing-toplevel-p
57 :documentation "T if the environment is currently processing toplevel forms."))
58 (:documentation ""))
59
60(defgeneric compiler-in-situation-p (comp-env situation)
61 (:documentation "Returns true when the compiler is considered 'in' the situation
62given by SITUATION, which is one of :compile-toplevel.")
63 (:method ((comp-env compilation-environment) situation)
64 (cond
65 ((eql situation :compile-toplevel) (processing-toplevel-p comp-env))
66 ((eql situation :execute) (not (processing-toplevel-p comp-env)))
67 (t nil))))
68
69(defgeneric processing-toplevel-p (comp-env)
70 (:documentation "T if we are compiling TOPLEVEL forms, as in
71http://www.lispworks.com/documentation/HyperSpec/Body/03_bca.htm")
72 (:method ((comp-env compilation-environment))
73 (comp-env-compiling-toplevel-p comp-env)
74 ))
75
76(defvar *compilation-environment* nil
77 "The active compilation environment.
78
79Right now all code assumes that *compilation-environment* is accurately bound to the
80current compilation environment--even some functions that take the compilation environment
81as arguments.")
82
83;;; parenscript packages
84(defun lisp-to-script-package (lisp-package &optional (comp-env *compilation-environment*))
85 "Gets a script package corresponding to the given Lisp package."
86 (gethash lisp-package (comp-env-lisp-to-script-package-table comp-env)))
87
88(defsetf lisp-to-script-package (lisp-package &optional (comp-env *compilation-environment*))
89 (script-package)
90 "Sets the script package corresponding to the given Lisp package."
91 `(setf (gethash ,lisp-package (comp-env-lisp-to-script-package-table ,comp-env))
92 ,script-package))
93
94(defun symbol-script-package (symbol &optional (comp-env *compilation-environment*))
95 "Gets the Parenscript package associated with a Lisp symbol."
96 (lisp-to-script-package (symbol-package symbol) comp-env))
97
98(defun find-script-package (name &optional (comp-env *compilation-environment*))
99 "Find the script package with the name NAME in the given compilation environment."
100 (find (string name) (comp-env-script-packages comp-env) :test #'equal))
101
102(defun destroy-script-package (script-package)
103 "Disposes of relevant resources when the script package is no longer relevant."
104 (when (script-package-exclusive-lisp-package-p script-package)
105 (delete-package (script-package-lisp-package script-package))))
106
107;; environmental considerations
108(defun make-basic-compilation-environment ()
109 "Creates a compilation environment object from scratch. Fills it in with the default
110script packages (parenscript, global, and parenscript-user)."
111 (let ((comp-env (make-instance 'compilation-environment)))
112 comp-env))
113
114(defun create-script-package (comp-env
115 &key name nicknames secondary-lisp-packages used-packages
116 lisp-package exports documentation)
117 "Creates a script package in the given compilation environment"
118 (labels ((normalize (string-like) (string string-like)))
119 (let* ((explicit-lisp-package-p (not (null lisp-package)))
120 (lisp-package
121 (or (and explicit-lisp-package-p (find-package lisp-package))
122 (make-package (gensym (string name))))))
123 (labels ((package-intern (string-like)
124 (intern (normalize string-like) lisp-package)))
125 (let ((script-package
126 (make-instance 'script-package
127 :name (normalize name)
128 :comp-env comp-env
129 :nicknames (mapcar #'normalize nicknames)
130 :lisp-package (find-package lisp-package)
131 :secondary-lisp-packages (mapcar #'find-package secondary-lisp-packages)
132 :exclusive-lisp-package? (not explicit-lisp-package-p)
133 :exports (mapcar #'package-intern exports)
134 :used-packages (mapcar #'(lambda (script-package-designator)
135 (find-script-package
136 script-package-designator comp-env))
137 used-packages)
138 :documentation documentation)))
139 (push script-package (comp-env-script-packages comp-env)))))))
140
141(defmethod initialize-instance :after ((package script-package) &key)
142 (assert (script-package-comp-env package))
143 (assert (script-package-lisp-package package))
144 (let ((lisp-packages (cons (script-package-lisp-package package)
145 (script-package-secondary-lisp-packages package))))
146 (dolist (lisp-package lisp-packages)
147 (when (lisp-to-script-package lisp-package (script-package-comp-env package))
148 (error "Lisp package already has corresponding script package: ~A" (package-name lisp-package)))
149 (setf (lisp-to-script-package lisp-package (script-package-comp-env package))
150 package))))
151
152(defgeneric comp-env-find-package (comp-env package-designator)
153 (:documentation "Finds the script package named by PACKAGE-DESIGNATOR in the current
154compilation environment. PACKAGE-DESIGNATOR is a string or symbol.")
155 (:method ((comp-env compilation-environment) (name string))
156 (find name (comp-env-script-packages comp-env)
157 :key #'script-package-name :test #'equal))
158 (:method ((comp-env compilation-environment) (package-designator symbol))
159 (comp-env-find-package comp-env (string package-designator))))
160
161;; TODO loop through all defined macros and add them to the script package's
162;; macro environment
163; (labels ((name-member (name)
164; (eql (script-package-lisp-package script-package) (symbol-package name)))
165; (import-macro (name function)
166; (when (name-member name)
167; (setf (gethash name (script-package-macro-table script-package))
168; function)))
169; (import-special-form (name function)
170; (when (name-member name)
171; (setf (gethash name (script-package-special-form-table script-package))
172; function))))
173; (maphash #'import-special-form *toplevel-special-forms*)
174; (maphash #'import-special-form *toplevel-special-forms*)
175
176;(defgeneric comp-env-select-package (comp-env script-package)
177; (:documentation "")
178; (:method ((comp-env compilation-environment) (package script-package))
179; (setf (comp-env-current-package
180
181
182(eval-when (:compile-toplevel :load-toplevel :execute)
183 (defvar *toplevel-special-forms* (make-hash-table)
184 "A hash-table containing functions that implement Parenscript special forms,
185indexed by name (as symbols)")
186
187 (defun undefine-script-special-form (name)
188 "Undefines the special form with the given name (name is a symbol)."
189 (declare (type symbol name))
190 (when (gethash name *toplevel-special-forms*)
191 (remhash name *toplevel-special-forms*))))
192
193(defmacro define-script-special-form (name lambda-list &rest body)
194 "Define a special form NAME. Arguments are destructured according to
195LAMBDA-LIST. The resulting Parenscript language types are appended to the
196ongoing javascript compilation."
197 (declare (type symbol name))
198 (let ((script-name
199 (intern (format nil "PAREN-~A" (symbol-name name))
200 (find-package :parenscript)))
201 (arglist (gensym "ps-arglist-")))
202 `(eval-when (:compile-toplevel :load-toplevel :execute)
203 (defun ,script-name (&rest ,arglist)
204 (destructuring-bind ,lambda-list
205 ,arglist
206 ,@body))
207 (setf (gethash (quote ,name) *toplevel-special-forms*) #',script-name))))
208
209(defun get-script-special-form (name)
210 "Returns the special form function corresponding to the given name."
211; (declare (type symbol name))
212 (when (symbolp name)
213 (gethash name *toplevel-special-forms*)))
214
215;;; sexp form predicates
216(defun script-special-form-p (form)
217 "Returns T if FORM is a special form and NIL otherwise."
218 (and (consp form)
219 (symbolp (car form))
220 (gethash (car form) *toplevel-special-forms*)))
221
222(defun funcall-form-p (form)
223 (and (listp form)
224 (not (op-form-p form))
225 (not (script-special-form-p form))))
226
227(defun method-call-p (form)
228 (and (funcall-form-p form)
229 (symbolp (first form))
230 (eql (char (symbol-name (first form)) 0) #\.)))
231
232;;; macro expansion
233(eval-when (:compile-toplevel :load-toplevel :execute)
234 (defun make-macro-env-dictionary ()
235 "Creates a standard macro dictionary."
236 (make-hash-table))
237 (defvar *script-macro-toplevel* (make-macro-env-dictionary)
238 "Toplevel macro environment dictionary. Key is symbol-name of the macro, value
239is (symbol-macro-p . expansion-function).")
240 (defvar *script-macro-env* (list *script-macro-toplevel*) ;(list nil)
241 "Current macro environment."))
242
243(defmacro get-macro-spec (name env-dict)
244 "Retrieves the macro spec of the given name with the given environment dictionary.
245SPEC is of the form (symbol-macro-op expansion-function)."
246 `(gethash ,name ,env-dict))
247
248(defun lookup-macro-spec (name &optional (environment *script-macro-env*))
249 "Looks up the macro spec associated with NAME in the given environment. A
250macro spec is of the form (symbol-macro-p function). Returns two values:
251the SPEC and the parent macro environment.
252
253NAME must be a symbol."
254 (when (symbolp name)
255 (do ((env environment (cdr env)))
256 ((null env) nil)
257 (let ((val (get-macro-spec name (car env))))
258 (when val
259 (return-from lookup-macro-spec
260 (values val (or (cdr env)
261 (list *script-macro-toplevel*)))))))))
262
263(defun script-symbol-macro-p (name &optional (environment *script-macro-env*))
264 "True if there is a Parenscript symbol macro named by the symbol NAME."
265 (and (symbolp name) (car (lookup-macro-spec name environment))))
266
267(defun script-macro-p (name &optional (environment *script-macro-env*))
268 "True if there is a Parenscript macro named by the symbol NAME."
269 (and (symbolp name)
270 (let ((macro-spec (lookup-macro-spec name environment)))
271 (and macro-spec (not (car macro-spec))))))
272
273(defun lookup-macro-expansion-function (name &optional (environment *script-macro-env*))
274 "Lookup NAME in the given macro expansion environment (which
275defaults to the current macro environment). Returns the expansion
276function and the parent macro environment of the macro."
277 (multiple-value-bind (macro-spec parent-env)
278 (lookup-macro-spec name environment)
279 (values (cdr macro-spec) parent-env)))
280
281(defmacro defscriptmacro (name args &body body)
282 "Define a ParenScript macro, and store it in the toplevel ParenScript
283macro environment."
284 (let ((lambda-list (gensym "ps-lambda-list-"))
285 (body (if (stringp (first body)) (rest body) body))) ;; drop docstring
286 (undefine-script-special-form name)
287 `(setf (get-macro-spec ',name *script-macro-toplevel*)
288 (cons nil (lambda (&rest ,lambda-list)
289 (destructuring-bind ,args
290 ,lambda-list
291 ,@body))))))
292
293(defmacro defpsmacro (name args &body body)
294 `(defscriptmacro (,name ,args ,@body)))
295
296(defun expand-script-form (expr)
297 "Expands a Parenscript form down to special forms."
298 (if (consp expr)
299 (let ((op (car expr))
300 (args (cdr expr)))
301 (cond ((equal op 'quote) expr) ;; leave quotes alone
302 ((script-macro-p op) ;; recursively expand parenscript macros in parent env.
303 (multiple-value-bind (expansion-function macro-env)
304 (lookup-macro-expansion-function op)
305 (expand-script-form (let ((*script-macro-env* macro-env))
306 (apply expansion-function args)))))
307 (t expr)))
308 ;; not a cons
309 (cond ((script-special-form-p expr)
310 ;; leave special forms alone (expanded during compile)
311 expr)
312 ((script-symbol-macro-p expr)
313 ;; recursively expand symbol macros in parent env.
314 (multiple-value-bind (expansion-function macro-env)
315 (lookup-macro-expansion-function expr)
316 (expand-script-form (let ((*script-macro-env* macro-env))
317 (funcall expansion-function)))))
318 ;; leave anything else alone
319 (t expr))))
320
321(defun process-eval-when-args (args)
322 "(eval-when form-language? (situation*) form*) - returns 3 values:
323form-language, a list of situations, and a list of body forms"
324 (let* ((rest args)
325 (form-language
326 (when (not (listp (first rest)))
327 (setf rest (rest args))
328 (first args)))
329 (situations (first rest))
330 (body (rest rest)))
331 (when (and (find :compile-toplevel situations) (find :execute situations))
332 (error "Cannot use EVAL-WHEN to execute COMPILE-TOPLEVEL and EXECUTE code simultaneously."))
333 (when (null form-language)
334 (setf form-language
335 (cond
336 ((find :compile-toplevel situations) :lisp)
337 ((find :execute situations) :parenscript))))
338 (values form-language situations body)))
339
340;;;; compiler interface ;;;;
341(defgeneric compile-parenscript-form (compilation-environment form &key toplevel-p)
342 (:documentation "Compiles FORM, which is a ParenScript form.
343If toplevel-p is NIL, the result is a compilation object (the AST root).
344Subsequently TRANSLATE-AST can be called to convert the result to Javascript.
345
346If the compiler is in the COMPILE-TOPLEVEL stage, then the result will
347be a Parenscript form (after it has been processed according to semantics
348like those of Lisp's COMPILE-FILE). See
349http://www.lispworks.com/documentation/HyperSpec/Body/03_bca.htm"))
350
351(defmethod compile-parenscript-form ((comp-env compilation-environment) form &key toplevel-p)
352 (setf form (expand-script-form form))
353 ;; ensures proper compilation environment TOPLEVEL-P slot value
354 (setf (comp-env-compiling-toplevel-p comp-env) toplevel-p)
355 (if
356 toplevel-p
357 (cond
358 ((not (listp form)) form)
359 ;; process each clause of a progn as a toplevel form
360 ((eql 'progn (car form))
361 `(progn
362 ,@(mapcar #'(lambda (subform)
363 (compile-parenscript-form comp-env subform :toplevel-p t))
364 (rest form))))
365 ;; TODO process macrolets, symbol-macrolets, and file inclusions
366 ;; process eval-when. evaluates in :COMPILE-TOPLEVEL situation and returns
367 ;; the resultant form. for :EXECUTE situation it returns
368 ((eql 'eval-when (car form))
369 (multiple-value-bind (body-language situations body)
370 (process-eval-when-args (rest form))
371 (cond
372 ((find :compile-toplevel situations)
373 (when (eql body-language :lisp)
374 (let ((other-situations (remove :compile-toplevel situations)))
375 (multiple-value-bind (function warnings-p failure-p)
376 (compile nil `(lambda () ,@body))
377 (declare (ignore warnings-p) (ignore failure-p))
378 `(progn
379 ,(funcall function)
380 ,@(when other-situations
381 (list `(eval-when ,other-situations ,@body))))))))
382 ;; if :compile-toplevel is not in the situation list, return the form
383 (t form))))
384 (t form))
385 (cond ((stringp form)
386 (make-instance 'string-literal :value form))
387 ((characterp form)
388 (make-instance 'string-literal :value (string form)))
389 ((numberp form)
390 (make-instance 'number-literal :value form))
391 ((symbolp form) ;; is this the correct behavior?
392 (let ((c-macro (get-script-special-form form)))
393 (if c-macro
394 (funcall c-macro)
395 (make-instance 'script-variable :value form))))
396 ((and (consp form)
397 (eql (first form) 'quote))
398 (make-instance 'script-quote :value (second form)))
399 ((consp form)
400 (let* ((name (car form))
401 (args (cdr form))
402 (script-form (get-script-special-form name)))
403 (cond (script-form
404 (apply script-form args))
405
406 ((op-form-p form)
407 (make-instance 'op-form
408 :operator (script-convert-op-name (compile-to-symbol (first form)))
409 :args (mapcar #'compile-to-expression (rest form))))
410
411 ((method-call-p form)
412 (make-instance 'method-call
413 :method (compile-to-symbol (first form))
414 :object (compile-to-expression (second form))
415 :args (mapcar #'compile-to-expression (cddr form))))
416
417 ((funcall-form-p form)
418 (make-instance 'function-call
419 :function (compile-to-expression (first form))
420 :args (mapcar #'compile-to-expression (rest form))))
421
422 (t (error "Unknown form ~S" form)))))
423 (t (error "Unknown atomar expression ~S" form)))))
424
425(defun compile-script-form (form &key (comp-env *compilation-environment*))
426 "Compiles a Parenscript form to an AST node."
427 (compile-parenscript-form comp-env form ))
428
429(defun compile-to-expression (form)
430 "Compiles the given Parenscript form and guarantees the result is an expression."
431 (let ((res (compile-script-form form)))
432 (assert (typep res 'expression))
433 res))
434
435(defun compile-to-symbol (form)
436 "Compiles the given Parenscript form and guarantees a symbolic result."
437 (let ((res (compile-script-form form)))
438 (when (typep res 'script-variable)
439 (setf res (value res)))
440 (assert (symbolp res) ()
441 "~a is expected to be a symbol, but compiles to ~a. This could be due to ~a being a special form." form res form)
442 res))
443
444(defun compile-to-statement (form)
445 "Compiles the given Parenscript form and guarantees the result is a statement."
446 (let ((res (compile-script-form form)))
447 (assert (typep res 'statement))
448 res))
449
450(defun compile-to-body (form &key (indent ""))
451 "Compiles the given Parenscript form and guarantees the result is of type SCRIPT-BODY"
452 (let ((res (compile-to-statement form)))
453 (if (typep res 'script-body)
454 (progn (setf (b-indent res) indent)
455 res)
456 (make-instance 'script-body
457 :indent indent
458 :statements (list res)))))