JavaScript SDK
The SDK lets you stay in JavaScript — your own loops, your own objects, your own
try/catch — while BioLang's Rust runtime does the sequence
work, the statistics and the file parsing. Every builtin the module reports is callable as
a method, and none of them is reimplemented in JavaScript.
If you want the raw WebAssembly module instead — no npm package, no wrapper — see Embedding (WASM). This page is the layer above it.
Pre-release. The biolang package is not
on the public npm registry yet, so npm install biolang will not resolve.
Until it does, build it from a checkout with node scripts/build-npm.mjs and
install the npm/ directory as a local dependency. Everything else on this
page is current.
A first session
BioLang.create() loads the WebAssembly module and returns a session that owns
its own interpreter. Methods run immediately and return ordinary JavaScript values —
a number is a number, not a wrapper you have to unpack.
import { BioLang } from "biolang";
const bl = await BioLang.create();
bl.mean([1, 2, 3]); // 2
bl.gcContent(bl.dna("ATGC")); // 0.5
bl.runtimeVersion(); // the version compiled into the WASM
bl.dispose();
Builtin names work in both spellings: bl.gc_content(...) as BioLang writes it,
and bl.gcContent(...) as JavaScript does. They are the same function.
Values cross the boundary with their meaning intact
Small values are copied into ordinary JavaScript data. Types that would lose their meaning as a bare array keep a wrapper: a table stays a table, a sequence knows whether it is DNA, RNA or protein, and quality scores stay distinguishable from a byte buffer.
bl.evalValue("summary([12, 14, 15, 19])");
// { count: 4, mean: 15, median: 14.5, min: 12, max: 19, sd: 2.943... }
bl.setValue("sample", { id: "S1", values: [12, 14, 15, 19] });
bl.getValue("sample"); // { id: "S1", values: [12, 14, 15, 19] }
const t = bl.table({ gene: ["TP53", "BRCA1"], expr: [1.0, null] });
bl.dropNull(t); // rows containing null are dropped
Integers beyond JavaScript's safe range arrive as bigint. Numeric matrices use
Float64Array and quality scores use Uint8Array. Values the SDK
cannot represent unambiguously are refused rather than guessed — a bare
Uint8Array, or a NaN, raises a TypeError saying what
to pass instead.
Large values stay in Rust
Anything past the inline limit is left where it is and handed back as a handle. A handle can be paged, converted to a typed array, and passed straight back into the session without a copy in either direction.
const genes = bl.evalValue("repeat([1], 1000000)", { maximumInlineBytes: 1024 });
genes.valueType; // "List"
genes.length; // 1000000
genes.page({ offset: 100, limit: 20 });
bl.len(genes); // passed back without copying
genes.dispose();
Handles belong to the session that made them. One passed to a different session is rejected rather than silently resolving to an unrelated value with the same internal id.
Sessions are isolated
Each create() owns a separate interpreter. Variables, registered modules and
reset() do not cross between them, even in one Node process or one browser
Worker, and file and network policy is reapplied per call so a second session cannot
retarget the first one's cwd.
const a = await BioLang.create();
const b = await BioLang.create();
a.run("let secret = 42");
b.run("secret"); // { ok: false, error: "undefined variable 'secret'" }
b.reset(); // does not touch a
a.dispose();
b.dispose();
Calling JavaScript from BioLang
A synchronous JavaScript function can be registered and then used inside BioLang, including
by higher-order builtins such as map.
bl.registerFunction(
"calibrate",
{ parameters: ["Number"], returns: "Number" },
(measurement) => measurement * 1.08,
);
bl.evalValue("map([10, 20, 30], calibrate)"); // [10.8, 21.6, 32.4]
Callbacks must finish synchronously and cannot re-enter their own session, which is what keeps interpreter state predictable. They are also the slowest way across the boundary: for a tight numeric loop it is faster to pull the data into JavaScript, or to express the whole thing as one BioLang call, than to call back per element.
Building BioLang source instead of running it
The package root executes. The biolang/expressions subpath constructs: its
functions return lazy BioExpression objects and calculate nothing until an
expression is given to a session. The split is deliberate —
bl.mean(values) always runs and mean(values) from the subpath
always builds source, so a name never means two things.
import { function_, if_, program, ref, return_ } from "biolang/expressions";
const classify = program(
function_("classify", ["x"], if_(ref("x").gte(10), return_("high"), return_("low"))),
);
bl.run(classify);
JavaScript cannot overload ===, >= or &&,
so the builder API spells them .eq(), .gte() and
.and(). An expression that a JavaScript operator would silently discard is
reported rather than dropped — a scientific predicate that quietly disappears is worse
than one that fails loudly.
Converting existing BioLang
transpileJavaScript turns a .bl program into the direct API, which
is useful for porting a script or for showing the two side by side.
bl.transpileJavaScript('let m = [12, 14, 15]\nsummary(m)');
// let m = [12, 14, 15];
// let result = bl.summary(m);
// result;
Structural builders appear only where a BioLang construct has no direct JavaScript spelling. See the verified equivalents for generated JavaScript that has been executed and compared against its BioLang.
Files, Node and the browser
Under Node, relative paths resolve against cwd and network reads can be
switched off. In the browser there is no filesystem, so supply a synchronous reader over
files you prepared beforehand, and run the session in a Worker so a blocking read cannot
freeze the page.
// Node
const bl = await BioLang.create({ cwd: "./data", network: false });
// Browser
const files = new Map([["reads.fa", ">a\nACGT\n"]]);
const web = await BioLang.create({
fetchSync: (path) => files.get(path) ?? "ERROR:not found",
});
What is checked, and how
The claims on this page are enforced by the build rather than maintained by hand.
| Check | What it proves |
|---|---|
check:coverage |
Every builtin the WASM module reports has a JavaScript method and a TypeScript declaration. A builtin added in Rust without a wrapper fails the build. |
check:corpus |
Every tracked .bl file transpiles, and JavaScript's own parser compiles the result. |
check:equivalence |
Deterministic programs are executed as BioLang and as their generated JavaScript, and the decoded results compared. This is what catches a translation that parses but computes something else. |
check:package |
The published package carries one shared WASM payload, not one per target. |
The low-level API
biolang/raw exposes the wasm-bindgen surface directly. Its module-level
evaluate, reset and list_variables functions share one
default interpreter by design and are not isolated sessions. Anything handling
concurrent, private or patient-derived data should use BioLang.create().
Licensing
MIT, like the rest of BioLang. The licence text travels with the package.