Table of Contents
Hopc Development
Note: This documentation page is intended for export developers only, in particular those that are willing to experiment with new compilation optimizations or analysis.
Hopc is the Hop multi-language compiler. It accepts has source languages Scheme and JavaScript and it can generates any of these two languages as target. This section describes how to modify the compiler when the source language is JavaScript.
The Hopc JavaScript compiler is multi-staged. As of branch 3.3 it contains about 50 stages. The compiler can be tweaked in two ways:
- modifying the order of stage executions.
- using new custom compilation stages.
Multi-staged compilation
In the compiler terminology, a driver is the compiler entity that controls how stages flow one from the other. The compiler exports a list of builtin drivers. This list can be obtained with:
$ hopc --js-drivers-list
A particular builtin driver can be selected with:
$ hopc --js-driver DRIVER-NAME
The list and order of compilation stages that correspond to that driver are obtained with:
$ hopc --js-driver DRIVER-NAME --js-show-driver
Custom drivers can be used to compile a source file by explicitly listing the stages to use. Example:
$ hopc --js-driver syntax,hopscript-header,loopexit,bestpractice,symbol,this,read-only,return,property,scheme foo.js -v2
Note that when the option -v2
or higher is used on the command line, the
compiler displays all the compilation stages it executes.
If the shell HOPTRACE
environment variable is bound to a string
prefixed with j2s:
, for instance HOPTRACE=j2s:stage
, the compiler will
dump its internal abstract syntax tree after executing each stage. These
dumps will be located in /tmp/$USER/J2S/your-source/...
.
A dedicated syntax enables users to insert a custom stage in a predefined driver. The syntax is as follow
$ hopc --js-driver ...,ANCHOR,USER foo.js -v2
Using this syntax, USER
stage will be executed after ANCHOR
stage,
found in the normal driver. For instance,
$ hopc -Ox --js-driver ...,arguments,stats.hop foo.js -v2
Will execute the stats.hop
user stage after the arguments
stage of
the optim
driver (because of the use of the -Ox
option).
External Compilation Stages
External compilation stages can be used to specify a custom compilation
driver. For that the URL of the stage is used instead of a the name of
a builtin stage. For instance, let us assume that a Hop server is running
and accepting connections on port 8888
and let us assume that it implements
a compilation stage accessible with a service named js2http
, then
a source compilation can be obtained with:
$ hopc --js-driver syntax,hopscript-header,loopexit,bestpractice,symbol,this,read-only,return,property,http://localhost:8888/hop/js2http,scheme foo.js -v2
The service invoked by the compiler will receive one object with two properties:
ast
: the compiler AST.config
: the compilation configuration.
The service will have to return a valid AST that will be used by the following compilation stages.
Several external compilation stages can be used in one compilation.
The Abstract Syntax Tree
The Hopc Abstract Syntax Tree (AST henceforth) is defined as a Scheme
class hierarchy declared in the file hop/js2scheme/ast.scm
. The Hop
hop.hopc
module enables easy access and manipulation from within
JavaScript. This modules maps all the Scheme class to JavaScript
classes and it provides two functions used to import and export the
AST.
hopc.intern(ast)
This function accepts as input an AST as received by the service implementing the stage and it re-constructs the sharing between AST nodes that have been serialized by the compiler. This is named an interned ast.
A compilation must return an interned ast, so the simplest possible compilation stage is:
const hopc = require(hop.hopc);
service js2http({ ast, config }) {
return hopc.intern(ast);
}
A compilation stage can modify or annotate the ast it receives. These modifications will be visible to the following compilation stages.
hopc.extern(ast)
Interned ast cannot be serialized into the JSON format using the
regular toJSON
function as these trees are cyclic data structure.
The function hopc.extern
serializes the tree into a JSON
representation handling the sharing so that they could be re-established
by the hopc.intern
function.
Here is an example of a compilation stage that dumps its tree in a json file and read it again for its return value.
js2json.js
const fs = require('fs');
var exec = require('child_process').exec;
const hopc = require(hop.hopc);
service js2http({ ast, config }) {
const prg = hopc.intern(ast);
return new Promise((res, rej) => {
fs.writeFile("/tmp/test.json", hopc.extern(prg), function(err) {
// should call the analyzer here.
var child = exec('cat /tmp/test.json');
var result = '';
child.stdout.on('data', function(data) {
// analyser result.
result += data;
});
child.on('close', function() {
var o = JSON.parse(result);
// pass the result to the next step.
res(hopc.intern(o));
});
});
});
}
Ast Walker
The hop.hopc
module provides a facility for traversing ast.
new hopc.HiopcAstWalker()
Builds an ast walker.
walker.walk(prg)
Walks along the ast using the depth-first traversal. That is, this function traverses all the ast node scanning all nodes fields.
The traversal of each node can be individually modified by declaring new methods to a walker. Methods are named after class names. For instance, the following declares a custom walker for the nodes that correspond to JavaScript object accesses:
const w = new hopc.HopcAstWalker();
w.J2SAccess = function(node) {
console.log("an access ", node.loc);
return node;
}
w.walk(prg);
A Complete Example
This example shows how to implement an external Hopc compilation stage
that modifies the ast it receives (by replacing +
with `` and by
displaying some information about the program.)*
To execute this example, a server must be executed with:
% hop -v -g -p 8888 js2http.js
The compiler should be invoked with:
% hopc -v fact.js --js-driver syntax,hopscript-header, \
loopexit,bestpractice,symbol,this,read-only,return, \
property,http://localhost:8888/hop/js2http, \
scheme
% ./a.out
js2http/js2http.js
var hopc = require("hopc");
var ast = require(hopc.ast);
service js2http({ ast, config }) {
var prg = hopc.intern(ast);
var w = new hopc.HopcAstWalker();
w.J2SDeclFun = function(node) {
console.log("scanning function ", node.id.__symbol__);
w.walk(node);
return node;
}
w.J2SDeclSvc = function(node) {
console.log("scanning service ", node.id.__symbol__);
w.walk(node);
return node;
}
w.J2SBinary = function(node) {
w.walk(node);
if (node.op.__symbol__ === "*") {
console.log('transforming operator "*" into "+"');
node.op = { __symbol__: '+' };
}
return node;
}
w.J2SAccess = function(node) {
console.log('profiling access');
var sym = { __symbol__: "console" };
var con = new ast.J2SUnresolvedRef(node.loc, undefined, false, sym);
var log = new ast.J2SString(node.loc, undefined, "log", false);
var access = new ast.J2SAccess(node.loc, undefined, false, con, log)
var name = new ast.J2SString(node.loc, undefined, accessName(node.obj), false);
var call = new ast.J2SCall(node.loc, undefined, access, [ name ]);
var exprs = [ call, w.walk(node) ];
return new ast.J2SSequence(node.loc, undefined, exprs);
}
w.walk(prg);
return prg;
}
function accessName(node) {
if (node.__node__ == "J2SUnresolvedRef") {
return node.id;
} else if (node.__node__ == "J2SRef") {
return node.decl.id.__symbol__;
} else {
return "???";
}
}