# Packages > The standard library, and how imports resolve. ## Standard packages `time`, `net`, `json`, `proc`, `fs`, `log`, `crypto`, `sql`, and `regex` are compiler-provided native packages — no source files, just `import "time";` / `import "net";` / `import "json";` / `import "proc";` / `import "fs";` / `import "log";` / `import "crypto";` / `import "sql";` / `import "regex";` like any other package. `http`, `http2` and `byteutil` are slang-source stdlib packages under `stdlib/`. `import "http"` / `import "byteutil"` resolve to a local directory first, then a native package, then `stdlib/` (`SLANG_STDLIB` or the compiler's `SLANG_STDLIB_DIR`). #### `time` ```slang import "time"; let t0 = time.mono(); // monotonic clock; a `duration` (int64 ns) time.sleep(20000000); // sleep for a duration (ns) let elapsed = time.mono() - t0; // duration arithmetic let deadline = time.mono() + 5000000; // timeout math for net calls let wall = time.wall(); // unix epoch time in nanoseconds ``` #### `net` TCP listener/dialer built on `bytes` and fixed-width ints; every fallible call returns a `result[_, str]` unwrapped with `guard let`. ```slang import "net"; let lr: result[i32, str] = net.listen(8080); // 0 = ephemeral port guard let lfd = lr else { exit(1); } let pr: result[i32, str] = net.port(lfd); // assigned port number let ar: result[i32, str] = net.accept(lfd); // blocks until a peer connects guard let cfd = ar else { exit(1); } net.send(cfd, b"hello"); let rr: result[bytes, str] = net.recv(cfd, 4096); let data: bytes = rr ?? b""; net.nonblock(cfd); // switch to non-blocking mode let wr: result[bytes, str] = net.recv(cfd, 16); // "would block" err if idle net.close(cfd); ``` ##### Deadlines `net.recv` and `net.send` wait for as long as the peer takes, which on a public listener is indefinitely: a client that connects and then neither sends nor reads parks the serving task on the reactor forever, holding its stack and its GC roots. That is slowloris, and the defence is `recv_until` / `send_until`, which take an `until` — an absolute monotonic instant, not a duration: ```slang import "net"; import "time"; let deadline = until_of(time.mono() + 5000000000); // 5s from now let rr = net.recv_until(cfd, 4096, deadline); guard let data = rr else let e = err_of(rr) { if e == "timeout" { net.close(cfd); return; } // peer went quiet log.error("recv: " + e); // peer broke return; } ``` `"timeout"` is a reserved error string: it means the deadline passed, and it is the only error text these calls invent rather than take from the OS. Every other error is `strerror`/OpenSSL text as before. One asymmetry worth knowing: a `send_until` that times out **has already written some bytes**, and `result[i32, str]` has no room to report both "timed out" and "wrote this much". A `"timeout"` from `send_until` therefore means the stream is at an unknown offset and the connection must be closed, not retried. For a framed protocol that is the right contract regardless — a half-written frame is unrecoverable. `net.tls_recv_until` / `net.tls_send_until` are the same thing over TLS, with the same reserved string. The `link` API takes an `until` on `accept`/`send`/`recv` already. See `examples/httpd/` for a minimal HTTP server on `link` plus the `http` stdlib package. #### TLS `net.tls_*` adds a TLS listener/dialer on top of the plain `net` primitives above, built on OpenSSL (linked automatically, and only when a program actually calls one of these — a plain-TCP `net` program stays dependency-free). A `SSL_CTX`-equivalent config is created once (`tls_server_ctx` / `tls_client_ctx`) and reused across many connections; each connection is a separate `rawptr` handle. ```slang import "net"; // server: load a cert + key once, reuse the context for every connection let sctx_r: result[rawptr, str] = net.tls_server_ctx("cert.pem", "key.pem"); guard let sctx = sctx_r else { exit(1); } let lr: result[i32, str] = net.listen(8443); guard let lfd = lr else { exit(1); } let ar: result[rawptr, str] = net.tls_accept(lfd, sctx); // TCP accept + handshake guard let sconn = ar else { exit(1); } net.tls_send(sconn, b"hello"); net.tls_close(sconn); // client: verify against a CA file, or "" for the system trust store let cctx_r: result[rawptr, str] = net.tls_client_ctx(""); guard let cctx = cctx_r else { exit(1); } let dr: result[rawptr, str] = net.tls_dial("example.com", 443, cctx); guard let cconn = dr else { exit(1); } let rr: result[bytes, str] = net.tls_recv(cconn, 4096); net.tls_close(cconn); ``` Client verification is strict by default: `tls_client_ctx` enables peer verification, and `tls_dial` checks the certificate against *both* the CA and the hostname you asked for (`SSL_set1_host` — the check that's easy to forget and, if skipped, leaves you with "TLS" that validates a certificate chain without checking it belongs to the host you're actually talking to). Sending/receiving is blocking, same as plain `net` — call these from a `spawn`ed task if you need a connection handled without stalling anything else. **ALPN** (RFC 7301) negotiates the protocol during the handshake, which is how HTTP/2 over TLS is selected — there is no in-band upgrade. `tls_ctx_alpn(ctx, "h2,http/1.1")` sets the list on a server context (in preference order, so the *server* decides) or the offer on a client one, and `tls_alpn(conn)` returns what was actually negotiated, or `""` if the peer offered nothing that overlapped. The list is comma-separated, not the length-prefixed wire form; building that by hand is an easy way to produce a subtly broken handshake. A client offering no protocol we support completes the handshake without ALPN rather than failing, so it simply falls back to HTTP/1.1. ```slang net.tls_ctx_alpn(sctx, "h2,http/1.1"); let conn = ...; // after tls_accept if net.tls_alpn(conn) == "h2" { serve_h2(conn); } else { serve_h1(conn); } ``` Mutual TLS: `tls_ctx_require_client(sctx, client_ca)` on the server context demands a client certificate chained to that CA (`SSL_VERIFY_FAIL_IF_NO_PEER_CERT`). The client presents one with `tls_ctx_use_cert(cctx, cert, key)`. Extra server names on one listener: `tls_ctx_add_sni(sctx, host, cert, key)` swaps in that cert when the ClientHello SNI matches; unmatched names keep the default `tls_server_ctx` cert. `require_client` applies to SNI certs too, regardless of call order. TLS 1.3 can let `tls_dial` return before the server has rejected a missing client certificate; the first send or recv then fails. #### `json` `json.decode`/`json.encode` (de)serialize `str`/`bytes` against a concrete slang type — the target type for `decode` is inferred from the binding's annotation, the same mechanism `ok()`/`err()` already use to infer `result[T,E]`. There is no dynamic "JSON value" type: every decode is checked field-by-field against the struct shape you asked for, and a mismatch is a `result` error, not a silent `null` or a runtime panic. ```slang import "json"; gc struct Address { city: str, zip: str } gc struct Person { name: str, age: i32, email: opt[str], // JSON null / missing key <-> none tags: [str], addr: Address, // structs nest } let p = Person{ name: "Ada", age: 36, email: some("ada@example.com"), tags: ["math"], addr: Address{ city: "London", zip: "SW1" } }; let s: str = json.encode(p); let r: result[Person, str] = json.decode(s); guard let p2 = r else { exit(1); } ``` Supported: `struct`, `opt[T]`, `[T]`, `map[str, V]` (JSON object keys are always strings — a map with any other key type is a compile error), every scalar, and `bytes` (RFC 4648 base64 strings on the wire). `rawptr`, `chan[T]`, and `result[T,E]` can't appear anywhere in a decode/encode target type. A missing JSON key defaults an `opt[T]` field to `none`; for any other field type it's a decode error. Unknown JSON keys are ignored. Every decode error names where it happened, composed through nesting — `json.decode` on `{"addr":{"city":5}}` against the `Person` shape above fails with `field 'addr': field 'city': expected a string, got a number`. Malformed input is a decode error, never a crash — the parser caps nesting depth at 512 so adversarial input can't blow the C stack. #### `proc` Graceful shutdown and environment variables. `proc.shutdown_requested()` turns true once the process receives `SIGTERM` or `SIGINT`; a blocked `net.accept()`/`net.recv()`/`net.dial()` on the main thread is interrupted the instant the signal arrives (an `err` result, not a hang), so a listener loop notices without needing `select` or a timeout. `proc.active_tasks()` counts currently-running `spawn`ed tasks. `proc.wait_idle()` parks until that count is zero, so a shutting-down program can drain in-flight work without polling. ```slang import "net"; import "proc"; import "time"; fn accept_and_serve(lfd: i32) { let ar: result[i32, str] = net.accept(lfd); guard let cfd = ar else { return; } // interrupted, or a real error spawn serve(cfd); } let lr: result[i32, str] = net.listen(8080); guard let lfd = lr else { exit(1); } while !proc.shutdown_requested() { accept_and_serve(lfd); } proc.wait_idle(); ``` `proc.getenv(name)` reads an environment variable, returning `opt[str]` (`none` if unset). `proc.args()` is the process argument list (`[str]`); `args[0]` is the executable path. `proc.cwd()` is the working directory as `result[str, str]`. #### `fs` POSIX file I/O on integer fds. `open` is read-only; `create` is write/trunc. `read`/`write`/`close` use the fd. `mkdir` creates one directory. Every call returns `result[_, str]`. These calls block the worker — use them for config and small files, not the accept loop. ```slang import "fs"; let cr = fs.create("/tmp/note"); guard let fd = cr else { exit(1); } fs.write(fd, b"hi"); fs.close(fd); let or = fs.open("/tmp/note"); guard let in_fd = or else { exit(1); } let rr = fs.read(in_fd, 16); guard let data = rr else { exit(1); } fs.close(in_fd); ``` #### `os` The operating system *around* a program: the environment, the process, and everything you can ask or do about a path without opening it. Pure libc, so importing `os` adds no link flag. **The `fs`/`os` boundary**: `fs` owns open file **handles** and their contents; `os` owns paths you have not opened. `fs.mkdir` predates that split and stays where it is rather than breaking existing programs. | | | |---|---| | `os.setenv(k, v)` / `os.unsetenv(k)` | `result[bool, str]` | | `os.environ()` | `[str]` of `KEY=VALUE` | | `os.pid()` / `os.tmpdir()` | `int` / `str` | | `os.hostname()` | `result[str, str]` | | `os.exists(p)` / `os.is_dir(p)` / `os.is_file(p)` | `bool` | | `os.size(p)` / `os.mtime(p)` | `result[int, str]` | | `os.read_dir(p)` | `result[[str], str]` | | `os.remove(p)` / `os.rename(a, b)` | `result[bool, str]` | ```slang import "os"; // serve a static file, the shape this package exists for if !os.is_file(path) { return not_found(); } let sr = os.size(path); guard let n = sr else let e = err_of(sr) { log.error("stat " + path + ": " + e); // "No such file or directory" return server_error(); } ``` The three predicates are bare `bool` on purpose. "Does this exist" has two useful answers: a missing path and an unreadable parent are both "no, you cannot use it", and code branching on the difference is racing anyway — the answer can change between the check and the use. The accessors return a value that has to come from somewhere, so those carry the errno text. `environ()` is a list rather than a map because an environment may legally hold a repeated key, and a map would silently drop one. `read_dir` returns entry names without `.` and `..`, since forgetting to filter those is how a directory walk becomes an infinite loop. `remove` takes files and empty directories alike, so a caller need not know which it has. `proc.getenv`, `proc.args` and `proc.cwd` stay in `proc`; `os` adds what `proc` has no answer for rather than duplicating it. #### `log` Stderr logging with a timestamp and level. Each function accepts a `str` or a `fault` (`to_str`/`+` already convert faults the same way), so `err_of` bindings and `fault` values log without manual conversion. ```slang import "log"; log.debug("cache miss for key foo"); log.info("listening on :8080"); log.warn("retrying dial after timeout"); log.error("could not load config: " + e); log.warn(fault_timeout()); ``` #### `crypto` SHA-256, HMAC-SHA256, and a CSPRNG over OpenSSL. Hash and HMAC are infallible on valid inputs and return `bytes` directly; `rand` can fail and returns `result[bytes, str]`. ```slang import "crypto"; let h: bytes = crypto.sha256(b"abc"); // 32 bytes let m: bytes = crypto.hmac_sha256(key, msg); // 32 bytes let r = crypto.rand(32); guard let b = r else let e = err_of(r) { log.error("rand failed: " + e); } ``` #### `sql` A SQLite driver (linked automatically, only when a program imports `sql`). Connections and prepared statements are opaque `rawptr` handles, exactly like `net.tls_*`; free them with `sql.close` / `sql.finalize`. **Every fallible call returns `result[_, str]` whose error is SQLite's own message** — `no such table: users`, `near "SELCT": syntax error`, `UNIQUE constraint failed: users.id` — so a bad query stays as visible as a bad socket read (`guard let … else let e = err_of(r)`), never a silent `null`. The column getters are infallible (SQLite coerces types; an out-of-range index is a programming error, returning `0` / `""`), so they return bare values. | Function | Signature | |----------|-----------| | `sql.open(path)` | `result[rawptr, str]` — `":memory:"` for in-memory | | `sql.close(db)` | — | | `sql.exec(db, sql)` | `result[int, str]` — runs statement(s), returns rows changed | | `sql.last_insert_id(db)` | `int` | | `sql.prepare(db, sql)` | `result[rawptr, str]` | | `sql.finalize(st)` | — | | `sql.reset(st)` | `result[bool, str]` — clears bindings, re-run | | `sql.bind_int/bind_float/bind_text/bind_blob(st, idx, v)` | `result[bool, str]` — `idx` is 1-based | | `sql.bind_null(st, idx)` | `result[bool, str]` | | `sql.step(st)` | `result[bool, str]` — `true` = row ready, `false` = done | | `sql.col_count(st)` | `int` | | `sql.col_name(st, i)` / `col_text(st, i)` | `str` — `i` is 0-based | | `sql.col_int(st, i)` | `int` | | `sql.col_float(st, i)` | `float` | | `sql.col_blob(st, i)` | `bytes` | | `sql.col_is_null(st, i)` | `bool` | ```slang import "sql"; import "log"; let dr = sql.open("app.db"); guard let db = dr else let e = err_of(dr) { log.error("db open: " + e); exit(1); } sql.exec(db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); let pr = sql.prepare(db, "SELECT id, name FROM users WHERE id > ?"); guard let st = pr else let e = err_of(pr) { log.error("prepare: " + e); // e.g. "no such table: users" exit(1); } sql.bind_int(st, 1, 0); while true { let sr = sql.step(st); guard let more = sr else let e = err_of(sr) { log.error("step: " + e); break; } if !more { break; } println(to_str(sql.col_int(st, 0)) + " " + sql.col_text(st, 1)); } sql.finalize(st); sql.close(db); ``` SQLite calls block the worker — use them for real work off the accept loop (wrap in a `spawn`ed task), the same caveat as `fs`. One connection per `rawptr`; there is no pool, no networked backend (Postgres/MySQL), and no async stepping. Query complexity is capped per connection so SQLite's recursion stays inside the task stack: at most **50 terms in a compound `SELECT`** (`UNION`/`INTERSECT`/`EXCEPT`) and an **expression depth of 400** (roughly, terms in one `AND`/`OR` chain). SQLite's stock limits of 500 and 1000 allow a single legal query to want ~325KB of C stack, which would force a task stack far too fat to spawn per connection. Long `IN` lists, wide result sets, and recursive CTEs are *not* affected — they don't recurse. Exceeding a cap is a normal error through `result[_, str]` (`too many terms in compound SELECT`), not a crash. #### `regex` Regular expressions on `str` or `bytes`, matched by slang's own Thompson NFA — no external library, so a program that matches text stays as dependency-free as a plain TCP one. `compile` returns an opaque `rawptr` handle (freed with `regex.free`, like `net.tls_*` and `sql`), and a bad pattern comes back as a descriptive `result[rawptr, str]`. **Matching is linear time, always.** There is no backtracking, so the classic catastrophic pattern `(a+)+$` — which makes a backtracking engine take exponential time on a hostile input — runs in the same microseconds here as any other pattern. That is the point of choosing this engine for a server language: patterns and subjects both arrive from the network. The price is the RE2/Go one, and it is not negotiable: **no backreferences and no lookaround**. Both require backtracking; `(?=...)` and friends are a compile error, not a silent mis-parse. | Function | Signature | |----------|-----------| | `regex.compile(pat)` | `result[rawptr, str]` | | `regex.free(re)` | — | | `regex.groups(re)` | `int` — number of capture groups | | `regex.is_match(re, s)` | `bool` — `s` is a `str` | | `regex.is_match_bytes(re, b)` | `bool` — `b` is `bytes` | | `regex.find(re, s)` / `find_bytes(re, b)` | `[int]` | | `regex.find_at(re, s, from)` / `find_bytes_at(re, b, from)` | `[int]` | `find` returns byte offsets as `[start, end, g1start, g1end, ...]`, or an **empty list** when there is no match — so the result is GC-owned and there is no match handle to leak. `find_at` starts at an offset, which is how you walk every match. ```slang import "regex"; let cr = regex.compile("(\\d{4})-(\\d{2})-(\\d{2})"); guard let re = cr else let e = err_of(cr) { log.error("bad pattern: " + e); // e.g. "missing ) at offset 9" exit(1); } if regex.is_match(re, "due 2026-09-09") { let m = regex.find(re, "due 2026-09-09"); println(to_str(m[0]) + ".." + to_str(m[1])); // whole match: 4..14 println(to_str(m[2]) + ".." + to_str(m[3])); // year: 4..8 } regex.free(re); ``` Supported: literals, `.`, classes `[a-z]` `[^...]` `[[:digit:]]`, escapes `\d \D \w \W \s \S \b \B \A \z \xHH`, quantifiers `* + ? {n} {n,} {n,m}` and their lazy `?` forms, groups `(...)` and `(?:...)`, alternation `|`, anchors `^ $`. Matching is leftmost-first (Perl-style priority), and `.` does not match `\n`. Subjects are matched with an explicit length, so a `bytes` containing NUL matches correctly rather than stopping at the NUL — and `\D` matches a NUL byte like any other non-digit. Bounds, so a hostile pattern can't exhaust memory or stack: 4096 compiled instructions, 100 nesting levels, 32 capture groups, and `{n,m}` counts up to 1000. Each is a descriptive compile error, never a crash. A compiled regex is safe to share across tasks, and is meant to be: it carries a small pool of reusable match buffers, so concurrent matchers allocate nothing per match. Compiling is the expensive part (it is also the only part that grows the task stack) — compile once, match many times, ideally not once per request. **Where this lands on speed.** Measured single-threaded on `^(GET|POST|PUT) (/[a-z0-9/_-]*) HTTP/1\.([01])$` against a 26-byte subject, 200k iterations: | engine | matches/sec | on `(a+)+$` vs a hostile input | |--------|-------------|-------------------------------| | slang `regex` | ~721k | 2µs, correct answer | | POSIX `regexec` | ~372k | fast here, but no limits | | PCRE2 (interpreted) | ~2.4M | 0.2s, then gives up (`MATCHLIMIT`) | | PCRE2 (JIT) | ~8.6M | same — JIT does not save it | So: ~1.9x faster than libc's POSIX engine, and several times slower than PCRE2 on *benign* input — PCRE2's interpreter and especially its JIT are very good, and this is an honest gap. The trade is deliberate: on adversarial input the ordering inverts completely, because linear time is a guarantee here and a hope there. `is_match` is markedly cheaper than `find` (it binds no capture slots at all), so prefer it when you only need a yes/no. Matching scales with tasks — ~3.2M/sec across 16. Because matching never grows the task stack, regex is cheap to use per connection: 600 concurrently-live tasks each matching and then parking peak at **3.9MB RSS** — about 17x lighter than the same shape holding `sql` connections (65.9MB), which does grow every task's stack. #### `http2` HTTP/2 framing and HPACK header compression (RFC 9113, RFC 7541), written in slang — the frame codec is what the bitwise operators were added for. ```slang import "http2"; let f = http2.decode(buf, 0, 16384); // one frame, bounds-checked guard let fr = f else let e = err_of(f) { return; } let d = http2.decoder_new(4096); // per-connection HPACK state let hr = http2.decode_block(d, fr.payload, 64); guard let hs = hr else let e = err_of(hr) { return; } for i in 0..len(hs) { println(hs[i].name + ": " + hs[i].value); } ``` Frame layer: `decode` / `encode` / `header_bytes`, the reserved bit masked off the stream id as the RFC requires, `strip_padding`, and the common control frames (`settings_frame`, `settings_ack`, `ping_ack`, `rst_stream`, `goaway`, `window_update`). HPACK: prefix integers, string literals, the 61-entry static table, a dynamic table with the RFC's +32-per-entry accounting and eviction, and a **canonical Huffman decoder**. Header blocks decode through `decode_block`; `encode_block` builds one. The encoder is deliberately **stateless** — every field goes out as a static-table index or a literal *without* indexing, and nothing is added to a dynamic table on the encode side. That is conformant and it removes a whole bug class: an encoder's dynamic table must stay in lockstep with the peer's decoder table, and any drift silently corrupts every later block on the connection. Bounds against hostile peers: a frame longer than the advertised `SETTINGS_MAX_FRAME_SIZE` is refused before allocating, `decode_block` takes a `max_headers` cap (a small compressed block can otherwise expand without limit), a Dynamic Table Size Update above the agreed maximum is rejected, and NUL in a field name or value is the protocol error RFC 9113 §8.2.1 says it is. Huffman padding must be under 8 bits and all ones, and EOS inside a string is refused. Validated against **nghttp2** — the HPACK implementation curl and the browser stacks use — in both directions: blocks it produces decode here, and blocks produced here inflate there. Those fixtures are baked into `tests/http2` as literals, so the suite needs no nghttp2 to run. **Connection layer, with concurrent streams.** One task reads frames and dispatches each request to its own `spawn`ed handler; every byte leaving the connection goes through a single writer task fed by a `chan[bytes]`. No mutex is involved, and none is needed: each channel message is a complete frame sequence written with one `net.send`, so handlers cannot interleave inside a frame, and a HEADERS block plus its CONTINUATIONs stays contiguous by construction (RFC 9113 §6.2) rather than by careful ordering. Frames for different streams interleave at frame boundaries, which is what multiplexing means. Measured: four 500ms requests multiplexed on one connection complete in **0.53s**; served one at a time they would take about 2.0s. The connection is addressed by a **`Transport`**, not a `link`. `link` is move-only, so `spawn writer_task(c)` consumes it and the reader can no longer use it — the two-task design is impossible with that type. A `Transport` is freely copyable, and one reader plus one writer in opposite directions on a socket is safe. `net.recv` also returns `bytes` directly, so no byte-at-a-time copy sits on the read path. A `Transport` is either a plain fd or a TLS handle, and everything above it is identical either way: ```slang http2.transport_fd(fd) // h2c: cleartext, prior knowledge http2.transport_tls(ssl) // h2 over TLS, from net.tls_accept ``` ```slang fn handle(stream: i32, path: str, wch: chan[http2.WMsg]) { let hs: [http2.Header] = []; // The body goes over UNFRAMED: the writer owns the peer's windows, // so it decides how it is cut into DATA frames and when each may go. chan_send(wch, http2.response_msg(stream as int, "200", hs, to_bytes("hello"))); } fn serve(fd: i32) { let cn = http2.conn_new(); let rd = http2.reader_new(); let wch: chan[http2.WMsg] = make_chan(32); let lim = http2.default_limits(); spawn http2.writer_task(fd, wch, lim.write); guard let _p = http2.accept_preface(rd, fd, wch, until_of(time.mono() + lim.handshake)) else { return; } while true { let rr = http2.read_request(cn, rd, fd, wch, lim); guard let req = rr else let e = err_of(rr) { if http2.is_timeout(e) { /* slow peer; shed it */ } chan_close(wch); return; } spawn handle(req.stream as i32, req.path, wch); } } ``` Verified against real `curl --http2-prior-knowledge`: GET, POST with a body, five requests multiplexed on one connection, and a 64KB upload that exercises DATA chunking and flow-control `WINDOW_UPDATE`. ##### Deadlines Every read and every write is bounded, so a peer that connects and then dribbles — or one that stops reading our responses — is disconnected rather than left holding a task forever. `http2.Limits` carries four separate budgets because they defend against four different peers: | Budget | Covers | |---|---| | `handshake` | connect → valid client preface | | `idle` | no request in flight, waiting for the next frame | | `request` | first HEADERS octet → END_STREAM | | `write` | one `writer_task` send | `idle` is deliberately generous (2 minutes by default): an HTTP/2 connection sitting open with no streams is completely normal, and timing it out aggressively breaks correct clients. `request` is the strict one and applies to the request **as a whole** — it is never refreshed by incoming frames, so dribbling DATA one octet at a time cannot extend it. That distinction is the whole defence; a per-read timeout would never fire against a slowloris, because every individual read makes progress. `http2.is_timeout(e)` distinguishes a slow peer from a broken one, so a server can answer the first with `GOAWAY` / `E_ENHANCE_YOUR_CALM`. `tests/http2_deadline` runs all three attacker shapes — silent, idle-after-handshake, and octet-at-a-time dribbling — against a server with sub-second budgets and requires all three to be shed. ##### Flow control DATA is flow-controlled at two levels, per-stream and per-connection (RFC 9113 §5.2), and the server may not exceed either. Both windows live in the writer task, because they are connection-wide state that the **read** side replenishes (`WINDOW_UPDATE` arrives there) and the **write** side spends — routing both into one task is what makes the accounting correct without a lock. A handler therefore hands over its body unframed and moves on. If the peer's window is too small, the *body* waits in the writer's queue, not the handler's task — a peer advertising a tiny window costs a queue entry rather than a parked task. `SETTINGS_INITIAL_WINDOW_SIZE` adjusts every open stream's window by the delta rather than resetting it, and does not touch the connection window (§6.9.2). A `WINDOW_UPDATE` that would push a window past 2³¹−1 is a `FLOW_CONTROL_ERROR` and ends the connection with a `GOAWAY` rather than being clamped. `tests/http2_flow` drives both levels: a client advertising a 100-octet stream window against a 5000-octet body, and a client with a large stream window against a 100000-octet body where the default 65535 connection window is what binds. Each phase checks the exact octet the server stops at, that it resumes for exactly the credit granted, and that the resumed bytes carry the right content for their absolute offset in the body. ##### TLS and ALPN Browsers speak HTTP/2 **only** over TLS, and only when ALPN negotiates it — there is no in-band upgrade in a browser. So h2c alone, however conformant, cannot serve one. The server advertises what it can speak, and then checks what was actually chosen: ```slang net.tls_ctx_alpn(sctx, "h2,http/1.1"); // offer both, h2 preferred // ... net.tls_accept(lfd, sctx) -> ssl if !http2.alpn_is_h2(net.tls_alpn(ssl)) { // the peer picked http/1.1; serve it as HTTP/1.1 or hang up } let t = http2.transport_tls(ssl); ``` Checking is not optional politeness. A server that offers `http/1.1` must expect to get it, and feeding an HTTP/1.1 client into the frame parser produces `bad connection preface` — true, but a poor explanation of what went wrong. `tests/http2_tls` runs both halves over a real handshake: h2 frames across `SSL_read`/`SSL_write`, and an http/1.1-only client being declined rather than misparsed. ##### Interop Checked against **Go's `golang.org/x/net/http2`**, which shares no ancestry with nghttp2 (curl's stack, and where the HPACK fixtures came from) — agreement between two implementations that share code proves less than it appears to. It covers a GET, a 50KB POST, a 200KB response verified byte-for-byte against its absolute offset, and six concurrent streams on one connection. Run it with `sh tests/http2_interop/run.sh`; it skips cleanly without a Go toolchain. ##### Stream floods The connection layer cannot cap concurrency by itself: it does not spawn the handlers, *you* do. (slang has function values now, so handing it a callback would compile — but a callback would only move the same question inside, and the gate below is the answer either way.) So the bound is a **gate** — a token channel you hold. `gate_enter` takes a token and blocks when none are left, `gate_leave` returns one, and that blocking is the backpressure: the reader stops pulling frames while every slot is busy. Without it, a peer that sends 1000 requests down one connection gets 1000 concurrent handler tasks — measured, against a `SETTINGS_MAX_CONCURRENT_STREAMS` of 100 that we were advertising and not keeping. Advertising a limit you do not enforce is worse than advertising none, because peers size their behaviour by it. `gate_drain` also makes shutdown safe. Closing the writer channel while handlers are still in flight panics them with *send on closed channel*, and draining is what knows when none are left. **The one rule: `gate_leave` must run on every path out of a handler**, error returns included. A lost token permanently shrinks that connection's capacity; losing all of them wedges that one connection — bounded and visible, not a crash, but not something to leave in. Separately, `RST_STREAM` is counted. A peer that opens a stream and cancels it immediately (CVE-2023-44487, *Rapid Reset*) never looks concurrent, so a cap alone never trips; after a burst of 100 free cancellations, a peer whose resets outnumber half of what it opened ends the connection. Cancelling is legitimate — a browser navigating away resets its in-flight streams — so the burst and the ratio are both needed to tell a normal client from a flood. `tests/http2_flood` drives both shapes, resetting and not, and fails if either exceeds the cap. ##### Known gaps `PRIORITY` is validated but not acted on: it is deprecated in RFC 9113 §5.3.2, so ignoring the prioritisation is conformant, but a malformed frame is still rejected as the connection error it is (§6.3) rather than waved through to desync the stream. No browser has been run against the TLS path yet — the machinery is there and tested against slang's own client, but a real browser is different evidence. #### `byteutil` Search, trim, and split on the `bytes` type — no new syntax. The package cannot be named `bytes` because that token is the type. ```slang import "byteutil"; byteutil.find(b"hello", 0, 108); // 2, or -1 byteutil.has_prefix(b"hello", b"he"); byteutil.has_suffix(b"hello", b"lo"); byteutil.trim(b" hi\r\n"); // b"hi" (space/tab/CR/LF) byteutil.split(b"a,b", 44); // [b"a", b"b"] ``` #### `http` HTTP/1.1 over `link` / `wire` / `until` / `fault`. Parse a request from `bytes`, or `read` from a connection into a caller-sized `wire` (the max request size). `read` takes the unconsumed prefix length and returns `Incoming` with leftover compacted to the front of the wire, so one connection can carry many requests. `write` serializes a `Response` through an arena. Headers are stored lowercased; `header(req, name)` looks up case-insensitively. `Content-Length` is honored; chunked `Transfer-Encoding` is rejected. `wants_close` follows HTTP/1.1 keep-alive (and HTTP/1.0 close-by-default). ```slang import "http"; fn serve(c: link) { let ra = arena_new(16384); let sa = arena_new(16384); let buf = ra.wire(8192); let filled = 0; while true { let rr = http.read(&mut c, buf, filled, until_never()); guard let got = rr else { return; } let wr = http.write(&mut c, http.ok_text(got.req.path), &mut sa, until_never()); guard let _n = wr else { return; } sa.reset(); if http.wants_close(got.req) { return; } filled = got.filled; } } ``` See `examples/httpd/` for a listener loop on this package. This works because every `spawn`ed thread has `SIGTERM`/`SIGINT` blocked in its own signal mask from birth (inherited at creation, restored in the spawning thread right after) — so the OS can only ever pick the main thread to run the handler, which is what lets the main thread's blocked `accept()` call reliably observe the interruption instead of the signal silently landing on some unrelated connection's worker thread mid-request. There's a narrow startup race inherent to this: a signal that arrives in the brief window before `main()` installs the handler gets the OS's default disposition (immediate termination) instead of graceful handling, same as any signal-handling program. ## All packages - [byteutil](packages/byteutil.md) -- source package, 5 public items - [crypto](packages/crypto.md) -- compiler-provided, 3 public items - [fs](packages/fs.md) -- compiler-provided, 6 public items - [http](packages/http.md) -- source package, 19 public items - [http2](packages/http2.md) -- source package, 122 public items - [json](packages/json.md) -- compiler-provided, 0 public items - [log](packages/log.md) -- compiler-provided, 4 public items - [net](packages/net.md) -- compiler-provided, 24 public items - [os](packages/os.md) -- compiler-provided, 14 public items - [proc](packages/proc.md) -- compiler-provided, 6 public items - [regex](packages/regex.md) -- compiler-provided, 9 public items - [sql](packages/sql.md) -- compiler-provided, 20 public items - [time](packages/time.md) -- compiler-provided, 3 public items --- slang is built and maintained by **Dolphlabs** (Dolph Tech Limited) — https://dolphlabs.com