jennifer programming language

Jennifer. an interpreted programming language.

Batteries included. Not just in name.

41 built-in libraries and 72 distributable modules - over 1,800 functions, constants and types - in one self-contained static binary. The libraries and modules ship with the interpreter; there is nothing to install alongside it.

yay -S jennifer-bin

Arch / AUR. Debian, tarball and source →

Declarations are bare, uses carry a sigil, and every library is asked for by name.

01-hello.j 19 lines
use io;
use strings;

# Nothing auto-loads: each library is asked for by name.
# Declarations are bare; every use of a variable carries $.
def const GREETING as string init "hello";
def name as string init "world";

func shout(word as string) {
    return strings.upper($word) + "!";
}

io.printf("%s, %s\n", GREETING, $name);

# Cooked strings interpolate; raw '...' strings never do.
def loud as string init shout($name);
for (def i in 1..4) {
    io.printf("{$i}: {$loud}\n");
}

jennifer run 01-hello.j

hello, world
1: WORLD!
2: WORLD!
3: WORLD!

Typed structs, a higher-order filter over a func value, and JSON out - no annotations, no reflection.

02-data.j 33 lines
use io;
use json;
use lists;
use strings;

def struct Deck {
    name as string,
    downloads as int,
    tags as list of string
};

func popular(d as Deck) {
    return $d.downloads >= 1000;
}

def decks as list of Deck init [
    Deck{name: "@jennifer/scheduler", downloads: 4210, tags: ["cron", "jobs"]},
    Deck{name: "@acme/labels", downloads: 87, tags: ["print"]},
    Deck{name: "@jennifer/ledger", downloads: 1904, tags: ["finance", "csv"]}
];

# A method's bare name is the func value the filter takes.
def top as list of Deck init lists.filter($decks, popular);

for (def d in $top) {
    io.printf(
        "%s|pad=20 %d|pad=6|group=3|sep=,  %s\n",
        $d.name,
        $d.downloads,
        strings.join($d.tags, ", "));
}

io.printf("%s\n", json.encode($top));

jennifer run 02-data.j

@jennifer/scheduler   4,210  cron, jobs
@jennifer/ledger      1,904  finance, csv
[{"name":"@jennifer/scheduler","downloads":4210,"tags":["cron","jobs"]},{"name":"@jennifer/ledger","downloads":1904,"tags":["finance","csv"]}]

Routes bind to plain methods. Each request is served in its own spawned worker.

03-web.j 28 lines
use json;
import "web.j" as web;

# Handlers are ordinary methods, handed to the router as
# func values. Each request runs in its own spawned worker.
func showDeck(ctx as web.Context) {
    def out as json.Value init json.map();
    $out = json.set($out, "/deck", web.param($ctx, "name"));
    $out = json.set($out, "/registry", "registry.jennifer-lang.dev");
    web.sendJson($ctx, 200, $out);
}

func health(ctx as web.Context) {
    web.text($ctx, 200, "ok\n");
}

# Middleware returns true to continue to the route handler.
func stamp(ctx as web.Context) {
    web.setHeader($ctx, "X-Powered-By", "jennifer");
    return true;
}

def app as web.App init web.new();
$app = web.before($app, stamp);
$app = web.get($app, "/healthz", health);
$app = web.get($app, "/decks/:name", showDeck);

web.run($app, "127.0.0.1:8080");

curl -i localhost:8080/decks/scheduler

HTTP/1.1 200 OK
Content-Type: application/json
X-Powered-By: jennifer
Date: Fri, 21 Aug 2026 00:23:30 GMT
Content-Length: 60

{"deck":"scheduler","registry":"registry.jennifer-lang.dev"}

Launch work with spawn, collect it with task, stream it over a channel. Nothing is shared, so nothing races.

04-concurrency.j 33 lines
use io;
use task;
use channel;

func square(n as int) {
    return $n * $n;
}

# spawn deep-copies its scope at launch, so there is no
# shared memory to race on - and no lock to forget.
def jobs as list of task of int init [];
for (def i in 1..5) {
    def job as task of int init spawn {
        return square($i);
    };
    $jobs[] = $job;
}

def total as int init 0;
for (def t in $jobs) {
    $total = $total + task.wait($t);
}
io.printf("1..4 squared sums to %d\n", $total);

# Channels stream values; each one is copied on send.
def ch as channel of string init channel.make(0);
def worker as task of int init spawn {
    channel.send($ch, "finished");
    channel.close($ch);
    return 0;
};
io.printf("the worker says: %s\n", channel.recv($ch));
task.wait($worker);

jennifer run 04-concurrency.j

1..4 squared sums to 30
the worker says: finished

Who may call a route, with which scope, and what the body has to satisfy - declared per route and enforced by one guard.

08-webapi.j 51 lines
use json;

import "web.j" as web;
import "webapi.j" as webapi;
import "validate.j" as validate;

# The authenticator turns a bearer token into an identity. How a token is
# checked stays your business - the module never learns.
func verifyToken(token as string) {
    if ($token == "admin-token") {
        return webapi.Identity{ok: true, subject: "u-admin", display: "admin", scopes: ["publish"]};
    }
    def anonymous as webapi.Identity;
    return $anonymous;
}

func showDeck(ctx as web.Context) {
    webapi.sendJson($ctx, 200, json.set(json.map(), "/deck", web.param($ctx, "name")));
}

func publish(ctx as web.Context) {
    def who as webapi.Identity init webapi.identity($api, $ctx);
    def form as map of string to string init webapi.validated($api, $ctx);
    def out as json.Value init json.set(json.map(), "/published", $form["tag"]);
    webapi.sendJson($ctx, 201, json.set($out, "/by", $who.subject));
}

# One shim binds the Api into web's middleware chain: there are no closures yet.
func apiGuard(ctx as web.Context) {
    return webapi.guard($api, $ctx);
}

def api as webapi.Api init webapi.new();
$api = webapi.mount($api, 1, "/v1");
$api = webapi.authenticator($api, verifyToken);
$api = webapi.get($api, "/deck/:name", showDeck, webapi.public());
# A route's contract is declarative: who may call it, with which scope, and
# what the body has to satisfy. The guard enforces all three.
def publishing as webapi.Spec init webapi.Spec{
    summary: "publish a deck version",
    auth: webapi.Auth.Bearer,
    scopes: ["publish"],
    rules: {"tag": [validate.required(), validate.maxLen(16)]},
    rateLimit: 30,
    produces: webapi.Produces.Json
};
$api = webapi.post($api, "/publish", publish, $publishing);

def app as web.App init web.new();
$app = webapi.install($api, $app, apiGuard);
web.run($app, "127.0.0.1:8081");

curl localhost:8081/v1/... (four calls)

GET  /v1/deck/scheduler         -> 200 {"deck":"scheduler"}
POST /v1/publish   (no token)   -> 401 {"error":"missing bearer token"}
POST /v1/publish   (no tag)     -> 422 {"error":"invalid request","failures":[{"field":"tag","rule":"required","message":"is required"}]}
POST /v1/publish   (admin)      -> 201 {"published":"v2.1.0","by":"u-admin"}

Ed25519 signatures, AES-256-GCM sealing and PBKDF2 key derivation, with the nonce handled for you.

09-crypto.j 35 lines
use io;
use crypto;
use convert;

def manifest as bytes init convert.bytesFromString("scheduler 1.4.0", "utf-8");

# Ed25519: sign with the private half, verify with the public half alone.
def keys as crypto.Keypair init crypto.signKeypair();
def signature as bytes init crypto.sign($keys.private, $manifest);
io.printf("signature ok      %t\n", crypto.verify($keys.public, $manifest, $signature));

def tampered as bytes init convert.bytesFromString("scheduler 1.4.1", "utf-8");
io.printf("tampered rejected %t\n", not crypto.verify($keys.public, $tampered, $signature));

# A passphrase is low entropy, so it is stretched before it becomes a key.
def salt as bytes init crypto.randBytes(16);
def phrase as bytes init convert.bytesFromString("correct horse battery staple", "utf-8");
def key as bytes init crypto.pbkdf2($phrase, $salt, 200000, 32, "sha256");

# AES-256-GCM. The nonce is generated and prepended for you - one less thing
# to get catastrophically wrong.
def sealed as bytes init crypto.encrypt($key, $manifest);
def opened as bytes init crypto.decrypt($key, $sealed);
io.printf(
    "sealed %d bytes -> opened \"%s\"\n",
    len($sealed),
    convert.stringFromBytes($opened, "utf-8"));

# Opening with the wrong key is an authentication failure, not garbage.
try {
    def wrong as bytes init crypto.decrypt(crypto.randBytes(32), $sealed);
    io.printf("unreachable\n");
} catch (e) {
    io.printf("wrong key         %s\n", $e.message);
}

jennifer run 09-crypto.j

signature ok      true
tampered rejected true
sealed 43 bytes -> opened "scheduler 1.4.0"
wrong key         crypto.decrypt: authentication failed (wrong key or tampered ciphertext)

Fit a line by solving the normal equations with linalg, then check it against stats - two built-in libraries, the same answer.

10-science.j 45 lines
use io;
use linalg;
use stats;

# Eight builds: modules touched, and the seconds each one took.
def modules as list of float init [18.0, 15.0, 24.0, 19.0, 16.0, 22.0, 17.0, 21.0];
def seconds as list of float init [10.2, 9.8, 11.1, 10.4, 9.9, 10.8, 10.1, 10.6];

# Fit seconds = a + b*modules the linear-algebra way: build the design matrix,
# then solve the normal equations (A^T A) x = A^T y.
def design as list of list of float init [];
for (def m in $modules) {
    $design[] = [1.0, $m];
}

def at as list of list of float init linalg.transpose($design);
def lhs as list of list of float init linalg.matmul($at, $design);
def rhs as list of float init linalg.matmul($at, $seconds);
def fit as list of float init linalg.solve($lhs, $rhs);
io.printf("normal equations  a=%f|prec=4  b=%f|prec=4\n", $fit[0], $fit[1]);

# The stats library reaches the same line by its own route.
def ols as stats.Regression init stats.linearRegression($modules, $seconds);
io.printf(
    "stats regression  a=%f|prec=4  b=%f|prec=4  r2=%f|prec=4\n",
    $ols.intercept,
    $ols.slope,
    $ols.r2);

# The identities hold, to floating-point precision.
def residual as list of list of float init linalg.sub(
    linalg.matmul($lhs, linalg.inverse($lhs)),
    linalg.identity(2));
io.printf(
    "det=%f|prec=1  norm(A A^-1 - I)=%f|sci=true|prec=1\n",
    linalg.determinant($lhs),
    linalg.norm($residual));

# A singular matrix raises rather than handing back nonsense.
try {
    def singular as list of list of float init [[1.0, 2.0], [2.0, 4.0]];
    io.printf("%v\n", linalg.inverse($singular));
} catch (e) {
    io.printf("singular          %s\n", $e.message);
}

jennifer run 10-science.j

normal equations  a=7.6243  b=0.1441
stats regression  a=7.6243  b=0.1441  r2=0.9955
det=544.0  norm(A A^-1 - I)=7.1e-15
singular          linalg.inverse: matrix is singular (not invertible)

Every sample is a file in this site's repository, highlighted by a Jennifer program. All 10 →

41

built-in libraries

sql, json, regex, crypto, httpd, task ...

72

shipped modules

plain .j source you can read and fork

1,800+

functions, constants, types

documented, one cheatsheet

1

static binary

~23 MB, or ~10 MB embeddable

01

All included

Everything below ships in the binary. For anything outside it, there is jvc and the deck registry.

01

Databases and data formats

Talk to MySQL / MariaDB / PostgreSQL through parameterized, injection-safe sql, or map rows with the orm. Read and write JSON, YAML, TOML, XML and CSV. Match text with RE2 regular expressions. Send and receive mail over SMTP, POP3 and IMAP. Cache in Redis or memcached.

02

HTTP, client and server

An HTTP/S client and a thin rest layer to call out; the httpd engine, the web framework and webapi conventions to serve. Sessions, cookies, CORS, CSRF, ETags and rate limiting are included.

03

Model Context Protocol

Expose your own methods as Model Context Protocol tools, resources and prompts - or call another MCP server as a client. Tested against the official MCP SDK. JENNIFER.md is a single-file language reference to ship beside your code for an AI assistant to read.

04

Concurrency

spawn deep-copies its enclosing scope, so there is no shared memory to race on. Wait, poll, cancel or time out a task; stream values between workers over CSP channels that copy on send.

05

Strict checking

Conditions must be bool. Conversions are spelled out. Names never shadow. Missing keys, out-of-range reads and undefined math raise positioned errors instead of handing back a silent null, a NaN or a wrapped integer.

06

One binary

Copy it and run. The interpreter and every built-in library are statically linked; the modules ride along as readable .j files with no compile step. A TinyGo build drops to ~10 MB for embedded and minimal-container use.

02

Seven design stances

The rules the language is designed against. Convenience is rejected when it creates a second way to do the same thing, or hides what the code does.

  1. 1

    One way per thing

    No ++, no +=, no second spelling of printf. One canonical form reads better than three convenient ones.

  2. 2

    Explicit over implicit

    Sigils mark use-site references, def carries the type, libraries are imported per topic, conversions are spelled out. Nothing important is implicit.

  3. 3

    Presentation, not transformation

    Format verbs shape how a value renders. Changing the value itself is a library call.

  4. 4

    Strict at boundaries

    Undefined math, missing keys, out-of-bounds reads and type mismatches are positioned runtime errors. No NaN, no silent default value.

  5. 5

    Value semantics

    Lists and maps copy on assignment and on parameter binding. const is deep: no rebinding, no mutation at any depth.

  6. 6

    No shadowing

    A name binds once in any visible scope. Inner scopes inherit; they never redeclare.

  7. 7

    Topic-based, opt-in libraries

    The standard library is split by topic and never bundled. Nothing auto-loads.

The full table and the rationale

03

The toolchain

Everything below is the same binary. Nothing else to install, configure or keep in sync.

  • jennifer run app.j run a program
  • jennifer repl an interactive REPL
  • jennifer fmt -w src/ canonical formatting
  • jennifer lint src/ compile-legal but suspect patterns
  • jennifer test app.j discover and run test methods
  • jennifer profile app.j an instrumented evaluator profile
  • jennifer serve app.j --watch run a web app, reload on change
  • jennifer tokens / ast dump the token stream or the AST

Pre-1.0 notice

While the major version stays at 0.x.y, anything can change at any time - syntax, semantics, library names, signatures, file formats. We aim for best-effort stability between minor versions but make no guarantees. Pin a version if you need reproducibility; expect to migrate when you upgrade. From 1.0.0 on, Jennifer follows Semantic Versioning.

See what is implemented and what is coming

Everything above ships in one binary.