Beyond 1.0.0 - idea collection
Jennifer’s near-term target is a rich, dependable set of language features, libraries, and modules - enough to tag a 1.0.0. That work is tracked in milestones.md. This file collects ideas for after 1.0.0: directions worth recording so the design doesn’t foreclose them, none committed to a timeline.
Two kinds:
- Drafts - concrete, already-shaped directions (embedding, WASM, specialised-domain libraries). Each has a design; it just has no schedule.
- Loose ideas - a grab-bag of smaller or vaguer possibilities, jotted down when they come up so they are not lost.
Nothing here is a commitment. An idea graduates into milestones.md if and when it earns a slot.
Drafts
Embedding, WASM, and specialised domains. Recorded so the design doesn’t foreclose them, roughly ordered by the size of the structural change each is: the embedding restructure is a single-repo refactor; the WASM runtime brings in a whole new dependency; the specialised-domain libraries are indefinite-in-count families.
Each carries a stable DRAFT# handle so it can be referenced and later
graduated into a numbered milestone (e.g. “shift DRAFT#9 to M20.6”).
Handles are assigned once and retired on graduation - never reused, and
never renumbered when the list is reordered - so a reference stays valid for
the life of the idea. DRAFT# is deliberately not a milestone number; an
idea only gets an M-number when it graduates into
milestones.md.
Each draft also states its Requires - the DRAFT# handles and/or
milestones (M-numbers) that must land first, or none - so its blockers
are explicit before it is scheduled.
DRAFT#1 - Public interpreter API for third-party embedding
Extract the interpreter core out from under internal/ and expose a
documented Go-side surface so external programs can embed Jennifer. Today
internal/interpreter, internal/parser, internal/lexer, and
internal/lib/* are unreachable from any module that isn’t
jennifer-lang.dev/jennifer - Go’s internal/ visibility rule is not a
convention, it’s a compile-time barrier. No submodule / require / replace
workaround exists; embedding is impossible without a restructure.
It comes ahead of the WASM runtime (DRAFT#2) because a Go-side embedding
API is a strictly smaller change (repository restructure, no new external
dependency), it unblocks the most immediate embedding scenarios (scripting
slot in a Go host, LSP / formatter tooling, test harnesses), and it does
not foreclose WASM (DRAFT#3) - a plugin surface can layer on the same
pkg/ facade once Wazero (or similar) is in play.
Concretely. Add a pkg/ top-level (working name; the final path
settles at the start of this work):
pkg/interpreterre-exportsInterpreter,Value, error types, and theInstall(in *Interpreter)registration API that every stdlib library already uses. Theinternal/packages stay as the implementation;pkg/is the stable facade with semver-covered surface once we ship 1.0.pkg/lib/*re-exports each shipped library (convert,math,strings, …) so a host can install the ones it wants and leave out the rest. Non-breaking for the current CLI -cmd/jenniferpicks up the sameInstallcalls, just throughpkg/libshims instead of directly.- Documented pluggable interfaces for the host-provided facilities the
OS-touching libraries currently reach for:
io.Writerforio.printfoutput (already a*Interpreterfield; formalize as an interface).io.Readerforio.readLine/io.readBytes/io.readCharsstdin.Clockfortime.now()/time.local()/time.sleep(thenowFunc/sleepFunctest hooks ininternal/lib/timeare the shape).Randformath.rand*/lists.shuffle(the shared random source).- Filesystem / network / process hooks left as future work - a host
wanting those either installs the stdlib libraries as-is (accepting the
Go
os/netdependencies) or ships its own shims. A documented registration pattern is the deliverable; the shims themselves are per-host and out of scope here.
Stdlib-backed defaults. Each pluggable interface carries a working
default so pkg/interpreter.New() plus pkg/lib/io.Install(in) produces a
running interpreter without every embedder wiring up seven interfaces
first. Clock defaults to Go’s time.Now, Rand to a math/rand
source, io.Writer to os.Stdout, io.Reader to os.Stdin. Hosts
override only what they need. A no-os embedder replaces every default; a
Slack-bot embedder swaps just io.Writer for its outgoing-message pipe and
leaves the rest.
Boundary rules at the Install site. Three explicit error paths so hosts get loud, positioned failures instead of subtle misbehaviour:
- Duplicate library
Installat the Go level is rejected, mirroring how a duplicateuse NAME;errors at the Jennifer level (the duplicate-userule, lifted). A host installingpkg/lib/mathand then its own shim that also claims themathnamespace fails at the secondInstallcall, not silently overlaid. Installand pluggable-interface setters are frozen onceRun()starts. Attempts to callInstall,SetClock,SetOut, or friends after the interpreter has begun executing produce a positioned “cannot configure interpreter mid-run” error at the Go call site. The interpreter can then trust its host bindings for the rest of the run without defensive re-checks.- Host implementations are trusted at the interface boundary. The
interpreter uses whatever
Clock.Now()orRand.Int63()returns without validation - a broken host implementation is the host’s problem, not the interpreter’s. Stated so hosts don’t expect defensive checks that aren’t there and so downstream bug reports are triaged to the correct side of the API boundary.
Non-goals.
- A hosted no-
osbuild target. Even with this restructure, the shipping stdlib libraries lean on Go’sos/net/timepackages. A truly bare-metal orno-osembedding can only use the pure-value libraries (convert,math,strings,lists,maps,hash,crc,encoding,regex) plus whatever host-provided shims the embedder wires up. That’s a design constraint on the embedder, not a milestone on Jennifer’s side. - Semver freezing the public API. Jennifer stays pre-1.0 through this work; it documents what’s exported and how libraries plug in, but breaking changes to that surface remain allowed until 1.0.0.
Motivation. Third-party embedding has multiple concrete consumers
already imagined: scripting-language slot in a Go application, tooling that
needs direct AST / interpreter access (LSP, formatter integrations, syntax
highlighters), test harnesses that want to drive .j programs from Go,
config-DSL runtimes, plugin systems for game engines and similar. None of
them require an OS-free build; all of them need the internal/ -> pkg/
restructure. The Install pattern already works this way - every stdlib
library is a pkg.Install(in) call. The missing piece is visibility, plus
documented hooks for the pieces of host state currently exposed only as
package-level test vars.
Requires: none - a self-contained restructure of the current codebase.
Best sequenced once the core library / module surface has settled, so the
pkg/ facade is stable, but nothing blocks it.
DRAFT#2 - WASM runtime embedding
Wazero or similar inside the interpreter binary. TinyGo-size cost evaluated
honestly before commitment. Without it, no WASM libraries (DRAFT#3).
Requires: none (embeds Wazero directly).
DRAFT#3 - WASM libraries
If the WASM runtime (DRAFT#2) ships, sandboxed plugins via
use wasm:libname;. Each library its own piece.
Requires: DRAFT#2 (the runtime) and DRAFT#1 (the plugin surface
layers on its pkg/ facade).
Specialised domains
Each domain its own effort with sub-pieces as needed:
- ML.
DRAFT#5stats- descriptive statistics overlist of int|float: mean, median, mode, variance, stddev, percentile, min / max / sum, correlation. Pure-value, TinyGo-clean; the highest-value, simplest piece, so first. Requires: none.DRAFT#6linalg- vectors aslist of float(dot, norm, cross, scale, add / sub) and matrices aslist of list of float(matmul, transpose, determinant, inverse, solve, identity). Algorithms implemented directly - nogonum, too large a dependency. Matrices staylist of list of floatfor v1 (idiomatic and value-semantic); a Go-backed matrix handle is the noted future escape hatch when big-matrix performance demands it. Requires: none.DRAFT#7ML primitives - atopstats/linalg, when demand surfaces. Requires:DRAFT#5+DRAFT#6.
DRAFT#8Bioinformatics. Sequence alignment (Smith-Waterman, Needleman-Wunsch), FASTA/FASTQ parsers, molecule structures. Requires: none.- Encoding / binary protocols.
DRAFT#9asn1- ASN.1 BER/DER encode/decode, as a Go system library. Byte-level binary parsing belongs in Go, not.j(thejsonlesson: a char-by-char parser in the interpreter pays overhead per byte). This is the enabler for a family of binary protocols and PKI formats - LDAP, SNMP, X.509, PKCS. Go’s stdlibencoding/asn1is DER-only, so the full BER that LDAP / SNMP use (indefinite lengths, alternative encodings) needs either a BER dependency (e.g.go-asn1-ber) or a hand-rolled BER codec in Go. Requires: none (it is the enabler for the rest of this group).DRAFT#10ldap/snmp(layered onasn1+net). Withasn1doing the byte crunching in Go andnetproviding TCP/UDP + TLS (connectTLS/startTLSalready cover LDAPS / StartTLS), the protocol orchestration (bind, build request, iterate results) is not per-byte hot and can live in a.jmodule or a thin Go library. SNMP is the natural first client (simpler PDUs, UDP, no SASL); LDAP adds controls + SASL (SCRAM builds on thecryptolibrary). A pure-.jimplementation of the BER layer itself is explicitly not the plan. Existing pure implementations (e.g. PHP FreeDSx) are a protocol reference, not a port target - their heavy OO shape does not map to Jennifer’s value-semantic structs. Requires:DRAFT#9(asn1) and the shippednetlibrary; LDAP’s SASL / SCRAM path additionally needscrypto(M20.1).
DRAFT#11Sandbox. Restricted-capability execution. Requires: none hard; relates toDRAFT#1(embedding) andDRAFT#3(WASM isolation).
Ordered when demand surfaces. The WASM libraries idea (DRAFT#3) may cover
some of this space first.
DRAFT#12 - jvc package manager (decks)
A package manager for Jennifer, in the shape of PHP’s Composer (or Rust’s
Cargo): declare dependencies in a manifest, jvc install resolves and
fetches them, and the app imports what it pulled. Installing an app becomes
git clone + jvc install, and jvc update advances within the declared
constraints.
- Packages are “decks”. A deck is a distributable, versioned bundle of
.jmodules, published to a public deck repository / registry (provided later) thatjvcresolves and fetches from - packagist-style; a deck can also come straight from a git URL. - Installed into the
vendor/tree M19.7 resolves.jvcwrites decks into the project-localvendor/tree that the interpreter already addresses through the@scope/packageimport form and vendor-root discovery shipped in M19.7 - so a hand-populatedvendor/imports beforejvcexists, andjvcis just the manager layered over that resolver. Nothing is global; each app owns its decks beside it. - The one remaining language-surface question: inline version selectors.
M19.7 resolves
import "@jennifer/supercms/" as cms;(the trailing/expands to the package-named entrysupercms/supercms.j) against whatever is installed.jvcsupplies the default - plainimport @jennifer/supercms;takes the versionjvcresolved (declared indeck.toml, pinned in the lockfile), version-transparent, which is what almost every script wants. The opt-in for side-by-side versions is a per-import selector matched against the installed set (never triggering a fetch):@jennifer/supercms=1.2.3(exact),>=1.2.3/~/^(asemverconstraint over what is installed), or#cefa234(a git commit); one script can pin=1.xwhile another pins=2.x, and two versions in one file take distinctasaliases. An unsatisfiable selector errors pointing atjvc install, not a silent download. Cost: the selector is new grammar (the lexer reads@vendor/deck+semverop +#commitas one token up to;); the plain M19.7 string-path form is the no-new-grammar fallback that loses only the inline selector. deck.tomlmanifest + lockfile.deck.toml(TOML, so it needs thetomllibrary) declares required decks and constraints (bitcoin = ">=1.2.0"), andjvcproduces acamcorder.lockpinning exact resolved versions (content hash per deck) sogit clone+jvc installis reproducible. Dependency sets split by section ([prod]/[dev],jvc install --prod), with a taxative (the section is the exact set) vs additive (base plus the section’s extras) mode still to design.jvcowns the lifecycle: dependency resolution (semver constraint solving across the graph), downloading,jvc update(advance to the newest constraint-satisfying versions, rewritecamcorder.lock), integrity pinning, and the publish flow to the registry.
Migrating bundled modules out to decks. Once decks exist, niche or
product-specific modules that ship bundled today should graduate out into
decks (the archetype is gotify - a single-product push integration every
install need not carry); language-fundamental modules stay bundled. Moving one
changes its import, so it is a breaking change under semver: within 1.x ship it
both ways (bundled + @-deck) with the bundled copy marked @deprecated so
imports re-point at their own pace, let the two drift without breaking, and
remove the bundled copy in 2.0.0.
A whole track of its own. Requires: M19.7 (the
@scope/package resolver + vendor root the scheme rests on); the toml library
(M18.8, for deck.toml); the shipped module system and the semver module
(constraint solving - its satisfies / maxSatisfying / minSatisfying /
validRange range surface is the resolver primitive); and http / git
(fetching). The public deck registry is separate infrastructure, provided
later.
DRAFT#13 - Higher-level PDF: font metrics, layout, and Markdown -> PDF
A three-phase build on top of the shipped pdfwriter module (M18.35), taking it
from “place text and shapes at coordinates” to “flow a document”. The phases are
strictly ordered because each depends on the one before; they land as separate
milestones when graduated, but share one draft handle because they are one arc.
Phase 1 - font metrics (the keystone). Today pdfwriter can place text but
cannot measure it, so there is no way to wrap a paragraph, auto-size a table
column, or align text - all of which need the rendered width of a string. Add
the standard-14 AFM width tables (public Adobe Font Metrics: per-glyph
advance widths, in 1/1000 em, for WinAnsiEncoding) and a
pdfwriter.stringWidth(font, size, text) -> int that sums them. Courier is
monospace (600 units/glyph, trivial); Helvetica and Times need the real
per-character tables, generated into an included .j data file the way the
encoding codecs were generated from the Unicode mapping files
(gen_*.go -> *_gen.j). First payoff: textAligned(pg, x, y, font, size, align, str) for left / center / right placement. Nothing in the later phases is
expressible without this.
Phase 2 - table and flow layout. The layer that removes the manual
cell-positioning pain (the ergonomic win people reach for TCPDF’s writeHTML
tables to get - but as a typed API, not an HTML subset). On top of stringWidth:
table(columns, rows, options) (auto column sizing, in-cell word wrap, borders,
a header row, page-break across rows); paragraph(pg, text, x, y, width, font, size) -> int (wrapped flowing text that returns the y it ended at, so callers
stack blocks); and heading / list helpers.
Phase 3 - Markdown -> PDF. The markup-driven document story, and the reason
it is Markdown rather than HTML: Jennifer ships a markdown parser (GFM tables,
headings, lists, emphasis) but no HTML parser (htmlwriter only builds HTML),
so the cheap, ergonomic path to “write markup, get a PDF” is to drive the Phase-2
layout layer from the existing Markdown parse rather than write a quirky
HTML-subset parser (Markdown tables are also easier to author than HTML tables).
A prerequisite to verify first: whether the markdown module exposes a reusable
parse tree or only renders straight to a string - if the latter, it needs a small
refactor to surface the intermediate document model. An HTML-subset front-end
(TCPDF-style) stays a later option, only worth it for consuming pre-existing
HTML, and it would sit on the same layout foundation.
Stays pure .j (static data + lookups + layout math), both binaries.
Requires: none hard for Phase 1 (builds on the shipped pdfwriter module,
M18.35); Phase 2 requires Phase 1; Phase 3 requires Phase 2 and the shipped
markdown module (plus possibly a parse-tree surface on it).
DRAFT#14 - Project governance, licensing, and contribution policy
The rules for how the project is run and how outside contributions are taken -
organizational, not code. Untouched while the project is solo (one author,
Copyright (C) 2026 <developer@mplx.eu>, LGPL-3.0-only, no outside PRs), but
it must be settled before the first external contribution is merged: several
of the choices are hard to reverse once other people’s copyrightable work is in
the tree. The open questions, roughly by urgency:
- Copyright-holder model. Under distributed copyright (the default, no paperwork) every non-trivial contributor automatically holds copyright in their patch, so the tree becomes a mosaic of holders and any future relicensing needs each one’s agreement. The alternatives are a CLA (contributor grants the project a broad license, keeps their own copyright) or an assignment / CAA (contributor transfers copyright to a single holder) - both consolidate the rights but add contributor friction, and assignment needs an entity to hold them. This is the decision that is expensive to undo.
- The copyright notice. Whether headers stay per-author (
(C) <name>) or move to a collective label ((C) The Jennifer Authors, defined by git history). The trap to avoid: a two-fileAUTHORS(holders) /CONTRIBUTORS(credit) split only carries information when a work-for-hire contributor exists (employer holds copyright, individual is merely credited); for an all-volunteer project the two lists are identical, so the split is pointless. Either keep no enumerated holder file (the collective label refers to git history) or consolidate ownership via CLA / assignment. - Relicensing headroom. LGPL already lets anyone embed / link Jennifer
without permission, so ordinary use never needs a contributor’s sign-off. The
only thing distributed copyright forecloses is issuing a different license -
e.g. a commercial embedding exception for a deep-embedded
jennifer-tinytarget that cannot meet LGPL’s static-relink terms. If keeping that option open matters (embedding is a first-class goal), a CLA is the tool; if “LGPL-only forever” is acceptable, distributed copyright is fine and the constraint never bites. - Contribution mechanics.
CONTRIBUTING.md, the sign-off mechanism (a lightweight DCOSigned-off-byline, which asserts “I have the right to submit this” without a license grant, vs a full CLA-bot, which also grants one - the choice follows from the relicensing decision above), a code of conduct, and the PR / review workflow. - Project governance. Who decides (BDFL vs a maintainer group), how commit
rights are granted (judgment and sustained involvement, never an LOC or
commit-count threshold - metrics are a bad proxy and get gamed), and a
MAINTAINERSfile once more than one decision-maker exists. Being listed as a contributor confers no authority; credit and governance are separate. - Name / mark. Whether the “Jennifer” /
jennifer-langidentity needs any trademark-style usage policy (forks, the deck registry) or stays informal.
The license itself stays LGPL-3.0-only unless a deliberate relicensing
decision above changes it; this draft is about the process and ownership around
it, not a license change.
Requires: none (organizational, independent of the codebase). Socially paired with the M19.8 org move and triggered by the first external contribution, but no code prerequisite. Not legal advice - the chosen model should get a real legal review before it is published.
Loose ideas
A grab-bag, recorded when it comes up.
label: embed a bitmap image in the job. Todaylabel.imagereferences an image already stored on the printer (by name). The heavier alternative is to embed the bitmap in the rendered job so a logo travels with the label and needs no pre-loading: convert a source image (PNG / mono bitmap) to each dialect’s raster - cab embedded-ASCII image data, ZPL^GFgraphic field - which needs image decoding plus 1-bit dithering / thresholding. That is a real raster-conversion capability (a Go-side helper or animagelibrary), not the pure-text.jthe rest of the module is, so it is a separate piece of work rather than another encoder branch. Until then,label.image(by reference) covers the stored-logo case.- Extra distribution packaging. Beyond the Linux
.deb(shipped in M15.8) and the two distribution requirements for 1.0.0 stable (cross-build for macOS / Windows, a real apt repository), the additional package formats are nice-to-haves, each shipped only when a user asks and a maintainer will keep it green: a Homebrew tap (macOS), a Snap package, a Nix flake / Nix package, and Flatpak / AppImage or any other Linux distribution format. None blocks a release; none is a 1.0.0 requirement. - Cross-platform support. Linux is the only supported platform, but
best-effort unsupported macOS and Windows binaries (the standard-Go
jennifer, via cross-compile) already ship with each release - so the work here is not “add the ports” but promoting them to supported (real CI coverage, per-OS golden strategy, the platform-specific edge cases). When touching filesystem, paths, line endings, or process behavior, prefer portable stdlib helpers (path/filepath, not hardcoded/); avoid Linux-only assumptions so those binaries stay genuinely portable, not just compile-clean. time: IANA / DST zones. Real zone names (Europe/Berlin) with historically-correct daylight-saving resolution, added to thetimesystem library - not a hand-maintained.jdata map. A.jmap is the wrong shape: abbreviations (CSTis US Central and China Standard and Cuba Standard) don’t identify a zone, and the real model is offset-per-(zone, instant) over a transition history that ships several updates a year. Back it with Go’stime.LoadLocation+ the embeddabletime/tzdata(or the host’s/usr/share/zoneinfo), so the database is the toolchain’s problem and resolution is correct at any instant. Standard-jenniferonly: TinyGo’stimecan’t load zones, sojennifer-tinystays fixed-offset (a build-tag split likenet). Level 1 first - an offset-at-instant resolver (time.offsetAt(name, $t)/time.zoneFor(name, $t) -> time.Zone) that leaves thetime.Time {nanos, offset}model untouched (the snapshot is fixed, so DST-crossing arithmetic must re-resolve); Level 2 - a zone-carryingtime.Timewith DST-correct arithmetic - is a larger, optional follow-up needing a Go-backed zone handle.- Password hashing (Argon2id / bcrypt / scrypt). The modern default for
password storage, deferred out of the
cryptolibrary because it lives ingolang.org/x/crypto(a dependency, unlike the stdlib KDFscryptoships) and wants its own surface distinct from the KDFs: a self-describingcrypto.hashPassword(pw) -> string($argon2id$...) plus a constant-timecrypto.verifyPassword(pw, hash) -> bool. Added when password storage is a concrete need, taking thex/cryptodependency then - crypto is the one place the dependency-free stance bends, since you never hand-roll it. encoding- the harder codecs. The single-byte character codecs and binary-to-text formats all shipped; the deferred remainder, picked up only when a real program needs one: variable-width Asian encodings (Shift-JIS,Big5,GB2312,GBK,GB18030,EUC-JP,EUC-KR) - each a state machine with variant / ambiguity edge cases, a whole piece apiece;UTF-16/UTF-16LE/UTF-16BE/UTF-32(BOM, surrogate pairs, endianness); andUTF-7(mail-transport - thoughquoted-printablealready shipped as a general codec).- FCGI.
use FCGI as web;library whennetandhttpdmature. Lets Jennifer host CGI / FastCGI workloads end-to-end. - Inline assembler.
- Binary AST cache (
.jcfiles). Pre-parsed loading for big programs and embedded scripting hosts. Its own effort when it lands - file-format design, versioning, and TinyGo-safe serialization are enough work to merit dedicated treatment. The text JSON form viajennifer astis the placeholder until then. - Profiler: max-call-depth metric. Have
jennifer profiletrack Jennifer call depth (bump inevalCall, drop on return) and report the max reached, per source position and overall. Names stack-limit problems directly - the recursion-depth-vs--stack-sizeheadroom that the recursivefibinexamples/benchmark.jexercises onjennifer-tiny. Small and additive to the existing hit-count / wall-clock /--allocscollector; deferred because stack limits are diagnosable by hand today. Heap-per-position stays out of scope (--allocsalready proxies copy churn; true RSS needsruntime.ReadMemStatssampling, coarse under TinyGo). tinygo_devtoolsbuild tag. The dev subcommands (tokens/ast/fmt/lint/profile/test) are!tinygofor binary size, not compatibility - they are TinyGo-clean Go. A//go:build !tinygo || tinygo_devtoolsconstraint (stub astinygo && !tinygo_devtools) plus amake build-tinygo-devtarget would let them run under the actual TinyGo runtime - e.g. toprofilea TinyGo-specific perf or stack issue in situ. Pairs with the depth metric above: together they are “TinyGo runtime introspection.” Deferred - build-tag complexity across ~6 files and a larger dev-tiny binary, for a diagnostic reached for only occasionally.- Build-time library selection. Choose which system (Go) libraries are
baked into a binary at compile time. Motivated by
jennifer-tinysize (an embedded target needing onlyio+mathshouldn’t carrynet/regex/hash) and by opt-in niche Go libraries that don’t merit defaulting. The install point is already consolidated - every entry path (run/repl/profile/testand the test harnesses) callsinternal/stdlib.InstallAll, so a library is one line there - and that is the seam a build-tag scheme would cut along: gate each entry behind//go:build lib_net(or aminimal/fullprofile) and growmake build-minimal/make build TAGS=..., exactly like the existing!tinygodev-tool split. Compile-time only - Go’spluginpackage is Linux/macOS-only and unsupported by TinyGo, and dynamic linking contradictsjennifer-tiny’s no-hosted-runtime goal, so PHP-style loadable.soextensions are out. Two caveats to design for: (1) a trimmed build breaks the “any.jruns on any binary” portability promise (use net;becomes a runtime error), so the default build stays full and trimmed builds are an explicit opt-out - ideally with ameta-level “is library X present?” query for graceful degradation; (2) CI grows a couple of profiles (default / minimal), not 2^N. Complementary to, not a substitute for, the module system:.j-level extensibility (community / uncommon libraries writable in Jennifer) is the module system’s job with zero binary cost; build-time selection is only for the curated Go-level core. - SQLite (
sqlengine backend). The client-server half of relational support - MySQL / MariaDB + PostgreSQL - is committed as M20.7 (asqlsystem library over Go’sdatabase/sql, pure-Go drivers). SQLite stays parked here, and it is worth being precise about why, because it is not the reason it first looks like. SQLite is also just a pure-Godatabase/sqldriver -modernc.org/sqlite, registered with the same one-lineimport _asgo-sql-driver/mysqlorpgx, cross-compiling cleanly like any pure-Go package (the cgomattn/go-sqlite3, which does break static / cross-compile / TinyGo and needs a C toolchain, is rejected in its favor). So integration effort and API are identical to M20.7’s two drivers; SQLite is in every practical sense “just a third driver” for the same library, sharing its surface and opaquesql.Rowresult shape. The one real difference is weight.modernc.org/sqliteis the entire SQLite C source transpiled to Go plusmodernc.org/libc(a Go libc reimplementation) - multiple MB of generated code, versus the hand-written, few-hundred-KB protocol clients M20.7 ships. Baking that into every defaultjenniferbloats the binary for the many users who only ever touch a network database. That, and only that, is why SQLite is gated as a build-tag opt-in (-tags sqlite), surfaced as ajennifer-fullrelease artifact - a build variant of the default binary, not a third supported brand. The binary ladder becomesjennifer-tiny(DBs stubbed) ⊂jennifer(MySQL + Postgres) ⊂jennifer-full(+ SQLite). The dependency break from “libraries stay dependency-free” is already accepted at M20.7; SQLite adds size, not a new principle. TinyGo is the one place SQLite is categorically worse, and it is architectural, not a build choice:modernc.org/sqlite’s libc emulation (unsafe, goroutines, syscall-level memory management) cannot compile under TinyGo, and no TinyGo-compatible SQLite exists. Unlike a wire-protocol database - which could in principle be reimplemented as pure.jover a net-enabled tiny rebuild - SQLite has no wire protocol and so can never reach the embeddable binary. That is the genuinely ironic gap: a local, file-based store is exactly what a minimal embedded target would most want, and it is the one database that binary can’t have with current tooling. Because SQLite is really just another driver, the only open call is timing: fold it into M20.7 behind the-tags sqlitegate, or keep it deferred here until thejennifer-fullvariant earns its place in the release / CI / packaging matrix. Contrast the text-protocol storesredis/memcache, pure Jennifer overnet, which need none of this. - Explicit map-to-struct conversion. A spelled-out, validating way to
turn a
json.Valueobject (or a homogeneousmap of string to T) into a typed struct - the sanctioned counterpart to the rejected implicit coercion (see technical/rejected.md). Deferred: once JSON is destructured throughjson.Valueaccessors, the by-hand rebuild covers the need, so a one-call form is a convenience, not a blocker. Two candidate shapes, decided on consistency not brevity - aconvert.toStruct($map, "Point")library call (a two-arg, stringly-typed outlier in the otherwise one-argconvert.toXfamily, or else not self-contained if it reads the binding’s declared type) versus aPoint{ ..$map }struct-literal spread (names its type statically, at the cost of new literal syntax). Either way strict: every declared field present with a matching type, recursing into nested structs / lists / maps, value-semantic, no partial fills or defaults. io.lines() -> list of string. Slurp the whole stdin into a list. Additive on top of the streamingreadLine()+eof()idiom; nice-to-have for tiny scripts, not blocking.- i18n. Locale-aware case folding, collation, number / date formatting, BiDi. Gated on the CLDR-data binary-size question (likely an optional library shipped after the WASM runtime, so locale tables aren’t baked into every build).
- Advanced scheduling knobs. CPU affinity, work-stealing pool sizing,
NUMA awareness,
GOMAXPROCS-equivalent runtime tuning. Runtime-config surface for the spawn scheduler, not new language features. Ships when a real use case forces it (the default - “let Go’s scheduler decide” - handles every workload we’ve imagined so far). - Performance & memory. Interpreter-internal optimizations that preserve
stance #5 (value semantics) at the user level: copy-on-write for lists /
maps / bytes / structs (share underlying storage until a write splits it),
per-frame arena allocation, and read-only slice views (
xs[1..5]as a non-owning window that errors on assignment). Strictly optimizations - no user-visible aliasing or mutation rules change. Stance-breaking variants (mutable references, interior mutability, shared mutable state) are turned down in technical/rejected.md. Best landed once the language is settled and the interpreter doesn’t churn under it.