Table of Contents

HipHop Environment

 

The source files used in this chapter are avaiable in the HipHop github repository.

Running HipHop programs on the Web

In this chapter, we show how to run HipHop programs on a server-side of an application, e.g., on Nodejs, and on the client-side of a web application, .e.g., on Firefox. To run the examples, first create a fresh directory and install HipHop inside it.

mkdir example
cd example
npm install https://www-sop.inria.fr/members/Manuel.Serrano/software/npmx/hiphop.tgz

In the rest of this section we assume the file abro.hh.js defined as:

abro.hh.mjs

import { ReactiveMachine } from "@hop/hiphop";

const prg = hiphop module() {
   in A;
   in B;
   in R;
   out O = 0;
   
   do {
      fork {
         await (A.now);
      } par {
         await (B.now);
      }
      emit O(O.preval + 1);
   } every (R.now)
}

export const mach = new ReactiveMachine(prg);

Server-side execution

To execute a HipHop program on an unmodified JavaScript execution engines, let it be Nodejs, Hop, or any other compliant engine, it has to be compiled first. This is accomplished by the hhc.mjs compiler that is part of the standard HipHop distribution. To compile our abro.hh.js example, one has to use the following command:

./node_module/@hop/hiphop/bin/hhc.mjs abro.hh.mjs -o abro.mjs

This will generate two files:

Once, compiled, the program can be imported by any regular ES6 module and executed using the HipHop API. Example:

hello.mjs

import { mach } from "./abro.mjs";

mach.addEventListener("O", e => console.log(e.nowval));

mach.react();
mach.react({ A: undefined });
mach.react({ B: undefined });
mach.react({ B: undefined });
mach.react({ R: undefined });
mach.react({ A: undefined, B: undefined });
mach.react();

This program can be executed with:

nodejs --enable-source-maps hello.mjs 

Client-side execution with a Nodejs server

In this section we show how to use HipHop on client-side of web applications. We show how to proceed to implement a web app executing the abro.hh.mjs reactive program on a web browser. Let's implement a minimal web server only using the bare Nodejs' http api.

Let us first describe the web page the server delivers.

index.html

<html>
  <script type="importmap"> {
     "imports": {
        "@hop/hiphop": "/hiphop-client.mjs"
     }
  }
  </script>
  <script type="module">
    import { mach } from "./abro.mjs";
    globalThis.mach = mach;
    mach.addEventListener("O", (evt) => {
       document.getElementById("console").innerHTML = evt.nowval;
    });
    mach.react();
  </script>
  <div>
    <button onclick="mach.react({A: 1})">A</button>
    <button onclick="mach.react({B: 1})">B</button>
    <button onclick="mach.react({R: 1})">R</button>
  </div>
  <div id="console">-</div>
</html>   

The script type="importmap" specify a mapping from JavaScript module names to URL. As such, when the JavaScript code running on the browser will import the module @hop/hiphop, the browser will request the URL /hiphop.mjs to the server (we'll see in a moment how we configure the server so that it can respond to this request). By specifying a module-to-URL mapping, we can re-use the same program abro.mjs as the one we use when running on the server, althought the module @hop/hiphop has different implementations for the server and for the client.

The configuration of the web server is as follows:

node-server.mjs

import { createServer } from "node:http";
import { readFileSync, readdirSync } from "node:fs";

const host = "localhost";
const port = 8888;

const contents = {
   "/abro.mjs": readFileSync("./abro.mjs"),
   "/": readFileSync("./index.html"),
   "/hiphop-client.mjs": readFileSync("./node_modules/@hop/hiphop/hiphop-client.mjs")
}

for (let file of readdirSync("./node_modules/@hop/hiphop/lib")) {
   if (file.match(/\.m?js$/)) {
      contents["/lib/" + file] = readFileSync("./node_modules/@hop/hiphop/lib/" + file);
   }
}

const handler = function(req, res) {
   const content = contents[req.url];

   if (content) {
      if (req.url.match(/\.m?js$/)) {
         res.setHeader("Content-Type", "text/javascript");
      } else {
         res.setHeader("Content-Type", "text/html");
      }

      res.writeHead(200);
      res.end(content);
   } else {
      res.writeHead(404);
      res.end("no such file");
   }
}

const server = createServer(handler);
server.listen(port, host, () => {
    console.log(`Server is running on http://${host}:${port}`);
});

It is a standard implementation of a web server using Nodejs and contains no code specific to HipHop.

Client-side execution with a Hop Server

We can simplify the implementation of our simple HipHop web application by using a hop server instead of a plain Nodejs server.

As for the previous example, the Hiphop abro.hh.mjs file remain unchanged. In Hop, the standard way to generate HTML documents is to build them dynamically using the builtin XML syntax. The HTML page will be then generated by the index function defined as:

index.hop.mjs

export function index(R) {
   return (o) => <html>
      <script type="importmap"> {
         "imports": {
            "@hop/hiphop": "${R.register('./node_modules/@hop/hiphop/hiphop-client.mjs')}"
         }
      }
      </script>
      <script type="module">
         import { mach } from ${R.register("./abro.mjs")};
         globalThis.mach = mach;
         mach.addEventListener("O", (evt) => {
            document.getElementById("console").innerHTML = evt.nowval;
         });
         mach.react();
      </script>
      <div>
         <button onclick=~{mach.react({A: 1})}>A</button>
         <button onclick=~{mach.react({B: 1})}>B</button>
         <button onclick=~{mach.react({R: 1})}>R</button>
      </div>
      <div id="console">-</div>
   </html>
}

Before being executed, this module has to be compiled to plain JavaScript

./node_modules/@hop/hop/bin/hopc index.hop.mjs -o index.mjs

The main difference with the static document we have used for Nodejs is the use of the R variable. The Nodejs version was only able to deliver a fixed set of files and the mapping between URL and files was fixed once for all at the initialization of the program. Hop uses a more elaborated mechanism for implementing the mapping. The only useful information here is that R is a bidirectional data structure mapping actual file names to URL and vice-versa.

The server-side module is defined as follows:

hop-server.mjs

import { readFileSync, readdirSync } from "node:fs";
import { dirname } from "node:path";
import * as hop from "@hop/hop";
import { index } from "./index.mjs";

const hopConfig = {
   ports: { http: 8888 },
   users: [ {name: "anonymous", services: "*", directories: "*"} ]
};

hop.init(hopConfig);

const cwd = dirname(import.meta.url.toString().replace(/^file:\/\//, ""));
const R = new hop.Resolver(cwd);

const rootSvc = hop.Service(index(R), "/");

hop.listen().then(v => console.log(`Server is running on http://${rootSvc()}`));

Typed HipHop (a.k.a. TypeScript compilation)

In this section we show to use add type annotations to HipHop programs and how to use the TypeScript compiler to detect type errors statically.

The build process is more complex because TypeScript requires all the sources to be available when it compiles a module (otherwise, it cannot type-check the module being compiled) and because of the import rules TypeScript uses. In a TypeScript import form, the module file name designates the name of the generated JavaScript file, not the name of the TypeScript source file. The last difficulty is to deal with source-map files. The TypeScript compiler generates source-map but does not read any. Meaning that if a TypeScript file is generated, the TypeScript compiler cannot associates the program it compiles with another source file. In this section, we show how to use the hhc, the HipHop compiler, and hopc, the Hop compiler to solve all these issues.

First let's add type annotations to the abro HipHop module. This typed version is stored in a file named abro.hh.ts.

abro.hh.ts

import { ReactiveMachine } from "@hop/hiphop";

const prg = hiphop module() {
   in A;
   in B;
   in R;
   out O = 0;
   
   do {
      fork {
         await (A.now);
      } par {
         await (B.now);
      }
      emit O(O.preval + 1);
   } every (R.now)
}

interface abroType {
   A: undefined;
   B: undefined;
   R: boolean;
   O: number;
};

export const mach = new ReactiveMachine(prg) as ReactiveMachine<abroType>;

This file has to be compiled in two steps. First, it has to be compiled to plain TypeScript.

./node_module/@hop/hiphop/bin/hhc.mjs abro.hh.mjs -o abro.ts

and the, using the hopc compiler into a plain JavaScript file. The hopc compiler is a mere wrapper around the tsc compiler. To let tsc generates a JavaScript ES module instead of the CommonJS module, there must be a package.json. For instance:

abro.hh.ts

{
   "name": "TypedHipHop",
   "type": "module",
   "dependencies": {
      "@hop/hiphop": "https://www-sop.inria.fr/members/Manuel.Serrano/software/npmx/hiphop.tgz",
      "@hop/hopc": "https://www-sop.inria.fr/members/Manuel.Serrano/software/npmx/hopc.tgz"
   },
   "scripts": {
      "test": "node --enable-source-maps hello.mjs",
      "build": "node_modules/@hop/hiphop/bin/hhc.mjs abro.hh.ts -o abro.ts && node_modules/@hop/hopc/bin/hopc.mjs abro.ts -o abro.mjs && node_modules/@hop/hopc/bin/hopc.mjs hello.ts -o hello.mjs"
   }
}

The declaration "type": "module" tells tsc to generate an ES module. The compilation is executed with:

./node_module/@hop/hiphop/bin/hopc.mjs abro.ts -o abro.mjs

 

We compiled hopc instead of tsc for compiling the generated TypeScript file, because hopc is able to deal with multiple source-map file, so that the file abro.map.json that will get generated will correctly map any possible runtime erroor into the orignal abro.hh.ts file. If we had used tsc instead, the generated abro.map.json file would have pointed to abro.ts instead.

Let us assume a main program that imports the HipHop machine:

hello.ts

import { mach } from "./abro.mjs";

mach.addEventListener("O", e => console.log(e.nowval));

mach.react();
mach.react({ A: undefined });
mach.react({ B: undefined });
mach.react({ B: undefined });
mach.react({ R: true });
mach.react({ A: undefined, B: undefined });
mach.react();

This program can be compiled and executed with:

./node_module/@hop/hiphop/bin/hopc.mjs hello.ts -o hello.mjs
node --enable-source-maps hello.mjs

Visualizing the Net List

The HipHop compiler generates a net list from a HipHop source. This compiled program can be executed by simulating the generated circuit. The tools tools/nets2dot.mjs can be used in conjunction with the dot graph visualizer to generate PDF files. Here is how to proceed for generating these files, considering a HipHop source file named foo.hh.js:

  1. Add the option { dumpNets: true } to the reactive machine for which you want to dump the net list.
  2. Run your program. This will generate two files: foo.hh.js.nets-.json and foo.hh.js.nets+.json. The former is the net list before optimization the latter with optimization.
  3. Generate the .dot files:
    • bin/nets2dot.js foo.hh.js.nets-.json > nets-.dot
    • bin/nets2dot.js foo.hh.js.nets+.json > nets+.dot
  4. Generate the PDF files:
    • dot -T pdf nets-.dot > nets-.pdf
    • dot -T pdf nets+.dot > nets+.pdf