Scuffed Web Development With The Mellifera WebAssembly Build And JavaScript FFI API
In a previous blog post, I talked about how I was able to get my scripting language with value semantics, Mellifera, to run fully client-side in the browser by compiling a special-sauce version the Go-based interpreter to WebAssembly.
We can run the same Mellifera code1 from the CLI using the native build of the Mellifera interpreter:
$ cat test.mf
println("hello 🐝");
$ mf test.mf
hello 🐝
and in the browser using the web build of the interpreter:
This web build was originally developed for the purpose of making the Mellifera
language more accessible to students and developers who may not have the means
or desire to install the native Mellifera interpreter onto their computer. But
as a nice unintentional side effect of this work, the web build has access to
Go's
syscall/js
package, which contains utilities for interacting with the host JavaScript
environment from within a Go WebAssembly program. Since the Mellifera web build
is just a regular Go program compiled to WebAssembly, it is possible to expose
this package / functionality to Mellifera through dedicated built-in functions.
syscall/js package could be
used to add some neat tools to Mellifera. Are there any plans to update the
Mellifera web build?Actually, I already went ahead and added these built-in functions to the
WebAssembly build! The Mellifera web interpreter has a new top-level
js namespace that provides a foreign function interface to the
browser's JavaScript environment. The Mellifera program below will call the
JavaScript alert() function with the JavaScript string
argument "peekaboo" from within the Mellifera runtime.
alert("peekaboo") in
JavaScript. Why does the Mellifera code look so much more... complicated?This is a result of the way that we expose JavaScript values to Mellifera. I
briefly mentioned before that the js namespace provides a
foreign function interface (FFI) to the browser's JavaScript
environment. This interface lets us work with JavaScript types, values, and
behaviors that exist outside of the Mellifera runtime. JavaScript and Mellifera
have different language semantics, so we need this "complicated" looking code
to help manage those differences when interacting with one language from the
other.
That is a reasonable question to ask, especially since the Mellifera code above
looks like it has this painfully-manual conversion of a Mellifera
string with js::from_mellifera("peekaboo"). It would probably
help if we walked through an example of how this FFI interface works, and how
the differences in language semantics can get tricky in practice.
As mentioned previously, the js library in Mellifera is using
Go's syscall/js package under the hood (specifically
syscall/js@go1.26.2
at the time of writing). If we dump() the Mellifera
js library, we can see that there is a
js::value type, which directly corresponds to Go's
js.Value
type.
When a user calls js::global() in Mellifera, that call will
invoke Go's
js.Global()
function behind the scenes. The result of that invocation is a
js.Value struct that is directly handed back to Mellifera as
an external value with type js::value. An
external value in Mellifera is just a value that corresponds
to a resource outside of the Mellifera language and runtime, similar to a
void* with extra runtime type information in C, or
userdata in Lua. If we try to display this
external value in Mellifera with something like
dump(), then we can see that the value is stringified as
external(<object>) where <object> is Go's
%v representation of the global JavaScript object returned by
js.Global() (i.e. the window object in your
browser).
If we then take our window object in Mellifera and call the
js::value::get() method on that object, we are once again
reaching into Go's syscall/js library, this time invoking
js.Value.Get()
on the js.Value data contained within the
external JavaScript window object. And if the
value returned from that invocation is itself a JavaScript object, then we can
call js::value::get() on that object as well, and so on and so
forth.
In the snippet below, we call js::global() to get the
JavaScript window object, an external value
with type js::value. We then call
js::value::get() on that window object to get
the property of that window with the name document,
corresponding to the window.document object in JavaScript. And
finally, we once again call js::value::get(), this time on the
external value corresponding to the
window.document object, to get the property of that document
with the name title, corresponding to the
window.document.title value in JavaScript (i.e. the title of
the current page). This title is a JavaScript string, which is still
represented as a js.Value in Go code, and will therefore still
be represented as an external value with type
js::value in Mellifera. Go's %v
representation of a string js.Value is the text content of the
string itself, so the value gets dumped as: external(Scuffed Web
Development With The Mellifera WebAssembly Build And JavaScript FFI
API).
So far, it seems like mapping values from JavaScript to Mellifera is pretty simple. One could imagine defining a magic implicit conversion of JavaScript objects into Mellifera maps by iterating over the entries in a JavaScript object and transforming those into the key-value elements of a map.
Object.entries(window).find((x) => x[0] === "document") // key-value pair for the document
> Array [ "document", HTMLDocument https://ashn.dev/blog/2026-05-07-scuffed-web-development-with-mellifera.html ]
And this is actually exactly what the js::into_mellifera()
function does in Mellifera, converting an external js::value
argument into a native Mellifera value. For primitive values like numbers and
strings, this conversion extracts the data from the js::value
and returns a Mellifera primitive with the equivalent data. For a composite
value like a JavaScript object, this conversion will iterate over all of the
entries of that object and produce a Mellifera map with converted versions of
each key-value pair.
window.example_string = "foo";
> "foo"
window.example_object = {"abc": 123, "def": ["foo", "bar", "baz"]};
> Object { abc: 123, def: (3) […] }
But this conversion strategy runs aground almost immediately when you interact
with real-world JavaScript values. Consider the case of a JavaScript method
such as
window.document.getElementById().
How would we represent this natively in Mellifera? JavaScript method calls
require context in order to properly bind the
this
value, but every function in Mellifera is treated as a free function. Should
every JavaScript function that is converted into a Mellifera function
implicitly flow through
Function.prototype.bind()?
And what about JavaScript objects with getter and setter methods? Consider this
JavaScript Pixel class that stores RGBA components privately,
and uses getter methods to retrieve individual those components as readonly
values.
class Pixel {
#r;
#g;
#b;
#a;
constructor(r, g, b, a = 0xff) {
this.#r = r & 0xff;
this.#g = g & 0xff;
this.#b = b & 0xff;
this.#a = a & 0xff;
}
get rgba() {
const rBits = this.#r << 0x00;
const gBits = this.#g << 0x08;
const bBits = this.#b << 0x10;
const aBits = this.#a << 0x18;
return rBits | gBits | bBits | aBits;
}
get r() {
return this.#r;
}
get g() {
return this.#g;
}
get b() {
return this.#b;
}
get a() {
return this.#a;
}
}
window.example_pixel = new Pixel(0xAA, 0xBB, 0xCC);
> Object { #r: 170, #g: 187, #b: 204, #a: 255 }
Attempting to convert a Pixel object into a Mellifera map will
not produce a useful result, as the object does not have any public entries.
window.example_pixel.r
> 170
Object.entries(window.example_pixel)
> Array []
What should the behavior be here? Should Mellifera treat all getters as
properties when converting? That seems like it could easily lead to a data
redundancy error. Should Mellifera ignore getters entirely? Well then cases
like our Pixel class here would fail to properly convert into
a meaningfully useful value, which could be especially surprising to a user if
converted as a sub-object within a larger data structure.
All of these problems also exist in the reverse case when trying to convert
from Mellifera to JavaScript with js::from_mellifera(). How do
we represent Mellifera functions, in JavaScript? I mean heck how do we even
really represent Mellifera map values in JavaScript? Maps in Mellifera can have
have arbitrary values as keys: strings, numbers, and even other maps, just to
name a few. Do we try to represent Mellifera maps as a JavaScript
Map
instead? I don't have good solutions to these problems right now, and I have
not had enough experience with Mellifera in the browser to feel confident with
any decision I could make.
So to go back to your question about why Mellifera can't just use
interpreter magic to convert between Mellifera and JavaScript
automatically: it turns out that the differences between the languages make a
generalized conversion strategy real tricky, and would necessarily require the
Mellifera language to take an opinionated stance on how such conversion should
take place. Instead of attempting to tackle that mess of a problem, the
js FFI API chooses to explicitly acknowledge the differences
between JavaScript and Mellifera, and purposefully forces the programmer to
think about those differences when interacting across the FFI barrier.
It seems like there isn't really a whole lot you can do with this API given all of these incompatibilities. Is this thing even really usable?
Despite these incompatibilities, there is actually a surprising amount that one
can do in pure Mellifera now. You just need to be careful with the conversions
back and forth, and be mindful of when you are working with
external JavaScript values and when you are working with
native Mellifera values.
Let's take a look at the "hello world" of interactive web development: building a to-do list application. For this exercise, we want to have some list of tasks that can be added and removed interactively. In pure HTML/JavaScript a simple version of this to-do list would look something like the following:
<div style="margin: 1rem auto; padding: 1rem; border: solid 2px var(--color-orange);">
<h2>To-Do List (JavaScript)</h2>
<input type="text" id="todo-js-input" placeholder="Add a task...">
<button onclick="todoAddTask()">Add</button>
<ul id="todo-js-list"></ul>
</div>
<script>
function todoAddTask() {
let input = document.getElementById("todo-js-input").value.trim();
if (input === "") {
return; // ignore empty tasks
}
let ul = document.getElementById("todo-js-list");
let li = document.createElement("li");
li.innerHTML = `${input} <button onclick="this.parentElement.remove()">Remove</button>`;
ul.appendChild(li);
document.getElementById("todo-js-input").value = "";
}
</script>
This HTML/Javascript snippet appears on this page as the following:
To-Do List (JavaScript)
We can write this same basic to-do app with Mellifera, translating the JavaScript code into Mellifera line-by-line, which would look something like the following:
<div style="margin: 1rem auto; padding: 1rem; border: solid 2px var(--color-orange);">
<h2>To-Do List (Mellifera)</h2>
<input type="text" id="todo-mf-input" placeholder="Add a task...">
<button onclick='mellifera.evalById("todo-mf-source", {});'>Add</button>
<ul id="todo-mf-list"></ul>
</div>
<textarea hidden readonly id="todo-mf-source">
let input = js::into_mellifera(js::global().get("document").call("getElementById", [js::from_mellifera("todo-mf-input")]).get("value")).trim();
if input == "" {
return; # ignore empty tasks
}
let ul = js::global().get("document").call("getElementById", [js::from_mellifera("todo-mf-list")]);
let li = js::global().get("document").call("createElement", [js::from_mellifera("li")]);
li.set("innerHTML", js::from_mellifera($`{input} <button onclick="this.parentElement.remove()">Remove</button>`));
ul.call("appendChild", [li]);
js::global().get("document").call("getElementById", [js::from_mellifera("todo-mf-input")]).set("value", js::from_mellifera(""));
</textarea>
This HTML/Mellifera (with a little bit of JavaScript) snippet appears on this page as the following:
To-Do List (Mellifera)
Yea, look I know this Mellifera code isn't the most pleasant thing to look at. There is a reason I am calling this scuffed web development. There is pretty much no reason anyone would want to use Mellifera as their primary web-dev language. But there is a kind of cursed beauty that comes from being able interact with the browser from a completely separate scripting language in a somewhat impractical way.
More importantly though, this FFI API makes it possible to add more interactive elements to Mellifera programs in the browser. It may not be pretty, and it may not be ergonomic, but giving Mellifera programs a means to interact with the web page they are hosted in opens up new doors for creative and educational content on this blog, which I am still excited for.
Footnotes
1. Well mostly the same Mellifera code... The browser environment is an entirely different beast from a conventional desktop operating system, and some concepts do not translate one-to-one from the native build of the interpreter. For example, at the time of writing there is no way toimport() a
library in the WebAssembly build, as the browser is a sandboxed environment
with no access to the client or server local filesystem.