A read-eval-print-loop for JavaFX

Thu, 26 Feb 2009 16:51:28 -0800
JavaFX, UI

JavaFX now supports a read-eval-print-loop ("repl"). This will be in the next ("Marina") release, but you can get it now from the Mercurial repository - see the compiler home page, which links to these instructions.
A read-eval-print-loop is a well-known feature of many language implementations, but isn't that common with compiled languages, especially ones that have static typing and lexical name lookup.
See this companion article on the scripting API and implementation issues.
The class com.sun.tools.javafx.script.ScriptShell implements the actual repl. It is an application that was based on the language-independent jrunscript command. but you can also subclass it if you want to customize it.
To run the REPL, start the ScriptShell application - for example:

$ CLASSPATH=/tmp:dist/lib/shared/javafxc.jar \
  java com.sun.tools.javafx.script.ScriptShell

Then you type commands at it:

/*fx1*/ var xy = 12
12
/*fx2*/ xy+10
12

The prompt has the form of a comment, to make it easier to cut-and-paste. The name inside the comments is used as the name of a "scriptlet" which shows up in error messages and exceptions.
You can define functions, modify variables, and call functions:

/*fx3*/ function xy_plus (n:Integer):Integer { xy + n }
/*fx4*/ ++xy
13
/*fx5*/ xy_plus(100)
113

You get warnings:

/*fx6*/ xy=2.5
fx6:1: possible loss of precision
found   : Number
required: Integer
2

and errors:

/*fx7*/ xy="str"
fx7:1: incompatible types
found   : String
required: Integer

and exceptions:

/*fx8*/ xy/0
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:616)
    at com.sun.tools.javafx.script.JavaFXCompiledScript.eval(JavaFXCompiledScript.java:59)
    at com.sun.tools.javafx.script.ScriptShell.evaluate(ScriptShell.java:350)
    at com.sun.tools.javafx.script.ScriptShell.evaluateString(ScriptShell.java:311)
    at com.sun.tools.javafx.script.ScriptShell.processSource(ScriptShell.java:244)
    at com.sun.tools.javafx.script.ScriptShell$1.run(ScriptShell.java:177)
    at com.sun.tools.javafx.script.ScriptShell.main(ScriptShell.java:51)
Caused by: java.lang.ArithmeticException: / by zero
    at fx8.javafx$run$(fx8]:1)
    ... 10 more

Each command is single line. In the future I hope we'll be able to tell when a command is incomplete, so the reader can keep reading more lines until it has a complete command.
Re-definition
You can redefine variables (and functions and classes), but previously-compiled references refer to the earlier definition:

/*fx9*/ var xy:String="str"
str
/*fx10*/ xy
str
/*fx11*/ xy_plus(100)
102

Now this is correct in terms of static name-binding, but perhaps not the most useful. What you probably want is for xy_plus to use the new definition of xy. This is what would happen in a typical dynamic language with dynamic typing and dynamic name-lookup, but with of compilation and static name-lookup xy_plus uses the the defintion seen when it was compiled.
Fixing this would involve some combination of automatic re-compilation (if xy_plus depends on xy, and xy is redefined, then re-compile xy_plus), and extra indirection.
A related issue is that forward references aren't allowed, which means you can't declare mutually dependent functions or classes - unless you type them all in the same command - i.e. on one line! Automatic recompilation could alleviate this: When a command is compiled, we not only remember the declarations it depends on, but also any missing symbols, so we can re-compile it if those later get declared.
Memory and start-up issues

Be aware that executing the first command causes a noticable pause. While I haven't profiled this, I'm guessing it is just because we need to load most of javafxc (which includes most of (a hacked-up version of) javac), and that takes some time. Subsequent commands run much faster.
Right now we don't do any smart reclaiming of classes. Each command compiles into one or more classes, which are kept around because they may be needed for future commands. Also, the bytecode array of a command is also kept around, since it may be needed when compiling future commands. Hopefully in the future we'll be more clever, so we can get-rid of no-longer-useful classes.