diff options
author | Camil Staps | 2021-06-23 19:20:29 +0200 |
---|---|---|
committer | Camil Staps | 2021-06-23 19:20:29 +0200 |
commit | bbc049b5c7749b8f9c4e1ade9cf1da4f91f37c0d (patch) | |
tree | 9fe20b0549b51a7165b847ac1a6a5f56a128ff92 | |
parent | Add author on articles (diff) |
Add article series on the clean sandbox
18 files changed, 803 insertions, 45 deletions
diff --git a/gulpfile.js b/gulpfile.js index 37a07c5..8fd7481 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,6 +3,9 @@ var gulp = require('gulp'), hljs = require('highlight.js'), md = require('jstransformer')(require('jstransformer-markdown-it')), + md_anchor = require('markdown-it-anchor'), + md_footnote = require('markdown-it-footnote'), + md_toc = require('markdown-it-table-of-contents'), minifyhtml = require('gulp-minify-html'), minifyjs = require('gulp-minify'), notify = require('gulp-notify'), @@ -56,8 +59,20 @@ function html() { return ''; }, + html: true, langPrefix: 'lang-', linkify: true, + plugins: [ + [md_anchor, { + permalink: md_anchor.permalink.ariaHidden({ + placement: 'before' + }), + }], + md_footnote, + [md_toc, { + containerHeaderHtml: '<h1>Contents</h1>', + }], + ], }, options); return md.render(text, options).body; } diff --git a/package-lock.json b/package-lock.json index 8ce8b8b..fb73791 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,10 @@ "gulp-sass-next": "^6.0.0", "highlight.js": "^11.0.1", "jquery": "^3.6.0", - "jstransformer-markdown-it": "^2.1.0" + "jstransformer-markdown-it": "^2.1.0", + "markdown-it-anchor": "^8.0.3", + "markdown-it-footnote": "^3.0.3", + "markdown-it-table-of-contents": "^0.5.2" } }, "node_modules/@babel/helper-validator-identifier": { @@ -4470,6 +4473,30 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/markdown-it-anchor": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.0.3.tgz", + "integrity": "sha512-YU5kfZDKjqRiY6wySd3ouk6wVmByUczPCeTvKWCl7St/AnYEkiCxj29T1pK0phuvJFZJc1aNVoSFqzZ7I4XB/A==", + "dev": true, + "peerDependencies": { + "markdown-it": "*" + } + }, + "node_modules/markdown-it-footnote": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz", + "integrity": "sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==", + "dev": true + }, + "node_modules/markdown-it-table-of-contents": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.5.2.tgz", + "integrity": "sha512-6o+rxSwzXmXCUn1n8QGTSpgbcnHBG6lUU8x7A5Cssuq5vbfzTfitfGPvQ5PZkp+gP1NGS/DR2rkYqJPn0rbZ1A==", + "dev": true, + "engines": { + "node": ">6.4.0" + } + }, "node_modules/matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -11256,6 +11283,25 @@ "uc.micro": "^1.0.5" } }, + "markdown-it-anchor": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.0.3.tgz", + "integrity": "sha512-YU5kfZDKjqRiY6wySd3ouk6wVmByUczPCeTvKWCl7St/AnYEkiCxj29T1pK0phuvJFZJc1aNVoSFqzZ7I4XB/A==", + "dev": true, + "requires": {} + }, + "markdown-it-footnote": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz", + "integrity": "sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==", + "dev": true + }, + "markdown-it-table-of-contents": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.5.2.tgz", + "integrity": "sha512-6o+rxSwzXmXCUn1n8QGTSpgbcnHBG6lUU8x7A5Cssuq5vbfzTfitfGPvQ5PZkp+gP1NGS/DR2rkYqJPn0rbZ1A==", + "dev": true + }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", diff --git a/package.json b/package.json index 3a80179..4103d0a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "gulp-sass-next": "^6.0.0", "highlight.js": "^11.0.1", "jquery": "^3.6.0", - "jstransformer-markdown-it": "^2.1.0" + "jstransformer-markdown-it": "^2.1.0", + "markdown-it-anchor": "^8.0.3", + "markdown-it-footnote": "^3.0.3", + "markdown-it-table-of-contents": "^0.5.2" }, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.3", diff --git a/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.md b/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.md new file mode 100644 index 0000000..b118b28 --- /dev/null +++ b/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.md @@ -0,0 +1,83 @@ +*My [interpreter][abc-interpreter] for the pure, functional language [Clean][] +allows interpretation in both C and WebAssembly. The Clean compiler is written +in Clean, with a C backend. In my [Clean Sandbox][], I compile the C backend +with [Emscripten][] to [WebAssembly][], and interpret the Clean frontend in the +WebAssembly interpreter. Combined with a simple `make`-like tool and an editor, +this allows you to compile and run Clean code in the browser.* + +[[toc]] + +# Introduction + +Browsers can run programs in only few languages; mainly JavaScript. But +JavaScript is a high-level scripting language that makes it hard to optimize +performance. [WebAssembly][] operates on a lower level than JavaScript, is more +complicated to write code in, but is a relatively easy compilation target for +common imperative languages like C. This means that C applications and +libraries can be ported to the browser relatively easily. + +However, WebAssembly lacks some features that make it a difficult compilation +target for functional programming languages (in particular, non-local control +flow and tail calls). Some of these omissions are being implemented as post-MVP +features, but until then we need a workaround. + +Back in 2019 I implemented such a workaround for the pure, functional +programming language [Clean][] (a close cousin of Haskell). The Clean compiler +compiles to the intermediate language ABC, which is designed for an abstract +imperative machine for graph rewriting. This ABC code is then converted to +actual machine code. My solution, published in IFL'2019,[^ifl-2019] involves an +interpreter for this intermediate ABC language. The interpreter logic is +written in a DSL which targets both C and WebAssembly, so that you can run it +both natively and in the browser. + +Initially, we used this system in the [iTasks][] framework for workflow +management web applications. There, it allows you to send arbitrary Clean +values (including thunks) to the browser for evaluation there. This has two +benefits: (1) reducing server load by evaluating more on the client; (2) the +ability to write custom frontend components in Clean, which allows you to make +use of the same utility functions, the benefits of strong typing, etc. + +However, the ABC interpreter is more widely applicable than the iTasks system. +To demonstrate this, I set out to run the Clean compiler itself in the browser. +You can see the result in the [Clean Sandbox][]. + +# High-level overview + +In the end, the architecture turned out to be relatively simple. I needed the +following components: + +- A file system to keep the files (taken from the Emscripten library). +- A simple `make`-like tool that checks which files have been modified and + calls the compiler for those files only. +- The compiler frontend (written in Clean), running in the ABC interpreter. +- The compiler backend (written in C), compiled with Emscripten to + WebAssembly. +- A linker for the generated ABC files (written in C, compiled to WebAssembly). +- The ABC interpreter to run the resulting 'executable'. +- A nice editor to let the user modify his Clean code. I use something based on + the [Monaco Editor][], but will not discuss it here as I want to focus on the + build pipeline. + +Apart from the `make`-like tool, all these components were already available, I +only had to connect them together. + +*Continue to read about the [implementation details][pipeline].* + +[^ifl-2019]: + Camil Staps, John van Groningen and Rinus Plasmeijer, 2019. + [Lazy Interworking of Compiled and Interpreted Code for Sandboxing and Distributed Systems](https://camilstaps.nl/assets/pdf/ifl2019.pdf), + in Jurriën Stutterheim and Wei Ngan Chin (eds.), + *Implementation and Application of Functional Languages (IFL '19), September 25–27, 2019, Singapore, Singapore*. + doi: [10.1145/3412932.3412941](https://doi.org/10.1145/3412932.3412941) + +[abc-interpreter]: https://gitlab.com/clean-and-itasks/abc-interpreter/ +[Clean]: http://clean.cs.ru.nl/ +[Clean Sandbox]: https://camilstaps.gitlab.io/clean-sandbox/ +[Emscripten]: https://emscripten.org/ +[iTasks]: https://gitlab.com/clean-and-itasks/itasks-sdk/ +[Monaco Editor]: https://microsoft.github.io/monaco-editor/ +[WebAssembly]: https://webassembly.org/ + +[introduction]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.html +[pipeline]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.html +[integration]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.html diff --git a/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.md b/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.md new file mode 100644 index 0000000..b766560 --- /dev/null +++ b/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.md @@ -0,0 +1,328 @@ +*This is part 2 in a series on running the [Clean][] compiler in +[WebAssembly][], with the proof of concept in the [Clean Sandbox][]. In this +part I discuss the compilation pipeline and the program used to rebuild +generated files. See the [introduction][] for a high-level overview.* + +[[toc]] + +# The compilation pipeline + +Basically, this is what we do when we compile a Clean program: + +1. For each Clean file, generate intermediate ABC code. +2. For each ABC file, generate machine code. +3. Link the machine code together with the system linker into an executable. + +When we compile for interpretation, this looks a little different: + +1. For each Clean file, generate intermediate ABC code. +2. For each ABC file, generate ABC bytecode. + + Normal ABC code is human-readable. ABC bytecode is easier to parse. + Furthermore, the ABC bytecode defines some custom instructions to speed up + interpretation, like frequent instruction combinations (e.g. a `pop` + followed by a `return`). +3. Link the ABC bytecode together with the ABC linker. +4. Prelink the ABC bytecode for use in the WebAssembly interpreter. + + Prelinking removes symbol names and relocations, assuming the code segment + starts at address 0. This is possible because the WebAssembly memory always + starts at address 0. This way, the WebAssembly interpreter does not need to + deal with relocations. + +For step 1, we use the Clean compiler, written in Clean and C. The other tools +are maintained in the [abc-interpreter][] repository, and are written in C. We +compile the compiler frontend to prelinked bytecode, and the C tools to +WebAssembly using emscripten. + +# The file system + +Because the Clean compilation pipeline expects to read and write files (rather +than `stdin` and `stdout`), we need to set up a file system. Luckily, +[Emscripten][] already includes one with the +[`FS` library](https://emscripten.org/docs/api_reference/Filesystem-API.html). +I use the `FS` library for file access, and the `IDBFS` backend, which is based +on IndexedDB, for persistent storage. + +Unfortunately, it is [currently not possible to let the different C tools (code +generator, linker, etc.) share the same file +system](https://groups.google.com/g/emscripten-discuss/c/_K61fo-9oKY/m/4m6LYrcPo5sJ). +To circumvent this problem, I link the file system of each tool to the same +IndexedDB. When switching from one tool to another (e.g., going from the code +generator to the linker), I sync the file systems with the IndexedDB. This +incurs some overhead, but makes sure that the contents are the same everywhere. + +There is a `make`-like tool which checks which files need to be rebuilt based +on timestamps. This tool is written in Clean, compiled to prelinked bytecode, +and runs in the WebAssembly interpreter. + +# Building the C tools + +Each C tool is built to its own JavaScript and WebAssembly source using +[Emscripten][]. This is similar to building these tools to a native executable, +except that we use `emcc` instead of `gcc` and add some other options. For +example, the bytecode generator is normally built like this: + +```make +CLIBS:=-lm + +BCGEN:=bcgen + +SRC_BCGEN:=abc_instructions.c bcgen.c bcgen_instructions.c bcgen_instruction_table.c bytecode.c parse_abc.c util.c +DEP_BCGEN:=$(subst .c,.h,$(SRC_BCGEN)) settings.h + +$(BCGEN): $(SRC_BCGEN) $(DEP_BCGEN) + $(CC) $(CFLAGS) $(SRC_BCGEN) $(CLIBS) -DBCGEN -o $@ +``` + +With Emscripten we use: + +```make +EMCC:=emcc + +JS_BCGEN:=bcgen.js + +$(JS_BCGEN): $(SRC_BCGEN) + $(EMCC) -O2 -DPOSIX -DBCGEN\ + -s ENVIRONMENT=web\ + -s INVOKE_RUN=0\ + -s MODULARIZE -s EXPORT_NAME=bcgen\ + -lidbfs.js -s FORCE_FILESYSTEM=1 -s EXPORTED_RUNTIME_METHODS=[FS,IDBFS]\ + $^ -o $@ +``` + +This compiles the C sources to WebAssembly and generates a file `bcgen.js` that +acts as a wrapper around it. Loading `bcgen.js` will then define the function +`bcgen` which returns a promise resolving in an Emscripten module with the +property `_main`, corresponding to the C function `main`. + +This sounds complicated, but we can instantiate the Emscripten modules like +this: + +```html +<script type="text/javascript" src="js/bcgen.js"></script> +<script type="text/javascript" src="js/bclink.js"></script> +<script type="text/javascript" src="js/bcprelink.js"></script> +``` + +```js +var c_bcgen, c_bclink, c_bcprelink; +Promise.all([ + bcgen().then(instance => c_bcgen = instance), + bclink().then(instance => c_bclink = instance), + bcprelink().then(instance => c_bcprelink = instance), +]) +``` + +# Building the compiler + +The Clean frontend of the compiler is relatively easy to build. We take the +normal project file (Clean's `Makefile`) and use `cpm` (Clean's `make`) to +build the prelinked bytecode. + +The C backend is a little trickier. We use the same `make` recipe as above. +However, the compiler expects some global functions to be defined. These +functions are normally provided by the native Clean run-time system or a +supporting C library, but are not present in the interpreter. We need to +define: + +- `set_return_code` which takes an `int` and sets the process exit code. +- `ArgEnvCopyCStringToCleanStringC` which takes a null-terminated C string and + builds a Clean string from it (which is not null-terminated but includes the + number of bytes). +- `ArgEnvGetCommandLineArgumentC` which takes an `int` and returns a pointer to + a C string with the *n*th command-line argument. +- `ArgEnvGetCommandLineCountC` which returns the number of command-line + arguments (`argc`). + +To implement these functions, we make use of custom properties on the +Emscripten module: `clean_argc`, `clean_argv`, and `clean_return_code`. The +JavaScript looks like this: + +```js +var c_compiler; +compiler().then(instance => { + c_compiler = instance; + + c_compiler._ArgEnvGetCommandLineCountC = () => c_compiler.clean_argc; + + c_compiler._ArgEnvGetCommandLineArgumentC = (i, sizep, sp) => { + const arg = c_compiler.HEAP32[c_compiler.clean_argv/4+i]; + var p = arg; + for (; c_compiler.HEAPU8[p] != 0; p++); + c_compiler.HEAP32[sizep/4] = p-arg; + c_compiler.HEAP32[sp/4] = arg; + }; + + c_compiler._ArgEnvCopyCStringToCleanStringC = (cs, cleans) => { + const size = c_compiler.HEAP32[cleans/4]; + cleans += 4; + for (var i = 0; i < size; i++) + c_compiler.HEAPU8[cleans++] = c_compiler.HEAPU8[cs++]; + }; + c_compiler._ArgEnvCopyCStringToCleanStringC.sync_strings_back_to_clean = true; + + c_compiler._set_return_code = (c) => {c_compiler.clean_return_code = c;}; +}); +``` + +# Preparing the file system + +With all the tools set up, we need to create the main directories in the file +system. We do this by adding them in the file system of the `c_compiler`, and +then sync the other file systems. + +```js +c_compiler.FS.mkdir('/clean'); +c_compiler.FS.mount(c_compiler.IDBFS, {}, '/clean'); +c_compiler.FS.mkdir('/clean/lib'); +c_compiler.FS.mkdir('/clean/src'); + +const mount = backend => new Promise(resolve => { + backend.FS.mkdir('/clean'); + backend.FS.mount(backend.IDBFS, {}, '/clean'); + backend.FS.syncfs(true, err => { + if (err) + throw err; + backend.FS.chdir('/clean/src'); + resolve(); + }); +}); + +return new Promise(resolve => { + c_compiler.FS.syncfs(false, () => { + Promise.all([ + mount(c_bcgen), + mount(c_bclink), + mount(c_bcprelink), + ]).then(resolve); + }); +}); +``` + +In the real sandbox, we also add some standard libraries to the file system on +start-up. + +# The `make`-like tool + +I had to build a simple `make`-like tool to call the compiler and regenerate +files when needed. This tool is a separate Clean program, which is compiled to +prelinked bytecode and interpreted in WebAssembly. + +The following snippet sets everything up: it sets the global variable +`sandbox.compile` to a Clean function that makes the sandbox project. In the +[last part][integration] I show how everything is put together, here I want to +focus on the implementation. + +`wrapInitFunction` makes this a Clean program that can run in the browser. The +`me` parameter can be ignored here. The `compile` function calls `make`, which +contains the actual logic. `make` is a higher-order function which takes +functions for compilation, code generation, etc., as parameters. These are here +provided as `call_main c_bcgen` etc. + +```clean +Start = wrapInitFunction start + +sandbox :== jsGlobal "sandbox" +c_compiler :== jsGlobal "c_compiler" +clean_compiler :== jsGlobal "clean_compiler" +c_bcgen :== jsGlobal "c_bcgen" +c_bclink :== jsGlobal "c_bclink" +c_bcprelink :== jsGlobal "c_bcprelink" + +start :: !JSVal !*JSWorld -> *JSWorld +start me w + # (cb,w) = jsWrapFun (compile me) me w + # w = (sandbox .# "compile" .= cb) w + = w +where + compile me {[0]=paths,[1]=main,[2]=callback} w + # (paths,w) = jsValToList` paths (fromJS "") w + = make + /* .. */ + do_compile + (call_main c_bcgen) (call_main c_bclink) (call_main c_bcprelink) + /* .. */ + paths (fromJS "" main) + w +``` + +The C tools we can call relatively easily, by copying `argv` into the memory of +the process, setting `argc`, and calling `main`. Afterwards we need to free the +memory: + +```clean + call_main backend args w + # (argc,args,argv,w) = copy_argv args backend w + # (res,w) = (backend .# "_main" .$? (argc, argv)) (-1, w) + = (res == 0, free_args_and_argv args argv backend w) +``` + +Compilation is a little trickier, because it is a Clean program. For this the +`clean_compiler` object, which is a WebAssembly interpreter, has a `.compile()` +method. This method makes sure that no global state is kept between runs (by +clearing CAFs), and that the memory does not get corrupted if compilation +fails: + +```js +clean_compiler.compile = function () { + clean_compiler.clear_cafs(); + c_compiler.clean_stdio_open = false; + + var res = 0; + try { + /* Interpret from the main entry point */ + clean_compiler.interpreter.instance.exports.set_pc(clean_compiler.start); + clean_compiler.interpreter.instance.exports.interpret(); + res = c_compiler.clean_return_code; + } catch (e) { + console.warn(e); + res = e; + /* Memory may be badly corrupted, create a new instance to be sure */ + create_clean_compiler(); + } + + /* Clear output buffers */ + if (sandbox.stdout_buffer.length > 0) + sandbox.stdout('\n'); + if (sandbox.stderr_buffer.length > 0) + sandbox.stderr('\n'); + + return res; +}; +``` + +The Clean wrapper also checks the exit code of the process, and catches any +errors: + +```clean + do_compile args w + # (_,args,argv,w) = copy_argv args c_compiler w + # (res_or_err,w) = (clean_compiler .# "compile" .$ ()) w + # w = free_args_and_argv args argv c_compiler w + # res = jsValToInt res_or_err + | isJust res + = (fromJust res==0, w) + # (err,w) = (res_or_err .# "toString" .$? ()) ("Compiler crashed.", w) + = (False, feedback False err w) +``` + +The make tool itself is now relatively simple. It keeps a queue of modules to +be compiled. It takes a module from the queue, compiles it, and checks the +generated code for any dependencies. These dependencies are added to the queue, +if they were not compiled yet. When the queue is emptied, the same is done for +bytecode generation. Finally, the generated code is linked into a single +bytecode file and prelinked bytecode file. + +*That's all about the pipeline. You can continue reading about [how it's all +put together][integration].* + +[abc-interpreter]: https://gitlab.com/clean-and-itasks/abc-interpreter/ +[Clean]: http://clean.cs.ru.nl/ +[Clean Sandbox]: https://camilstaps.gitlab.io/clean-sandbox/ +[Emscripten]: https://emscripten.org/ +[WebAssembly]: https://webassembly.org/ + +[introduction]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.html +[pipeline]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.html +[integration]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.html diff --git a/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.md b/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.md new file mode 100644 index 0000000..0244507 --- /dev/null +++ b/resources/md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.md @@ -0,0 +1,193 @@ +*This is part 3 in a series on running the [Clean][] compiler in +[WebAssembly][], with the proof of concept in the [Clean Sandbox][]. In this +part I discuss how all the individual components are integrated. See the +[introduction][] for a high-level overview. In the previous part, I discussed +[the compilation pipeline][pipeline].* + +[[toc]] + +# Overview + +We have to put all our different elements together in JavaScript. In particular +we have the following elements: + +- The C tools (compiler backend, bytecode generator, bytecode linker, and + bytecode prelinker) +- The WebAssembly interpreters in which the Clean tools run (compiler frontend, + `make`-like program) +- The WebAssembly interpreter for the compiled program itself. + +# Linking the compiler frontend and backend + +We have already seen how [the `make` tool communicates with the C tools and +with the compiler][pipeline]. It is a little trickier to set up the +communication between the compiler frontend and the backend. This is because +the `make` tool was written for this application specifically, while the +compiler wasn't. + +In Clean, the foreign function interface works through a special ABC +instruction, `ccall`. For example, if we have a C function `int c_add(int, +int)`, we can define `add` below. `II:I` here stands for the type: two integer +arguments and an integer return type. + +```clean +add :: !Int !Int -> Int +add _ _ = code { + ccall c_add "II:I" +} +``` + +This instruction is not supported by the interpreter, because there is no way +to implement it generically in that context. When interpretation reaches a +`ccall` (or any other unimplemented instruction), the WebAssembly interpreter +calls a JavaScript function that can try to handle it. This function can read +and modify the state of the ABC machine, and return to it after it has handled +the instruction. This allows us to link the compiler frontend to the backend in +JavaScript: + +```js +var clean_compiler; +function create_clean_compiler () { + return ABCInterpreter.instantiate({ + interpreter_imports: { + handle_illegal_instr: (pc, instr, asp, bsp, csp, hp, hp_free) => { + instr = ABCInterpreter.instructions[instr]; + if (instr == 'ccall') + return i_ccall.bind(c_compiler)(clean_compiler, pc, asp, bsp); + else + return 0; + }, + /* other options .. */ + } + }).then(abc => { + clean_compiler = abc; + }); +} +``` + +With `i_ccall` we can link a WebAssembly interpreter to an Emscripten module, +which contains the global C functions: + +```js +function i_ccall (abc, pc, asp, bsp) { + const fun = '_' + abc.get_clean_string(abc.memory_array[pc/4+2]-8); /* e.g. c_add; emscripten adds an underscore */ + var type = abc.get_clean_string (abc.memory_array[pc/4+4]-8); /* e.g. II:I */ + + if (!(fun in this)) + throw 'ccall: unknown function '+fun; + + /* parse type; get arguments from the Clean heap and stack .. */ + + const result = this[fun].apply(null, args); + + /* copy the result to the WebAssembly interpreter .. */ + + return pc+24; /* return the address of the next instruction */ +} +``` + +# Clean file I/O + +We have a similar problem with Clean programs doing file I/O. The compiler +frontend uses file I/O, but for this it needs ABC instructions that are not +implemented in the WebAssembly interpreter. We extend `handle_illegal_instr` to +also catch these instructions, and use Emscripten's `FS` library to implement +them. For example, `writeFS` writes a string to a file. `handle_illegal_instr` +becomes: + +```js +handle_illegal_instr: (pc, instr, asp, bsp, csp, hp, hp_free) => { + instr = ABCInterpreter.instructions[instr]; + if (instr == 'ccall') + return i_ccall.bind(c_compiler)(clean_compiler, pc, asp, bsp); + else if (instr == 'writeFS') + return i_writeFS.bind(c_compiler)(clean_compiler, pc, asp, bsp); + else + return 0; +}, +``` + +And again `i_witeFS` is defined to link an arbitrary WebAssembly interpreter +and Emscripten module together: + +```js + +function i_writeFS (abc, pc, asp, bsp) { + const i = abc.memory_array[bsp/4+2]; + + /* get arguments from the stack */ + const s_ptr = abc.memory_array[asp/4]; + const size = abc.memory_array[s_ptr/4+2]; + const s = new Uint8Array(abc.memory_array.buffer, s_ptr+16, size); + + this.FS.write(abc.files[i].stream, s, 0, size); + + abc.interpreter.instance.exports.set_asp(asp-8); /* pop the string from the stack */ + return pc+8; /* return the address of the next instruction */ +} +``` + +In reality we need about 10 instructions for file I/O, but the others are +similar. + +# Running the generated bytecode + +We can now run a Clean program from our editor through the [compilation +pipeline][pipeline] and generate a prelinked bytecode file. This is similar to +a native executable, but for the WebAssembly interpreter. What remains is +actually running the bytecode. + +We do this by simply creating a new instance of the ABC interpreter. The +`instantiate` function uses `fetch()` to get supporting WebAssembly and +bytecode files. In this case, our bytecode comes from the local file system, so +we monkey-patch `fetch()` to supply the bytecode from the Emscripten file +system (the case that the path is `null`): + +```js +function run (path) { + const opts = { + fetch: (p) => { + return p !== null ? fetch(p) : new Promise(resolve => { + const pbc = c_bcprelink.FS.readFile(path); + resolve({ + ok: true, + arrayBuffer: () => pbc.buffer + }); + }); + }, + }; + + return ABCInterpreter.instantiate(opts).then(abc => { + abc.interpreter.instance.exports.set_pc(abc.start); + var r = 0; + try { + r = abc.interpreter.instance.exports.interpret(); + } catch (e) { + sandbox.stderr(e+'\n'); + r = -1; + } + + /* flush output buffer */ + if (sandbox.stdout_buffer.length>0) + sandbox.stdout('\n'); + + if (r!=0) + sandbox.stderr('failed with return code '+r); + }); +} +``` + +# Wrapping up + +That's it! There are many other small tricks here and there, but these four +posts covered the architecture and most interesting implementation details. You +can try out the [Clean Sandbox][] to see it live, or check out the [source +code](https://gitlab.com/camilstaps/clean-sandbox). + +[Clean]: http://clean.cs.ru.nl/ +[Clean Sandbox]: https://camilstaps.gitlab.io/clean-sandbox/ +[WebAssembly]: https://webassembly.org/ + +[introduction]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.html +[pipeline]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.html +[integration]: 2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.html diff --git a/resources/md/2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly.md b/resources/md/2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly.md deleted file mode 100644 index 623a080..0000000 --- a/resources/md/2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly.md +++ /dev/null @@ -1,7 +0,0 @@ -Example of an article with markdown. - -See https://camilstaps.gitlab.io/clean-sandbox. - -```js -let xyz = 5; -``` diff --git a/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.pug b/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.pug new file mode 100644 index 0000000..9d5cf91 --- /dev/null +++ b/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.pug @@ -0,0 +1,18 @@ +extends /layout-articles.pug + +block prepend title + | Compiling Clean in the browser - + +block prepend menu + - var page = '2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction' + +block append breadcrumbs + +breadcrumb('Compiling Clean in the browser with WebAssembly, part 1: Introduction') + +block subtitle + | Compiling Clean in the browser with WebAssembly, part 1: Introduction +block subtitleDate + | 23 June 2021 + +block page + include:markdown ../../../md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.md diff --git a/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.pug b/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.pug new file mode 100644 index 0000000..713e97a --- /dev/null +++ b/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.pug @@ -0,0 +1,18 @@ +extends /layout-articles.pug + +block prepend title + | Compiling Clean in the browser - + +block prepend menu + - var page = '2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline' + +block append breadcrumbs + +breadcrumb('Compiling Clean in the browser with WebAssembly, part 2: The pipeline') + +block subtitle + | Compiling Clean in the browser with WebAssembly, part 2: The pipeline +block subtitleDate + | 23 June 2021 + +block page + include:markdown ../../../md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-2-the-pipeline.md diff --git a/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.pug b/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.pug new file mode 100644 index 0000000..e684350 --- /dev/null +++ b/resources/pug/finals/articles/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.pug @@ -0,0 +1,18 @@ +extends /layout-articles.pug + +block prepend title + | Compiling Clean in the browser - + +block prepend menu + - var page = '2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together' + +block append breadcrumbs + +breadcrumb('Compiling Clean in the browser with WebAssembly, part 3: Putting it all together') + +block subtitle + | Compiling Clean in the browser with WebAssembly, part 3: Putting it all together +block subtitleDate + | 23 June 2021 + +block page + include:markdown ../../../md/2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-3-putting-it-all-together.md diff --git a/resources/pug/finals/articles/2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly.pug b/resources/pug/finals/articles/2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly.pug deleted file mode 100644 index 6506192..0000000 --- a/resources/pug/finals/articles/2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly.pug +++ /dev/null @@ -1,12 +0,0 @@ -extends /layout-articles.pug - -block prepend menu - - var page = '2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly' - -block subtitle - | Running the Clean compiler in the browser with WebAssembly -block subtitleDate - | 23 June 2021 - -block page - include:markdown ../../../md/2021-06-23-running-the-clean-compiler-in-the-browser-with-webassembly.md diff --git a/resources/pug/finals/articles/index.pug b/resources/pug/finals/articles/index.pug index 3c6f618..b3100dd 100644 --- a/resources/pug/finals/articles/index.pug +++ b/resources/pug/finals/articles/index.pug @@ -9,6 +9,16 @@ block subtitle block subtitleExtra block page - p. - This is a collection of some software-related articles I wrote. - Perhaps it will be larger than this one day. + p + | This is a collection of some software-related articles I wrote. + | Perhaps it will be larger than this one day. + | You can also go back to my #[a(href='/') home page]. + + h1 + a(href='2021-06-23-compiling-clean-in-the-browser-with-webassembly-part-1-introduction.html'). + 23 June 2021: Compiling Clean in the browser with WebAssembly + blockquote. + My interpreter for the pure, functional language Clean allows interpretation in both C and WebAssembly. + The Clean compiler is written in Clean, with a C backend. + In my Clean Sandbox, I compile the C backend with Emscripten to WebAssembly, and interpret the Clean frontend in the WebAssembly interpreter. + Combined with a simple make-like tool and an editor, this allows you to compile and run Clean code in the browser. diff --git a/resources/pug/finals/ham/cw-decoder.pug b/resources/pug/finals/ham/cw-decoder.pug index b4285eb..ba8d727 100644 --- a/resources/pug/finals/ham/cw-decoder.pug +++ b/resources/pug/finals/ham/cw-decoder.pug @@ -4,14 +4,14 @@ block prepend menu - var page = 'cw-decoder' - var sections = ['Porting to PIC', 'Finishing', 'Future plans'] +block append breadcrumbs + +breadcrumb('CW Decoder') + block subtitle | CW Decoder block subtitleDate | January 2017 -block subtitle-right - +githubIconLink('camilstaps/CWDecoder') - block page h4 Introduction p. diff --git a/resources/pug/include/layout-articles.pug b/resources/pug/include/layout-articles.pug index 2672a36..10feaab 100644 --- a/resources/pug/include/layout-articles.pug +++ b/resources/pug/include/layout-articles.pug @@ -1,10 +1,21 @@ extends layout-sidebar.pug +block append breadcrumbs + +breadcrumb('/articles/', 'Articles') + block append menu - var base_url = '/articles/' +menu( {name: 'Home', link: ''}, {name: 2021, menu: [ - {name: 'Running the Clean compiler in the browser with WebAssembly', year: 2021, month: 6, day: 23}, + { name: 'Compiling Clean in the browser with WebAssembly, part 3: Putting it all together', + year: 2021, month: 6, day: 23 + }, + { name: 'Compiling Clean in the browser with WebAssembly, part 2: The pipeline', + year: 2021, month: 6, day: 23 + }, + { name: 'Compiling Clean in the browser with WebAssembly, part 1: Introduction', + year: 2021, month: 6, day: 23 + }, ]}, ) diff --git a/resources/pug/include/layout-ham.pug b/resources/pug/include/layout-ham.pug index bb20ef0..9c7ea79 100644 --- a/resources/pug/include/layout-ham.pug +++ b/resources/pug/include/layout-ham.pug @@ -3,6 +3,9 @@ extends layout-sidebar.pug block title | PA5ET +block append breadcrumbs + +breadcrumb('/ham/', 'Ham Radio') + block append menu - var base_url = '/ham/' +menu( diff --git a/resources/pug/include/layout-sidebar.pug b/resources/pug/include/layout-sidebar.pug index e53476b..9ce6566 100644 --- a/resources/pug/include/layout-sidebar.pug +++ b/resources/pug/include/layout-sidebar.pug @@ -18,7 +18,7 @@ block content function item_link (item) { if (!('link' in item)) { - item.link = new String(item.name).toLowerCase().replace(/\W/g, '-'); + item.link = new String(item.name).toLowerCase().replace(/\W/g, '-').replace(/--+/g, '-'); if ('year' in item && 'month' in item && 'day' in item) item.link = item.year + '-' + pad_zero(item.month) + '-' + pad_zero(item.day) + '-' + item.link; @@ -38,7 +38,7 @@ block content - const id = 'menu-' + item_link(item); a(href='#' + id, data-toggle='collapse', role='button', aria-expanded=active.toString(), aria-controls=id)= item.name div.collapse(id=id, class=active ? 'in' : '') - ul.nav.nav-pills.nav-stacked.col-xs-12.col-md-10.pull-right + ul.nav.nav-pills.nav-stacked.col-xs-11.col-md-10.pull-right +menu(...item.menu) br(style='clear:both;') @@ -47,18 +47,30 @@ block content li(role='presentation', class=item_is_active(item) ? 'active' : '') a(href=base_url + (link == '' ? '' : link + '.html'))= item.name - div.col-lg-7.col-md-6 - h3 - block subtitle - block subtitleExtra - small - |  by - block subtitleAuthor - | Camil Staps - | , - block subtitleDate - div.col-lg-2.col-md-2.text-right - block subtitle-right + div.col-lg-9.col-md-8 + ol.breadcrumb.hidden-xs.hidden-sm + mixin breadcrumb(link, title) + li + if typeof title != 'undefined' + a(href=link)= title + else + a(href=base_url + page + '.html')= link - div.col-lg-9.col-md-8.text-justify.markdown - block page + block breadcrumbs + +breadcrumb('/', 'Home') + + div.row.article + div.col-lg-12 + h3 + strong + block subtitle + block subtitleExtra + small + |  by + block subtitleAuthor + | Camil Staps + | , + block subtitleDate + + article.col-lg-12.text-justify.markdown + block page diff --git a/resources/pug/include/layout.pug b/resources/pug/include/layout.pug index afb8c8d..81f133d 100644 --- a/resources/pug/include/layout.pug +++ b/resources/pug/include/layout.pug @@ -12,7 +12,6 @@ html(lang="en") block titleHeading block title | Camil Staps - hr block content diff --git a/resources/sass/style.scss b/resources/sass/style.scss index e401645..1523344 100644 --- a/resources/sass/style.scss +++ b/resources/sass/style.scss @@ -96,11 +96,27 @@ span.tt { * title and subtitle. So we adapt the font size of the headings in the * markdown content so that they are not larger than the page title. */ .markdown { + h1, h2, h3 { font-weight: bold; } h1 { font-size: 18px; } /* size of h4 */ h2 { font-size: 14px; } /* size of h5 */ h3 { font-size: 12px; } /* size of h5 */ } +pre code { + tab-size: 4; + -moz-tab-size: 4; + white-space: pre; +} + +pre code.lang-js { + tab-size: 2; + -moz-tab-size: 2; +} + +blockquote { + font-size: unset; +} + footer { padding: 1em 0; } @@ -108,3 +124,7 @@ footer { .obfuscate > span { display: none; } + +.breadcrumb { + margin-bottom: 0; +} |