Plugin System
BioLang's plugin system lets you extend the language with custom functions written in Python, Deno (TypeScript/JavaScript), R, or as native executables. Plugins communicate with BioLang over a JSON subprocess protocol, making them language-agnostic and sandboxed by default. A plugin can register one or more functions that become available as BioLang builtins.
Plugin Structure
A plugin is a directory in ~/.biolang/plugins/<name>/ containing
a manifest file and the plugin source code:
~/.biolang/plugins/my-stats/
plugin.json # Plugin manifest
main.py # Plugin implementation (Python)
# Or for a Deno/TypeScript plugin:
~/.biolang/plugins/my-viz/
plugin.json
main.ts
# Or for an R plugin:
~/.biolang/plugins/deseq2-wrapper/
plugin.json
main.R
plugin.json Manifest
The manifest declares the plugin's metadata, runtime, and exported functions:
{
"plugin": {
"name": "my-stats",
"version": "0.1.0",
"description": "Custom statistical functions for BioLang",
"author": "Your Name",
"license": "MIT"
},
"runtime": {
"kind": "python",
"command": "python3",
"entry": "main.py",
"dependencies": ["scipy", "numpy"]
},
"functions": [
{
"name": "welch_ttest",
"description": "Two-sample Welch's t-test",
"params": [
{ "name": "group1", "type": "list", "description": "First group of values" },
{ "name": "group2", "type": "list", "description": "Second group of values" },
{ "name": "alpha", "type": "float", "default": 0.05, "description": "Significance level" }
],
"returns": "map"
},
{
"name": "mann_whitney",
"description": "Mann-Whitney U test",
"params": [
{ "name": "group1", "type": "list" },
{ "name": "group2", "type": "list" }
],
"returns": "map"
}
]
}
Writing a Python Plugin
Python plugins read JSON requests from stdin and write JSON responses to stdout. BioLang provides a helper module, or you can implement the protocol directly:
# main.py — Python plugin implementation
import sys
import json
from scipy import stats
def handle_request(request):
"""Dispatch to the correct function."""
fn = request["function"]
params = request["params"]
if fn == "welch_ttest":
return welch_ttest(params)
elif fn == "mann_whitney":
return mann_whitney(params)
else:
return {"error": f"Unknown function: {fn}"}
def welch_ttest(params):
"""Perform Welch's t-test."""
group1 = params["group1"]
group2 = params["group2"]
alpha = params.get("alpha", 0.05)
stat, pvalue = stats.ttest_ind(group1, group2, equal_var=False)
return {
"statistic": float(stat),
"pvalue": float(pvalue),
"significant": pvalue < alpha,
"alpha": alpha
}
def mann_whitney(params):
"""Perform Mann-Whitney U test."""
group1 = params["group1"]
group2 = params["group2"]
stat, pvalue = stats.mannwhitneyu(group1, group2, alternative='two-sided')
return {
"statistic": float(stat),
"pvalue": float(pvalue)
}
# Main loop: read requests, write responses
for line in sys.stdin:
request = json.loads(line.strip())
result = handle_request(request)
print(json.dumps(result), flush=True)
Using the Python Plugin in BioLang
# The plugin functions are available as builtins
let result = welch_ttest([1.2, 3.4, 5.6], [2.1, 4.3, 6.5])
print(result.statistic)
print(result.pvalue)
print(result.significant)
# In a pipeline
expr_data
|> group_by("gene")
|> summarize(|key, rows| {
gene: key,
ttest: welch_ttest(col(rows, "tumor_expr"), col(rows, "normal_expr"))
})
|> filter(|r| r.ttest.significant)
|> print()
Writing a Deno/TypeScript Plugin
{
"plugin": { "name": "my-viz", "version": "0.1.0", "description": "Visualization helpers" },
"runtime": { "kind": "deno", "entry": "main.ts" }
}
// main.ts — Deno plugin implementation
import { readLines } from "https://deno.land/std/io/mod.ts";
interface Request {
function: string;
params: Record<string, any>;
}
function handleRequest(request: Request): Record<string, any> {
switch (request.function) {
case "svg_heatmap":
return svgHeatmap(request.params);
default:
return { error: `Unknown function: ${request.function}` };
}
}
function svgHeatmap(params: Record<string, any>): Record<string, any> {
const { matrix, labels, width = 800, height = 600 } = params;
// Generate SVG...
const svg = `<svg width="${width}" height="${height}">...</svg>`;
return { svg, width, height };
}
// Main loop
for await (const line of readLines(Deno.stdin)) {
const request: Request = JSON.parse(line);
const result = handleRequest(request);
console.log(JSON.stringify(result));
}
Writing an R Plugin
{
"plugin": { "name": "deseq2-wrapper", "version": "0.1.0", "description": "DESeq2 differential expression from BioLang" },
"runtime": { "kind": "r", "command": "Rscript", "entry": "main.R", "dependencies": ["DESeq2", "jsonlite"] },
"functions": [
{
"name": "deseq2_de",
"description": "Run DESeq2 differential expression analysis",
"params": [
{ "name": "counts", "type": "table", "description": "Count matrix" },
{ "name": "conditions", "type": "list", "description": "Sample conditions" }
],
"returns": "table"
}
]
}
# main.R
library(jsonlite)
library(DESeq2)
handle_request <- function(request) {
fn <- request$function
params <- request$params
if (fn == "deseq2_de") {
return(run_deseq2(params))
}
return(list(error = paste("Unknown function:", fn)))
}
run_deseq2 <- function(params) {
counts <- as.matrix(as.data.frame(params$counts))
conditions <- factor(params$conditions)
col_data <- data.frame(condition = conditions)
dds <- DESeqDataSetFromMatrix(countData = counts,
colData = col_data,
design = ~ condition)
dds <- DESeq(dds)
res <- results(dds)
data.frame(
gene = rownames(res),
log2fc = res$log2FoldChange,
pvalue = res$pvalue,
padj = res$padj,
baseMean = res$baseMean
)
}
# Main loop
con <- file("stdin", "r")
while (TRUE) {
line <- readLines(con, n = 1)
if (length(line) == 0) break
request <- fromJSON(line)
result <- handle_request(request)
cat(toJSON(result, auto_unbox = TRUE), "\n")
}
Loading and Managing Plugins
# Install a plugin from the registry
bl add my-stats
# Install from a local directory
bl add path:./my-plugin
# Install from git
bl add git:https://github.com/user/bl-stats-plugin.git
# List installed plugins
bl plugins
# Name Version Kind Functions
# my-stats 0.1.0 python welch_ttest, mann_whitney
# deseq2-wrapper 0.1.0 r deseq2_de
# Show plugin details
bl plugins info my-stats
# Remove a plugin
bl remove my-stats
# Update all plugins
bl plugins update
JSON Protocol
The subprocess protocol is simple JSON-lines. BioLang sends one JSON object per line to the plugin's stdin and reads one JSON object per line from stdout:
// Request (BioLang → Plugin)
{"function": "welch_ttest", "params": {"group1": [1.2, 3.4], "group2": [2.1, 4.3]}}
// Success response (Plugin → BioLang)
{"statistic": -0.577, "pvalue": 0.624, "significant": false}
// Error response (Plugin → BioLang)
{"error": "group1 must not be empty"}
Plugin Discovery
Plugins are discovered from these locations, in order:
- Project-local:
./plugins/directory - User-global:
~/.biolang/plugins/directory - Path entries from
BIOLANG_PATHenvironment variable
Project-local plugins take precedence over global ones with the same name.
Native Plugins
For maximum performance, plugins can be compiled native executables that implement the same JSON-lines protocol:
{
"plugin": { "name": "fast-kmers", "version": "0.1.0" },
"runtime": { "kind": "native", "command": "./fast-kmers" },
"functions": [
{
"name": "super_kmer_count",
"description": "Optimized k-mer counting using minimal perfect hashing",
"params": [
{ "name": "sequence", "type": "dna" },
{ "name": "k", "type": "int" }
],
"returns": "map"
}
]
}
Native plugins are ideal for computationally intensive operations where the subprocess overhead is negligible compared to the computation time.