The new runtime
In the same sense that 1.2 was about changing the I/O model, 1.3 is about changing the runtime window hosting model. The existing hosting model has been in place since 1998, when it seemed simply the easiest way to get something running at the time; since then, it's been extended, patched, and twisted into something that I no longer wish to maintain. Fortunately, I now have the opportunity to replace that model: partly because the current model uses htmlctrl in a way incompatible with that control on WM 5.0, and partly because I'm dropping support for the Handheld PC platform, thus sparing me a lot of testing and a blizzard of #ifdef statements.
1.2, and indeed every version since 0.1.0, uses two threads of operation. The primary thread creates the user interface windows and receives notifications from the operating system of system memory exhaustion, application shutdown, and the like. A secondary thread interprets all Scheme code, leaving the primary thread free to respond to user and system actions in a timely manner even as an evaluation (let loop (forever) (loop)) runs. This dual-thread design makes Pocket Scheme a well-behaved application on the device.
To evaluate an expression, the user enters it into the lower pane of the application, then taps the Eval button. In 1.2, the UI thread takes the expression, echoes it to its output window, copies it to a common memory buffer, sets some flags to indicate that it's busy in an evaluation, then signals the Scheme thread, which has been asleep awaiting that signal. The Scheme thread makes a private copy of that data, then signals back to the UI thread that it may resume. The UI thread now runs in an "eval busy" mode, hiding the old eval input window, as the Scheme thread evaluates the submitted data as an s-expression. The Scheme thread emits output by sending text data to the appropriate output window. Should the Scheme thread need input from the user, it sends a notification to the UI thread, then goes to sleep awaiting a response; the UI thread receives the notification and changes its mode from "eval busy" to "input visible," displaying a new pane for user input. After the user enters data as requested, the UI thread signals the Scheme thread again, which in turn copies the data from the input window, then notifies the UI thread that it has finished with that data. Once Scheme has finished evaluating its expression, it prints the evaluated value to the output window, then goes back to sleep, notifying the UI thread that it has finished evaluation. The UI thread then moves from "eval busy" back into "eval ready" mode, displaying once again the eval input pane and awaiting a new expression from the user. What a modal mess.
Features of the 1.2 design (perhaps "design" is overstating it):
As I've said before, 1.2 does not work on WM 5.0 because of its reliance on the htmlctrl as an output window that it continually updates with new output. 1.3 fixes this by splitting this output function into two windows: a per-evaluation output window that displays any current output in real time, and a per-session transcript window that displays the results of all prior computations, updated with every pass through the REPL. To respond in real time, the new output window cannot use htmlctrl. The new transcript window, however, can continue to leverage htmlctrl for that control's text formatting benefits.
The 1.3 release unifies the data paths and synchronization methods between the UI and Scheme threads by making the Scheme thread drive the REPL, with the UI thread in every case responding to a notification from the Scheme thread. The UI thread creates some windows, or promises of windows, then hands them to the Scheme thread. The Scheme thread then runs something similar to the following loop:
(with-input-from-window THE-INPUT-WINDOW (lambda () (with-output-to-window THE-OUTPUT-WINDOW (lambda () (let loop (let ((value (eval (read (THE-EVAL-INPUT-WINDOW))))) (if (not (void? value)) (display value (THE-TRANSCRIPT-WINDOW)))) (loop))))))The UI thread satisfies any read request from an input window in the same manner. The transcript window only updates in response to a direct write request, at which time it displays all of the previous REPL pass, using data that the other three window I/O channels have logged to it.
Contrasting the 1.3 redesign with 1.2:
An additional asset of the new design is the ease with which I can either reduce or extend the number of windows. All UI modality now operates as a function of the current active windows, which in turn is a function of the pending requests from the Scheme thread. A lightweight program runtime will lack the eval input and transcript windows, while a better debugger would have at least one additional input window along with multiple output windows.
As of this writing,
the new REPL is in place,
along with the new windows for input, output, and transcript,
and all of the management logic coordinating those windows.
With the Scheme thread now always active,
I've lost the old means (an error code, returned from an evaluation pass)
by which the REPL UI knew to retain syntactically invalid expressions in the input pane
for editing and possible resubmission,
so I'm experimenting with some new ideas—
perhaps making errors on input ports call an optional function on that port,
or perhaps generalizing the current GC announcement mechanism
into a general hosting metainformation announcement mechanism.
I also need to make the styled transcript window
work on old-model Pocket PCs,
which had a more primitive HTML output control.
Finally, I need to think more about displaying error text,
which presently takes place via another text stream interleaved into the output text
(and displayed in a nice error-flag red in the transcript window).
posted at: 05:40 | path: /pscheme | permanent link to this entry