layout | title | date | permalink |
---|---|---|---|
post |
Out With The Old, In with ECMAScript 2015 |
2016-12-30 15:00:00 -0800 |
/blog/2016/12/30/ |
As 2016 was drawing to a close, browser support for ECMAScript 2015 (aka ES6) was looking pretty good, so maybe it was time to start taking advantage of a few ES6 features, especially:
- Classes
- Default Parameters
- const and let
- Computed Properties
- Octal and Binary Constants
- Template Literals (with String Interpolation)
Of the four types of machines (and CPUs) that PCjs currently supports:
- C1Pjs (6502)
- PCx86 (8086 through 80386)
- PC8080 (8080)
- PDPjs (PDP-11)
I decided to start by updating the code for the two newest machines: PDPjs and PC8080.
The single biggest change was the switch to ES6 classes. Fortunately, since PCjs machines were already using a class-like object hierarchy for all their components, all I had to do was select each of my constructor functions in the WebStorm IDE and tell it to convert the constructor to a class; for example:
/**
* BusPDP11(parmsBus, cpu, dbg)
*
* @constructor
* @extends Component
* @param {Object} parmsBus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
function BusPDP11(parmsBus, cpu, dbg)
{
// ...
}
would become:
class BusPDP11 extends Component {
/**
* BusPDP11(parmsBus, cpu, dbg)
*
* @param {Object} parmsBus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
constructor(parmsBus, cpu, dbg)
{
super("Bus", parmsBus, BusPDP11, MessagesPDP11.BUS);
// ...
}
// ...
}
However, I still had considerable work to do. For starters, WebStorm didn't detect any of the subclasses, despite all
the @extends
annotations, so I had to manually add the extends keyword.
Next, each of my subclass constructors would explicitly call their superclass constructor to initialize all the superclass properties, so all those calls had to be replaced with calls to super(). It's also worth noting that super() must be called BEFORE you attempt to access any class properties, because this will not be available until all the superclasses have been initialized. In the ES5 world, this is always available.
Finally, when I ran all the converted code through Google's Closure Compiler, I got a MASSIVE number of errors, nearly all of which were due to properties being added to the class instance, via this, which had not been defined in the constructor. It was only after spending several hours updating all my constructors to initialize every property that the class would ever use that I discovered that that wasn't strictly necessary.
It's long been known that JavaScript engines have an easier time optimizing your code if your objects are static, and
apparently Google's Closure Compiler seized the ES6 opportunity to try to enforce that, by treating classes more like
structures than dictionaries. However, you can change that assumption by prefacing a class with the pseudo-JSDoc
@dict
or @unrestricted
annotations;
the default is @struct
. Needless to say, when it came time to convert the next machine (PC8080) to classes, I prefaced
all my classes with @unrestricted
.
One downside of switching to ES6 one machine at a time is that I had to temporarily fork the shared modules into separate ES5 and ES6 folders. For example, one of the shared modules, Component, is the base class underlying most other machine components; ES5 objects subclass Component, whereas ES6 classes extend Component. Not all the shared modules needed to be forked, but creating a new shared folder was the simplest solution. Once all the machines have been converted to use ES6 classes, the new shared modules will become the default, and the old ones will fade away.
A final challenge was deciding how each component should reference its dependencies. For example, the PDP-11 Panel component originally included these lines at the top of the script:
if (NODE) {
var str = require("../../shared/lib/strlib");
var web = require("../../shared/lib/weblib");
var DumpAPI = require("../../shared/lib/dumpapi");
var Component = require("../../shared/lib/component");
var State = require("../../shared/lib/state");
var PDP11 = require("./defines");
var MessagesPDP11 = require("./messages");
}
and this worked well for both browsers (where I ensure that the global variable NODE is false) and for Node.
Going forward, I thought I should adopt the ES6 solution for declaring imports and exports, using the new import and export keywords. That worked well within the WebStorm environment, which happily recognized all my imports; for example:
import str from "../../shared/lib/strlib";
import web from "../../shared/lib/weblib";
import DumpAPI from "../../shared/lib/dumpapi";
import Component from "../../shared/lib/component";
import State from "../../shared/lib/state";
import PDP11 from "./defines";
import MessagesPDP11 from "./messages";
but any attempt to load that code into a browser caused an immediate exception, and Node support wasn't any better (in part because Node depends on Chrome's V8 JavaScript engine). Apparently, browser support hinges on a new way of loading JavaScript, using <module> tags, and I'm not sure that's been finalized yet, let alone implemented.
Long story short, until this all gets sorted, I'm retaining the require()
statements, since they make my development
environment happy, and Node still understands them, but I'm no longer wrapping them with if (NODE)
expressions. Instead,
I've modified the web server bundled with PCjs (server.js) to automatically comment out all import
, export
,
and require()
statements from .js files, so that no matter what module syntax is being used, your browser won't see it.
Similarly, before the code is compiled by the Closure Compiler, all those statements are removed by the preprocessing
step.
I'm also taking advantage of the Closure Compiler's ability to "transpile" ES6 code to ES5-compatible code. Even though all the browsers that PCjs targets now support the handful of ES6 features I'm using, I have no way of knowing whether every PCjs user has updated their browser. I would like to eventually generate ES6 code, because JavaScript engines should be able to optimize it better, but I will likely wait another year or two, and even then, I will probably want to include a small pre-loader that checks your browser's capabilities and then loads an ES5 or ES6 version as appropriate.
Last but not least, does the new ES6 code really work in Node, too? Happily, it does. To test, I wrote a small JavaScript shell app, pdp11, which reads a machine XML file (like this one), simulates the loading and initialization process that a web browser would perform, and then connects stdin and stdout to the machine's serial port:
Here's a sample run, from a macOS Terminal window:
cd modules/pdp11/bin
node pdp11 --cmd="load ../../../devices/pdp11/machine/1170/panel/debugger/machine.xml"
Panel object created: test1170.panel
Device object created: test1170.default
CPU object created: test1170.cpu
ROM object created: test1170.m9312
RAM object created: test1170.ram
SerialPort object created: test1170.dl11
PC11 object created: test1170.pc11
RK11 object created: test1170.rk11
RL11 object created: test1170.rl11
Debugger object created: test1170.debugger
bus: 8Kb H/W at 17760000
PDPjs v1.x.x
Copyright © 2012-2020 Jeff Parsons <[email protected]>
License: GPL version 3 or later <http://gnu.org/licenses/gpl.html>
Portions adapted from the PDP-11/70 Emulator v1.4 by Paul Nankervis <[email protected]>
bus: 256Kb RAM at 000000
Net.getResource("https://pdp11-disks.pcjs.org/dec/rk03/RK03-XXDP.json"): unimplemented
notice: Unable to load disk "RK03-XXDP" (error -1: RK03-XXDP.json)
Computer object created: test1170.computer
console connected to machine (alt-r for REPL prompt, alt-x to exit)
m9312: 512-byte ROM at 165000
ram: Loaded image "BOOTMON.json"
pc11: Loaded tape "BOOTSTRAP-16KB"
cpu: Model 1170
Type ? for help with PDPjs Debugger commands
R0=000000 R1=000000 R2=000000 R3=000000 R4=000000 R5=000000
SP=000000 PC=140000 PS=000013 PI=000000 SL=000000 T0 N1 Z0 V1 C1
140000: 000005 RESET
running
PDP-11 MONITOR V1.0
BOOT> help
COMMANDS ARE BOOT, HALT, TEST, DIAG, LIGHTS AND HELP
BOOT DEVICES ARE RK? RL? OR RP?
BOOT> alt-r detected, starting REPL...
PDP11> ?
>> ?
commands:
? help/print
a [#] assemble
b [#] breakpoint
c clear output
d [#] dump memory
e [#] edit memory
g [#] go [to #]
h halt
if eval expression
int [#] request interrupt
k stack trace
ln list nearest symbol(s)
m messages
p step over
print print expression
r dump/set registers
reset reset machine
s set options
t [#] trace
u [#] unassemble
var assign variable
ver print version
note: history disabled if no exec breakpoints
.exit exit REPL and connect console to machine
false
PDP11> r
>> r
R0=002114 R1=000000 R2=000000 R3=000000 R4=000000 R5=000000
SP=137522 PC=140074 PS=000004 PI=000000 SL=000000 T0 N0 Z1 V0 C0
140074: 000001 WAIT
false
PDP11> u
>> u
140076: 005200 INC R0
140100: 005767 000014 TST 140120
140104: 001773 BEQ 140074
140106: 012746 054000 MOV #54000,-(SP)
140112: 016746 000002 MOV 140120,-(SP)
140116: 000002 RTI
140120: 000000 HALT
140122: 002225 BGE 137576
false
PDP11> .exit
console connected to machine (alt-r for REPL prompt, alt-x to exit)
BOOT>
BOOT> alt-x detected, exiting...
@jeffpar
Dec 30, 2016