Embedding BioLang

The same WebAssembly module that runs this site's playground is a plain ES module you can import into your own application. It has no server, no network dependency and no install step: the interpreter, the bioinformatics builtins and the parser are all inside the .wasm file.

That makes it useful in places a CLI cannot go — a teaching site that runs student code in the student's own browser, a lab notebook that must keep patient sequences on the machine that holds them, an Electron or Tauri desktop app, or a VS Code web extension.

Everything on this page is verified. The API below is checked against the shipped module on every commit — all 274 Rosalind problems in the example packs are re-run through it, and the documentation's own claims about which problems run in a browser are audited against what actually happens.

Getting the module

Two files are needed, and they must come from the same build: the module loader bl_wasm.js and the binary bl_wasm_bg.wasm. Both are served from this site, so the quickest way to get a matching pair is to copy them:

curl -O https://lang.bio/wasm/bl_wasm.js
curl -O https://lang.bio/wasm/bl_wasm_bg.wasm

To build them yourself from a checkout — which is what you want if you are pinning a version or adding your own builtins:

wasm-pack build crates/bl-wasm --target web \
  --out-dir ./wasm --no-typescript --release

Serve both files from the same directory. The loader fetches the .wasm alongside itself, and your server must send it as application/wasm — a wrong MIME type is the single most common reason the module fails to start.

Running code

Three calls: load the binary, initialise, evaluate. init() is not optional — it installs the panic hook that turns a Rust panic into a readable error, and the file-reading bridge described further down.

import init, { evaluate } from "./wasm/bl_wasm.js";

await init();          // loads and instantiates the .wasm
const module = await import("./wasm/bl_wasm.js");
module.init();         // panic hook + file bridge

const result = JSON.parse(evaluate(`
  let genome = dna("ATGCGCGATCGATCG")
  println("GC: " + str(gc_content(genome)))
  reverse_complement(genome)
`));

console.log(result.output);   // "GC: 0.6\n"
console.log(result.value);    // "DNA(CGATCGATCGCGCAT)"
console.log(result.type);     // "DNA"

What evaluate returns

A JSON string, not an object — parse it. Errors are reported in the payload rather than thrown, so a syntax error in user code does not need a try/catch. Note that value is formatted for display, so a typed value arrives wrapped: DNA(ACGT), not ACGT. Reach for structured when you need the data itself.

// Success
{
  ok: true,
  value: "5",            // the final expression, formatted for display
  type: "Int",           // its runtime type
  output: "hi\n",        // everything print/println wrote
  structured: null,      // a table or chart as JSON, when the value is one
  results: [],           // every displayed value, for a notebook-style UI
  trace: [{ line: 1, text: "\"hi\"" }]   // which line produced what
}

// Failure
{
  ok: false,
  error: "undefined variable 'nope'",
  output: ""             // whatever was printed before it failed
}

output is filled in on failure too, which matters: a script that prints progress and then fails should still show the reader how far it got.

State persists between calls

The interpreter is kept alive between evaluate calls, so an embedded editor behaves like a REPL without any work on your part:

evaluate("let counts = read_fasta('reads.fa') |> map(|r| len(r.seq))");
evaluate("mean(counts)");   // counts is still in scope

reset();                    // drop every variable and start clean
evaluate("mean(counts)");   // { ok: false, error: "undefined variable 'counts'" }

This is worth deciding about deliberately. A notebook wants the state; a "run this snippet" widget on a documentation page usually does not, and should call reset() before each run so one example cannot silently depend on another. That failure mode is real — it is the reason this project runs each example pack as a single shared session in CI, which caught roughly a dozen examples that only worked because of a variable an earlier one had left behind.

Reading files

read_fasta, read_csv, read_vcf and the rest call out to JavaScript, because WebAssembly has no filesystem. The module looks for window.__blFetch.sync and calls it with the path. Install it before init():

window.__blFetch = {
  sync(url) {
    // Anything you return is the file's contents. Return a string beginning
    // with "ERROR:" to make the BioLang call fail with that message.
    if (localFiles[url]) return localFiles[url];
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url, false);   // synchronous: the interpreter is blocking
    xhr.send();
    return xhr.status === 200 ? xhr.responseText : "ERROR:404 " + url;
  },
};

The call is synchronous, which is the awkward part: the interpreter is running inside a single WebAssembly invocation and cannot await anything. On the main thread a synchronous XMLHttpRequest freezes the page, so for anything larger than a small file, run the module in a Web Worker and let the worker block instead. Serving a file to a user's own browser also means CORS applies — a remote host that does not allow your origin will fail here and nowhere else.

If your files are already in memory — dropped onto the page, or pulled from IndexedDB — you can skip the network entirely and return them from a lookup, as the example above does with localFiles.

The rest of the API

Every export returns a JSON string unless noted.

Export Returns Notes
init() Panic hook and file bridge. Call once, after the loader.
evaluate(source) result object Runs code in the persistent interpreter.
reset() Discards all variables and functions.
list_variables() [{ name, type, preview, members }] Everything currently in scope — for a variables pane.
list_builtins() [{ name, arity }] 789 in the current build. Use it to drive autocomplete.
tokenize(source) [{ start, end, kind }] Byte offsets, for syntax highlighting.
format(source, indent) source string The formatter behind bl fmt.
qc_metrics(kind, text) metrics or null kind is "fastq" or "vcf"; anything else is null.
import_source(source, format, filename) converted source Translates Python, R or a notebook into BioLang.
validate_import(source, notebook) report Checks a conversion without running it.

Running under Node

The --target web build expects a browser. Under Node it will try to fetch the .wasm and fail, so hand it the bytes yourself and stub the two globals it looks for:

import fs from "node:fs";

globalThis.window = globalThis;
globalThis.__blFetch = { sync: (url) => fs.readFileSync(url, "utf8") };

const bytes = fs.readFileSync("wasm/bl_wasm_bg.wasm");
const wasm = await import("./wasm/bl_wasm.js");
await wasm.default({ module_or_path: bytes });
wasm.init();

console.log(JSON.parse(wasm.evaluate("2 + 3")).value);   // "5"

This is exactly how this project's own CI runs all 274 example problems through the module, so it is a supported path rather than a trick.

What is not in the browser build

The WASM build ships a subset of the CLI's builtins — 789 of them. Anything that needs a real operating system is absent: writing files, spawning processes, opening sockets, and the API clients that would be blocked by CORS anyway. A call to a missing builtin fails at runtime with an ordinary "undefined variable" error.

list_builtins() is the authoritative answer for a given build, and checking against it beats assuming. Passing native code through CI is not evidence a script runs in a browser — a pack can be green natively and dead in the playground, which is why the example packs here are run through the real module separately.

Size and startup

The release module is a few megabytes and compiles once per page load. Two things worth doing: serve it with Content-Encoding: br or gzip, which roughly quarters the transfer, and start the load before the user needs it rather than on their first click. Once instantiated, evaluate is a direct call with no per-run startup cost.

Licensing

BioLang is MIT licensed, and the WebAssembly module carries the same terms — so embedding it in a commercial product is fine, provided the licence text travels with it. The full text is in the repository.