--- layout: post title: Out With The Old, In with ECMAScript 2015 date: 2016-12-30 23:00:00 permalink: /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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) - [Default Parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) - *[const](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const)* and *[let](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let)* - [Computed Properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Computed_property_names) - [Octal and Binary Constants](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Numeric_literals) - [Template Literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) (with [String Interpolation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Expression_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: ```javascript /** * BusPDP11(parmsBus, cpu, dbg) * * @constructor * @extends Component * @param {Object} parmsBus * @param {CPUStatePDP11} cpu * @param {DebuggerPDP11} dbg */ function BusPDP11(parmsBus, cpu, dbg) { // ... } ``` would become: ```javascript 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](http://usejsdoc.org/) `@dict` or `@unrestricted` [annotations](https://github.com/google/closure-compiler/wiki/@struct-and-@dict-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](/modules/shared/lib/component.js), is the base class underlying most other machine components; ES5 objects *subclass* [Component](/modules/shared/lib/component.js), whereas ES6 classes *extend* [Component](/modules/shared/lib/component.js). 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](/modules/pdp11/lib/panel.js) component originally included these lines at the top of the script: ```javascript 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: ```javascript 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](/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](/modules/pdp11/bin/pdp11), which reads a machine XML file (like [this one](/devices/pdp11/machine/1170/panel/debugger/machine.xml)), 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-2016 Jeff Parsons License: GPL version 3 or later Portions adapted from the PDP-11/70 Emulator v1.4 by Paul Nankervis bus: 256Kb RAM at 000000 Net.getResource("http://archive.pcjs.org/disks/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](http://twitter.com/jeffpar)* *Dec 30, 2016*