Changelog
All notable changes to SSPUR are recorded here. The format follows Keep a Changelog, and the project uses Semantic Versioning. From 1.0, code that works on a release keeps working on every later 1.x release; the stability policy lists the exceptions, and a feature is deprecated with a warning before 2.0 can remove it. Before 1.0, minor releases could change the syntax and the standard library.
Unreleased
Changed
- SSPUR Pulse refreshes every 3 hours instead of every 6, and every board refreshes on each run.
- Pulse’s launch times show in the visitor’s own time zone, written like the UTC time the page carries, which stays when the browser can’t name a zone.
Fixed
- Pulse’s reading board no longer fails when Wikimedia publishes a day’s most read list late: it looks back a week for the newest list, and when Wikimedia has nothing newer yet the board is up to date, not stale.
1.0.0 - 2026-10-18
Added
W_CONVERSION_MAY_TRAP(ADR 0044):sspur checkuses Z3 to prove that everyF64round floor ceil truncgets a finite value inInt’s range, from refinements,pre, enclosing conditions,clamp/min/max, literal arithmetic and, for functions that aren’tpub, their calls, and warns withchecked_round(etc.) or the guard to add where it can’t; code without the warning can’t trap in a conversion. The compatibility golden oftests/compat/pulsegains its three warnings.- The 1.x stability and deprecation policy,
docs/reference/stability.md: what 1.0 freezes, what stays experimental (marked(experimental)insspur --help), and theW_DEPRECATEDwarning a feature gets before 2.0 can remove it. A compatibility suite incargo test(tests/compat) checks frozen programs and packages against their recorded hashes, diagnostics,runandtestoutput in both tiers andsspur fmtoutput. - Release files carry GitHub artifact attestations: build provenance for every file attached to a release and a CycloneDX SBOM (
sspur-vX.Y.Z.cdx.json, also attached) for eachsspurarchive, checked withgh attestation verify --repo sspurlang/sspur;install.shchecks the provenance whenghis installed and signed in, andSSPUR_SKIP_ATTESTATION=1turns that off. sspur fix [FILE|DIR...] [--check]moves code written for 0.14 to 1.0: it rewrites what 1.0 would read differently (strings whose braces or escapes changed meaning, a nestedmatchwhose arms moved, binding annotations 0.14 ignored) so the program means what it meant, and lists what to check by hand (round floor ceil truncthat can now trap, comparisons with a zero,==on types with their ownEq). The migration guide covers every change with before and after.
Changed
- The playground’s WebAssembly is 5 KB smaller gzipped (941 KiB): the standard library’s hover and completion docs are a separate 6 KB file that the page fetches after the engine starts.
- The macOS binary lays out the functions of the hot commands first (ADR 0043,
tools/order/), sorun,check,fmt,testand the language server touch fewer pages: peak memory is 10 to 20% lower (hello world 2.4 to 2.0 MB,checkof Pulse’s library 10.1 to 9.0 MB), and code added later can’t move the hot paths. W_CONVERSION_MAY_TRAPproofs are cached by the content hashes they read, so a repeatedsspur checkof unchanged code proves nothing again, and an edit only proves again what depended on it.- Timing bounds in the tests measure CPU time, not wall-clock time, so they hold on a loaded machine.
Fixed
sspur deploy localno longer leaves a client waiting on a connection a function child inherited during a hot swap (each response now shuts its socket down), and routes to a new version only once its functions have started, so requests duringstartandswapdon’t time out with 502.- On
riscv64-qemuandaarch64-qemu, an interrupt handler that re-arms the timer no longer unmasks interrupts inside the handler, where a tick already due nested and the return jumped to garbage (exit 63). - The Zed grammar follows the 0.15 string rules (
{interpolates only an expression,\u{...}), scoped namespkg.module.Name, and soft keywords, soruleandeffectcan be names. - The reference documents 48 builtins it was missing (
Complex,FlatMap,MdSpan, views, the random distributions, zones, files), every CLI flag, and every environment variable; a test keeps the environment page complete.
0.15.0 - 2026-10-18
Added
-
Lambda parameters can name their types:
(x: Int) => x + 1,(k: Str, v) => .... -
A type alias can take an effect-row parameter,
type Handler[e] = Request -> Response ! e, used asfn apply[e](h: Handler[e], r: Request) -> Response ! eorHandler[log]; it wasE_UNKNOWN_TYPE unknown type 'e'. -
BigInt,DecandRatiohave the arithmetic operators their methods implied:big(2) + big(3),-d,ratio(1, 2) / ratio(1, 4)(no/forDec, whose division takes a scale), including through[T: Add]; they wereE_OPERATOR. -
W_UNUSED_RESULT: a statement that calls a pure function or method and drops a value that isn’tUnitis a warning, with the fix in the hint.b.add(s)orxs.push(x)on a line of its own compiled silently and did nothing, since values never change in place;_ = f(x)drops a value on purpose. -
Opt.and_then(f)andRes.and_then(f)chain a function that gives anOptor aRes;List.repeat(n)is the listntimes over ([0].repeat(n)isnzeros), named likeStr.repeat;Map.is_empty. In both tiers. -
req.peer: a server’sRequestcarries the address of the client the connection comes from (IPv4 as1.2.3.4, IPv6 without a zone), the same in both tiers and on Windows; the network reference says whenX-Forwarded-Forcan be trusted.Responsehasdata: Bytes, sent afterbody, andhttp.bytes(status, b)builds one, so a handler or a middleware can serve binary or compressed content. Literals may leavepeeranddataout, so existingRequest{...}andResponse{...}literals keep working, and content hashes don’t change. -
One rule for
Str,BytesandSecretarguments, in the language reference: the calls of the std namespaces (crypto.*,gzip.*,http.*and the rest) take aStrforBytes, anXforSecret[X], and aStrorSecret[Str]forSecret[Bytes]; everything else, your own functions included, converts explicitly (s.to_bytes,secret(x),k.map(t => t.to_bytes)).http.bytesfollows it. -
sspur fmtkeeps comments (ADR 0042), and so do the language server’s formatting, the playground’s Reformat and the wasmfmt. A comment on its own line stays on its own line before the same code, a comment after code stays after it, comments between definitions keep their blank lines, and a short list, call, record, parameter list, pattern or sum type with a comment inside is written one element per line so the comment stays next to its element. Comments are still not part of the canonical text or any hash. A property test formats every.sspfile in the repository and every corpus program at 40 and 100 columns and checks that every comment comes back once, in order, with the same code before it, and that the canonical text, the hashes and idempotency hold. -
Binding annotations:
x: T = eandvar x: T = eare checked (eagainstT) and kept in the canonical text, sovar seen: List[Int] = []survivessspur fmtandbuild.fmt. They used to be accepted and dropped. -
\u{...}escapes in strings (1 to 6 hex digits,"caf\u{e9}"),\}for a literal}and\'for a quote in a'...'string. -
_ => eand(acc, _) => e: lambdas that ignore a parameter. -
pkg.models.Docnames a public module’s definition in expressions, types, record literals and patterns, without ause. -
SSPUR_STACKletssspur run --interpandsspur test --interprecurse as deep as that much stack allows instead of stopping at 20,000 calls:SSPUR_STACK=256Mfits about 130,000 calls of a simple recursive function on macOS and Linux, fewer on Windows, and then traps withstack overflow in f. Without it the limit stays 20,000 calls, so a runaway recursion still fails within about 50 MB; native code andpartasks are unchanged.
Changed
- Cold native builds of big packages are 5 to 7 times faster (ADR 0041). On a generated package of 200 modules and 2.5 MB of source (
bench/bigpkg), a coldsspur testwent from 282 s to 40 s,sspur publish --dry-runfrom 291 s to 59 s and a dependent’s firstsspur runfrom 251 s to 41 s; testing again after editing one function of a module went from 36 s to 7 s. The functions of a package or module with 16 or more functions now share C units of up to about 32 instead of one each, so clang no longer parses and compiles the same declarations and helpers thousands of times; a function of the program itself keeps its own unit, and a dependency’s units are the same in every program that uses it, so dependents still share its objects. A build with enough C runs one clang process per core (at most 4 before), fewer when memory is short, andSSPUR_JOBSstill sets the number; the units are made on every core; and the Z3 questions of a cold build are answered on every core. Programs without such packages compile to the same objects and libraries as before, so run times and the cachedsspur runare unchanged. - The SMT cache is
smt2/in the cache directory: each answer is an empty file named by the question and the answer, and a build lists a directory once instead of opening a file per question, which takes a warmsspur testof the package above from 9.8 s to 4.9 s. Answers in the oldsmt/directory aren’t read. - The Homebrew tap moved to the sspurlang organization:
brew install sspurlang/sspur/sspur.packaging/homebrew/core/sspur.rbis the formula for homebrew-core, so that after 1.0brew install sspurworks. - The interpreter appends in place when the receiver is a local variable’s last use (
xs := xs.push(x), a fold’s accumulator,b := b.add(s),m := m.put(k, v),Set.add,concat,remove), so building a list, string or map one item at a time is linear under--interpas it is natively: 40,000 pushes in a loop went from 6.1 s to 0.12 s, a fold that pushes 40,000 times from 6.0 s to 0.10 s, 40,000Map.putfrom 14.8 s to 0.10 s, and 80,000StrBuf.addfrom 1.75 s to 0.14 s (wholesspur run --interp). A value still in use elsewhere is copied as before, and so is everything in the playground. - F64 semantics for 1.0, the same in both tiers and documented under Numbers in the standard library reference: comparison stays a total order (NaN equals itself and sorts above
inf()), but-0.0 == 0.0now, so-0.0 < 0.0is false andwhere _ >= 0.0accepts-0.0; the zeros stay distinct values (-0.0prints as-0.0).round floor ceil trunctrap withinteger overflowon NaN, an infinity or a result outsideInt, likeIntarithmetic, instead of saturating silently (nan().roundwas 0,1.0e19.roundwasIntmax);checked_round checked_floor checked_ceil checked_truncgiveOpt[Int]. - A
{in a string interpolates only an expression that is not a lone literal and doesn’t start with{:"a{3}","^a{2,4}$"and"{{x}}"are text, as"{}"and CSS braces already were. A brace that is text no longer swallows the code after it:s.contains("{") or s.contains("}")is two calls and"{" + x + "}"three strings (they were one string). No program in the repository or the corpus changes meaning. withbinds tighter than comparisons:p == q with a := 1isp == (q with a := 1)(it updated theBool, an error). The reference lists the precedence of every operator.- A
matchin a match arm ends at the first arm indented less than its own, so| some(x) => match xwith its arms indented deeper no longer takes the outer| none. Arms at the same column still belong to the inner match. build.fmtcounts comments now thatsspur fmtkeeps them: each one wheresspur fmtputs it (indent and blank lines don’t count). It also reads a long sum type broken one variant per line and a broken list pattern with...- The playground’s WebAssembly stays under its budget although it gained the 1.0 additions and keeps comments: hole filling sorts names instead of table entries, expression clones and walks are no longer inlined into every caller, and the checker, the editor features and the runtime helpers shared with native code are built for size. Running takes the same time.
- The playground checks an edit about 5 times faster: it parses the builtin signatures once instead of at every check, so checking the 10 examples takes 7 ms instead of 36 under Node; the first check after the page loads takes as long as before.
sspur fmtlexes and parses a file once, andsspur checkdrops each stage of elaboration before checking the next: on Pulse’s library (626 lines)fmtwent from 17.3 to 16.7 ms andcheckfrom 36.4 to 34.8 ms against 0.14.0, and the peak memory ofcheckfrom 11.2 to 10.1 MB.
Fixed
- A program whose only use of
BitsisList.to_bitsbuilds natively; it failed withunknown type name 'SBits'and fell back to the interpreter. Every C runtime section now declares the types it uses, and the test that compiles each section alone no longer declares them for it. - The native compiler no longer overflows its stack on a recursive function passed by name to
mapand also mapped inside a lambda (cs.map(depth)withgs.map(g => g.map(depth))): the cost estimate follows a function named as an argument at most three levels deep and never into a recursive one, as it already did for calls. - A dependency’s audit notes and warnings are no longer printed against the dependent’s file at lines that don’t exist, on every run. Its warnings are its own (its own check shows them); its audit notes are listed by the dependent’s
sspur check, not byrunortest, at the dependency’s file and line (frlib/lib.ssp:3:3 audit A_DECLASSIFY ...). The rule is in the error reference. - A path dependency outside the package that changed since
sspur.lockpinned it is no longer silent:sspur run,testandcheckprint a note naming it andsspur deps update NAME, and anE_PKG_NAMEfor a name only the new version has (a renamed export) carries the same hint instead of only listing the old exports. A change that leaves the exports as they are (a comment) gives no note. - An effect-row parameter shared by several arguments is the union of their effects:
twice(n => add_log(n), n => add_time(n), id)withfn twice[e](a: (Int -> Int ! e) -> (Int -> Int ! e), b: ..., f: Int -> Int ! e)no longer fails withexpected Int -> Int ! log, found Int -> Int ! log, time, and the same for a list of such lambdas. fail[E]in a parameter’s row takesEfrom the argument:fn rescue[E, e](next: Int -> Int ! fail[E], e) -> (Int -> Int ! e)catchingEgives a purerescue(failing); the argument’sfail[X]used to land ine.- A
catchwhose only arms are_handles every error its body raises, of any type, in both tiers; with two error types it wasE_CATCH_TYPE. db.txworks inside a function generic in its result or error type (fn txg[A, e](c: Db, f: () -> A ! e) -> A ! db, fail[DbErr], e), when the lambda ends inraise, and when it returnsok(())as aResthe function’s result type names: the expected type now reaches the lambda. A non-sum error type such asStrraised in a transaction also rolls back and is raised again; these were internalE_DEPerrors.- A
catchwith a binding arm| e => f(e)over a body that raises several error types handles the oneftakes. - A call’s expected result type informs its arguments, so
spans.fold([], (acc, s) => match acc | [..init, last] => ...)knows the accumulator’s element type from the function’s result type. ==is consistent with hand-writtenEqimpls: a type that derivesEqcompares a field whose type has its own impl with that impl, and so doOpt,List,Resand tuples of it, sosome(k) == some(k2)works for a type with animpl Eqaround a secret, where it wasE_SECRET_COMPARE. A derivedEqover a newtype ofSecretused to compare the secret structurally and ignore the newtype’s impl. Comparing a newtype of aSecretthat has no impl with==is nowE_SECRET_COMPARE, like the secret itself.json.decode[T],r.json[T],db.query[T],db.one[T],db.opt[T]and thecsvreaders work with a type parameterTof a generic function, which is then compiled once per type it is called with; each call checks that its type can be decoded (E_JSON,E_SQL_ROW,E_CSV_TYPE). A call can name the type argument,dec[Point](s), where it used to read as indexing.json.encode(x)withx: Ttakes[T: Json]. A method’s type argument may be a tuple,json.decode[(Int, Str)](s), without an alias.- A package whose function takes a function value with one of the package’s own effects (
pub fn handled(f: () -> Int ! tick) -> Int) builds as a dependency; the effect inside the parameter’s type was not renamed with the package. A package may also export functions named like a built-in effect (pub fn fail,pub fn log): itsfail[E]andlogrows stay the built-in effects. A user effect can’t take a built-in effect’s name (E_DUPLICATE). "\\{x}"is a backslash and the value ofx(it was the text\{x}).- The canonical text of corpus program t46, a
catchending a match arm, read back as a different program; amatch,catchorhandlethat ends an arm other than the last now prints insidedoon its own line. Its content hash is unchanged. _in apreorpostcondition is a placeholder lambda, as in any call argument:post r.map(_ + 1).len == xs.lenchecks (it wasE_PLACEHOLDER).- Error spans of names interpolated after an escaped quote point at the name.
- A
matchorcatchwith arms inside a string’s{...}prints on one line, sosspur fmtkeeps the string whole and a program usinghttp,dborcryptowith one builds; it failed withE_DEP internal: the elaborated network program does not parse: unterminated string.
0.14.0 - 2026-10-17
Added
sspur fmtbreaks lines over 100 columns (ADR 0040): lists, tuples, calls, records and parameter lists one element per line with a trailing comma,withupdates one per line,ifchains with eachelseon its own line, conditions afterandandor, other operators and method chains where the pieces then fit.ifblocks are written withelseunder theif. The canonical text that definitions are hashed and stored as is unchanged, so no content or export hash moves and everysspur.lockand published package still verifies. The language server’s formatting and the playground use the same formatter.- Public modules: a package marks a module
public = truein itssspur.toml(models = { path = "models", public = true }), and dependents import its definitions withuse pkg.models.{Doc}. The registry’s API listing, semver check and site include them asmodels.Doc. Without the flag a module stays private, so no published package’s API or export hash changes. - The playground’s Format is Reformat Code: Cmd+Option+L or Ctrl+Alt+L as well as Shift+Alt+F, the definitions a selection touches, the cursor kept after the same code and on the same screen line, one undo to go back, and code that doesn’t parse left alone with the error shown in place. A ruler marks column 100 when lines don’t wrap, phones get a Format button in the editor’s corner, and an optional “Format on run” (remembered, off by default) formats before Run, Test and Share. Enter keeps a list element’s indent, and a closing bracket typed at the start of a line goes back to its opener’s indent (also in VS Code and the language server).
Changed
- The registry’s site renders README tables (stacked on phones), shows the top 12 packages with descriptions capped at three lines, and its 404 page, like sspurlang.github.io’s, reads as a compiler error with a quick fix to the closest page or package. The docs link the registry and the home page from the top of the packages chapters.
- The playground scrolls long lines sideways on phones instead of wrapping them.
- The registry check
build.fmtaccepts whatsspur fmtnow writes, as well as the one-line form of 0.13, and a statement broken anywhere the lexer reads as one line. - The Claude Code plugin’s version is the release version (it was 0.2.1); a test keeps it and the editor extensions there.
- The playground’s WebAssembly is 3 KB smaller gzipped although it now breaks lines: the editor docs keep only the tables hover reads, and wasm-opt folds constant offsets into loads and stores.
Fixed
- A function imported with
useno longer shadows a record field of the same name:t.digestis the field whenthas one, as it already was for local functions, in both tiers. - A number after
.is always a tuple index, sop.0.1reads, and(p.0).1next to a trait-bounded function no longer fails with an internal error. - In a string literal, escapes apply inside literal braces too:
"{x\\y}"has one backslash, as the reference says. sspur qnames scoped dependencies by their key (pkg.f), not their link prefix.- The playground’s Reformat returns to the left edge after breaking long lines.
0.13.1 - 2026-10-17
Added
- Registry: a public member of an organization may publish the first version of a package from the organization’s repository, and a package whose repository was transferred or renamed on GitHub keeps publishing from its new home (GitHub resolves the old name).
sspur publish, the Publish page and the workflow all ask GitHub. - The registry check
pkg.fork(blocking): the first version of a new flat name can’t come from a fork, so nobody claims a name with a fork of someone else’s repository. A fork can still publish under its owner’s scope.sspur publish, the Publish page and the workflow all ask GitHub, and the Publish page now runs the index’s own checks.
Fixed
- The registry’s hourly sweep reruns unanswered requests one at a time; during a burst of requests, GitHub kept only one queued run of the index writer and cancelled the rest.
Changed
- SSPUR moved to the GitHub organization sspurlang: the repository is github.com/sspurlang/sspur, the docs, playground and Pulse are at sspurlang.github.io/sspur, and the registry is sspurlang/sspur-packages at sspurlang.github.io/sspur-packages. Old repository, release and raw URLs redirect, and the old Pages URLs forward to the new ones, so existing installs, lock files and links keep working.
sspurreads the registry from its new home, and the Homebrew tap staysutkarshavardhana/sspur. - The registry’s home page shows the top 10 packages (most depended on, then most recently updated) under a “Top packages” header with a count; search looks through all of them. The site’s header and favicon use the SSPUR logo.
- CI runs on branch pushes only. A release first checks that CI passed on the tagged commit and stops otherwise, instead of running CI again for the tag.
Fixed
- A record field wins over a function imported with
useof the same name, as it does over a local function:t.digestwithuse sessions.{Token, digest}read the fielddigestofTokenas the calldigest(t)and failed to typecheck. (p.0).1: a number after.is always a tuple index, sop.0.1no longer lexes aspand the float0.1.sspur fmtprinted that text and broke the program, and the elaborator, which prints and re-parses programs with traits, failed withexpected a field or method name, found '0.1'.- Escapes inside braces that are not an interpolation work as anywhere in a string:
"{x\\y}".lenis 5, as"\{x\\y}".lenis, in both tiers (the backslashes stayed doubled, which corrupted JSON written that way). sspur qnames a scoped dependency by its key,pkg.f, and accepts it in queries; it showed and needed the link prefix (acme_pkg_.f).build.fmtaccepted only part of whatsspur fmtprinted: amatchorcatchas a call argument or a parenthesized operand, andifblocks in aforbody.- Docs: ADR 0033 gave the
gpubenchmark’s peak as 97 MB and the benchmark table 92 MB; both are right, forsspur run --no-cacheand for the cached run that is the default since ADR 0034, and the ADR now says so.
0.13.0 - 2026-10-16
Added
- Scoped package names tied to GitHub owners (ADR 0039), so two companies can each have
pkg:name = "acme/pkg"insspur.toml, whereacmeis the GitHub user or organization that owns the repository. The new blocking checkpkg.scopeverifies it: the request must come from that user or, for an organization, from a public member (a private member is told how to make the membership public). Scoped packages live atpackages/@acme/pkg.jsonin the index, with releases namedacme--pkg-VERSION; flat names and existing index files are unchanged, and 0.12.0 clients keep installing flat packages. Every new name, scoped or not, still waits for a maintainer’s/approve. sspur add acme/pkgwritespkg = { package = "acme/pkg", version = "^1.0.0" }, using the name after the scope as the dependency key; when the key is taken it says so and--as NAMEpicks another, so two scopes’pkgwork in one program. Locks record full names,sspur deps treeshows the key a package is used under, andsspur publish,yank,search,source, the issue forms, the package site (pages atpackages/acme/pkg/) and the Publish page know scoped names.- Published packages can contain module packages: path dependencies inside the package’s directory, as the web-app template’s
models = { path = "models" }. Every check covers every module (parse, typecheck, formatting, docs, tests in both tiers, secrets, review rules) with paths from the repository’s top;sspur publishruns the modules’ tests side by side in a copy of the commit. Dependents build the modules under the package’s name (acme/pkg:models,source = "module"in the lock), so they never clash with a dependent’s own modules. The package’s API is the root module’s exports. The Publish page checks multi-module repositories too, listing the tree once with the git trees API. - The registry credits authors: the index commit is authored by the requester with their GitHub noreply address and committed by the bot, and the merge uses
gh pr merge --squash --author-emailwith aCo-authored-bytrailer, so the commit on main counts in the author’s contributions. The Publish page also lists a user’s public organizations and their repositories, and shows the scope check in the browser.
Changed
- The registry’s size limits are 2000 files, 1 MiB per file, 4 MiB of SSPUR source and 20 MiB per repository (were 500 files, 512 KiB of source and 5 MiB), room for a library the size of Apache Commons Lang.
sspur add money@1.2.3andsspur source money@1.2.3mean exactly that version (=1.2.3); requirements such as^1.2and~1.2.0are unchanged.- Builds of path packages are reused: a module that many others depend on is built once per command instead of once per path to it, and a build with the same inputs comes from the cache. On a synthetic package of 2.5 MB in 200 modules, a cold
sspur checkwent from 166 s to 11 s.
0.12.1 - 2026-10-16
Changed
- The first version of a new package name waits for a maintainer of sspur-packages to comment
/approveon its request, after every check has passed. The approval holds only for the commit the pull request showed. Later versions still merge on their checks alone.
Fixed
- The registry’s workflows verify sspur against the release’s
SHA256SUMS, keep their work files in the runner’s temp directory, and the Publish page’s username pattern is valid in Chrome and Firefox. sspur fmtkeeps record shorthand:Page{items, total, limit: r.limit}stays as written instead of becomingPage{items: items, total: total, ...}, and a field writtenx: xprints asx, as record patterns already did. The registry’sbuild.fmtcheck (and sosspur publish) no longer fails idiomatic packages, the web-app template’s own included: it reads a statement broken across lines after an operator or before.name, and anifchain with eachelseon its own line, as the one linesspur fmtprints, and accepts match arms indented one level under theirmatch.sspur update(andsspur update NAME,sspur deps update) reports a move to a newer version whose exports are unchanged,~ pagination 0.1.0 -> 0.1.1 (no API change), instead ofup to date, andsspur deps treeand the update diff show the locked version: versions with the same exports share a cache entry, which kept the first version’s number.- A native build no longer fails with
cannot load ...\native\on Windows when another build of the same program is writing its library’s name at that moment (seen as a CI failure of the GPU trap tests). Loads of native libraries also retry briefly on Windows, and a failed load keeps the system’s reason. sspur run,sspur testandsspur debugsetSSPURto their own executable for the program, in both tiers, unless it is already set, so a program that runssspur(such as the web-app template’stests/run.ssp) uses the same compiler instead of whichever is first onPATH.
0.12.0 - 2026-10-16
Added
- A public package registry (ADR 0038), sspur-packages, that runs entirely on GitHub at no cost: one JSON file per package, each version pinned to its tag, commit, the content hash of its exports and the SHA-256 of a deterministic source snapshot stored as a release asset, so a version survives its author deleting or rewriting the repository and a moved tag changes nothing for users. The template of the index repository, with its workflows, forms, schemas, standards and SSPUR programs, is in
registry/packages-template/. sspur add moneyandsspur add money@^1.2add published packages (money = { version = "^1.2" }), with Cargo’s requirement syntax and the one-version rule across the graph.sspur updatemoves them to the newest matching versions with the semantic diff. Installs download the snapshot, verify its SHA-256 and the export hash, fall back to the author’s repository at the pinned commit only when the snapshot is missing, refuse a tampered snapshot, and never run package code. Builds stay offline and reproducible fromsspur.lock, which recordsgit,snapshotandsha256for registry packages.sspur publishruns the registry’s 35 checks on the committed package (parse, typecheck, no warnings, formatting, tests natively and interpreted, version rules with an API diff by signature hashes, an effects audit that needs new capability effects acknowledged with--ack, licence, README, docs on every public definition, tests, naming, typosquatting, size, binaries, secrets, hooks, and review standards) and opens the publish request with the GitHub CLI, or prints the prefilled request form.sspur yank,sspur search,sspur source(download and verify without running),sspur api(the public API and the facts the checks read) andsspur archive(the snapshot of a commit).- The registry workflow turns each request into a pull request whose description lists every check with file, line and fix, the effects per function, the API changes and how to submit the next revision, and merges it when every blocking check passes; package code runs only in a read-only job.
- The package site on sspur-packages’ GitHub Pages, built by SSPUR programs: search, a page per package with its README, API docs, versions, effects, dependencies, dependents and licence, and a Publish page that lists your repositories and tags with GitHub’s API, runs the checks in the browser with a separate WebAssembly build of the checker, and opens the request on github.com.
- The handbook chapter Publishing a package, CLI reference for the new commands, and ADR 0038.
sspur new web-app NAMEwrites SSPUR’s reference application, a notes service with accounts, from a template built into the binary (templates/web-app/): a JSON API and server-rendered HTML pages through onesvc, SQLite with migrations, transactions and full-text search, Argon2id passwords, session tokens stored as SHA-256 digests, HMAC CSRF tokens, secure cookie defaults, login rate limiting per account and address, input rules aswhererefinements,config.loadsettings with a startup error for a missing production secret, structured JSON logs with request ids, gzip and graceful shutdown. Its modules are packages inside the app (models,storage,auth,views) and three are written to be reused and later published (sessions,pagination,passwords). It comes with unit tests for the storage, auth and views, handler tests that call the svc directly, an end-to-end test that starts the server on a free port and uses it with the HTTP client, a concurrency test (16 clients, no lost writes) and a load smoke test, all run natively and interpreted bysspur run tests/run.ssp; a zstd backup tool; a systemd unit and a Caddyfile; and a README covering running, testing, debugging with lldb, configuration, backups and deploying.sspur new --listlists the templates, and the new tutorial “Build a web app” walks through the code.- A
svccan take parameters: withsvc api(app: App),http.serve(port, api(app))serves it and every endpoint parameter namedappgets the value, so handlers reach a database, keys and settings without globals;api(app)is aRequest -> Responsefunction that tests can call directly.sspur deployrejects such a svc (E_EP_PARAM). json.decode[T],r.json[T]and svc request bodies accept types withwhererefinements: the value is decoded, every clause checked, and one that fails is anerr(a 400 for a body) naming the field and the clause,body.title: must satisfy _.len <= 200. Records, aliases, newtypes, lists andOptare covered; refined sums and generic types areE_JSON.config.loadreadsSecret[Str]fields, and a field that breaks itswhereclause makes it returnerr("port: must satisfy ...")instead of trapping.req.form(name)reads a field of a form body andreq.cookie(name)a cookie.proc.spawn(prog, args, env)starts a program without waiting for it, in both tiers; theProcit returns hasline(timeout_ms)for the next line of its output,running(),stop()(SIGTERM, then SIGKILL after 10 seconds) andwait(), and processes still running when the program ends are killed. This is what lets a program test its own server end to end.- With
SSPUR_LOG=json, a message that is a JSON object (log.info(json.encode(event))) puts its fields in the line instead ofmsg, and access log lines carrymethod,path,status,msandidas fields. - Path dependencies inside the package’s own directory follow their source: an edit to such a module package takes effect at the next build, which locks it again, while dependencies from outside the directory stay pinned until
sspur deps update.
Changed
- svc endpoints may return and take any type
http.servecan encode and decode, generic records such asPage[Note]andTimeincluded, instead of only the types a deployed service supported. Timevalues bound as SQL parameters are ISO 8601 text with milliseconds (2026-10-15T09:30:00.000Z), so text order in SQL is time order.- Commands run in a package without
srcin its manifest uselib.ssp, as dependents do, beforemain.ssp. - The playground, which can’t serve, leaves out svc parameters, refined JSON decoding and the interpreter’s typed
catchbelow, so its WebAssembly stays at 972 KiB gzipped.
Fixed
- In the interpreter a
catchwhose arms name constructors of one error type also caught other error types in its_arm, unlike native code (the playground still does). A svc endpoint that raised its own error type next toDbErranswered 500 interpreted; it now answers as in native code. - Debug builds of programs that elaboration rewrites (networking, the data libraries, traits) mapped their lines to the rewritten program rather than the file, so with comments or other formatting above a function a breakpoint by line stopped in the wrong place. Lines now map to the file.
0.11.0 - 2026-10-15
Added
Bytes, an immutable byte string:bytes(xs),s.to_bytes,b.len,get,slice,concat,to_list,to_str,hex,base64andbase64url,from_hex,from_base64andfrom_base64url, andread_binandwrite_binfor binary files. It compares byte by byte, shows as0xand hex, and is a base64 string in JSON. In the interpreter its memory is wiped when freed.- SQLite (ADR 0037), vendored as its amalgamation and compiled into the cache on first use, behind a new
dbeffect:db.open(path)(or":memory:"),db.exec(c, sql, params),db.query[T],db.one[T]anddb.opt[T], which map columns to a record’s fields by name (or to a tuple or one column type) and raiseMismatchwith the column and the reason when a value doesn’t fit,db.tx(c, f), which commits or rolls back whenfraises (savepoints when nested), anddb.migrate(c, steps), which applies ordered SQL steps and refuses applied steps that changed. Values reach SQL only through?parameters; a list literal of parameters may mix types. Errors areDbErr(Sql,Constraint,Busy,NoRow,Mismatch). Files use WAL,synchronous=NORMAL, foreign keys and a 5 second busy timeout; prepared statements are cached per connection; a database is a pool of connections that the HTTP server’s concurrent handlers share, with transactions queued for the write lock, and opening the same file again returns the same pool.http.serveanswers a raisedDbErrwith 404, 409, 503 or a 500 without SQL text. - Crypto (ADR 0037), from Monocypher and libsodium, compiled into the cache on first use:
crypto.sha256,sha512andblake2b,hmac256andhmac512,equal(constant time),sealandopen(XChaCha20-Poly1305 with a random nonce),hash_passwordandverify_password(Argon2id at OWASP’s parameters, PHC strings),keypair,public_key,signandverify(Ed25519), andrandom,tokenandkey, withuuid.v4()anduuid.v7(). Randomness comes from the operating system behind a newrandeffect; keys and passwords areSecretvalues, and a plain value is accepted where one is expected. Known-answer vectors from NIST and the RFCs are tested in both tiers. - Compression:
gzip,deflateandzstdwithcompress,compress_levelanddecompress(aRes, capped at 1 GiB), andcompress_fileanddecompress_filefor gzip and zstd, which stream with constant memory and never leave a partial output file. - CSV:
csv.parseandcsv.write(RFC 4180, with CRLF output),csv.decode[T]andcsv.read[T]for records by header name with errors that name the row and column,csv.fold[T]to stream a large file, andcsv.encodefor records. - The
\rstring escape. - Handbook chapters Databases, Security and Crypto, and Files, Compression and CSV; a reference page for the four libraries; ADR 0037; and
E_SQL_PARAM,E_SQL_ROW,E_CSV_TYPEandE_DB_SHADOW.
Changed
- The HTTP server’s gzip encoding uses miniz (level 1) instead of a hand-written encoder; responses are as before.
sspur fuzzskips the functions of the network and library preludes.- The playground leaves out
Bytesand the four libraries, which need the C runtimes, so its WebAssembly file stays at 972 KiB gzipped.
0.10.0 - 2026-10-15
Added
- Source-level debugging with lldb and gdb (ADR 0036).
sspur build --debug,run --debug,test --debugorSSPUR_DEBUG=1build native code with DWARF (CodeView on Windows) at-O0whose lines are the.ssplines: breakpoints byprog.ssp:12and by function name work, the backtrace shows SSPUR functions and lines, and parameters and locals have their SSPUR names (acc, notv_acc_$6), with the generated temporaries hidden.sspur debug prog.ssp [args]builds and starts lldb (macOS) or gdb (Linux) with breakpoints onmainand onsspur_trap, which every trap and contract failure calls.sspur debug-initwrites lldb and gdb formatters that showStras text,Boolastrueorfalse,Listelements,Optassome(3)ornoneand sum values asCircle{r: 2}, plus asspur-localscommand. A codebase’s projected source is written to the cache so the debugger can show it. Normal builds are unchanged: the same generated C and the same speed. The new Debugging page covers lldb, gdb, VS Code with CodeLLDB and Windows, and the VS Code extension allows breakpoints in.sspfiles. - Networking (ADR 0035): an HTTP client and an HTTP/1.1 server with TLS, behind a new
neteffect, the same in native code and the interpreter.http.get(url),http.post(url, body)andhttp.send(req)returnRes[Response, HttpErr](Dns,Connect,Tls,Timeout,HttpStatus,Io);http.fetch(url)returns the body and raises for anything but a 2xx;http.req(method, url)gives aRequestwhose timeout, retries (with backoff andRetry-After) and redirectswithchanges;http.download(req, path)streams to a file;r.header(name)andr.json[T]read responses. Certificates are verified by default (SSL_CERT_FILEadds roots), proxies come from the environment and gzip is decoded. The client is libcurl on macOS and Linux and WinHTTP on Windows. http.serve(port, handler)andhttp.serve_with(server, handler)serve a function fromRequesttoResponse, or asvc, whose endpoints now also run in-process: path, query (GET and DELETE) and JSON body parameters bind as in a deployed service, and results and raised errors map to the same statuses. TheServerrecord (http.server(port)andwith) sets the host (127.0.0.1by default), worker threads, body limit, timeouts, CORS origins, gzip, static files, a health path, TLS certificate and key (OpenSSL 3),max_requestsand the access log. Every request gets an id, connections are kept alive, and SIGINT or SIGTERM shuts down gracefully.http.text,http.htmlandhttp.json(status, v)build responses;req.path,req.query(name),req.header(name)andreq.json[T]read requests. Native handlers run on a thread pool, and the collector runs while none of them is running. A natively served 200-byte answer takes about 28,000 requests a second at concurrency 64 on an Apple M1 Pro.config.load(defaults, path, prefix)loads a record from a JSON file andPREFIX_FIELDenvironment variables, falling back to the defaults, withwhereclauses as validation.log.debug,log.info,log.warnandlog.errorwrite levelled lines;SSPUR_LOG_LEVELdrops lower levels andSSPUR_LOG=jsonprints each line as a JSON object with time, level and message.- The handbook has a Networking chapter, and the reference a Networking page.
Changed
svcendpoints may take aRequestor return aResponse, bind GET and DELETE parameters from the query string, and perform any effect butdb.*and user-defined ones when served withhttp.serve;sspur deploystill accepts only what Lambda supports and says so (E_EP_EFFECT,E_EP_BODY,E_EP_TYPE).- SSPUR Pulse fetches with the HTTP client instead of running
curl; its pages are unchanged except for the sentence that said so. - The playground’s WebAssembly file is 972 KiB gzipped, below 0.9.0’s 982 KiB (networking had taken it to 1,025 KiB): it leaves out the generator of svc dispatchers, since the playground can’t serve, keeps one copy of the builtin signature tables instead of two, and is built at opt-level 2, which also runs slightly fewer instructions.
- On macOS
sspuris linked with chained fixups, so the dynamic linker no longer writes every pointer in the binary at launch: the cachedsspur runof hello world peaks at 2.45 MiB instead of 2.56 MiB.
0.9.0 - 2026-10-14
Added
- SSPUR Pulse (
pulse/), a live dashboard of the world today at/pulse/on the docs site: eight boards (Wikipedia reading, space launches with a globe of the satellites in orbit, earthquakes, rising GitHub repos, a Hacker News time machine, AI papers, world weather, public holidays) fetched, checked and rendered by SSPUR programs every 6 hours by the Pages workflow, with snapshots kept on apulse-databranch. Every page works without JavaScript; a small script adds chart toggles, tooltips, keyboard reading, the weather where you are (from your IP or, if you allow it, your device) and a few hidden secrets. Each board falls back to its last good data when a fetch fails and publishes JSON; the quakes, papers and reading boards publish RSS, the launches and holidays boards ICS calendars. The docs page “SSPUR Pulse: how it works” describes the design, and CI runs the Pulse tests on bundled fixtures.
Changed
- The docs, README and benchmark pages are current for 0.8 and 0.9: native speed and memory, the large-codebase agent results, Pulse,
while e is p, the cached run and the Zed extension.
Fixed
- Native
json.decodeallocated every decoded string with room for the rest of the input, so decoding a large array was quadratic in memory: 1.3 MB of 20,000 small records hit the 8 GB heap ceiling and stopped with “out of memory”. Each string now gets room for its own source text. The same decode takes 0.02s and 19 MB of heap (the interpreter 0.06s), and Pulse’s section programs run natively again. g.OBJECT_NAME, a field whose name starts with a capital, parsed as the package-qualified nameg.OBJECT_NAMEand failed withunknown name, so records decoded from JSON with upper-case keys could only be read by destructuring.x.NAMEin an expression is now a field access, andpkg.Namestill resolves to the package’s name whenpkgis a dependency and not a local.
0.8.2 - 2026-10-14
Added
- Native code frees list buffers and strings at their last use when the compiler can prove they have one owner (ADR 0033), in front of the collector, which still reclaims everything else. Locals and temporaries that are only read are freed after the last read, a
varfrees its old value when it gets a new one, a parameter a function only reads is borrowed so the caller frees a fresh argument after the call, a list of strings built from fresh strings frees them with it, aMap.putthat takes an owned key frees it when the key was already there, andvar v = []that is only pushed onto and then joined is built as a string directly. Freed blocks are reused by the next allocation of their size. Peak memory, 0.8.1 to 0.8.2 (C++):simd273 to 39 MB (40),churn268 to 7 MB,strings_big92 to 29 MB (23),app137 to 22 MB (24, with the fused fold below),gpu344 to 97 MB (54, with the GPU changes below); every benchmark is as fast as before or faster (simd0.20s to 0.18s,churn0.08s to 0.03s). sspur run file.sspof a program that hasn’t changed runs the cached native library’smaindirectly, without parsing, checking or generating code (ADR 0034). The run is keyed by the source, thesspurbinary, theSSPUR_*variables and the compiler’s environment, checks the cached library’s contents, and falls back to the normal path on any difference, inside a package, with any flag (--no-cacheforces it) or when the program has warnings to print. A trap or raise inmainis described by the normal path, so the output and exit code are the same. Hello world takes 10.3 ms instead of 12.7 ms (C++ 8.0 ms), each benchmark 3 to 11 ms less, and peak memory is 4 to 5 MB lower;compute_bigis within 1% of C++ (1.02s against 1.01s).SSPUR_MEM=gcturns the frees off (the 0.8.1 behavior) andSSPUR_MEM=checkpoisons freed blocks and stops on a double free.SSPUR_GC_STATS=1also reports the blocks freed at last use and the blocks left for the collector.SSPUR_GC_MIN_MBandSSPUR_GC_FACTORtune when the collector runs (defaults 256 and 4).SSPUR_CFLAGSadds C compiler flags to native builds and their cache key, for example-fsanitize=address,undefined.
Changed
- The interval analysis of ADR 0007 sees locals again. Since per-definition translation units, local names contain a
$and were ignored, solet,varandforfacts, and the proofs of a callee’s contract from computed arguments, never applied; the in-place map update after a lookup of the same key (ADR 0008) had the same problem.appis 14% faster. a / banda % bwithaproven non-negative andbproven positive compile to unsigned division, and to a shift or a mask for a power of two.- A small
ifoverIntwhose arms have overflow checks compiles to a select of both arms, computed with wrapping arithmetic and exact overflow flags, instead of a branch; the chosen arm traps exactly as before. With the two changes above,compute_bigtakes 0.91s instead of 1.02s (C++ 1.02s),compute14% less,parallel7% andsimd5%. - Native libraries are linked with dead stripping on macOS, which drops the runtime functions a program never reaches and the libc imports only they need.
sspur runof an unchanged hello world takes 9.6 ms instead of 10.6 ms. xs.map(f).fold(init, g)andxs.filter(p).fold(init, g)run as one native loop, over blocks of 64 elements, whenghas no effects and captures nothing: the mapped list is never built. A trap ingis held untilfhas run on every element, so the program fails exactly as the unfused order does.apppeaks at 22 MB instead of 41 MB (C++ 24 MB) and is about 2% faster.dev_f32((a..b).map(f))and the otherdev_*constructors of a mapped range write each value straight into the device buffer instead of building a host list first, andd.to_list.sumsums the buffer without building the list. The GPU launch queue no longer clears each new mask of flagged threads (Metal zero-fills new buffers), which kept 512 KB resident per queued launch.gpupeaks at 97 MB instead of 178 MB and is about 7% faster; what remains is the device buffers, the host copies that make CPU reruns exact, and Metal.- On macOS, native code uses
_setjmpand_longjmp, which don’t save the signal mask with a system call, so each call from the interpreter into native code is about 0.3 microseconds cheaper. text.lower.words.counts, written without parentheses, now counts while it scans, as ADR 0008 describes, instead of building the whole word list first; non-ASCII text is detected during the scan instead of in a pass before it.- Interpolation and string building are about 7% faster: a string builder’s growth is in a separate cold function, and it frees the buffer it outgrew.
- The runtime frees its own temporaries: sort buffers,
sort_bykey pairs andcountshash tables.
Fixed
spliton a one-byte separator allocated a buffer and threw it away.- The allocator’s fast path did pointer arithmetic on a null cursor, which UndefinedBehaviorSanitizer reports.
0.8.1 - 2026-10-13
Added
- Five more large-codebase tasks in the agent benchmark (s2 to s6), on the shop codebase and two new generated codebases of about 1,000 definitions (library and fleet), in SSPUR, Python, TypeScript and Go, with hidden tests and reference solutions.
bench/agent/large/sets them up and scores them, andreport5.py --largereports them. - Benchmark runs L1 to L3 on the six large tasks. Sonnet 5.5 used 0.68x Python’s tokens, 0.59x TypeScript’s and 0.58x Go’s; Opus 5.5 0.85x TypeScript’s and Haiku 5.5 0.68x, cheaper than TypeScript on every task for all three models, with every hidden test passing.
0.8.0 - 2026-10-13
Added
-
while e is ploops:while q.pop_front is some((x, rest)) and x > 0bindsxandrestin the body, matches again before each pass and stops when the pattern doesn’t match. Tests join withandas they do afterif, and the interpreter and native code run it the same way. A pattern that always matches warnsW_ARM_UNREACHABLE, and the body must beUnitlike anywhilebody. -
A Zed extension in
packaging/zed/, attached to every release assspur-zed-vX.Y.Z.zip(and inSHA256SUMS) so it installs with zed: install dev extension and no registry: a Tree-sitter grammar for SSPUR (packaging/zed/tree-sitter-sspur/) for highlighting, brackets, indentation and the outline offn,type,test,traitandimpldefinitions, andsspur lspfound onPATHor set in Zed’s settings. The grammar reads every.sspfile in the repository without an error node.packaging/zed/package.shpins the grammar to a commit, the release’s or a local checkout’s (file://).
Fixed
- A signature with more than one effect clause,
fn calc(s: Str) -> Int ! fail[CalcErr] ! div, failed withE_PARSE_EXPECTED expected '=', found '!'. It is now accepted (also in function types) and stored as! fail[CalcErr], div, with the notestored as: ! a ! b -> ! a, b. sspur editwithremove subtotaland a newfn grosswhose text is subtotal’s with only the name changed was rejected withE_UNKNOWN_NAMEwhenever a stored definition outside the edit still calledsubtotal. The pair is now read asrename subtotal gross(plus a replace ofgrosswith the text given), so stored callers follow it and the output showssubtotal->gross, as for an explicit rename. It pairs only when the removed definition has exactly one such new definition and that definition matches no other removal; several pairs in one edit (a function and its test) are matched together. When nothing pairs, theE_UNKNOWN_NAMEerror for a caller of a removed name now has a hint that names the caller and suggestsrename X NEWor updating or removing the caller.- Recursion in the playground went only about 100 calls deep in Safari, 500 in Chrome and 1,000 in Firefox before the page reported a stack overflow. It now goes 20,000 calls deep in all three, as
sspur run --interpdoes, and the call after that is the samestack overflow in ferror. The build runs Binaryen’s asyncify pass, so once the interpreter’s frames fill a segment of the browser’s stack (measured when the page starts, smallest in Safari), the next call saves the frames under it to memory and runs on an empty stack. The interpreter’s frames are smaller and the playground’s stack in WebAssembly memory is 32 MiB instead of 16, so calls with large bodies or through lambdas reach 10,000 to 15,000 before the same error. The WebAssembly file is 37 KiB larger gzipped (981 KiB), loops run about 15% slower in the playground and calls about 10% faster. A build withoutwasm-optbehaves as before. - A string written with escaped quotes inside an interpolation,
"cycle {path.join(\" -> \")}", printed the braces as text; it now means the same as"cycle {path.join(" -> ")}". - An arm of
catch doormatch ... dowritten at the block’s own indent (catch do, the lines, then| _ => falseunder them) was read as a statement and failed withE_PARSE_EXPR; a|line now ends the block and starts the arms. - A test written
catch e | Err => false, which expectseto finish without an error, was stored ascatch do (_ = e) false | ..and so always failed; when every arm isfalseit is now stored ascatch do (_ = e) true | .., and a failing run shows the error that was raised.
Changed
- Hole filling reaches longer predicates: a lambda for
filter,any,allorfindcan now have up to 5 nodes, as inxs.filter(_ % 2 == 0), where other lambda bodies still stop at 3. The longer bodies are only built once a candidate has room for them, and only from parts that can still end in aBool, so holes that settle on something small cost the same as before. An equality or comparison with a literal is written with the literal on the right, andx - -ycounts as unlikely. The benchmark goes from 41 to 42 of 51 at 5000 ms (evens); at 200 ms it stays at 41, because that case needs about 400 ms. - A parse error at a
,right after a)that closes a parenthesized record update with several fields, as inthen (g with rooms := rs, inventory := inv), "Taken."), now has the hint that the)closes more than it opens and that a tuple holding a record update is written((r with a := x, b := y), other). Before,E_PARSE_TRAILING unexpected ',' after definitiongave no clue that the tuple was one(short. - The core agent spec is 584 tokens again (0.7.1: 698): the pattern line is shorter, and block lambdas and traits are in
sspur spec --moreonly.
0.7.1 - 2026-10-12
Fixed
- A binding broken after its
=((_, total) =with the value on the next line, which pressing Enter in an editor produces) failed withE_PARSE_EXPR; a line ending in=now continues on the next line. - A sum type can start with
|, so the variants can each go on their own line undertype S =.
0.7.0 - 2026-10-12
Added
e is pis aBoolexpression anywhere (ADR 0032):xs.filter(_ is some(_)),ok = r is ok(_),test t = parse("x") is err(_),o is some(v) and v > 0. The names a test binds are in scope in theand ...after it, and anifcondition can join several tests,if a is Circle{r} and b is Circle{r: r2} then r + r2 else 0.0, with the names of all of them in thethenbranch. A test that binds a name nothing can use isE_IS_BINDS, and one whose pattern can’t match the value written there (none is some(_)) warnsW_NEVER_MATCHES.iskeeps the precedence it had afterif: it takes everything before it, soa and b is pis stillE_PARSE_IS, and so is a test used as an operand without parentheses,e is p == x.- Guard clauses (ADR 0032):
some(user) = find(id) else raise NotFound{id}in a block binds the pattern for the rest of the block, or ends the block with theelsevalue when it doesn’t match, soelse 0returns 0 from a function body,else ()skips to the next element in a loop, andelse return err("bad")works too. Rust’slet p = e else { ... }and Swift’sguard let x = e else { ... }are stored in this form. Record destructuring,Point{x, y} = p, now works outsideprofile sysas well, andE_REFUTABLE_LETsuggests addingelse. - Hole filling proposes
s is Circle(a variant of a sum) andr is ok(_)forBoolholes; the benchmark stays at 41 of 51. - Long lines (ADR 0032): a line that ends in a binary operator (
and,+,==,,, …) continues on the next line, and so does a deeper-indented line that starts with.nameor an operator, so= xsthen.map(_ * 2)and.sumon their own lines is one expression.sspur fmtjoins them back into one line. - Pressing Enter after a line that ends in an operator, an open bracket,
=>,=ordoindents the next line one level, in the playground, in VS Code (onEnterRules) and through the language server (textDocument/onTypeFormatting, which LSP4IJ uses in JetBrains IDEs). A.methodchain stays aligned, and the line after a continued one goes back to where its statement started. sspur lsp, a language server over stdio (ADR 0031). It publishes the checker’s diagnostics as you type (checked off the main loop, a short pause after the last edit), then the candidates for each typed hole, and answers completion with signatures and the standard library’s docs, hover, code actions with the compiler’s fixes and the hole fills, formatting, go to definition, references, document highlights, rename, the outline and semantic tokens in the playground’s classes. The commandsspur.fillfills every hole that has a passing candidate. A file in a package is checked with its dependencies, and their names show aspkg.name;pkg.completes their public definitions.- Signature help in the language server and the playground: after
(or,the signature of the call shows with the argument being typed highlighted and its one-line doc, for the file’s functions (the comment above one is its doc), effect operations, builtins, methods on a typed receiver andpkg.f, also while the call is half-typed and the file doesn’t parse. - Inlay hints in the language server, and in the playground behind a toggle: the inferred type after
let, guard,forandvarbindings whose type isn’t written (total = items.map(_.price * _.qty).sumshowstotal: Int,some(user) = find(id) else ...showsuser: User) and after lambda parameters, and the parameter name before a literal argument (scale(3, total, true)showsn: 3, strict: true). Literals and records built by name get none.initializationOptions.inlayHintsor thesspur.inlayHintssetting turn each kind off, and the VS Code extension has a setting for each. - A VS Code extension in
packaging/vscode/, attached to every release assspur-vX.Y.Z.vsixso it installs with no marketplace account: a TextMate grammar that highlights like the playground, the language configuration, the language server, and the commands Run File, Test File and Fill Holes. It builds into a.vsixthat also installs in Cursor, Windsurf and VSCodium. - A template for LSP4IJ in
packaging/jetbrains/, attached to every release assspur-lsp4ij-vX.Y.Z.zip: in a JetBrains IDE with LSP4IJ, Import template… sets up the server in one step, with no account. - Editor setup: VS Code and its forks, Neovim, Vim, Helix, Emacs, Sublime Text, JetBrains IDEs through LSP4IJ, Kate and Eclipse, and what Zed, Visual Studio and Xcode lack.
Changed
- Record destructuring,
Point{x, y} = p, works in every profile, not onlyprofile sys. - The editor logic behind the playground moved from
crates/sspur-wasminto a new crate,sspur-ide, which the language server uses too. - Hole filling has more time per hole: 400 ms in
sspur checkand the playground, 600 ms in editors, where it runs in the background. Since it began counting the other candidates that fit, a classic hole such as a sum of field products took about 270 ms and was missed at the old 150 to 200 ms.
Fixed
- The language server marks bindings and fields as read-only semantic tokens, so IntelliJ (through LSP4IJ) no longer underlines them as reassigned variables.
0.6.0 - 2026-10-11
Added
-
Hole filling (ADR 0030): for each typed hole,
sspur checklists up to three complete expressions that fit, one line each,?total: items.map(_.qty * _.price).sum (passes 3 tests, post), and--jsongives them asfillfixes withevidenceandok. They come from a bottom-up search over the values in scope, literals, fields, constructors, the program’s functions and trait methods, operators and the common methods of lists, strings, options and numbers (lambdas likemap(_.f * _.g)included), limited to 7 nodes and 200 ms a hole (--fill-ms N,--no-fill). Candidates only use effects the function declares, and run against the tests and examples that reach the hole and against small and fuzzed inputs that checkpostand a refined result type. Expressions that agree on every observed input count once, which keeps the search fast. Files without holes are checked exactly as before. With weak tests the ranking uses a prior over what agents mean (every unused parameter used, no constant offsets or vacuous patterns, apreor the empty input respected), constants from the tests, one-stepOptdefaults andwithupdates, and the evidence saysN others also fitwhen the tests don’t decide; on the 51 holes ofbench/fill/holes.txtthe top candidate is the intended one for 41, up from 26. -
sspur fill [file] [?hole|fn]prints the best candidate for each hole and--applywrites it into the file (in parentheses where precedence needs them) or applies it to the codebase; with z3 installed a candidate whosepostit proves sayspost proved. -
sspur suggest [file] [fn]proposes contracts from runs: thepostclauses from a fixed set (r >= 0,r.len <= xs.len,r == x, sorted output, comparisons with each parameter) that held on every test-derived, small and fuzzed input, minus implied ones, and apresuch asxs.len > 0that rules out the inputs it traps on. Lines are ready to paste, markedproved(z3) orheld on N runs;--jsongivesrefinefixes and--applyadds them. -
The MCP server has
fillandsuggesttools, and itscheckshows the hole candidates. The playground’s quick fix for a hole offers the checked candidates. -
The playground has an Input tab: Run passes its text to the program as stdin, for
read_lineandread_lines, and share links carry it. The From Python example reads its words from there. -
SSPUR_FILL_MSsets the time hole filling may spend per hole, forcheck,filland the MCP tools.
Changed
- The playground was redesigned for phones and desktops: a menu on phones, icons on every action, an examples menu that opens below its button, a copied-link toast instead of the link box, a collapsible output panel on phones, a resizable split on desktops, no layout shift while a program runs, no zoom when the editor is focused, and a creator credit. The docs header links to it.
Fixed
- Concurrent native builds on Windows could fail to link (LNK1104) while another build published the same object file; outputs are now published without replacing a file that may be open.
0.5.0 - 2026-10-11
Added
- Or-patterns (ADR 0029):
| Circle{r} | Ring{r} => r, nested anywhere (some(0 | 1),(Red | Blue, _)); every alternative binds the same names with the same types, or it isE_PATTERN_OR_BINDSwith a hint naming the name or the two types. Rule cells accept them too. - List patterns:
[],[x],[x, y],[x, ..rest],[..init, last],[first, .., last],[_, ..], with patterns as elements ([ok(x), ..],[(a, b), ..rest],["GET", path]).restis aList; in native code it is a view of the matched list, so binding it allocates nothing.[x, ...rest]and[x, *rest]are stored as[x, ..rest]. if e is p then a else btests one pattern, with an optional guard (if o is some(v) and v > 0 then), an optionalelsewhen the branch isUnit, andelse if e2 is qchains.if let p = e thenis accepted and stored asif e is p then(noteif let p = e -> if e is p).E_PARSE_ISreportsif a and b is p, with a hint.- Exhaustiveness checking looks inside nested patterns, or-patterns and lists (lengths
[]plus[x, ..rest]cover every list), and lists the missing cases as patterns (some(none),[_, _, ..],(false, false)). The new warningW_ARM_UNREACHABLEreports amatchorcatcharm, or an or-pattern alternative, that the arms above already cover, and anif e is pwhose pattern always matches. - All of it runs in the interpreter and in native code (strict native compiles every function), is printed canonically and hashed; the program generator emits list patterns, or-patterns and
if e is pfor differential fuzzing. - A playground at utkarshavardhana.github.io/sspur/play runs the checker, formatter and interpreter in the browser through WebAssembly (
crates/sspur-wasm), in Web Workers with a Stop button. It checks as you type, offers the checker’s fix hints as quick fixes, completes names, fields and methods with their signatures, shows types on hover, and shares programs in the link. Files live in memory,envis empty, andproc,extern fnand native code report that they aren’t available. The JIT, the C back end and FFI are behind thejitfeature ofsspur-nativeand thenativefeature ofsspur-eval, on by default, so the interpreter builds forwasm32-unknown-unknown. - The docs header links to the playground, and every SSPUR example in the docs has a Run button that opens it, or the whole snippet file it comes from, in the playground.
- A logo, used in the README, the docs, the favicon and the playground (
docs/assets/logo/). - A
NOTICEfile with authorship and the project name, included in every release archive.
0.4.1 - 2026-10-10
Added
- Windows support (x86_64, and arm64 release builds). Native code compiles with LLVM’s
clanginto a DLL that sspur loads: the runtime’s GC heap reserves and commits withVirtualAlloc/VirtualFree,parand the parallel pool run on Win32 threads with SRW locks and condition variables, files go through the C runtime with UTF-8 paths, andrun_cmdusesCreateProcessW. The Windows C is produced by rewriting the POSIX runtime, so the C generated on macOS and Linux is byte-identical to before. The interpreter’sexterncalls follow the Win64 calling convention,export-cwrites a.lib(or a.dlland its import library) plus the header,bindmapslongto 32 bits, andsspur deploy localbuilds its host with a small Winsock HTTP client instead of libcurl. The cache is%LOCALAPPDATA%\sspur. - Releases ship
sspur-vX.Y.Z-x86_64-pc-windows-msvc.zipandaarch64-pc-windows-msvc.zipwith their checksums inSHA256SUMS, aninstall.ps1(irm https://raw.githubusercontent.com/utkarshavardhana/sspur/main/install.ps1 | iex) and a Scoop manifest; CI runs the build, strict native tests, clippy and the corpus check onwindows-latest.
0.4.0 - 2026-10-10
Added
- Traits (ADR 0027):
trait Shapewith indented method signatures (= eis a default body) andimpl Shape for Circlewith the methods, called asc.area,c.area()orarea(c); generic implsimpl[T: Show] Show for Box[T]; a trait method’s effect row bounds its impls. Bounds on type parameters,fn max_of[T: Ord](xs: List[T])and[T: Eq + Show], are checked where the function is defined, and a call with a type that lacks an impl is an error at the call site that names the impl and how to add it. Operators are trait methods for user types:Add Sub Mul Div(+ - * /),Neg(unary-),Eq(== !=),Ord(< <= > >=throughcmp) andIndex[K, V](x[k]); built-in types keep their semantics and overflow traps.type P = {..} derive Eq, Ord, Show, Hash, Jsonadds structural impls. Coherence is one impl per (trait, type), and an impl lives in its trait’s or its type’s package;pub traitandpub implexport them. Resolution is static: trait calls become calls of the impl, and bounded functions are specialized per type (max_of__Int) for the interpreter and native code alike, so the tiers agree, with every function native at-O2and-O3. New errors:E_TRAIT_UNKNOWN E_TRAIT_ARGS E_TRAIT_PARAMS E_TRAIT_SELF E_TRAIT_MISSING E_TRAIT_AMBIGUOUS E_TRAIT_RECURSION E_IMPL_TARGET E_IMPL_ORPHAN E_IMPL_DUP E_IMPL_BUILTIN E_IMPL_EXTRA E_IMPL_MISSING E_IMPL_SIG E_IMPL_EFFECT E_DERIVE_UNKNOWN E_DERIVE_FIELD E_SELF E_PARSE_IMPL, each with a fix hint;E_OPERATORon a user type now suggests the impl or the derive. Codebases store impls asimpl Trait for Type(remove impl Trait for Type). Suite programstraits,constraints,operatorsandderive; the program generator emits traits, impls and bounded generics. - Block lambdas (ADR 0028): a lambda body can be
doand a block indented past the line the lambda starts on,xs.map(x => do+ lines +y), with the existing captures (a capturedvaris shared), effects and both tiers; pure scalar ones fuse and vectorize inmap/filter/sumpipelines like one-line lambdas. A)on its own line,x =>withoutdo,x => {+ block +}and a finalreturn eare accepted and stored canonically; an earlyreturninside a lambda staysE_RETURN_IN_LAMBDA, now with a hint. The formatter prints a lambda with a multi-line body asx => doand an indented block - The agent benchmark covers TypeScript and Go: all 17 tasks are ported with matching hidden tests (
bench/agent/tasks), so runs compare SSPUR with Python, TypeScript and Go under one harness. - Trait methods as values:
xs.map(area),xs.fold(0, add)andxs.sort_by(area)resolve statically to the impl for the parameter type, in both tiers; where that type is not known yet the error hint showsx => x.area - The program generator emits block lambdas (map, fold and fused range pipelines with captures, logs and match bodies)
Fixed
sspur export-clists each linker flag once (Linux printed-lmtwice).- Native builds no longer fail with
incomplete typewhen a list of tuples over a recursive sum type (for exampleList[(Str, J)]insideJ) is generated before the sum: the list struct is now emitted ahead of its element definitions. Such programs fell back to the interpreter before. Found in agent bench run 11.
Changed
- The repository tracks about half as many files: benchmark runs keep only their token and score summaries, the evaluation corpus is one
bench/eval/corpus.jsonl, the 58 suite programs are 15 files by area with all 544 tests, and reject and fuzz cases are one table file per suite. - Documentation site rebuilt along the lines of the TypeScript docs: a landing page of section cards, Get Started (five minutes, pages for TypeScript, Python, Go and Rust programmers, agents, installation), an eleven-page Handbook, Reference by area (standard library, effects, every error code with its fix, CLI, agent spec, JSON schemas), six Tutorials, Project Configuration, a Cheat Sheet, and Design and Performance. Every code block in the handbook and tutorials is a snippet file run by
tests/tutorial.rs, which also checks thatdocs/reference/errors.mdlists every diagnostic code in the compiler. - Repository layout:
docs/is the single mdBook source (the oldsite/merged into it, withdesign/,reference/,agent/,adr/andsnippets/folders); the full reference thatsspur spec --fullprints isdocs/reference/language.md; the JSON schemas moved todocs/reference/schema/and now describe whatapplyaccepts andcheck --jsonprints; the Phase 1 token benchmark moved tobench/tokens/; the Claude Code plugin moved topackaging/claude-code/(the marketplace name and install commands are unchanged). sspur editstores spellings from other languages that have one meaning in their SSPUR form and reports them on astored as:line (at most three, thenand N more), instead of rejecting the edit:&& || !,elif,let,+=,Ctor{_}, typed locals,len(x),.length,.toLowerCaseand similar method names,None/Some/True,print,s.slice(a, b)onStr,.geton a value that is not anOpt,_in a call that takes no function,catchof a non-Boolvalue in a test, and effects a function performs but does not declare. Other entry points still reject them, with the canonical spelling in the message (N_FOREIGN). Definition keywords (queue,store,effect,pre,post, …) are identifiers elsewhere.- Failing tests print the values of the comparison that failed (
left 7, right 8), and a failedpreprints the parameters it mentions, in every tier. - The agent spec is a core of about 600 tokens (was 1,631), with one line on traits;
sspur spec --moreprints the rest, including the builtins and a Traits section. It recommends writing edits tochange.sspwith the agent’s file tool and runningsspur edit --test change.ssp. - Agent benchmark run 10: 0.72x Python’s tokens, 1.06x TypeScript’s and 0.96x Go’s on 17 cells with Sonnet 5.5 (run 9: 0.83x, 1.23x, 1.11x), 246/246 hidden tests.
- Agent benchmark run 11: 0.65x Python’s tokens, 0.95x TypeScript’s and 0.86x Go’s in total, 0.59x, 1.01x and 1.00x at the median, 58 API calls against 88, 61 and 66, 246/246 hidden tests: parity with TypeScript and Go.
0.3.2 - 2026-10-07
Fixed
- Deploy: CodeDeploy’s role grants
cloudwatch:DescribeAlarmson*. The action has no resource-level permissions, so the previous ARN list granted nothing and every canary failed on AWS (CloudFormation rolled back cleanly). Found by the first real canary run (ADR 0019). - Deploy:
rollback.shlists in-flight deployments per deployment group, as the CodeDeploy API requires.
Added
- Documentation site at https://utkarshavardhana.github.io/sspur/ (mdBook, published from
site/by.github/workflows/pages.yml): a 10-minute tutorial whose every snippet is a file checked bytests/tutorial.rs, a guide for AI agents, the language and library reference, benchmarks, and the design records. Ratio, exact rationals overBigInt:ratio(n, d),ratio_big(n, d),parse_ratio(s), always normalized, withadd sub mul div neg abs inv pow sign is_int floor ceil trunc round to_f64 to_dec, value ordering for<and sorting, and3/4display.to_f64is correctly rounded androundis half to even (ADR 0018 decision 35).Localevalues for en-US, en-GB, de-DE, fr-FR, ja-JP and hi-IN fromlocale(tag), passed explicitly:compareandsortwith UCA-style three-level collation (base letters, accents in DUCET mark order, case, then code points; NFC and NFD forms differ only at the last level), andformat_int,format_f64,format_dec,format_date,format_date_longandformat_timewith each locale’s separators, Indian digit grouping, date patterns, month names and am/pm markers. The rules are bundled in one Rust module that the C runtime’s tables are generated from, so no host or ICU data is read (decisions 37 and 38, which also list what is not full DUCET).copy_file,symlink,read_link,is_symlink,file_modeandset_modeunderfs, withsame file,not a symlinkandmode out of rangeerrors.tests/programs/std_collections.ssp(the valarray section):valarrayoperations (elementwise arithmetic, broadcast, reductions, masks, indirect arrays, shifts, strided slices) are fused view and list pipelines in both tiers, documented as covered rather than added as a new type (decision 36). ADR 0018’s coverage table is now 23 of 23 areas covered.F64inprofile bareon aarch64-qemu (the FPU is enabled on every core and the exception entries save the FP registers) and thumbv7em-mps2 (AEABI software double precision, checked bit for bit against the host FPU on 3,000,000 operand pairs).+ - * /, comparisons,Int.to_f64andabs sqrt floor ceil round trunc is_nan is_finite is_inf copysignare allowed; otherF64methods,%and**need libm and areE_PROFILE_BARE; riscv64-qemu has no FPU and fails at build time with a clear error.tests/bare/floats.sspprints the same 13 results on both boards as the host interpreter (ADR 0017 decisions 12 to 15).- Secondary cores on aarch64-qemu:
fn core_main(id: Int)runs on cores started withstart_core(id)(PSCICPU_ON),core_id()readsMPIDR_EL1, and statics use atomic instructions when a module hascore_main, withE_STATIC_RACEcovering core code.tests/bare/smp.sspadds from two cores and printstotal 200000 seen 3(decisions 16 and 17). QEMU runs aarch64 kernels with-smp 2. sspur start [NAME|PATTERN...]reads the spec and the code in one call: it prints the agent spec, then the codebase, all of it when the source is at most 12,000 bytes, otherwise the counts per kind,q packof the arguments that name a definition andq findof the others. The MCP server has a matchingstarttool (names: "a,b,c"), and its instructions tell the agent to call it first.sspur spec [--full] [src] [QUERY TARGET...]printssrcand queries after the spec (spec find 'a|b' pack A,B).q pack A,B,Cpacks several definitions in one call, each one with the types and signatures it uses, its tests and its callers, and prints every definition only once (a target that is also another’s caller is shown in full, not twice). This is what the agent benchmark’s large-codebase task needed: the five definitions it has to change cost one call of 540 tokens instead of three rounds of queries.
Changed
q packends with-- complete: every caller (N) and test (M) of NAME is abovewhen nothing was left out, so an agent does not spend a call re-checking callers (both run 8 s1_shop runs did).q findprints 25 signatures in full andq grep12 definitions, then the names of the rest on one line (-- 21 more: f11 f12 ...), so a broad pattern on a large codebase no longer fills the context with near-identical matches. Definitions whose name is exactly the pattern come first.--jsonis unchanged.- Agent benchmark run 7 (
bench/agent/README.md): the large-codebase task went from 1.33x to 1.04x of Python’s total tokens over two Sonnet 5.5 runs, 5 API calls instead of 6, still 10/10 hidden tests. The token attribution of the old run is in the README: one extra call was 57% of the gap, the spec 23% and broad query answers 21%. a1 and a7 were rerun unchanged (0.76x, 0.61x, 3 calls each). - The agent spec (
docs/agent/agent-spec.md) is 1,631 cl100k tokens, from 1,798: concurrency, services, packages and C, sys, bare and GPU, which no agent used in the 60 SSPUR transcripts of benchmark runs 4 to 7, are now one line that names their keywords and points tospec --full. The CLI line namesq pack. - The MCP server’s instructions are 825 characters and point to
start; they do not carry the spec, because Claude Code cuts server instructions at 2,048 characters. The Claude Code skill starts withsspur start NAME.... - Agent benchmark run 8: with
sspur start NAME...as the first step, the large-codebase task is 0.82x of Python’s total tokens over two Sonnet 5.5 runs (1.04x in run 7), 4 API calls against Python’s 5, 10/10 hidden tests. a1, a4 and a7 stay at 3 calls and 0.76x, 0.60x and 0.61x with every hidden test passing. The harness’s SSPUR prompts start with./sspur start(setup.py --r7andsetup_scale.py --r7give the old ones).
0.3.1 - 2026-10-06
Fixed
- Concurrent native builds of the same program could fail with
No such file or directory: the GPU runtime shim, the whole-program build and the OpenCL emit used fixed temporary file names in the shared cache, so one process renamed or deleted another’s files. Every build now uses per-process, per-build temporary names. This also caused the intermittent macOS CI failure inkernel_traps_are_identical_in_every_tier.
0.3.0 - 2026-10-06
Added
- Packages (ADR 0026).
sspur.tomlnames a package and its dependencies (a local path, or a git URL with a tag or revision);sspur.lockpins each one by the hash of its exports.pub fn,pub typeandpub effectexport definitions; dependents calllib.f(x), namelib.Tandlib.Ctor, or import withuse lib.{f, T}(a type brings its variants). Private names, unknown names and undeclared packages are diagnostics (E_PKG_PRIVATE,E_PKG_NAME,E_PKG_UNKNOWN). sspur add <path|git-url>[@rev],sspur deps fetch|update|tree, andsspur init --pkg NAME. Fetches are verified: a dependency whose content hash differs from the lock fails withE_DEP_HASH. Dependencies are cached by hash in~/.cache/sspur/pkgs.sspur deps updateprints a semantic diff of each dependency’s exports (signatures, effects, contracts, body-only changes, added and removed names), typechecks the dependent against the new versions, and refuses the upgrade with the exact diagnostics unless--forceis given.check,run,test,native,verifyandfuzz --differentialwork across packages. Dependencies’ effects appear in dependents’ signatures and their contracts are proved at the dependent’s call sites. Native objects of dependency definitions are shared between dependents through the per-definition cache.- Codebase mode:
editacceptsuselines and refuses to touch dependency code (E_DEP_READONLY),q list libandq sig lib.fshow a dependency’s exports, andsync push|pullcarries the dependencies with the commits, verified by hash. - Queries for large codebases:
q find 'a|b*|^c'lists definitions whose names match (substring,*glob,^/$anchors), with their signatures (exact names first, at most 60; a pattern with->,[or(is still a type-shape search),q grep TEXTlists the definitions whose source contains TEXT with up to 3 matching lines each, andq body A,B(orq body A B) prints several bodies. On the 1,117-definitions1_shopcodebase, locating the task’s functions and the callers ofship_feetakes 126 tokens of output withq findandq grep, against 1,875 for theq list | grepthe agent used before. - MCP:
sspur mcp --dir PATH(orSSPUR_DIR) serves a codebase outside the working directory, as Claude Desktop needs; the store is created on the first edit instead of at startup.docs/agent/mcp.mdcovers Claude Code, Claude Desktop and generic stdio clients, with a project.mcp.json. - A Claude Code plugin in
packaging/claude-code/with ansspurskill (the spec, query, oneedit --test,verify,deploy localworkflow) and the MCP server, listed by.claude-plugin/marketplace.jsonsoclaude plugin marketplace add utkarshavardhana/sspurinstalls it. - Fix hints for syntax and names from other languages, in
edit,checkand transactions:&& || !,x -> eand|x|lambdas,elif,let,+=,?:, braces after a signature, effect rows without commas,Ctor{_}andCtor{..}patterns, named arguments, unknown string escapes (regex\dis"\\d"),len(x)and other methods called as functions,None/Some/True,assert,print, foreign method names (head,max_by,to_string,compare, …), a record’s fields on an unknown field,Str + Int,Optwhere a value is expected, a missing return type, and a nestedmatchthat took the outer arms. - A rejected
editreports the syntax errors of every definition, not just the first, and ends withfix these and resend the whole edit in one call. - Agent benchmark run 6 (
bench/agent/README.md): with the fix hints, Haiku 4.5’s three worst SSPUR cells went from 57 calls to 30, and its 8-task total from 1.21x to 0.83x of Python’s. The large-codebase task stayed at 1.33x with Sonnet (6 calls both times).
Changed
q listgroups definitions by kind with counts (# fns 891). Tests are named on one line only when there are at most 20; otherwiseq list tests(or--tests) lists them one per line, soq list | grep WORDno longer returns every test name.q packwith more than 8 callers includes 3 tests, 3 callers in full and 5 as signatures, then-- not shown: N callers.pack moneyons1_shopis 178 tokens instead of 1,174.- MCP tools are named
spec,src,query,edit,test,check,run,fuzzandapply, with descriptions written for agents and read-only annotations; the oldsspur_*names still work.runandfuzzreturn text instead of JSON,checksetsisErrorwhen there are errors, andresources/listandprompts/listreturn empty lists. - The
E_NONEXHAUSTIVEhint suggests a bare constructor (| B => ?) instead of the invalidB{..}. - The agent spec’s CLI line names
q find|grep|body|callers(1,798 tokens, unchanged).
0.2.1 - 2026-10-06
This release closes the open items of the 0.2.0 hardening pass (ADR 0025). Native build failures are no longer silent, the program generator covers more of the language, and most effect handlers now compile to native code.
Added
--strict-nativeforrun,test,fuzz --differential,native --releaseandbuild, also settable withSSPUR_STRICT_NATIVE=1. A failed native build becomes an error with exit status 1. CI sets it for the test suite and the corpus check.--quiethides the native build warning.- The
fuzz --gengenerator now writesF64code, generic types and functions used at several types,partasks,profile sysresources with borrows, moves and drops, and more handler shapes (aborting, raising, code afterresume,loghandlers).--edgefeedsF64edge values such as-0.0, NaN, infinities and subnormals.
Changed
- When a native build fails,
runandtestprint one warning that names the function and the C error. A per-definition build failure falls back to the whole-program build, a whole-program failure to the interpreter. - These handler shapes now compile natively (ADR 0010): arms that finish without resuming, arms that raise, code after
resume,loghandlers, lambdas with declared effects passed tomap,filter,foldand similar,returnor a raise inside a generator loop, and bodies that shadow a name their handler uses. Intests/programsthe interpreted functions went from 25 to 15, and 14 of the 15 left are entry points that perform an operation. - Every NaN compares equal to every other NaN and above infinity, whatever its sign bit, in both tiers.
Fixed
- Interpreter: a closure made before a statement that rebinds a parameter or an outer name saw the new value.
- Native: assigning over a resource after a local function in the same function skipped the old value’s destructor.
- Native:
--O3builds could spend minutes in clang on code that indexes strings with a constant, such ass.get(30). - Native: NaN ordering and equality could differ from the interpreter, because the sign of a NaN is not stable across compilers and CPUs.
- Native: contract traps inside handled code named an internal
f__evcopy instead off. - Printer:
(-0.0).fmt(1)lost its parentheses and changed meaning, and a nestedifinside string interpolation printed as a block that did not parse.
0.2.0 - 2026-10-05
This release covers Phases 3 to 7 of the roadmap and ADRs 0003 to 0024. SSPUR now compiles to native code by default, has a standard library close to C++ coverage, and ships the agent, deploy, GPU and sync tooling.
Added
sspur fuzz --gen N --seed S: random well-typed programs compared between the interpreter and three native builds, with a reducer (--reduce) and a CI campaign (ADR 0025).
Agent loop (ADR 0003, 0021)
- Content-addressed codebase in
.sspur/with atomic, typechecked transactions (add,replace,rename,remove,refine,fill,attach), test gates and stale-base checks. sspur editandsspur apply -efor small inline edits, with--testto gate on the suite.- Query API (
sspur q):list sig body callers callees effects find pack why impact holes diag log, with a token budget. - MCP server over stdio (
sspur mcp). - Tolerant parsing of common agent mistakes,
withdeep updates, andexexamples in signatures. - Concurrent transactions on CRDT definition registers with typecheck-on-merge and conflict reports, tested with 100 concurrent agents.
- Replica sync by hash over a directory or TCP (
sspur sync serve|push|pull|status|resolve) and a global check cache. - Agent spec (
sspur spec) kept under 1.8k tokens.
Language
- Algebraic effect handlers with one-shot
resume, and generators withyieldandfor(ADR 0010). - Safety types,
Guess, decision tables,while, and local functions. - Structured concurrency:
partasks,Atomic[Int],Chan[T]and theconceffect, with races rejected by the checker (ADR 0013). - The
sysprofile: owned resources, borrows,Ptr,unsafe, and an ownership checker with a soundness suite (ADR 0012). - The
bareprofile: no runtime, MMIO, interrupt handlers, fixed arraysArray[T, N]and[v; N], static module state with atomic access and theE_STATIC_RACErule (ADR 0017, 0024). - Inline asm with named operands and clobbers in
sysandbare. storeandsvcdefinitions withdb.readanddb.writeeffects.kernel fnGPU kernels with@gridand@grid2, slice borrows and a device subset checker that rejects races (ADR 0020, 0023).
Native compiler (ADR 0004 to 0008, 0011, 0015, 0022, 0024)
- Cranelift JIT tier, and a C tier through clang that is now the default for
runandtest, covering the full language. - Garbage collector for native code with bounded memory for long-running programs.
- Runtime checks the compiler can prove are removed, using intervals and Z3.
- In-place reuse for owned trees and lists, pipeline fusion, self tail calls as loops, and traps that unwind via longjmp.
- Pure fused pipelines run in parallel on a pthread pool; Int and F64 pipelines vectorize with an exact sequential rerun on flagged blocks.
- Per-definition translation units cached by content, proven rewrites with
sspur explain-opt, and profile-guided inlining viarun --profile. - Opt-in PGO (
--pgo,--retrain) and explicit--lto off|thin|full. - A direct LLVM IR prototype for the scalar, control-flow and record subset (
sspur build --backend llvm). C stays the production path. - Bare-metal targets: riscv64 and aarch64 QEMU virt, and Cortex-M4 (
thumbv7em-mps2) with vector table, SysTick and NVIC.
Contracts and verification
sspur verifyprovespre,postandwhereclauses with Z3 and reports counterexamples (ADR 0009).sspur fuzzturns contracts into property tests, with--differentialcomparing native code against the interpreter.
Standard library (ADR 0018)
- Collections:
Set,Heap,HashMap,HashSet,Bits,FlatMap,MdSpan, lazyViewpipelines, stepped ranges and more list algorithms. - Text:
StrBuf, format specifiers, Unicode case folding, and a Pike VMRegexwith captures, replace and split. - Numbers: the
<cmath>set, integer bit ops,BigInt, fixed-pointDec,Complex, seeded distributions and a splittableRng. - System: files, streaming
Filehandles, directories, stdin,Time,Duration, IANA time zones, environment, child processes andexit, each behind its own effect. json.encodeandjson.decode[T]for any data type.- Every builtin behaves the same in the interpreter and native code, traps included.
Interop
- C FFI:
extern fnwith theffieffect,sspur bindfor C headers, andsspur export-cto build a C library and header (ADR 0014).
GPU (ADR 0020, 0023)
- Kernels run on Metal with exact F32 and identical traps, and fall back to C on the CPU. Device buffers, async launches, shared memory and barriers, deterministic atomics and inlined helper functions.
sspur gpu --emit metal|opencl|spirv|ptx.
Deploy (ADR 0016, 0019)
sspur deploy plan|localgenerates infrastructure and least-privilege IAM from a service’s effects and runs it locally.- Store migrations with lazy read-time upgrades and backfill, hot swap with canary and rollback, and request replay.
- Container-free Lambda builds through zig cross-compilation.
Benchmarks
- Agent benchmark of multi-step feature tasks in SSPUR and Python with hidden tests, and native benchmarks against C++ at
-O2and-O3.
Release tooling
sspur --version.- GitHub Actions CI on macOS arm64 and Linux x86_64 running build, clippy, tests and the corpus differential check (
tools/corpus_diff.py). - Release workflow building tarballs for aarch64 and x86_64 macOS and Linux with checksums, a Homebrew formula and
install.sh.
Changed
runandtestcompile to native code by default;--interpforces the interpreter.- Dual-licensed under MIT or Apache-2.0.
- Native builds link
libmandlibpthreadexplicitly off macOS, and LTO useslldon Linux when it is available.
Fixed
- Twelve bugs found by the hardening pass (ADR 0025), among them a printer dangling
elsethat the optimizer turned into wrong native results, lexical capture across same-block shadowing in the interpreter, a data race throughfor x in par(xs)over closures, removals that could not sync, and astr_findsegfault. - Checker stack overflow on field access of an unknown-typed value.
- Native
List.sorton primitive elements. - GC large-allocation trigger and page reuse.
- Huge ranges, lists and repeats trap with out of memory instead of aborting; single allocations are capped below machine memory.
Secrettype search no longer loops on alias cycles.- Records with C keyword field names compile natively.
- F64 sums start from -0.0 natively, matching the interpreter.
0.1.0 - 2026-10-02
Added
- Language design spec, JSON schemas and the token benchmark.
- Compiler core: parser, checker, content hashing, interpreter and the
sspurCLI, with an 11-program suite.