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.

ashley portrait
Ashley
Oh yea I remember you talking about the Mellifera web port in that previous blog post. It sounds like this 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.

ashley portrait
Ashley
That's cool and all, but this sure does seem like a lot of Mellifera code just to do the same thing as 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.

ashley portrait
Ashley
Right I mean I figured that Mellifera and JavaScript are different enough that there would need to be some translation between the two, but can't Mellifera just use interpreter magic to figure out how to go between Mellifera and JavaScript automatically?

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.

ashley portrait
Ashley
Wow I guess I did not really realize how complicated JavaScript is. *sigh* Why is it that my head starts to hurt every time I learn more about web development.

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)

      ashley portrait
      Ashley
      Of all the to-do applications, that is... certainly one of them.

      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 to import() a library in the WebAssembly build, as the browser is a sandboxed environment with no access to the client or server local filesystem.