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:

  1. Project-local: ./plugins/ directory
  2. User-global: ~/.biolang/plugins/ directory
  3. Path entries from BIOLANG_PATH environment 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.