SSPUR agent spec, more
The rest of what ./sspur spec leaves out; ./sspur spec --full is the whole reference.
Types
Int (i64) F64 Bool Str Unit List[T] Opt[T] Res[T, E] Map[K, V] Set HashMap Time BigInt Dec Regex (A, B) (t.0) (A, B) -> C ! e. Records {f: T}; sums A | B{f: T}; where refinements (_ = value); new Str (Id("a"), .raw). No implicit conversions (n.to_f64). == is structural; < orders numbers, Str, Time.
Functions
Return type required unless Unit. Effects after ! are declared (! fail[E], log): fail[E] (raise or call a failing fn), log, div (while), conc fs io time env proc net. Optional pre, post (r = result). Body: expression or do + block (last line is the value). x.f(a) is f(x, a); x.f is a field or a zero-arg call; a bare fn name is a value.
Block lines: x = e, (a, b) = e, var x = e, x := e, for x in xs, while c or while e is p (names in scope in the body) + body (0..n excludes n), return e, local fn. if without else is for Unit or raise.
Expressions
if c then a else b;then do/else do+ block.match e+ arms| Pat => e,| Pat if c => e. Exhaustive patterns:_, name, literal, tuple,Ctor Ctor{f, g: pat} some(p) none ok(p) err(p); bareCtorignores fields. A nestedmatchtakes the arms below it up to the first one indented less than its own. Lambda parameters may be typed:(x: Int) => e.- Or-patterns
| A | B{x} | C{x} => e, nestedsome(0 | 1); alternatives bind the same names. Lists[] [x] [x, ..rest] [..init, z] [a, .., z] [_, ..](restis a List;[]+[x, ..rest]is exhaustive). One case:if e is some(v) and v > 0 then a else b(notif let); tests join withand, andthensees all their names.e is pis a Bool anywhere (xs.filter(_ is some(_))) and tests everything beforeis:b is p and a, nota and b is p. Guard:some(u) = find(id) else raise NotFound{id}; theelsevalue ends the block. Long lines: end a line with an operator, or start the next, deeper, with.nameor an operator. raise Ctor{..};catch e+ arms; arms have the type ofe, so a test compares inside:catch f(x) == [] | Missing{sku} => .. | _ => false. Covering every variant removesfail[E].effect ask() -> Int(callers! ask);handle e+| ask() => resume(21)(once;| return(r) =>).yield(x)(! yield[T]) makes a generator forfor.Item{sku: "a", qty: 1},Item{sku, qty};x with qty := 2, a.b := 3, xs[0] := v.- Lambdas
x => e,(a, b) => e, orx => do+ a block indented past the lambda’s line, its last line the value and)right after;returnis not allowed inside._makes the innermost call argument a lambda:sort_by((-_.n, _.name)); inf(g(_.a))it binds insideg, so writex => f(g(x.a)). "n={n} {x.name}"interpolates, so a literal{is\{("\{\"a\":1}"). Escapes:\n \r \t \" \\ \{, so regex\dis"\\d".+joins Str and List.- Low to high (no
&& || !):or, and, not, == != < <= > >=, .., + -, * / %, **, unary-. Int/truncates; overflow,/ 0and badxs[i]trap.
Builtins
- List:
len is_empty map filter flat_map fold(init, (acc, x) => e) any all find index_of contains(x) sort sort_by(key) sort_with(cmp) unique reverse sum push(x) concat(ys) take(n) drop(n) slice(a, b) zip(ys) enumerate group_by(key) counts join(sep) to_set.find first last get(i) min maxgive Opt. Sorts are stable;countsisList[(A, Int)]in first-seen order. - Str:
len is_empty lower upper trim split(sep) words chars contains starts_with ends_with index_of replace(a, b) repeat(n) pad_left(n, fill) to_int to_f64(Opt),get(i) first last(Opt).wordssplits on non-alphanumerics..format(">10,.2f"). - Opt
or(d) is_some is_none map get ok_or(e); Resis_ok or(d) map, andgetraises its error (fail[E]). empty_map()/hash_map():get put(k, v) remove has keys values items;empty_set():add has union;str_buf().add(s).str.- Int
abs to_f64; F64abs round floor sqrt pow; any.str;min(a, b) max(a, b) log(s). big(n),decimal("1.25")(Opt);regex(p)(Res).is_match(s) captures(s)(Opt of groups, 0 first)find_all replace(s, "$1");date(y, m, d)(Opt);json.encode(v) json.decode[T](s)(Res).! fs:read_file(p) write_file(p, s)(Res[_, Str]);! io:read_line;! time:now;! env:env_var;! proc:run_cmd.
Traits
trait Sh+ indentedfn area(x: Self) -> F64lines;= e(same or next line) is a default. The first parameter isSelf, and a method’s effects bound its impls’.impl Sh for Circle+ indented fns with bodies (SelforCirclein signatures); a generic type:impl[T: Show] Show for Box[T]. Callc.area,c.area()orarea(c); passxs.map(area)where the parameter type is known.- Bounds
fn f[T: Ord + Show](xs: List[T])are checked inf; a call with a type that lacks an impl isE_TRAIT_MISSING. Bounded fns are compiled per type. - Built in:
Eq eq,Ord cmp(negative, 0, positive;< <= > >=),Show show,Hash hash,Json to_json,Add Sub Mul Div(+ - * /;fn add(a: Self, b: Self) -> Self),Neg neg(-x),Index[K, V] index(x[k]). Numbers,Str,Bool, lists, tuples,Opt,Maphave them;==uses a hand-writtenEqinsideOpt,List,Res, tuples and derived types;sortand.strstay structural. type P = {..} derive Eq, Ord, Show, Hash, Jsonadds structural impls (variants order by name).- One impl per (trait, type), in the trait’s or the type’s package;
pub traitandpub implexport them, anduse lib.{Sh}brings its methods.
Holes and contracts
Leave ?name where you don’t know the expression yet: ./sspur check lists up to 3 candidates under it, one per line, ?total: items.map(_.qty * _.price).sum (passes 3 tests, post), built from the values in scope and run against the tests and contracts. N others also fit means the tests don’t pin it down: add a test before you keep it. ./sspur fill --apply writes the best one. ./sspur suggest f proposes pre/post lines from runs of f (proved or held on N runs); --apply adds them.
Edit
./sspur edit --test change.ssp is one atomic, typechecked transaction. Spellings from other languages that have one meaning (&&, len(x), None, s.slice(a, b), ! a ! b, a missing effect) are stored in SSPUR form and listed after stored as:. A failing test prints the values it compared (left 7, right 8). An impl is the definition impl Sh for P (remove impl Sh for P).
HTTP, config, logs
! net.http.get(url) post(url, json) send(req)giveRes[Response, HttpErr](a 404 isok;HttpErrDns Connect Tls Timeout Io{msg},HttpStatus{code, body});http.fetch(url)is the body, raisingHttpErrunless 2xx.http.req("PUT", url) with headers := [("a", "b")], body := s, timeout_ms := 5000, retries := 2, redirects := 0.r.status r.body r.header("x") r.json[T].- Server: a
svcis the handler:{id}path params, GET/DELETE query params (Optif optional), one JSON body param, aRequestparam for the raw request,svc api(app: App)params fromhttp.serve(8080, api(app)); a value is 200 (201 POST),Unit204,none404, a raisedNotFound404 (Invalid400, else 422).http.serve(8080, api), or anyreq => respfn:http.text(200, s) html json(200, v),req.path query("q") header("h") json[T].http.serve_with(http.server(port) with host := "0.0.0.0", cors := ["*"], gzip := true, files := [("/static", "public")], health := "/healthz", cert := c, key := k, workers := 8, max_body := n, timeout_ms := n, max_requests := n, access_log := false, h). Handlers can’t assign outervars. config.load(Config{port: 8080}, "app.json", "APP_")isRes[Config, Str] ! fs, env(envAPP_PORT, then the file, then the default).log.debug/info/warn/error(m);SSPUR_LOG=json,SSPUR_LOG_LEVEL=warn.
Data, crypto, compression
Bytes:bytes([1, 2]),s.to_bytes,b.len get(i) slice(a, b) concat(c) to_list to_str hex base64 base64url,from_hex(s) from_base64(s) from_base64url(s)(Opt),read_bin(p) write_bin(p, b)! fs.! db, raisingDbErr(Sql Constraint Busy Mismatch{msg},NoRow):c = db.open("app.db")(or":memory:"),db.exec(c, sql, [a, b])(rows changed),db.query[T](c, sql, params)(columns toT’s fields by name; or a column type),db.one[T](NoRowif none),db.opt[T],db.tx(c, () => ...)(rolls back on raise),db.migrate(c, [sql1, sql2]). Values only through?placeholders;noneis NULL;insert ... returning *. Share oneDbbetween server handlers.crypto.sha256 sha512 blake2b hmac256(key, data) hmac512 equal(a, b)(pure; data isBytesorStr, keysSecret[Bytes]orBytes);! rand:crypto.random(n) token(n) key() seal(key, data) keypair() hash_password(pw),uuid.v4() v7();crypto.open(key, sealed)(Opt),verify_password(pw, hash) sign(sk, data) verify(pk, data, sig).gzip.compress(b) decompress(b)(Res),compress_level(b, n),compress_file(src, dst) decompress_file! fs; the same inzstd,deflate(no files).csv.parse(text) write(rows),csv.decode[T](text)(header names to fields, Res),csv.read[T](path),csv.fold[T](path, init, (acc, row) => ...),csv.encode(xs).
Elsewhere
./sspur spec --full covers concurrency (! conc, par, atomic, chan), services (store, db.get, svc, ep, migrate_S), packages (./sspur add, pub, use lib.{f}) and C, sys, bare and GPU (extern fn, profile, kernel fn).