author | Alan Dipert
<alan@dipert.org> 2020-02-23 06:38:34 UTC |
committer | Alan Dipert
<alan@dipert.org> 2020-02-23 06:38:34 UTC |
parent | baf81418e6f2c267cb33c20e3a9e94a822e20b42 |
paper/jacl-els-2020.bib | +16 | -0 |
paper/jacl-els-2020.tex | +118 | -156 |
diff --git a/paper/jacl-els-2020.bib b/paper/jacl-els-2020.bib index 8768202..227887d 100644 --- a/paper/jacl-els-2020.bib +++ b/paper/jacl-els-2020.bib @@ -22,6 +22,14 @@ note = "[Online; accessed February 12, 2020]" } +@misc{wiki:TreeShaking, + author = "{Wikipedia contributors}", + title = "Tree shaking --- {W}ikipedia{,} The Free Encyclopedia", + year = "2020", + url = "https://en.wikipedia.org/w/index.php?title=Tree_shaking&oldid=908332079", + note = "[Online; accessed February 22, 2020]" +} + @online{Cannon07, author={Howard I. Cannon}, year=2007, @@ -201,6 +209,14 @@ lastaccessed="February 19, 2020", } +@online{EventLoop, + author={{Mozilla, Inc.}}, + year={2020}, + title="Concurrency model and the event loop", + url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop", + lastaccessed="February 22, 2020", +} + @book{Bolin10, author = {Michael Bolin}, title = {Closure: The Definitive Guide: Google Tools to Add Power to Your JavaScript}, diff --git a/paper/jacl-els-2020.tex b/paper/jacl-els-2020.tex index 1db40ac..fd9485c 100644 --- a/paper/jacl-els-2020.tex +++ b/paper/jacl-els-2020.tex @@ -88,22 +88,20 @@ Lisp. The primary goal of the JACL project is to ease SPA development by applying Common Lisp --- a proven\cite{Cannon07,Garnet90,Action} substrate for UI innovation --- to the difficult challenges now faced by developers. The JACL language is envisioned as the means to that -goal, and to have at least the following ancillary affordances: +goal. -\begin{itemize} - \item Compelling interactive development experience. - \item Ability to load code from the host filesystem during - development. - \item Fluent interoperation with external JavaScript libraries. - \item Ability to produce fast and small deliverable executables. -\end{itemize} +Most popular, contemporary compile-to-JavaScript languages are +oriented around the affordances of static type checking; JACL, as a +Lisp, is not. Compared to similar languages that \emph{are} Lisps, +JACL differentiates itself in two particular ways: with its approach +to the REPL, and with its compilation techniques. -First, similar efforts will be surveyed. Then, novel aspects of JACL's -design and implementation with respect to those efforts will be -introduced. Finally, details about the current state of JACL's -implementation, and plans for future work, will be elaborated upon. +In this paper, similar efforts will first be surveyed. Then, novel +aspects of JACL's design and implementation with respect to those +efforts will be introduced. Finally, conclusions will be drawn and +future work will be outlined. -\section{Previous Work} +\section{Related Work} Many Lisps exist that either compile to JavaScript or are interpreted by a JavaScript program. Here, only Lisps that have either @@ -222,10 +220,6 @@ Lisp to JavaScript compiler to perform, because they are exactly the kind that a JavaScript optimizer could not later perform, provided only JavaScript code. -On the other hand, the arithmetic expression code-size optimizations -performed by the second, code-generation stage of the JSCL compiler -could effectively be performed by a JavaScript optimizer. - \subsection{ClojureScript} A discussion of industrial Lisp technology in the SPA setting would be @@ -268,9 +262,15 @@ work in pursuit of this balance. The basis for interactive development in Lisp is undeniably the REPL, but as JSCL's ``pre-reader'' demonstrates, even the direct approach to this simple mechanism is hampered by JavaScript's asynchronous model -of input. To resolve this difficulty, JACL's reader facility is -completely asynchronous. Conceptually, it is the JSCL REPL -``pre-reader'' taken to its inevitable conclusion. +of input\cite{EventLoop}. Traditionally, Lisp readers are implemented +in environments with a blocking function for obtaining input, like +\texttt{getc(1)} on Unix. The blocking nature of input consumption +allows the reader to consume nested input recursively, using the call +stack to accumulate structures. In JavaScript, input arrives +asynchronously, and only when the call stack is empty. To mitigate +this difficulty, JACL's reader facility is completely +asynchronous. Conceptually, it is the JSCL REPL ``pre-reader'' taken +to its inevitable conclusion. JACL's reader is implemented as a JavaScript class, \texttt{Reader}. \texttt{Reader} instances are parameterized by an @@ -382,12 +382,12 @@ following: As a simple command-line application with a textual interface, \texttt{jacl-repl} can be run in various contexts. For example, it could be run within an Emacs ``inferior-lisp'' buffer, and then Lisp -forms could sent from other Emacs buffers for evaluation in the -REPL. It could be also be run as part of a build process that pipes -Lisp sources over the WebSocket for batch compilation. It is -anticipated that additional host-side tools that depend on -\texttt{jacl-client} will be necessary in the future to support -loading source files in dependency order. +forms could be sent from other Emacs buffers for evaluation in the +REPL. It could also be run as part of a build process that pipes Lisp +sources over the WebSocket for batch compilation. It is anticipated +that additional host-side tools that depend on \texttt{jacl-client} +will be necessary in the future to support loading source files in +dependency order. Unlike the pre-readers of SLip and JSCL, \texttt{jacl-client} is completely ignorant of Lisp syntax. \texttt{jacl-client} merely @@ -409,15 +409,12 @@ produce a representation that can be read back in. Unlike JSCL, the JACL compiler is organized to facilitate high-level optimizations such as those that could support efficient compilation -of \texttt{TAGBODY} and other Common Lisp operators. Low-level -optimizations are deferred to users, who may optionally run -deliverables produced by JACL through third-party JavaScript -optimizers such as Google Closure Compiler \cite{Bolin10}. +of \texttt{TAGBODY} and other fundamental Common Lisp operators. -The compiler has one analysis pass that results in an AST. The AST is -then converted to JavaScript strings by a code generation step. AST -nodes are represented by generic JavaScript objects with at least the -following keys: +The first compiler pass expands macros and produces an AST. The second +compiler pass performs optimizations and produces a new AST. The final +pass produces JavaScript code. AST nodes are represented by generic +JavaScript objects with at least the following keys: \begin{itemize} \item \texttt{op}: The node's name, as a JavaScript string. @@ -501,7 +498,7 @@ variable \texttt{X} 10 times: (tagbody start (when (zerop x) (go end)) - (decf x) + (setq x (1- x)) (go start) end)) \end{verbatim} @@ -528,7 +525,7 @@ LOOP: while (true) { switch(label) { case 0: if (X === 0) throw new Jump(id, 1); - X--; + X = X-1; throw new Jump(id, 0); case 1: default: @@ -544,31 +541,44 @@ LOOP: while (true) { } \end{verbatim} -The mechanism is ingenious and general. \texttt{GO} tags became -\texttt{switch} labels, and jumps are performed by throwing an -instance of \texttt{Jump} containing the destination label. - -Unfortunately, every jump requires a JavaScript exception be thrown, -severely penalizing iteration constructs built on -\texttt{TAGBODY}. Fortunately, a slight improvement in the compilation -strategy can yield a tremendous performance benefit. The improvement -follows from the observation that any \emph{local jump} --- such as -that from a \texttt{GO} to its relevant enclosing \texttt{TAGBODY}, -with no lexically-intervening \texttt{LAMBDA} forms --- may rely on -JavaScript's \texttt{continue}\cite{MozLabel} operator with a label -argument for control transfer, instead of on throwing an exception. - -Additionally, \texttt{TAGBODY} forms that are determined statically to -contain \emph{only} local jumps need not generate a \texttt{try/catch} -block, saving on generated code size. - -The following is an example of what the generated code would look like -after applying local jump optimization. Cursory benchmarks run on -Google Chrome \ref{appendix:benchmarks} show it runs several orders of -magnitude faster than the previous, unoptimized version, and is nearly -as fast as the JavaScript statement \texttt{while(X--)}: - -\newpage +The mechanism is ingenious and general, as it results in correct +behavior of both \emph{local} (to a lexically-enclosing +\texttt{TAGBODY} tag) and \emph{non-local} (to a dynamically-enclosing +\texttt{TAGBODY} tag) jumps. \texttt{GO} tags became \texttt{switch} +labels, and jumps are performed by throwing an instance of +\texttt{Jump} containing the destination label. + +Unfortunately, in this scheme, every jump requires a JavaScript +exception be thrown, severely penalizing \texttt{TAGBODY} as +previously discussed. Fortunately, a straightforward \emph{local jump + optimization} can be applied that yields a tremendous performance +benefit. Local jump optimization is a known +technique\cite{SICLTagbody}, but JACL is the first Lisp targeting +JavaScript to apply it. + +In order to perform this optimization, the JACL compiler first +identifies local \texttt{GO}s in its analysis pass. These are +\texttt{GO} nodes with no intervening \texttt{LAMBDA} +nodes\footnote{Note that \texttt{LAMBDA} doesn't necessarily preclude + local jump optimization if the \texttt{LAMBDA} is inlined, but JACL + currently does not inline functions.} between them and their +respective, lexically-enclosing \texttt{TAGBODY}s. Then, +\texttt{TAGBODY}s are identified that consist of only local +\texttt{GO}s. + +JavaScript generated for local \texttt{GO}s does not throw an +exception, but instead leverages the labeled form of JavaScript's +\texttt{continue}\cite{MozLabel} statement to transfer control +appropriately. JavaScript generated for \texttt{TAGBODY}s that have +been determined to consist only of local jumps omits the +\texttt{try/catch} block, saving on generated code size. + +The following code is similar\footnote{Once more, actual compiler + output has been significantly modified and reformatted for brevity.} +to that generated by the JACL compiler. Cursory benchmarks +\ref{appendix:benchmarks} show JACL's code runs several orders of +magnitude faster than JSCL's, and that JACL's code is almost as fast +as the JavaScript statement \texttt{while(X--)}: \begin{verbatim} var X = 10; @@ -580,7 +590,7 @@ LOOP: while (true) { label = 1; continue LOOP; } - X--; + X = X - 1; label = 0; continue LOOP; case 1: @@ -590,105 +600,57 @@ LOOP: while (true) { } \end{verbatim} -JACL's AST will facilitate the identification of usages of -\texttt{TAGBODY} that are amenable to local jump optimization, and the -transformation of such code into a dramatically faster -equivalent. Local \texttt{GO} optimization is a known technique -\cite{SICLTagbody}, but its implementation using JavaScript's -\texttt{continue} is not. - -\subsection{Delivery} - -All of the JACL features described so far are in the very early stages -of implementation, but JACL's delivery capabilities have yet to be -implemented at all. - -Fortunately, since JACL development is image-based, JACL should -support the traditional approach of specifying a Lisp function -entrypoint and dumping the Lisp image to native (JavaScript) -code. SBCL's \texttt{SAVE-LISP-AND-DIE}\cite{SBCLManual} and -LispWork's \texttt{DELIVER}\cite{LispWorksDeliver} functions are two -examples of this in other implementations. - -It is imagined that JACL's \texttt{DELIVER} function will be called in -a REPL at development time by a developer, or by an automated build -script. When the function is called, a JavaScript deliverable will be -produced, and the Web browser will either prompt the user to accept a -download, or the JavaScript code will be written to standard output by -\texttt{jacl-client}. - -JavaScript whole-program optimization tools such as Babel or Google -Closure Compiler may then be optionally applied to the -executable. Such tools perform sophisticated optimizations such as -dead-code elimination, loop invariant code motion, and constant -folding. - -It is anticipated that one important high-level optimization must be -performed in order for dumped images to be amenable to further -optimization by JavaScript tools: global symbol and function -references must be converted to static references. In order to perform -this optimization, JACL will need to retain the ASTs associated with -global definitions, so that statically-linked code for these -definitions can be generated at delivery time. Alternatively, -statically-linked code could be created automatically whenever -dynamically-linked code, and stored as an implementation-specific -attribute of the function object. - -\section{Conclusions and Future Work} +\section{Conclusion} JACL, a new Common Lisp created to ease SPA development, was -introduced. Four novel aspects of JACL were described with respect to -other, comparable Lisp implementations: - -\begin{enumerate} - \item An asynchronous reader that doesn't necessitate a pre-reader - and is suitable for embedded use. - \item An analyzing compiler and AST to support high-level - optimizations, such as those necessary for a high-performance - \texttt{TAGBODY} implementation. - \item \texttt{jacl-client}, the Chrome DevTools-based REPL that - complements the asynchronous reader to provide a command-line REPL - facility on the host. - \item The tentative delivery facility design, which will support the - creation of competitively small and fast application artifacts. -\end{enumerate} - -Most of Common Lisp that JACL will be capable of supporting remains -unimplemented. However, JACL is nearly in a state in which it can be -used for application development. - -The most immediate body of remaining work is population of the -\texttt{COMMON-LISP} package, towards the end goal of supporting MIT -\texttt{LOOP}. Such support would imply comprehensive support for many -fundamental Lisp operators. - -A longer term goal is support for the Common Lisp Object System -(CLOS). Key historical Lisp innovations were achieved with CLOS or one -of its predecessors \cite{Cannon07}, and so its inclusion is a high -priority. - -Many other design and implementation tasks remain, such as support for +introduced. JACL is designed as an efficient, practical tool, with the +needs of industrial SPA developers in mind. JACL integrates tightly +with the Web browser platform and interoperates easily with +JavaScript. Compared to other browser-based Lisps, JACL places a +higher emphasis on the value of the REPL, and introduces new +techniques for integrating the REPL into the development workflow. + +\section{Future Work} + +In order to be practical for application development, JACL must +support the creation of standalone executables. In JACL's case, these +would be single JavaScript files that may be included in an HTML page +and are executed on page load. Fortunately, since JACL development is +image-based, JACL should support the traditional approach of +specifying a Lisp function entrypoint and dumping the Lisp image to +native (JavaScript) code. SBCL's +\texttt{SAVE-LISP-AND-DIE}\cite{SBCLManual} and LispWork's +\texttt{DELIVER}\cite{LispWorksDeliver} functions are two examples of +this in other implementations. + +JACL should be able to perform rudimentary optimizations such as +global function and variable tree shaking\cite{wiki:TreeShaking} in +order to reduce the size of generated executables. In addition, JACL +should make dynamic function and variable references in executables +static, so that third party tools like Google Closure +Compiler\cite{Bolin10} may optionally be used to perform additional +optimization. + +Other than the ability to produce optimized standalone executables, +many other design and implementation tasks remain, such as support for special variables in lambda lists, \texttt{EVAL-WHEN}, macro lambda -lists, \texttt{DECLARE} et al, various other data types, compiler -macros, etc. The list of tasks is enormous, but it is anticipated that -these features can be implemented over time, in the order demanded by -application development. - -Bootstrapping is a desirable milestone, but since the fact that JACL -is not currently bootstrapped doesn't preclude its use for application -development, bootstrapping is not a high priority. +lists, \texttt{DECLARE} et al, CLOS, various other data types, +compiler macros, etc. The list of tasks is enormous, but it is +anticipated that these features can be implemented over time, in the +order demanded by application development. \section{Acknowledgments} -The author wishes to thank Micha Niskin, Bart Botta, and Kevin Lynagh -for invaluable feedback on early versions of this paper. +The author wishes to thank Micha Niskin, Bart Botta, Kevin Lynagh, +Lionel Henry, and Andy Keep for invaluable feedback on early versions +of this paper. The author wishes to express particular thanks to Robert Strandh not -only for his feedback, but also for the encouragement and guidance he -provided throughout the writing process. +only for his feedback, but also for his guidance on the writing +process. Finally, the author wishes to express special gratitude to his -beautiful wife, Sandra Dipert, for her skillful editing. +beautiful wife, Sandra Dipert, for her encouragement and support. \bibliographystyle{ACM-Reference-Format} \bibliography{jacl-els-2020} @@ -716,7 +678,7 @@ function tagbody_unoptimized(X) { switch(label) { case 0: if (X === 0) throw new Jump(id, 1); - X--; + X = X - 1; throw new Jump(id, 0); case 1: default: @@ -742,7 +704,7 @@ function tagbody_optimized(X) { label = 1; continue LOOP; } - X--; + X = X - 1; label = 0; continue LOOP; case 1: @@ -759,17 +721,17 @@ function baseline_js(X) { var start = performance.now(); for (var i = 0; i < 1e6; i++) tagbody_unoptimized(10); console.log("tagbody_unoptimized", performance.now() - start); -// tagbody_unoptimized 61572.78000000224 +// tagbody_unoptimized 58994.43500000052 var start = performance.now(); for (var i = 0; i < 1e6; i++) tagbody_optimized(10); console.log("tagbody_optimized", performance.now() - start); -// tagbody_optimized 26.714999999967404 +// tagbody_optimized 23.61499999812804 var start = performance.now(); for (var i = 0; i < 1e6; i++) baseline_js(10); console.log("baseline_js", performance.now() - start); -// baseline_js 21.240000001853332 +// baseline_js 15.055000003427267 \end{verbatim} \end{document}