Recursive Descent Parsers in Scala 3: Cross Compiling Scala Parser Library to Javascript Using Scala.js
In the previous blogs in this series, we discussed how to write context-free grammars for recursive descent parser and implement the same in Scala using FastParse Scala library. For this blog post, we will see how can utilize libraries built in Scala into Javascript ecosystem.
Why Scala.js?
Scala.js is essentially a compiler that converts your Scala code into an equivalent yet highly optimized and efficient Javascript executable. This facilitates writing robust front-end web applications in a type-safe way with an added benefit of code sharability. With Scala.js you can now write libraries in Scala that could be utilized both in your JVM as well as JS ecosystems.
To see this in action, we will make use of Scala.js to cross compile the parser we built in Scala in our last post into a Javascript executable which we will eventually utilize in a small Javascript application called Parser Playground.
Project Structure
To start with let's understand the necessary project structuring first.
We will need two different projects, for Scala.js (root/js/src/main/scala) and Scala JVM (root/jvm/src/main/scala), respectively, and a folder (root/shared/src/main/scala) that would maitain the code shared amongst these two ecosystems.
Now to let sbt know about this project structuring, we will require a separate sbt plugin, sbt-scalajs-crossproject besides sbt-scalajs compiler plugin.
We then use the crossProject
builder from this plugin to create separate projects for our javascript and jvm ecosystems, like so:
Notice how settings common to both the ecosystems go under settings
builder method. This may include dependencies that are common to both the ecosystems. However, when defining common dependencies you must use %%%
instead of %%
. This allows sbt to automatically determine which ecosystem you are in, at compile time. Another thing to keep in mind when using crossProject
is to not include enablePlugins(ScalaJSPlugin)
in your build.sbt
.
Shared Parser Library
Having layed out our basic project structure, let's now move our parser code we wrote in the last post to the shared folder. We will define our parser in Parser.scala
like so:
User Interface
Now before we can make use of this shared library in our javascript ecosystem, let's create a simple user interface for our application. We will create index-dev.html
under root directory with a bootstrap container carrying our application's basic structure:
This would result into something like this:
The two main components to make note of here are "Editor" and "Result". Editor is a component where user can type strings from the language of our simple arithmetic expressions and results is a component where the validation success or failure results from the parser are displayed.
Scala.js
Now we have to implement our scala.js code that would essentially accept user input from the editor component, make use of the shared parser library to validate the user input and display the results in the result component. For this purpose we will create a Validator.scala
file that will house all this logic, like so:
There are couple of things to notice here:
- In order to make use of the shared parser code, all we need to do is to import it (at line: 1) and use it (line: 15) like so,
parser: P[_] => P[Any] = expr(_)
. Here we have definedexpr
as the default parser as an argument to thereparse
method - In our
build.sbt
, we defined fastparse as a common dependency which is why we are able to make use of it here in our scala.js code - To access dom elements, we can make use of imports from
org.scalajs.dom
package. This however, requires dependency onscalajs-dom
library - In our
build.sbt
, as part of js settings, we have definedscalaJSUseMainModuleInitializer
astrue
. This allows themain
method from ourValidator
code to be called as soon as the application is loaded. All we have to do is to include the js executable in the html. There are ways, however, to exort Scala.js APIs directly to Javascript. See API documentation for more details
Compile & Test
In order to compile our scala.js code, all we have to do is to run fastOptJS
sbt task to generate executable js suitable for development environment and fullOptJS
sbt task suitable for the production environment:
The difference between the two executables is in the amount of optimization and the overall file size. This clearly is evident in the amount to time taken to generate either of the files.
Another thing to notice here is that the files are generated under js/target/scala-2.12/
as simple-arithmetic-parser-fastopt.js
and simple-arithmetic-parser-opt.js
for developement and production environments, respectively.
Now since we have already included the generated js file in our html code, in order to test our application, all we have to do is to visit our index-dev.html
in chrome. You can try the working application here. If you were to deploy the application in production, you may probably want to create a similar index.html
file and include the full optimized executable javascript like so, <script type="text/javascript"
src="./js/target/scala-2.12/simple-arithmetic-parser-opt.js"></script>
.
Conclusion
With this we put a closure on this series. Just to iterate one last time. We have seen how to write context-free grammar for recursive descent parser in our first post, following which we saw how to translate that grammar in Scala using FastParse library in the second post, eventually concluding with this post where we saw how to make use of libraries build in Scala in javascript ecosystem using Scala.js.
You can ofcourse have a look at the working code and working app on github.