Per's UI blog entries
The new read-eval-print-loop (REPL) for JavaFX
required some major changes in the scripting
API
for JavaFX.
JavaFX had for a while supported the javax.script API
specified by JSR-223.
However, that JavaFX implementation didn't allow you to declare a variable in
one "eval" and make it visible in future commands, which made it
rather useless for a REPL.
This turns out to be somewhat tricky for a
compiled language with static name binding and typing.
Unfortunately, JSR-223 has some fundamental design
limitations in that respect.
(Embarrassingly, I was a not-very-active member of the JSR-223 expert group.
It would have been better if we could have thought a little more about
scripting staticly-typed languages, but we didn't have time.)
Design limitations in javax.script
Top-level name-to-value bindings in JSR-223 are
stored in a javax.script.Bindings instance, which is
basically a Map. Unfortunately, Bindings is a
concrete class that the programmer can directly instantiate
and add bindings to, and there is no way to synchronize
a Bindings instance with the internal evaluator state.
(Note that top-level bindings have to be maintained in a language-dependent
data structure if the language supports static typing, since typing
is language-dependent.) This means the script engine has to
explicitly copy every binding from the Bindings object
to the language evaluator - or the script evaluator has to be
modified to search the Bindings. Worse, if a script
modifies or declares a binding, that has to be copied back to
the Bindings, which is an even bigger pain.
This problem could have been avoided
if Bindings were an abstract class to be implemented
by the language script engine.
An alternative solution could allow the evaluator to register
itself as as listener
on the Bindings object.
JSR-223 does support compilation being separate from evaluation. Unfortunately, it assumes that name-to-value bindings are needed during evaluation but not during compilation. This is problematic in a language with lexical binding, and fails utterly in a language with static typing: The types of bindings have to be available at compile-time, not just run-time.
The Invocable interface is also problematical.
It invokes a function or method in a ScriptEngine,
without passing in a ScriptContext, even though
invoking a function is basically evaluation, which should require
a ScriptContext.
The ScriptContext interface has an extra
complication in that it consists of two Bindings objects,
of which the global
one seems to have limited utility.
Making it worse is that some methods use
the default ScriptContext of a ScriptEngine
and some take an explicit ScriptContext parameter.
The new JavaFX scripting API
Because of these problems with javax.script,
I implemented a new JavaFX-specific API.
These classes are currently in the com.sun.tools.javafx.script
package; after some shaking down
they might be moved to
some other package, perhaps javafx.script.
The class JavaFXScriptCompiler manages the compilation
context
, including the typed bindings preserved between
compilations, and a MemoryFileManager pseudo-filesystem
that saves the bytecode of previously compiled commands.
The main method of JavaFXScriptCompiler is compile.
This takes a script, compiles it, and (assuming no errors) returns
a JavaFXCompiledScript.
The main method of JavaFXCompiledScript is eval,
which takes a JavaFXScriptContext, evaluates the script,
and returns the result value.
The JavaFXScriptContext contains the evaluation state,
which is primarily a ClassLoader. Creating a JavaFXScriptContext
automatically creates a JavaFXScriptCompiler, so in the
current implementation there is a one-to-one relationship between
JavaFXScriptContext and JavaFXScriptCompiler.
(In the future we might allow multiple JavaFXScriptContext
instances to share a single JavaFXScriptCompiler.)
This API is very much subject to change.
The REPL API
The actual REPL is provided by the ScriptShell class,
which is implemented using the above API. It first reads in commands
from the command line (the -e option), or a named
file (the -f option). It then enters the classic
REPL loop: Print a prompt, read a line, evaluate it, and
print the result.
Most of these behaviors can be customized by overriding
a method. For example when the compiler finds an error it creates a
Diagnostic object, which is passed to a report method.
The default handling
is to print the Diagnostic, but a sub-class of
ScriptShell could override report to do
something else with the Diagnostic.
The ScriptShell API is also very much subject to change
as we get more experience with it.
The new javax.script wrapper
The class JavaFXScriptEngineImpl is the JavaFX implementation
of the standard javax.script.AbstractScriptEngine class.
It was re-written to be a wrapper on top of the API discussed earlier.
Because of the previously-mentioned limitations of javax.script
it is not completely faithful implementation of JSR-223.
It uses a WeakHashMap to translate from the
Bindings used by the javax.script API
into the JavaFXScriptContext objects used by the
internal API. Compilation and evaluation of a script need to use
JavaFXScriptContext consistently.
This means there are basically two usage modes that should work:
-
It is best to always use the default
ScriptContextas returned by theScriptEngine'sgetContextmethod. Don't callcreateBindings,getBindings,setBindings, orsetContext. -
If you really need to use any of the above four methods, don't
try to use either of the
compilemethods. The reason for this is that compilation does need to use the script context, and it should be the same as used for evaluation.
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 12The 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.
I've long been interested in improved commend-line interfaces. The latest Kawa (in SVN) has a new implementation of a command interface window. If you start up Kawa with the -w flag:
kawa -w
you get a new Swing JFrame, whose
interesting component is a JTextPane
for typing in expressions, and displaying the results.
The following example uses Kawa's default language Scheme,
but works for other Kawa languages, including XQuery.

Color coding
Here the command prompt is in green, and the input command is in bold-face, because it seems useful to emphasize the input as a way of visually separating one command from the previous. The standard error output is in red. Standard output has the plain default style. This is because standard output may have embedded in it other objects and nested styled text.
All of these are implemented using Swing style objects. There is currently no mechanism to modify one these styles, except by modifying the source code, but at some point we will support that.
Input editing
While the color-coding is pretty, even more important is
command-line editing. That is you can move the input cursor
along the input line, and insert, delete, or replace text
before hitting Enter.
This is previously available using
the GNU readline library, which has some nice features, including
a searchable history mechanism. However, you cannot move the
cursor using the mouse, which is surprising to many.
Currently, the entire text pane is editable using the
default JTextPane keystrokes and mouse handlers.
(It would be better that at least the prompt be non-editable.)
Nothing gets sent to the receiving reader until you type
Enter. At that point the entire line containing
the cursor, except for the prompt, is sent to the reader.
(More precisely: If the cursor is at or after the output position,
the rest of the line after the output position is sent.
Otherwise, usually it's a way of repeating or modifying a previous
line. In that case the contents of that line, except any initial
prompt segment, are first copied to the end of the text buffer,
as if typed there, before being sent to the reader.)
If you paste a multi-line string (commonly using ctrl-V),
then ReplPane magically interleaves the prompt string and
output with the input text. For example if you paste the following text:
(display "foo") (newline) (display "bar") (newline)
You get this result:

Note that the prompt for line 4 is different because it's a continuation line. (Alas, this feature is not very robust; it is easy to get Swing exceptions.)
Embedding Components
A powerful feature is that you "print" java.awt.Component
objects. These are embedded in the JTextPane.
The following Scheme example creates a list of 3 JButton
objects, and that list is then "printed":

We can of course save a reference to the button in a variable:

That allows us to modify properies of the button.
Here we change its text property, in line 4,
and the button is updated as soon as we hit enter:

One anomaly when "printing" a Component
is that a Component can only appear once.
If we "print" it a second time, it is as a side-effect
removed from the first place it was printed:

This isn't really the right behavior, but it's unavoidable
when "printing" a Component.
To avoid the problem, we need to "print" a some kind of
"model" object rather than "view" objects.
Embedding Viewable objects
The experimental swing-gui library provides
"viewable model" objects that can also be "printed" to a TextPane.
For example, the gnu.kawa.models.Button class
goes beyond Swing's ButtonModel in also having
text and an action procedure.
When a gnu.kawa.models.Button is "printed" the
implementation automatically creates a JButton that
is co-ordinated with the gnu.kawa.models.Button:

Again, we can change the text property:

Here we do some more complex layout,
creating a Row displaying the same button twice,
separated by a spacer. Then we create a column
that displays the same row twice, separated by a text field.
(For some reason the text field isn't displaying properly.)

Playing with composable Java2D pictures
The swing-gui library also provides
convenience wrappers to the
gnu.kawa.models.Paintable interface,
which makes it easy to create, compose, and transform Java2D Shape
objects.

Issues
- The
ReplPaneis not very solid. It's not hard to type something which causes a Swing exception, which is usually not recoverable. - There should be a
Viewmenu to modify fornts and styles, and a standardEditmenu to Cut/Copy/Paste, at least. TheFilemenu should have a way to save the typescript. - The
Utilities->Purge Buffermenu item doesn't work. - The display should be integrated with the Kawa pretty-printer, so that changing the window width recalculates line-breaks.
- It would be nice to emit styled "inline" (text) objects,
such as a "red string".
Maybe we should be using Swing's
HTMLDocumentinstead ofDefaultStyledDocument. Another toolkit to consider is Flying Saucer.