chore: Nim replaces Mojo

Mojo 1.2.0 can't produce a single-file binary (runtime ships only as
shared libraries); Paul removed it from the bakeoff. Platform idea:
carlos/platform discussion 360.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 9630107fafa38c37090978aa4154ff7479c1688a parent 23eab4c
34 files changed, +685 −2210
  • apps/mojo/.gitignore +0 −1
  • apps/mojo/AGENTS.md +0 −28
  • apps/mojo/DEPS +0 −1
  • apps/mojo/Makefile +0 −123
  • apps/mojo/README.md +0 −109
  • apps/mojo/STATUS.md +0 −57
  • apps/mojo/scripts/embed.py +0 −38
  • apps/mojo/src/app.mojo +0 −179
  • apps/mojo/src/db.mojo +0 −189
  • apps/mojo/src/http.mojo +0 −237
  • apps/mojo/src/libc.mojo +0 −183
  • apps/mojo/src/main.mojo +0 −196
  • apps/mojo/src/render.mojo +0 −190
  • apps/mojo/static/app.css +0 −59
  • apps/mojo/static/app.js +0 −69
  • apps/mojo/tests/smoke.sh +0 −33
  • apps/mojo/tests/test_main.mojo +0 −312
  • builders/extra/mojo.md +0 −9
  • builders/extra/nim.md +0 −0
  • docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md +1 −1
  • docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md +2 −1
  • docs/toolchains.md +10 −15
  • harness/apps.go +1 −1
  • results/go-rastrillo.json +133 −15
  • results/go.json +160 −15
  • results/mojo.json +0 −102
  • results/ocaml.json +14 −14
  • results/rust.json +169 −15
  • results/zig.json +151 −15
  • scripts/measure-wake.sh +24 −0
  • scripts/smoke.sh +4 −1
  • scripts/smoke/nim/main.nim +1 −0
  • scripts/toolchains.sh +11 −2
  • scripts/zigcc-aarch64-musl +4 −0
diff --git a/apps/mojo/.gitignore b/apps/mojo/.gitignore
deleted file mode 100644
index 9a4d45b..0000000
--- a/apps/mojo/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-src/assets.mojo
diff --git a/apps/mojo/AGENTS.md b/apps/mojo/AGENTS.md
deleted file mode 100644
index 47d9a9a..0000000
--- a/apps/mojo/AGENTS.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# 🤖 AGENTS.md: todo-mojo
-
-The one rule: **the server owns the truth, and the page must work without
-JavaScript.** Every change is a plain form POST answered with a 303; the
-JavaScript only replays those same POSTs in the background and swaps in
-the server's own rendering. Nothing lives only in the browser.
-
-## Tenancy and trust
-
-- **One instance for the whole app.** It runs as `todo-mojo` on account
- `bfj`, host `todo-mojo.bfj.oncarlos.com`, channel `edge`.
-- **Each visitor's list is kept separately by an anonymous cookie.** The
- cookie holds 256 random bits from `getrandom(2)`, and is `HttpOnly;
- SameSite=Lax; Path=/`, plus `Secure` over https. A value the app
- doesn't recognise gets a fresh, empty list. Every query is scoped to
- the caller's list, so an id from someone else's list answers 404.
-- **No sign-in.** There are no accounts; losing the cookie loses the list.
-- **Trust class C, chosen on 2026-10-23.** Todo text is stored as plain
- text in SQLite, readable by the server and its operator. That's
- acceptable for a demo todo list and is written down here and in the
- README so nobody mistakes it for private storage.
-
-## Working here
-
-- Build, run, test and deploy with the `Makefile` targets (see README).
-- SQLite stays in WAL mode, one file at `--db`. Migrations only add.
-- Bind `127.0.0.1` only when running with `--listen`.
-- Commits end with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
diff --git a/apps/mojo/DEPS b/apps/mojo/DEPS
deleted file mode 100644
index a82556c..0000000
--- a/apps/mojo/DEPS
+++ /dev/null
@@ -1 +0,0 @@
-sqlite (https://www.sqlite.org/2026/sqlite-amalgamation-3540100.zip, 3.54.1, compiled from source and linked statically)
diff --git a/apps/mojo/Makefile b/apps/mojo/Makefile
deleted file mode 100644
index 6286538..0000000
--- a/apps/mojo/Makefile
+++ /dev/null
@@ -1,123 +0,0 @@
-# todo-mojo. See README.md.
-#
-# The host build uses the x86 Mojo from pixi. The shipped linux-arm64 build
-# runs Mojo inside the emulated arm64 root (scripts/arm64.sh), because Mojo
-# can't cross-link. SQLite is compiled from its amalgamation, once per
-# target, into .cache/ and linked in statically.
-
-SHELL := bash
-.SHELLFLAGS := -eu -o pipefail -c
-.DEFAULT_GOAL := build
-
-ROOT := $(abspath ../..)
-HERE := $(abspath .)
-export PATH := $(HOME)/.pixi/bin:$(PATH)
-
-VERSION ?= $(shell git rev-parse --short HEAD)
-MESSAGE ?= $(shell git log -1 --format=%s)
-
-ARM64 := $(ROOT)/scripts/arm64.sh
-ARM64_MOJO := /root/.pixi/bin/mojo
-
-CACHE := $(HERE)/.cache
-# Mojo's compilation cache is per user by default; keep ours in .cache/.
-HOST_MOJO_CACHE := $(CACHE)/mojo-cache/host
-ARM64_MOJO_CACHE := $(CACHE)/mojo-cache/arm64
-
-SQLITE_VERSION := 3540100
-SQLITE_ZIP := sqlite-amalgamation-$(SQLITE_VERSION).zip
-SQLITE_URL := https://www.sqlite.org/2026/$(SQLITE_ZIP)
-SQLITE_SHA3 := 92281c86e2957f9b9305890f9ac04204caa7b677cc868dc326d942689df28e30
-SQLITE_SRC := $(CACHE)/src/sqlite-amalgamation-$(SQLITE_VERSION)/sqlite3.c
-SQLITE_CFLAGS := -O2 -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION \
- -DSQLITE_DQS=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_OMIT_DEPRECATED \
- -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1
-HOST_SQLITE := $(CACHE)/build/host/libsqlite3.a
-ARM64_SQLITE := $(CACHE)/build/arm64/libsqlite3.a
-
-SOURCES := $(wildcard src/*.mojo)
-ASSETS := src/assets.mojo
-STATIC := $(ROOT)/shared/base.css $(ROOT)/shared/index.css static/app.css static/app.js
-VERSION_STAMP := $(CACHE)/version-$(VERSION)
-
-.PHONY: build build-arm64 test clean clean-deps bump unbump deploy run boot-check
-
-build: dist/app
-build-arm64: dist/app-linux-arm64
-
-dist/app: $(SOURCES) $(ASSETS) $(HOST_SQLITE) $(VERSION_STAMP)
- @mkdir -p dist $(HOST_MOJO_CACHE)
- MODULAR_CACHE_DIR=$(HOST_MOJO_CACHE) mojo build src/main.mojo -I src -D VERSION=$(VERSION) \
- -Xlinker $(HOST_SQLITE) -o $@
-
-dist/app-linux-arm64: $(SOURCES) $(ASSETS) $(ARM64_SQLITE) $(VERSION_STAMP)
- @mkdir -p dist $(ARM64_MOJO_CACHE)
- $(ARM64) /usr/bin/env MODULAR_CACHE_DIR=$(ARM64_MOJO_CACHE) $(ARM64_MOJO) build \
- $(HERE)/src/main.mojo -I $(HERE)/src -D VERSION=$(VERSION) \
- -Xlinker $(ARM64_SQLITE) -o $(HERE)/$@
-
-$(ASSETS): scripts/embed.py $(STATIC)
- python3 scripts/embed.py $@ BASE_CSS=$(ROOT)/shared/base.css INDEX_CSS=$(ROOT)/shared/index.css \
- APP_CSS=static/app.css APP_JS=static/app.js
-
-# The version is compiled in, so a new commit means a new build.
-$(VERSION_STAMP):
- @mkdir -p $(CACHE)
- @rm -f $(CACHE)/version-*
- @touch $@
-
-$(CACHE)/src/$(SQLITE_ZIP):
- @mkdir -p $(dir $@)
- curl -fsSL -o $@.part $(SQLITE_URL)
- @test "$$(openssl dgst -sha3-256 -r $@.part | cut -d' ' -f1)" = $(SQLITE_SHA3) \
- || { echo "sqlite: checksum mismatch" >&2; rm -f $@.part; exit 1; }
- mv $@.part $@
-
-$(SQLITE_SRC): $(CACHE)/src/$(SQLITE_ZIP)
- cd $(CACHE)/src && unzip -oq $(SQLITE_ZIP)
- @touch $@
-
-$(HOST_SQLITE): $(SQLITE_SRC)
- @mkdir -p $(dir $@)
- cc $(SQLITE_CFLAGS) -c $< -o $(dir $@)sqlite3.o
- ar rcs $@ $(dir $@)sqlite3.o
-
-$(ARM64_SQLITE): $(SQLITE_SRC)
- @mkdir -p $(dir $@)
- $(ARM64) cc $(SQLITE_CFLAGS) -c $< -o $(dir $@)sqlite3.o
- $(ARM64) ar rcs $@ $(dir $@)sqlite3.o
-
-test: dist/app $(SOURCES) $(ASSETS) tests/test_main.mojo $(HOST_SQLITE)
- @mkdir -p $(CACHE)/test $(HOST_MOJO_CACHE)
- MODULAR_CACHE_DIR=$(HOST_MOJO_CACHE) mojo build tests/test_main.mojo -I src \
- -Xlinker $(HOST_SQLITE) -o $(CACHE)/test/test_main
- $(CACHE)/test/test_main
- tests/smoke.sh dist/app
-
-# Runs the host build on 127.0.0.1 for the shared suite.
-PORT ?= 39517
-DB ?= $(TMPDIR)/mojo.db
-run: dist/app
- dist/app --listen 127.0.0.1:$(PORT) --db $(DB)
-
-boot-check: dist/app-linux-arm64
- $(ROOT)/scripts/boot-check.sh dist/app-linux-arm64
-
-clean:
- rm -rf dist $(ASSETS) $(CACHE)/test $(CACHE)/version-*
-
-# Keeps the downloaded SQLite source; drops everything compiled from it,
-# and Mojo's compilation caches.
-clean-deps: clean
- rm -rf $(CACHE)/build $(CACHE)/mojo-cache
-
-BUMP_FROM := served by todo-mojo -->
-BUMP_TO := served by todo-mojo (bumped) -->
-bump:
- sed -i 's/$(BUMP_FROM)/$(BUMP_TO)/' src/app.mojo
-unbump:
- sed -i 's/$(BUMP_TO)/$(BUMP_FROM)/' src/app.mojo
-
-deploy: dist/app-linux-arm64
- carlos deploy --account bfj --app todo-mojo --target linux-arm64 --no-prompt \
- --message "$(MESSAGE)" dist/app-linux-arm64
diff --git a/apps/mojo/README.md b/apps/mojo/README.md
deleted file mode 100644
index 55c5069..0000000
--- a/apps/mojo/README.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# 🤖 todo-mojo
-
-TodoMVC for the CARLOS bakeoff, written in [Mojo](https://mojolang.org)
-1.2.0. The server renders every page, every change is a plain form POST,
-and the whole app works with JavaScript turned off. A small script makes
-it behave like the reference TodoMVC when JavaScript is on.
-
-It builds and passes the shared suite, but it isn't deployed: see
-`STATUS.md`. In short, every Mojo binary loads
-`libKGENCompilerRTShared.so` at start-up, Mojo ships no static version of
-it, and Carloku's binary kind takes exactly one file.
-
-## The stack, and why
-
-Mojo's standard library has no networking, no SQLite binding and no
-templating, and Mojo 1.2.0 has no package that covers them and builds for
-linux-arm64 without other shared libraries. So the app is small, plain
-Mojo on top of two C interfaces it already has:
-
-- **libc, through `external_call`** (`src/libc.mojo`). The server is one
- thread running a `poll(2)` loop over non-blocking sockets, so a slow or
- idle browser connection never holds up anyone else. It speaks
- HTTP/1.1 with keep-alive, which is enough for browsers and the
- Carloku edge. The Mojo runtime starts its own worker threads and
- catches SIGTERM, so the app installs a small C-ABI handler that writes
- to a pipe the loop watches, and shutdown is an ordinary return from the
- loop: close the sockets, close SQLite (which
- checkpoints the WAL), remove the socket file, exit 0.
-- **SQLite's C API** (`src/db.mojo`), from the 3.54.1 amalgamation,
- compiled by the Makefile for each target and linked in statically.
- It runs in WAL mode with `synchronous=NORMAL`, and migrations are
- numbered in `PRAGMA user_version` and only ever add.
-- **HTML is built with string appends** (`src/render.mojo`), with one
- `escape` function for every piece of user text. The page is TodoMVC's
- own template from `shared/reference.html`.
-- **The static files are compiled in.** `scripts/embed.py` turns
- `base.css`, `index.css`, `app.css` and `app.js` into Mojo string
- constants, so the binary is the whole app and the CSS is served byte
- for byte.
-
-### Without JavaScript, and with it
-
-Each control the contract asks for is a real form or link, laid over the
-spot you'd click with JavaScript on, so the page looks the same either
-way: an invisible submit button over each checkbox, another over the
-"mark all" chevron, and an edit link over each title. The forms use
-`display: contents` so they add no boxes to TodoMVC's layout.
-
-`static/app.js` (one ES module, no build step) marks the page with
-`html.js`, which hides those controls, and then sends the same form POSTs
-with `fetch`. It adds the header `X-Requested-With: fetch`, and for that
-header the server answers 200 with the updated page instead of a 303; the
-script swaps in the new `section.main` and `footer.footer`. Requests go
-one at a time, in order. Double-click, Enter, blur and Escape work as in
-TodoMVC.
-
-### Lists and cookies
-
-Each visitor's list is keyed by a cookie, `todo_list`, holding 256 random
-bits from `getrandom(2)` as hex. A missing or malformed value gets a new
-one, and so a new, empty list. Every query includes the list key, so an
-id from another list answers 404 and changes nothing. A POST whose
-`Origin` isn't `https://$CARLOS_HOST` (or `http://<listen address>`
-locally) answers 403.
-
-## Honest trade-offs
-
-- **Trust class C** (2026-10-23): todo text is stored as plain text and
- the server can read it. The cookie value is also stored as is, so a
- copy of the database is enough to open anyone's list.
-- **One thread.** Requests are handled one after another. Each takes well
- under a millisecond, so this is fine for a todo list, and it means one
- SQLite connection with no locking to think about.
-- **A deliberately small HTTP server.** No chunked request bodies (a 400),
- no compression, request heads up to 64 KiB and bodies up to 1 MiB.
-- **Not one file.** The binary depends on Mojo's runtime libraries, which
- is why it isn't on Carloku.
-
-## Build, run, test, deploy
-
-Toolchains come from `source ../../scripts/env.sh` (the Makefile also
-puts pixi's `mojo` on `PATH`).
-
-| Command | What it does |
-|---|---|
-| `make build` | Host build into `dist/app` |
-| `make build-arm64` | linux-arm64 build into `dist/app-linux-arm64`, run inside the emulated arm64 root (`scripts/arm64.sh`); slow, since everything is emulated |
-| `make test` | Builds and runs `tests/test_main.mojo`: parsing, escaping, the store, and every route through `App.handle` |
-| `make run` | Runs the host build on `127.0.0.1:39517` (`PORT=`, `DB=` to change) |
-| `make boot-check` | `scripts/boot-check.sh` on the arm64 binary, under qemu |
-| `make clean` | Removes `dist/` and generated files |
-| `make clean-deps` | Also removes the compiled SQLite and Mojo's compilation caches (kept in `.cache/`); the downloaded SQLite source stays |
-| `make bump` / `make unbump` | Changes / restores the page comment in `src/app.mojo` |
-| `make deploy` | `carlos deploy` of `dist/app-linux-arm64` to `todo-mojo` on account `bfj` |
-
-The first build downloads the SQLite amalgamation into `.cache/src/` and
-checks its SHA3-256.
-
-The shared behaviour suite:
-
-```sh
-make build
-dist/app --listen 127.0.0.1:39517 --db "$TMPDIR/mojo.db" &
-../../scripts/test-app.sh http://127.0.0.1:39517 "$TMPDIR/mojo-suite.json"
-```
-
-The binary takes `--socket <path> --db <path>` on Carloku, or
-`--listen 127.0.0.1:<port> --db <path>` locally, and refuses to listen on
-any other address.
diff --git a/apps/mojo/STATUS.md b/apps/mojo/STATUS.md
deleted file mode 100644
index b8cb175..0000000
--- a/apps/mojo/STATUS.md
+++ /dev/null
@@ -1,57 +0,0 @@
-BLOCKED platform: single-file binaries only (Mojo needs libKGENCompilerRTShared.so)
-
-## Where it got stuck
-
-The app is finished and tested; it can't be shipped as Carloku's binary
-kind, which takes a single file (`carlos deploy --target`: "binary kind,
-single file").
-
-What works, at commit `a874c81` (2026-10-23):
-
-- `make test`: 19 unit tests plus a process-level smoke test (unix
- socket, keep-alive, cookie round trip, clean exit on SIGTERM) pass.
-- Shared behaviour suite against the host build
- (`dist/app --listen 127.0.0.1:39517`): **67 passed, 3 skipped**, the
- same as the reference fixture.
-- `make build-arm64` produces `dist/app-linux-arm64` inside the arm64
- root (about 45 s for the app once SQLite is compiled; SQLite itself
- takes about 15 minutes emulated, once).
-- The shared suite against that arm64 binary under qemu: **67 passed,
- 3 skipped**. `scripts/boot-check.sh dist/app-linux-arm64`: `/healthz`
- 200, `/api/version` 200 `a874c81`, clean exit on SIGTERM.
-
-Why it isn't deployed:
-
-- `readelf -d dist/app-linux-arm64` shows `NEEDED
- libKGENCompilerRTShared.so` (besides `libc.so.6`) and `RUNPATH
- /root/.pixi/envs/mojo/lib`. That library in turn needs `libstdc++`,
- `libMSupportGlobals.so` and `libAsyncRTRuntimeGlobals.so`. Boot-check
- and the qemu run only pass because `qemu-aarch64-static -L <arm64 root>`
- finds them in the root's pixi install; a Carloku instance sees only its
- own build.
-- I looked for a supported static route and found none: Mojo 1.2.0 ships
- no static archive of its runtime (no `libKGEN*.a` in either the x86 or
- the arm64 pixi env, only the `.so`), and `mojo build` has no static or
- standalone option (`--emit` offers `exe`, `object`, `shared-lib` and
- IR formats; `--help-hidden` adds nothing for linking the runtime). The
- runtime also starts 16 worker threads before `main`, so even
- `--emit object` output depends on it.
-- As the brief says, I didn't bundle, embed or self-extract the
- libraries to get around the single-file rule, and I didn't create the
- Carloku app or instance, since nothing could be deployed to it.
-
-What would unblock it: either Carloku's binary kind accepting a binary
-plus its shared libraries (a directory or archive release), or Modular
-shipping a static `libKGENCompilerRTShared` (and its dependencies). With
-either, `make build-arm64 && make deploy` after the three first-time
-`carlos` commands in BRIEF.md should be all that's left.
-
-Notes for the report:
-
-- SIGTERM: the Mojo runtime installs its own SIGTERM handler, which ends
- the process with status 143, and its threads exist before `main`, so a
- signal mask or `signalfd` can't catch it. The app installs a C-ABI
- handler that writes to a pipe its poll loop watches (`src/libc.mojo`).
-- Mojo's standard library has no sockets or SQLite binding, so both go
- through `external_call` to libc and to SQLite compiled from the
- amalgamation.
diff --git a/apps/mojo/scripts/embed.py b/apps/mojo/scripts/embed.py
deleted file mode 100644
index 76055d8..0000000
--- a/apps/mojo/scripts/embed.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""Writes src/assets.mojo: the static files as Mojo string constants.
-
- python3 scripts/embed.py src/assets.mojo NAME=path [NAME=path ...]
-
-The files are served byte for byte, so they must be UTF-8. Quotes,
-backslashes and control characters are escaped; everything else,
-non-ASCII included, is written as is, because Mojo's \\x escapes name
-code points, not bytes.
-"""
-import sys
-
-
-def literal(data: bytes) -> str:
- out = []
- for c in data.decode("utf-8"):
- if c == "\\":
- out.append("\\\\")
- elif c == '"':
- out.append('\\"')
- elif ord(c) < 0x20 or ord(c) == 0x7F:
- out.append("\\x%02x" % ord(c))
- else:
- out.append(c)
- return '"' + "".join(out) + '"'
-
-
-def main() -> None:
- dest, pairs = sys.argv[1], sys.argv[2:]
- lines = ['"""Generated by scripts/embed.py from the static files. Do not edit."""', ""]
- for pair in pairs:
- name, path = pair.split("=", 1)
- with open(path, "rb") as f:
- lines.append(f"comptime {name} = {literal(f.read())}")
- with open(dest, "w", encoding="utf-8") as f:
- f.write("\n".join(lines) + "\n")
-
-
-main()
diff --git a/apps/mojo/src/app.mojo b/apps/mojo/src/app.mojo
deleted file mode 100644
index 5d803c4..0000000
--- a/apps/mojo/src/app.mojo
+++ /dev/null
@@ -1,179 +0,0 @@
-"""Routes: the pages and the form actions from CONTRACT.md."""
-
-from std.sys import get_defined_string
-from db import Store
-from http import Request, Response, text, redirect
-from render import page, filter_path
-from libc import random_bytes
-import assets
-
-comptime VERSION = get_defined_string["VERSION", "dev"]()
-comptime LANGUAGE = "Mojo"
-# `make bump` edits this string so the next build has to recompile this file.
-comptime PAGE_COMMENT = "<!-- served by todo-mojo -->\n"
-comptime COOKIE = "todo_list"
-comptime TOKEN_BYTES = 32
-
-
-struct App(Movable):
- var store: Store
- # The origin a browser sends for our own pages, e.g. https://<CARLOS_HOST>.
- # A POST from any other origin is refused.
- var origins: List[String]
- var secure_cookie: Bool
-
- def __init__(out self, var store: Store, var origins: List[String], secure_cookie: Bool):
- self.store = store^
- self.origins = origins^
- self.secure_cookie = secure_cookie
-
- def handle(self, req: Request) -> Response:
- try:
- return self._route(req)
- except e:
- print("error:", req.method, req.path, e)
- return text(500, "Internal Server Error")
-
- def _route(self, req: Request) raises -> Response:
- var get = req.method == "GET" or req.method == "HEAD"
- var p = req.path
- if p == "/healthz":
- return text(200, "ok") if get else _not_allowed()
- if p == "/api/version":
- return text(200, VERSION) if get else _not_allowed()
- if p.startswith("/static/"):
- return _static(p) if get else _not_allowed()
- if p == "/" or p == "/active" or p == "/completed":
- if not get:
- return _not_allowed()
- var filter = "all" if p == "/" else String(p[byte=1:])
- return self._page(req, filter)
- if p.startswith("/todos"):
- if req.method != "POST":
- return _not_allowed()
- return self._action(req)
- return text(404, "Not Found")
-
- def _list(self, req: Request, mut resp: Response) raises -> String:
- """The caller's list token, issuing a fresh one (and list) if theirs is missing or garbled."""
- var token = req.cookie(COOKIE)
- if _valid_token(token):
- return token^
- token = _new_token()
- var cookie = COOKIE + "=" + token + "; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax"
- if self.secure_cookie or req.header("x-forwarded-proto") == "https":
- cookie += "; Secure"
- resp.set("Set-Cookie", cookie)
- return token^
-
- def _page(self, req: Request, filter: String) raises -> Response:
- var resp = Response(200, "text/html; charset=utf-8", String())
- var list = self._list(req, resp)
- var edit_id = -1
- var edit = req.query_value("edit")
- if edit:
- try:
- edit_id = Int(edit)
- except:
- pass
- resp.body = page(self.store.todos(list), filter, edit_id, LANGUAGE, VERSION) + PAGE_COMMENT
- resp.set("Cache-Control", "no-store")
- _page_headers(resp)
- return resp^
-
- def _action(self, req: Request) raises -> Response:
- var origin = req.header("origin")
- if req.has_header("origin") and origin not in self.origins:
- return text(403, "Forbidden: cross-origin request")
- var filter = req.form("filter")
- if filter != "active" and filter != "completed":
- filter = "all"
- var resp = redirect(String(filter_path(filter)))
- var list = self._list(req, resp)
- var parts = req.path.split("/") # "", "todos", <id or verb>, <verb>
- var found = True
- if len(parts) == 2:
- var title = String(req.form("title").strip())
- if title:
- self.store.add(list, title)
- elif len(parts) == 3 and parts[2] == "toggle-all":
- self.store.toggle_all(list)
- elif len(parts) == 3 and parts[2] == "clear-completed":
- self.store.clear_completed(list)
- elif len(parts) == 4:
- var id: Int
- try:
- id = Int(parts[2])
- except:
- return text(404, "Not Found")
- var verb = parts[3]
- if verb == "toggle":
- found = self.store.toggle(list, id)
- elif verb == "destroy":
- found = self.store.destroy(list, id)
- elif verb == "edit":
- var title = String(req.form("title").strip())
- found = self.store.rename(list, id, title) if title else self.store.destroy(list, id)
- else:
- return text(404, "Not Found")
- else:
- return text(404, "Not Found")
- if not found:
- return text(404, "Not Found")
- # The page's own script asks for the updated page in the same round trip.
- if req.header("x-requested-with") == "fetch":
- var page_resp = self._page(req, filter)
- for h in resp.headers:
- if h.name == "Set-Cookie":
- page_resp.set(h.name, h.value)
- return page_resp^
- return resp^
-
-
-def _valid_token(t: StringSlice) -> Bool:
- if t.byte_length() != TOKEN_BYTES * 2:
- return False
- for c in t.as_bytes():
- if not ((c >= 48 and c <= 57) or (c >= 97 and c <= 102)):
- return False
- return True
-
-
-def _new_token() raises -> String:
- comptime digits = "0123456789abcdef"
- var out = String(capacity_bytes=TOKEN_BYTES * 2)
- for b in random_bytes(TOKEN_BYTES):
- out += digits[byte = Int(b >> 4)]
- out += digits[byte = Int(b & 15)]
- return out^
-
-
-def _page_headers(mut resp: Response):
- resp.set("X-Content-Type-Options", "nosniff")
- resp.set("Referrer-Policy", "same-origin")
- resp.set(
- "Content-Security-Policy",
- "default-src 'self'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
- )
-
-
-def _not_allowed() -> Response:
- return text(405, "Method Not Allowed")
-
-
-def _static(path: StringSlice) -> Response:
- var name = path[byte=8:]
- var r: Response
- if name == "base.css":
- r = Response(200, "text/css; charset=utf-8", String(assets.BASE_CSS))
- elif name == "index.css":
- r = Response(200, "text/css; charset=utf-8", String(assets.INDEX_CSS))
- elif name == "app.css":
- r = Response(200, "text/css; charset=utf-8", String(assets.APP_CSS))
- elif name == "app.js":
- r = Response(200, "text/javascript; charset=utf-8", String(assets.APP_JS))
- else:
- return text(404, "Not Found")
- r.set("Cache-Control", "public, max-age=3600")
- r.set("X-Content-Type-Options", "nosniff")
- return r^
diff --git a/apps/mojo/src/db.mojo b/apps/mojo/src/db.mojo
deleted file mode 100644
index deb88bd..0000000
--- a/apps/mojo/src/db.mojo
+++ /dev/null
@@ -1,189 +0,0 @@
-"""The todo store: SQLite, linked in statically and called through its C API."""
-
-from std.ffi import external_call, c_int
-from libc import cstring
-
-comptime SQLITE_OK = 0
-comptime SQLITE_ROW = 100
-comptime SQLITE_DONE = 101
-comptime SQLITE_OPEN_READWRITE = 0x2
-comptime SQLITE_OPEN_CREATE = 0x4
-comptime SQLITE_OPEN_NOMUTEX = 0x8000
-# SQLITE_TRANSIENT: SQLite copies bound text before the call returns.
-comptime SQLITE_TRANSIENT = -1
-
-def migrations() -> List[StaticString]:
- """Migrations only ever add. Each runs once, in order, inside a
- transaction, and its index + 1 is recorded in PRAGMA user_version."""
- return [
- """
- CREATE TABLE todos (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- list TEXT NOT NULL,
- title TEXT NOT NULL,
- completed INTEGER NOT NULL DEFAULT 0,
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
- );
- CREATE INDEX todos_by_list ON todos (list, id);
- """,
- ]
-
-
-@fieldwise_init
-struct Todo(Copyable, Movable):
- var id: Int
- var title: String
- var completed: Bool
-
-
-struct Stmt(Movable):
- """One prepared statement, finalized when it goes out of scope."""
-
- var handle: Int
- var db: Int
-
- def __init__(out self, db: Int, sql: StringSlice) raises:
- self.db = db
- self.handle = 0
- var rc = external_call["sqlite3_prepare_v2", c_int](
- db, sql.unsafe_ptr(), c_int(sql.byte_length()), Pointer(to=self.handle), Int(0)
- )
- if rc != SQLITE_OK:
- raise Error("prepare: " + _errmsg(db))
-
- def __deinit__(deinit self):
- _ = external_call["sqlite3_finalize", c_int](self.handle)
-
- def bind(mut self, i: Int, v: Int) raises:
- if external_call["sqlite3_bind_int64", c_int](self.handle, c_int(i), Int64(v)) != SQLITE_OK:
- raise Error("bind: " + _errmsg(self.db))
-
- def bind(mut self, i: Int, v: StringSlice) raises:
- var rc = external_call["sqlite3_bind_text", c_int](
- self.handle, c_int(i), v.unsafe_ptr(), c_int(v.byte_length()), Int(SQLITE_TRANSIENT)
- )
- if rc != SQLITE_OK:
- raise Error("bind: " + _errmsg(self.db))
-
- def step(mut self) raises -> Bool:
- """Advances one row. True while there is a row to read."""
- var rc = external_call["sqlite3_step", c_int](self.handle)
- if rc == SQLITE_ROW:
- return True
- if rc == SQLITE_DONE:
- return False
- raise Error("step: " + _errmsg(self.db))
-
- def int(self, col: Int) -> Int:
- return Int(external_call["sqlite3_column_int64", Int64](self.handle, c_int(col)))
-
- def text(self, col: Int) -> String:
- var p = external_call["sqlite3_column_text", Pointer[UInt8, MutAnyOrigin]](self.handle, c_int(col))
- var n = Int(external_call["sqlite3_column_bytes", c_int](self.handle, c_int(col)))
- if n == 0:
- return String()
- return String(from_utf8_lossy=Span[UInt8](unsafe_ptr=p, length=n))
-
-
-def _errmsg(db: Int) -> String:
- var p = external_call["sqlite3_errmsg", Pointer[UInt8, MutAnyOrigin]](db)
- return String(unsafe_from_utf8_ptr=p)
-
-
-struct Store(Movable):
- """Every query takes the caller's list token, so one list never sees another."""
-
- var db: Int
-
- def __init__(out self, path: String) raises:
- self.db = 0
- var p = cstring(path)
- var flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX
- var rc = external_call["sqlite3_open_v2", c_int](p.unsafe_ptr(), Pointer(to=self.db), c_int(flags), Int(0))
- if rc != SQLITE_OK:
- var msg = _errmsg(self.db) if self.db != 0 else String("out of memory")
- _ = external_call["sqlite3_close", c_int](self.db)
- raise Error("open " + path + ": " + msg)
- _ = external_call["sqlite3_busy_timeout", c_int](self.db, c_int(5000))
- self.exec("PRAGMA journal_mode = WAL")
- self.exec("PRAGMA synchronous = NORMAL")
- self.migrate()
-
- def __deinit__(deinit self):
- _ = external_call["sqlite3_close", c_int](self.db)
-
- def exec(self, sql: StringSlice) raises:
- var s = cstring(String(sql))
- var rc = external_call["sqlite3_exec", c_int](self.db, s.unsafe_ptr(), Int(0), Int(0), Int(0))
- if rc != SQLITE_OK:
- raise Error("exec: " + _errmsg(self.db))
-
- def migrate(self) raises:
- var st = Stmt(self.db, "PRAGMA user_version")
- _ = st.step()
- var have = st.int(0)
- var all = migrations()
- for i in range(have, len(all)):
- self.exec("BEGIN IMMEDIATE")
- try:
- self.exec(all[i])
- self.exec("PRAGMA user_version = " + String(i + 1))
- self.exec("COMMIT")
- except e:
- self.exec("ROLLBACK")
- raise e^
-
- def changes(self) -> Int:
- return Int(external_call["sqlite3_changes", c_int](self.db))
-
- def todos(self, list: String) raises -> List[Todo]:
- var st = Stmt(self.db, "SELECT id, title, completed FROM todos WHERE list = ? ORDER BY id")
- st.bind(1, list)
- var out = List[Todo]()
- while st.step():
- out.append(Todo(st.int(0), st.text(1), st.int(2) != 0))
- return out^
-
- def add(self, list: String, title: String) raises:
- var st = Stmt(self.db, "INSERT INTO todos (list, title) VALUES (?, ?)")
- st.bind(1, list)
- st.bind(2, title)
- _ = st.step()
-
- def toggle(self, list: String, id: Int) raises -> Bool:
- var st = Stmt(self.db, "UPDATE todos SET completed = 1 - completed WHERE id = ? AND list = ?")
- st.bind(1, id)
- st.bind(2, list)
- _ = st.step()
- return self.changes() > 0
-
- def rename(self, list: String, id: Int, title: String) raises -> Bool:
- var st = Stmt(self.db, "UPDATE todos SET title = ? WHERE id = ? AND list = ?")
- st.bind(1, title)
- st.bind(2, id)
- st.bind(3, list)
- _ = st.step()
- return self.changes() > 0
-
- def destroy(self, list: String, id: Int) raises -> Bool:
- var st = Stmt(self.db, "DELETE FROM todos WHERE id = ? AND list = ?")
- st.bind(1, id)
- st.bind(2, list)
- _ = st.step()
- return self.changes() > 0
-
- def toggle_all(self, list: String) raises:
- """Completes every item, or reopens them all if all were completed."""
- var st = Stmt(
- self.db,
- """UPDATE todos SET completed = NOT (
- SELECT coalesce(min(completed), 0) FROM todos WHERE list = ?1
- ) WHERE list = ?1""",
- )
- st.bind(1, list)
- _ = st.step()
-
- def clear_completed(self, list: String) raises:
- var st = Stmt(self.db, "DELETE FROM todos WHERE list = ? AND completed = 1")
- st.bind(1, list)
- _ = st.step()
diff --git a/apps/mojo/src/http.mojo b/apps/mojo/src/http.mojo
deleted file mode 100644
index d5b5502..0000000
--- a/apps/mojo/src/http.mojo
+++ /dev/null
@@ -1,237 +0,0 @@
-"""HTTP/1.1 requests and responses: just enough for a browser and a proxy."""
-
-comptime MAX_HEAD = 64 * 1024
-comptime MAX_BODY = 1024 * 1024
-
-
-@fieldwise_init
-struct Header(Copyable, Movable):
- var name: String # lower-cased
- var value: String
-
-
-struct Request(Movable):
- var method: String
- var path: String
- var query: String
- var headers: List[Header]
- var body: String
- var keep_alive: Bool
-
- def __init__(out self, method: String, target: String):
- self.method = method
- var q = target.find("?")
- if q >= 0:
- self.path = String(target[byte=:q])
- self.query = String(target[byte = q + 1 :])
- else:
- self.path = target
- self.query = String()
- self.headers = List[Header]()
- self.body = String()
- self.keep_alive = True
-
- def header(self, name: StringSlice) -> String:
- """The first header called `name` (lower-case), or empty."""
- for h in self.headers:
- if h.name == name:
- return h.value
- return String()
-
- def has_header(self, name: StringSlice) -> Bool:
- for h in self.headers:
- if h.name == name:
- return True
- return False
-
- def cookie(self, name: StringSlice) -> String:
- for h in self.headers:
- if h.name != "cookie":
- continue
- for part in h.value.split(";"):
- var kv = part.strip()
- var eq = kv.find("=")
- if eq > 0 and kv[byte=:eq] == name:
- return String(kv[byte = eq + 1 :])
- return String()
-
- def form(self, name: StringSlice) -> String:
- return form_value(self.body, name)
-
- def query_value(self, name: StringSlice) -> String:
- return form_value(self.query, name)
-
-
-struct Response(Movable):
- var status: Int
- var headers: List[Header]
- var body: String
-
- def __init__(out self, status: Int, content_type: String, var body: String):
- self.status = status
- self.headers = List[Header]()
- self.body = body^
- if content_type:
- self.set("Content-Type", content_type)
-
- def set(mut self, name: String, value: String):
- self.headers.append(Header(name, value))
-
- def header(self, name: StringSlice) -> String:
- for h in self.headers:
- if h.name == name:
- return h.value
- return String()
-
- def write_to(self, mut out: List[UInt8], head_only: Bool, keep_alive: Bool):
- var head = String("HTTP/1.1 ", self.status, " ", reason(self.status), "\r\n")
- for h in self.headers:
- head += h.name + ": " + h.value + "\r\n"
- head += String("Content-Length: ", self.body.byte_length(), "\r\n")
- if not keep_alive:
- head += "Connection: close\r\n"
- head += "\r\n"
- out.extend(head.as_bytes())
- if not head_only:
- out.extend(self.body.as_bytes())
-
-
-def text(status: Int, body: String) -> Response:
- return Response(status, "text/plain; charset=utf-8", body + "\n")
-
-
-def redirect(location: String) -> Response:
- var r = Response(303, "", String())
- r.set("Location", location)
- return r^
-
-
-def reason(status: Int) -> StaticString:
- if status == 200:
- return "OK"
- if status == 303:
- return "See Other"
- if status == 400:
- return "Bad Request"
- if status == 403:
- return "Forbidden"
- if status == 404:
- return "Not Found"
- if status == 405:
- return "Method Not Allowed"
- if status == 413:
- return "Content Too Large"
- if status == 501:
- return "Not Implemented"
- return "Internal Server Error"
-
-
-# Parsing --------------------------------------------------------------------
-
-comptime NEED_MORE = 0
-comptime PARSED = 1
-comptime BAD = 2
-comptime TOO_LARGE = 3
-
-
-def find_head_end(buf: List[UInt8]) -> Int:
- """Index just past the blank line ending the head, or -1."""
- var n = len(buf)
- var i = 3
- while i < n:
- if buf[i] == 10 and buf[i - 1] == 13 and buf[i - 2] == 10 and buf[i - 3] == 13:
- return i + 1
- i += 1
- return -1
-
-
-def parse_request(buf: List[UInt8], mut consumed: Int, mut req: Request) -> Int:
- """Parses one request from the front of `buf`.
-
- Returns PARSED (with `consumed` bytes used and `req` filled), NEED_MORE,
- BAD or TOO_LARGE.
- """
- var end = find_head_end(buf)
- if end < 0:
- return TOO_LARGE if len(buf) > MAX_HEAD else NEED_MORE
- if end > MAX_HEAD:
- return TOO_LARGE
- var head = String(from_utf8_lossy=Span(buf)[:end - 4])
- var lines = head.split("\r\n")
- var first = lines[0].split(" ")
- if len(first) != 3 or not first[2].startswith("HTTP/1."):
- return BAD
- req = Request(String(first[0]), String(first[1]))
- req.keep_alive = first[2] == "HTTP/1.1"
- var length = 0
- for i in range(1, len(lines)):
- var line = lines[i]
- var colon = line.find(":")
- if colon <= 0:
- return BAD
- var name = String(line[byte=:colon]).lower()
- var value = String(line[byte = colon + 1 :].strip())
- if name == "content-length":
- try:
- length = Int(value)
- except:
- return BAD
- if length < 0:
- return BAD
- elif name == "transfer-encoding":
- return BAD
- elif name == "connection":
- var v = value.lower()
- if v == "close":
- req.keep_alive = False
- elif v == "keep-alive":
- req.keep_alive = True
- req.headers.append(Header(name^, value^))
- if length > MAX_BODY:
- return TOO_LARGE
- if len(buf) < end + length:
- return NEED_MORE
- req.body = String(from_utf8_lossy=Span(buf)[end : end + length])
- consumed = end + length
- return PARSED
-
-
-# Forms ----------------------------------------------------------------------
-
-
-def _hex(c: UInt8) -> Int:
- if c >= 48 and c <= 57:
- return Int(c) - 48
- if c >= 97 and c <= 102:
- return Int(c) - 87
- if c >= 65 and c <= 70:
- return Int(c) - 55
- return -1
-
-
-def url_decode(s: StringSlice) -> String:
- """Decodes application/x-www-form-urlencoded text: `+` and `%XX`."""
- var b = s.as_bytes()
- var out = List[UInt8](capacity=len(b))
- var i = 0
- while i < len(b):
- var c = b[i]
- if c == 43: # '+'
- out.append(32)
- elif c == 37 and i + 2 < len(b) and _hex(b[i + 1]) >= 0 and _hex(b[i + 2]) >= 0:
- out.append(UInt8(_hex(b[i + 1]) * 16 + _hex(b[i + 2])))
- i += 2
- else:
- out.append(c)
- i += 1
- return String(from_utf8_lossy=out)
-
-
-def form_value(encoded: StringSlice, name: StringSlice) -> String:
- """The first value of `name` in an urlencoded string, decoded, or empty."""
- for pair in encoded.split("&"):
- var eq = pair.find("=")
- var key = pair if eq < 0 else pair[byte=:eq]
- if url_decode(key) == name:
- return url_decode(pair[byte = eq + 1 :]) if eq >= 0 else String()
- return String()
diff --git a/apps/mojo/src/libc.mojo b/apps/mojo/src/libc.mojo
deleted file mode 100644
index dfadd9d..0000000
--- a/apps/mojo/src/libc.mojo
+++ /dev/null
@@ -1,183 +0,0 @@
-"""The few libc calls the server needs, declared once.
-
-Mojo's standard library has no sockets, so the server talks to the kernel
-through these thin wrappers. Every constant here has the same value on
-linux-x86_64 and linux-arm64.
-"""
-
-from std.ffi import external_call, c_int
-
-comptime AF_UNIX = 1
-comptime AF_INET = 2
-comptime SOCK_STREAM = 1
-comptime SOCK_NONBLOCK = 0o4000
-comptime SOCK_CLOEXEC = 0o2000000
-comptime SOL_SOCKET = 1
-comptime SO_REUSEADDR = 2
-comptime MSG_NOSIGNAL = 0x4000
-
-comptime POLLIN: Int16 = 1
-comptime POLLOUT: Int16 = 4
-comptime POLLERR: Int16 = 8
-comptime POLLHUP: Int16 = 16
-
-comptime EINTR = 4
-comptime EAGAIN = 11
-
-comptime SIGINT = 2
-comptime SIGTERM = 15
-
-
-def errno() -> Int:
- var p = external_call["__errno_location", Pointer[Int32, MutAnyOrigin]]()
- return Int(p[])
-
-
-def cstring(s: String) -> List[UInt8]:
- """Returns the bytes of `s` followed by a NUL, for C APIs that want one."""
- var b = List[UInt8](capacity=s.byte_length() + 1)
- for c in s.as_bytes():
- b.append(c)
- b.append(0)
- return b^
-
-
-def close_fd(fd: Int):
- _ = external_call["close", c_int](c_int(fd))
-
-
-def unlink(path: String):
- var p = cstring(path)
- _ = external_call["unlink", c_int](p.unsafe_ptr())
-
-
-def now() -> Int:
- """Seconds since the epoch."""
- return external_call["time", Int](Int(0))
-
-
-def random_bytes(n: Int) raises -> List[UInt8]:
- var buf = List[UInt8](length=n, fill=0)
- var got = 0
- while got < n:
- var r = external_call["getrandom", Int](buf.unsafe_ptr().unsafe_offset(got), n - got, c_int(0))
- if r < 0:
- if errno() == EINTR:
- continue
- raise Error("getrandom failed")
- got += r
- return buf^
-
-
-def _set_reuseaddr(fd: Int):
- var one = Int32(1)
- _ = external_call["setsockopt", c_int](
- c_int(fd), c_int(SOL_SOCKET), c_int(SO_REUSEADDR), Pointer(to=one), c_int(4)
- )
-
-
-def _bind_listen(fd: Int, addr: List[UInt8], what: String) raises:
- if external_call["bind", c_int](c_int(fd), addr.unsafe_ptr(), c_int(len(addr))) != 0:
- var e = errno()
- close_fd(fd)
- raise Error("bind " + what + ": errno " + String(e))
- if external_call["listen", c_int](c_int(fd), c_int(512)) != 0:
- var e = errno()
- close_fd(fd)
- raise Error("listen " + what + ": errno " + String(e))
-
-
-def listen_unix(path: String) raises -> Int:
- """Listens on a unix socket at `path`, replacing a stale one."""
- if path.byte_length() > 107:
- raise Error("socket path too long: " + path)
- var fd = Int(external_call["socket", c_int](c_int(AF_UNIX), c_int(SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC), c_int(0)))
- if fd < 0:
- raise Error("socket: errno " + String(errno()))
- unlink(path)
- # struct sockaddr_un: sa_family_t, then a NUL-terminated path.
- var addr = List[UInt8](length=110, fill=0)
- addr[0] = UInt8(AF_UNIX)
- var i = 2
- for c in path.as_bytes():
- addr[i] = c
- i += 1
- _bind_listen(fd, addr, path)
- return fd
-
-
-def listen_tcp(host: String, port: Int) raises -> Int:
- """Listens on an IPv4 address such as 127.0.0.1."""
- var fd = Int(external_call["socket", c_int](c_int(AF_INET), c_int(SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC), c_int(0)))
- if fd < 0:
- raise Error("socket: errno " + String(errno()))
- _set_reuseaddr(fd)
- # struct sockaddr_in: family, port (big-endian), address, padding.
- var addr = List[UInt8](length=16, fill=0)
- addr[0] = UInt8(AF_INET)
- addr[2] = UInt8((port >> 8) & 0xFF)
- addr[3] = UInt8(port & 0xFF)
- var h = cstring(host)
- if external_call["inet_pton", c_int](c_int(AF_INET), h.unsafe_ptr(), addr.unsafe_ptr().unsafe_offset(4)) != 1:
- close_fd(fd)
- raise Error("not an IPv4 address: " + host)
- _bind_listen(fd, addr, host + ":" + String(port))
- return fd
-
-
-def accept(listen_fd: Int) -> Int:
- """Accepts one pending connection, non-blocking. Returns -1 if none."""
- return Int(external_call["accept4", c_int](c_int(listen_fd), Int(0), Int(0), c_int(SOCK_NONBLOCK | SOCK_CLOEXEC)))
-
-
-def read_into(fd: Int, mut buf: List[UInt8], limit: Int) -> Int:
- """Appends up to `limit` bytes read from `fd` to `buf`. Returns read(2)'s result."""
- var start = len(buf)
- buf.resize(start + limit, 0)
- var n = external_call["read", Int](c_int(fd), buf.unsafe_ptr().unsafe_offset(start), limit)
- buf.resize(start + (n if n > 0 else 0), 0)
- return n
-
-
-def send_some(fd: Int, buf: List[UInt8], offset: Int) -> Int:
- """Sends as much of `buf[offset:]` as the socket takes, without SIGPIPE."""
- return external_call["send", Int](c_int(fd), buf.unsafe_ptr().unsafe_offset(offset), len(buf) - offset, c_int(MSG_NOSIGNAL))
-
-
-def poll(mut fds: List[Int32], timeout_ms: Int) -> Int:
- """`fds` is a flat array of struct pollfd: fd, then events | revents << 16."""
- return Int(external_call["poll", c_int](fds.unsafe_ptr(), Int(len(fds) // 2), c_int(timeout_ms)))
-
-
-# The write end of the shutdown pipe sits at this fixed descriptor, because
-# a signal handler can't capture state and Mojo has no global variables.
-comptime TERM_PIPE_FD = 100
-comptime F_GETFD = 1
-
-
-def _on_termination(sig: c_int):
- # Runs in a signal handler, on whichever thread the kernel picks: only
- # async-signal-safe calls here.
- var b = UInt8(sig)
- _ = external_call["write", Int](c_int(TERM_PIPE_FD), Pointer(to=b), Int(1))
-
-
-def termination_fd() raises -> Int:
- """Returns a descriptor that becomes readable on SIGTERM or SIGINT.
-
- The Mojo runtime starts worker threads before `main` and catches SIGTERM
- itself, so blocking the signal in this thread (or using a signalfd)
- can't work. Instead a handler writes a byte to a pipe the poll loop
- watches.
- """
- var fds = List[Int32](length=2, fill=0)
- if external_call["pipe2", c_int](fds.unsafe_ptr(), c_int(SOCK_NONBLOCK | SOCK_CLOEXEC)) != 0:
- raise Error("pipe2: errno " + String(errno()))
- if external_call["fcntl", c_int](c_int(TERM_PIPE_FD), c_int(F_GETFD)) != -1:
- raise Error("descriptor " + String(TERM_PIPE_FD) + " is already in use")
- if external_call["dup3", c_int](fds[1], c_int(TERM_PIPE_FD), c_int(SOCK_CLOEXEC)) < 0:
- raise Error("dup3: errno " + String(errno()))
- close_fd(Int(fds[1]))
- _ = external_call["signal", Int](c_int(SIGTERM), _on_termination)
- _ = external_call["signal", Int](c_int(SIGINT), _on_termination)
- return Int(fds[0])
diff --git a/apps/mojo/src/main.mojo b/apps/mojo/src/main.mojo
deleted file mode 100644
index a308630..0000000
--- a/apps/mojo/src/main.mojo
+++ /dev/null
@@ -1,196 +0,0 @@
-"""Todo-mojo: TodoMVC served from one process, one SQLite file.
-
- app --socket <path> --db <path> (on Carloku)
- app --listen 127.0.0.1:<port> --db <path> (locally)
-"""
-
-from std.os import getenv
-from std.sys import argv
-from app import App
-from db import Store
-from http import Request, Response, text, parse_request, PARSED, NEED_MORE, TOO_LARGE
-import libc
-from libc import POLLIN, POLLOUT, POLLERR, POLLHUP, EAGAIN, EINTR
-
-comptime IDLE_SECONDS = 30
-comptime READ_CHUNK = 64 * 1024
-
-
-struct Conn(Movable):
- var fd: Int
- var inbuf: List[UInt8]
- var outbuf: List[UInt8]
- var sent: Int
- var closing: Bool # close once outbuf is sent
- var last_active: Int
-
- def __init__(out self, fd: Int):
- self.fd = fd
- self.inbuf = List[UInt8]()
- self.outbuf = List[UInt8]()
- self.sent = 0
- self.closing = False
- self.last_active = libc.now()
-
-
-def usage() raises:
- raise Error("usage: app (--socket <path> | --listen 127.0.0.1:<port>) --db <path>")
-
-
-def main() raises:
- var socket_path = String()
- var listen = String()
- var db_path = String()
- var args = argv()
- var i = 1
- while i < len(args):
- var a = args[i]
- if i + 1 >= len(args):
- usage()
- if a == "--socket":
- socket_path = String(args[i + 1])
- elif a == "--listen":
- listen = String(args[i + 1])
- elif a == "--db":
- db_path = String(args[i + 1])
- else:
- usage()
- i += 2
- if not db_path or (not socket_path) == (not listen):
- usage()
- var term_fd = libc.termination_fd()
-
- var origins = List[String]()
- var host = getenv("CARLOS_HOST", "")
- if host:
- origins.append("https://" + host)
- var listen_fd: Int
- if socket_path:
- listen_fd = libc.listen_unix(socket_path)
- else:
- var colon = listen.rfind(":")
- if colon < 0:
- usage()
- var addr = String(listen[byte=:colon])
- if addr != "127.0.0.1":
- raise Error("--listen must be 127.0.0.1:<port>; refusing to bind " + addr)
- listen_fd = libc.listen_tcp(addr, Int(listen[byte = colon + 1 :]))
- origins.append("http://" + listen)
-
- var app = App(Store(db_path), origins^, secure_cookie=Bool(socket_path))
- print("todo-mojo listening on", socket_path if socket_path else listen)
- serve(app, listen_fd, term_fd)
-
- libc.close_fd(listen_fd)
- if socket_path:
- libc.unlink(socket_path)
- print("todo-mojo stopped")
-
-
-def serve(app: App, listen_fd: Int, term_fd: Int):
- """One thread, non-blocking sockets, poll(2). Returns on SIGTERM or SIGINT."""
- var conns = List[Conn]()
- var fds = List[Int32]()
- while True:
- fds.clear()
- fds.append(Int32(listen_fd))
- fds.append(Int32(POLLIN))
- fds.append(Int32(term_fd))
- fds.append(Int32(POLLIN))
- for idx in range(len(conns)):
- fds.append(Int32(conns[idx].fd))
- fds.append(Int32(POLLOUT if len(conns[idx].outbuf) > conns[idx].sent else POLLIN))
- var n = libc.poll(fds, 1000)
- if n < 0 and libc.errno() != EINTR:
- print("poll failed: errno", libc.errno())
- break
- if _revents(fds, 1) != 0:
- break
-
- var now = libc.now()
- # Service existing connections first; new ones join at the end.
- for idx in range(len(conns)):
- ref c = conns[idx]
- var ev = _revents(fds, idx + 2) if n > 0 else Int16(0)
- if ev & (POLLIN | POLLHUP | POLLERR) and len(c.outbuf) == c.sent:
- _on_readable(app, c)
- c.last_active = now
- if len(c.outbuf) > c.sent and (ev & POLLOUT or ev == 0):
- _flush(c)
- var done = c.closing and len(c.outbuf) == c.sent
- if done or now - c.last_active > IDLE_SECONDS:
- libc.close_fd(c.fd)
- c.fd = -1
- # Drop closed connections.
- var j = len(conns) - 1
- while j >= 0:
- if conns[j].fd < 0:
- _ = conns.pop(j)
- j -= 1
-
- if _revents(fds, 0) & POLLIN:
- while True:
- var fd = libc.accept(listen_fd)
- if fd < 0:
- break
- conns.append(Conn(fd))
- _on_readable(app, conns[len(conns) - 1])
-
- for idx in range(len(conns)):
- libc.close_fd(conns[idx].fd)
-
-
-def _revents(fds: List[Int32], slot: Int) -> Int16:
- return Int16(fds[slot * 2 + 1] >> 16)
-
-
-def _on_readable(app: App, mut c: Conn):
- """Reads what has arrived, answers every complete request, and starts sending."""
- while True:
- var r = libc.read_into(c.fd, c.inbuf, READ_CHUNK)
- if r > 0:
- continue
- if r == 0:
- c.closing = True # peer finished sending
- elif libc.errno() != EAGAIN and libc.errno() != EINTR:
- c.closing = True
- c.inbuf.clear()
- break
- while len(c.inbuf) > 0:
- var consumed = 0
- var req = Request("GET", "/")
- var st = parse_request(c.inbuf, consumed, req)
- if st == NEED_MORE:
- break
- if st != PARSED:
- var resp = text(413 if st == TOO_LARGE else 400, "Bad Request" if st != TOO_LARGE else "Content Too Large")
- resp.write_to(c.outbuf, False, False)
- c.inbuf.clear()
- c.closing = True
- break
- var resp = app.handle(req)
- resp.write_to(c.outbuf, req.method == "HEAD", req.keep_alive)
- if not req.keep_alive:
- c.inbuf.clear()
- c.closing = True
- break
- var rest = List[UInt8](capacity=len(c.inbuf) - consumed)
- rest.extend(Span(c.inbuf)[consumed:])
- c.inbuf = rest^
- _flush(c)
-
-
-def _flush(mut c: Conn):
- while c.sent < len(c.outbuf):
- var w = libc.send_some(c.fd, c.outbuf, c.sent)
- if w > 0:
- c.sent += w
- continue
- if w < 0 and (libc.errno() == EAGAIN or libc.errno() == EINTR):
- return
- c.closing = True
- c.outbuf.clear()
- c.sent = 0
- return
- c.outbuf.clear()
- c.sent = 0
diff --git a/apps/mojo/src/render.mojo b/apps/mojo/src/render.mojo
deleted file mode 100644
index a7b94fc..0000000
--- a/apps/mojo/src/render.mojo
+++ /dev/null
@@ -1,190 +0,0 @@
-"""Server-side HTML: the TodoMVC template from shared/reference.html."""
-
-from db import Todo
-
-
-def escape(s: StringSlice) -> String:
- """Escapes text for HTML element content and quoted attribute values."""
- var out = String(capacity_bytes=s.byte_length() + 16)
- var start = 0
- var b = s.as_bytes()
- for i in range(len(b)):
- var c = b[i]
- var rep: StaticString
- if c == 38:
- rep = "&amp;"
- elif c == 60:
- rep = "&lt;"
- elif c == 62:
- rep = "&gt;"
- elif c == 34:
- rep = "&quot;"
- elif c == 39:
- rep = "&#39;"
- else:
- continue
- out += s[byte=start:i]
- out += rep
- start = i + 1
- out += s[byte=start:]
- return out^
-
-
-def filter_path(filter: StringSlice) -> StaticString:
- if filter == "active":
- return "/active"
- if filter == "completed":
- return "/completed"
- return "/"
-
-
-def _filter_link(mut out: String, filter: StringSlice, name: StringSlice, label: StaticString):
- out += '<li><a href="'
- out += filter_path(name)
- out += '"'
- if filter == name:
- out += ' class="selected"'
- out += ">"
- out += label
- out += "</a></li>"
-
-
-def _hidden_filter(mut out: String, filter: StringSlice):
- out += '<input type="hidden" name="filter" value="'
- out += filter
- out += '">'
-
-
-def _item(mut out: String, t: Todo, filter: StringSlice, editing: Bool):
- var id = String(t.id)
- var title = escape(t.title)
- out += '<li data-id="'
- out += id
- out += '"'
- if t.completed or editing:
- out += ' class="'
- if t.completed:
- out += "completed"
- if t.completed and editing:
- out += " "
- if editing:
- out += "editing"
- out += '"'
- out += '><div class="view"><input class="toggle" type="checkbox" aria-label="Toggle"'
- if t.completed:
- out += " checked"
- out += "><label>"
- out += title
- out += '</label><a class="nojs edit-link" data-action="edit" href="'
- out += filter_path(filter)
- out += "?edit="
- out += id
- out += '">Edit</a><form method="post" action="/todos/'
- out += id
- out += '/toggle">'
- _hidden_filter(out, filter)
- out += '<button class="nojs toggle-button" data-action="toggle">Toggle</button></form>'
- out += '<form method="post" action="/todos/'
- out += id
- out += '/destroy">'
- _hidden_filter(out, filter)
- out += '<button class="destroy" aria-label="Delete"></button></form></div>'
- out += '<form method="post" action="/todos/'
- out += id
- out += '/edit">'
- _hidden_filter(out, filter)
- out += '<input class="edit" name="title" value="'
- out += title
- out += '" aria-label="Edit"'
- if editing:
- out += " autofocus"
- out += "></form></li>"
-
-
-def page(todos: List[Todo], filter: StringSlice, edit_id: Int, language: StaticString, version: StaticString) -> String:
- """The whole page for one filter, with item `edit_id` (or none, -1) being edited."""
- var active = 0
- for t in todos:
- if not t.completed:
- active += 1
- var completed = len(todos) - active
- var editing_shown = False
- for t in todos:
- if t.id == edit_id:
- editing_shown = True
-
- var out = String(capacity_bytes=4096 + len(todos) * 700)
- out += """<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>TodoMVC</title>
-<link rel="stylesheet" href="/static/base.css">
-<link rel="stylesheet" href="/static/index.css">
-<link rel="stylesheet" href="/static/app.css?v="""
- out += version
- out += """">
-<script type="module" src="/static/app.js?v="""
- out += version
- out += """"></script>
-</head>
-<body>
-<section class="todoapp">
-<header class="header">
-<h1>todos</h1>
-<form method="post" action="/todos">"""
- _hidden_filter(out, filter)
- out += '<input class="new-todo" name="title" placeholder="What needs to be done?" autocomplete="off" aria-label="New todo"'
- if not editing_shown:
- out += " autofocus"
- out += "></form>\n</header>\n"
-
- out += '<section class="main"'
- if len(todos) == 0:
- out += " hidden"
- out += '>\n<input id="toggle-all" class="toggle-all" type="checkbox"'
- if len(todos) > 0 and active == 0:
- out += " checked"
- out += '>\n<label for="toggle-all">Mark all as complete</label>\n'
- out += '<form method="post" action="/todos/toggle-all">'
- _hidden_filter(out, filter)
- out += '<button class="nojs toggle-all-button" data-action="toggle-all">Mark all as complete</button></form>\n'
- out += '<ul class="todo-list">\n'
- for t in todos:
- if filter == "active" and t.completed:
- continue
- if filter == "completed" and not t.completed:
- continue
- _item(out, t, filter, t.id == edit_id)
- out += "\n"
- out += "</ul>\n</section>\n"
-
- out += '<footer class="footer"'
- if len(todos) == 0:
- out += " hidden"
- out += '>\n<span class="todo-count"><strong>'
- out += String(active)
- out += "</strong> "
- out += "item left" if active == 1 else "items left"
- out += '</span>\n<ul class="filters">'
- _filter_link(out, filter, "all", "All")
- _filter_link(out, filter, "active", "Active")
- _filter_link(out, filter, "completed", "Completed")
- out += '</ul>\n<form method="post" action="/todos/clear-completed">'
- _hidden_filter(out, filter)
- out += '<button class="clear-completed"'
- if completed == 0:
- out += " hidden"
- out += ">Clear completed</button></form>\n</footer>\n</section>\n"
- out += """<footer class="info">
-<p>Double-click to edit a todo</p>
-<p>Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> · """
- out += language
- out += """</p>
-<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
-</footer>
-</body>
-</html>
-"""
- return out^
diff --git a/apps/mojo/static/app.css b/apps/mojo/static/app.css
deleted file mode 100644
index 3f65520..0000000
--- a/apps/mojo/static/app.css
+++ /dev/null
@@ -1,59 +0,0 @@
-/* What the TodoMVC stylesheets don't cover: the no-JavaScript controls. */
-
-[hidden] {
- display: none !important;
-}
-
-/* Forms wrap controls without adding boxes, so the layout is TodoMVC's own. */
-.todoapp form {
- display: contents;
-}
-
-/* With JavaScript on, the page's script takes over and these controls go. */
-html.js .nojs {
- display: none !important;
-}
-
-/* Without JavaScript, each control below sits invisibly over the spot a
- person would click with JavaScript on, and submits the matching form. */
-.todo-list li .toggle-button,
-.toggle-all-button {
- position: absolute;
- margin: 0;
- padding: 0;
- border: 0;
- background: none;
- font-size: 0;
- color: transparent;
- cursor: pointer;
-}
-
-.todo-list li .toggle-button {
- top: 0;
- bottom: 0;
- left: 0;
- width: 40px;
-}
-
-.toggle-all-button {
- top: -65px;
- left: 0;
- width: 45px;
- height: 65px;
-}
-
-.todo-list li .edit-link {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 60px;
- right: 60px;
- font-size: 0;
- color: transparent;
-}
-
-.todo-list li .toggle-button:focus-visible,
-.toggle-all-button:focus-visible,
-.todo-list li .edit-link:focus-visible {
- outline: 1px solid #CF7D7D;
-}
diff --git a/apps/mojo/static/app.js b/apps/mojo/static/app.js
deleted file mode 100644
index fc3ce74..0000000
--- a/apps/mojo/static/app.js
+++ /dev/null
@@ -1,69 +0,0 @@
-// Progressive enhancement for the server-rendered TodoMVC page. Every
-// change is the same form POST the page makes without JavaScript; this
-// script sends it in the background, asks for the updated page in the same
-// response, and swaps in the server's list and footer.
-document.documentElement.classList.add('js');
-
-const app = document.querySelector('.todoapp');
-let queue = Promise.resolve();
-
-function send(form) {
- const body = new URLSearchParams(new FormData(form));
- queue = queue
- .then(async () => {
- const res = await fetch(form.action, {
- method: 'POST',
- body,
- headers: { 'X-Requested-With': 'fetch' },
- });
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
- const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
- for (const sel of ['.main', '.footer']) {
- app.querySelector(sel).replaceWith(document.adoptNode(doc.querySelector(sel)));
- }
- })
- .catch(() => location.reload());
- return queue;
-}
-
-app.addEventListener('submit', (e) => {
- e.preventDefault();
- const form = e.target;
- const edit = form.querySelector('.edit');
- if (edit) edit.closest('li').dataset.done = '1';
- send(form);
- const input = form.querySelector('.new-todo');
- if (input) input.value = '';
-});
-
-app.addEventListener('change', (e) => {
- const t = e.target;
- if (t.matches('.toggle')) send(t.closest('li').querySelector('form[action$="/toggle"]'));
- else if (t.matches('.toggle-all')) send(app.querySelector('form[action="/todos/toggle-all"]'));
-});
-
-app.addEventListener('dblclick', (e) => {
- const label = e.target.closest('.todo-list label');
- if (!label) return;
- const li = label.closest('li');
- const input = li.querySelector('.edit');
- delete li.dataset.done;
- li.classList.add('editing');
- input.value = label.textContent;
- input.focus();
- input.setSelectionRange(input.value.length, input.value.length);
-});
-
-app.addEventListener('keydown', (e) => {
- if (e.key !== 'Escape' || !e.target.matches('.edit')) return;
- const li = e.target.closest('li');
- li.dataset.done = '1';
- li.classList.remove('editing');
- e.target.value = li.querySelector('label').textContent;
-});
-
-app.addEventListener('focusout', (e) => {
- if (!e.target.matches('.edit')) return;
- const li = e.target.closest('li');
- if (li.classList.contains('editing') && !li.dataset.done) e.target.form.requestSubmit();
-});
diff --git a/apps/mojo/tests/smoke.sh b/apps/mojo/tests/smoke.sh
deleted file mode 100755
index 6a7b33f..0000000
--- a/apps/mojo/tests/smoke.sh
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env bash
-# Runs the real binary on a unix socket and checks what the unit tests
-# can't: the poll loop, keep-alive, the cookie round trip, and a clean
-# exit on SIGTERM.
-# tests/smoke.sh <binary>
-set -euo pipefail
-bin=$1
-work=$(mktemp -d -p "${TMPDIR:-$HOME/.cache}")
-pid=
-trap '[ -n "$pid" ] && kill "$pid" 2>/dev/null; rm -rf "$work"' EXIT
-CARLOS_HOST=smoke.invalid "$bin" --socket "$work/app.sock" --db "$work/app.db" > "$work/log" 2>&1 &
-pid=$!
-for _ in $(seq 1 50); do [ -S "$work/app.sock" ] && break; sleep 0.1; done
-# The cookie is Secure in socket mode, so curl wouldn't send it back over http.
-c() { curl -s --unix-socket "$work/app.sock" -H "Cookie: todo_list=$(printf "5%.0s" {1..64})" "$@"; }
-fail() { echo "smoke: $*" >&2; cat "$work/log" >&2; exit 1; }
-
-[ "$(c http://smoke.invalid/healthz)" = ok ] || fail "healthz"
-# Two requests on one connection.
-n=$(c -o /dev/null -w '%{num_connects}\n' http://smoke.invalid/healthz -o /dev/null http://smoke.invalid/api/version | awk '{s+=$1} END {print s}')
-[ "$n" = 1 ] || fail "keep-alive: $n connections"
-code=$(c -o /dev/null -w '%{http_code}' -H 'Origin: https://smoke.invalid' --data-urlencode 'title=naïve <b>' http://smoke.invalid/todos)
-[ "$code" = 303 ] || fail "add answered $code"
-c http://smoke.invalid/ | grep -q '<label>naïve &lt;b&gt;</label>' || fail "item not shown"
-code=$(c -o /dev/null -w '%{http_code}' -H 'Origin: https://evil.example' -d title=x http://smoke.invalid/todos)
-[ "$code" = 403 ] || fail "cross-origin POST answered $code"
-
-kill -TERM "$pid"
-set +e; wait "$pid"; status=$?; set -e
-pid=
-[ "$status" = 0 ] || fail "exit status $status after SIGTERM"
-[ ! -e "$work/app.sock" ] || fail "socket left behind"
-echo "smoke: ok"
diff --git a/apps/mojo/tests/test_main.mojo b/apps/mojo/tests/test_main.mojo
deleted file mode 100644
index edd65e8..0000000
--- a/apps/mojo/tests/test_main.mojo
+++ /dev/null
@@ -1,312 +0,0 @@
-"""Unit tests: parsing, rendering, the store, and the routes end to end.
-
-Built and run by `make test`. Each test is a function; main runs them all
-and exits non-zero on the first failure.
-"""
-
-from std.testing import assert_equal, assert_true, assert_false
-from app import App
-from db import Store
-from http import Request, Response, Header, parse_request, url_decode, form_value, PARSED, NEED_MORE, BAD, TOO_LARGE
-from render import escape
-
-comptime ORIGIN = "http://127.0.0.1:9999"
-comptime TOKEN_A = "a" * 64
-comptime TOKEN_B = "b" * 64
-
-
-def bytes_of(s: String) -> List[UInt8]:
- var b = List[UInt8]()
- b.extend(s.as_bytes())
- return b^
-
-
-def new_app() raises -> App:
- var origins = List[String]()
- origins.append(ORIGIN)
- return App(Store(":memory:"), origins^, secure_cookie=False)
-
-
-def get(app: App, target: String, token: String = TOKEN_A) -> Response:
- var req = Request("GET", target)
- if token:
- req.headers.append(Header("cookie", "todo_list=" + token))
- return app.handle(req)
-
-
-def post(app: App, target: String, body: String, token: String = TOKEN_A, origin: String = ORIGIN) -> Response:
- var req = Request("POST", target)
- req.headers.append(Header("content-type", "application/x-www-form-urlencoded"))
- if token:
- req.headers.append(Header("cookie", "todo_list=" + token))
- if origin:
- req.headers.append(Header("origin", origin))
- req.body = body
- return app.handle(req)
-
-
-def count(haystack: String, needle: String) -> Int:
- return haystack.count(needle)
-
-
-# http ------------------------------------------------------------------------
-
-
-def test_url_decode() raises:
- assert_equal(url_decode("a+b%20c"), "a b c")
- assert_equal(url_decode("na%C3%AFve+%F0%9F%9A%80"), "naïve 🚀")
- assert_equal(url_decode("100%"), "100%")
- assert_equal(url_decode("%zz"), "%zz")
-
-
-def test_form_value() raises:
- var body = "filter=active&title=%3Cb%3E+x&empty="
- assert_equal(form_value(body, "title"), "<b> x")
- assert_equal(form_value(body, "filter"), "active")
- assert_equal(form_value(body, "empty"), "")
- assert_equal(form_value(body, "missing"), "")
-
-
-def test_parse_request() raises:
- var raw = "POST /todos?x=1 HTTP/1.1\r\nHost: h\r\nContent-Length: 7\r\nCookie: a=1; todo_list=zz\r\n\r\ntitle=x"
- var buf = bytes_of(raw + "GET / HTTP/1.1\r\n\r\n")
- var req = Request("GET", "/")
- var used = 0
- assert_equal(parse_request(buf, used, req), PARSED)
- assert_equal(req.method, "POST")
- assert_equal(req.path, "/todos")
- assert_equal(req.query, "x=1")
- assert_equal(req.body, "title=x")
- assert_equal(req.cookie("todo_list"), "zz")
- assert_equal(used, raw.byte_length())
- assert_true(req.keep_alive)
-
-
-def test_parse_request_incomplete_and_bad() raises:
- var req = Request("GET", "/")
- var used = 0
- var partial = bytes_of("POST / HTTP/1.1\r\nContent-Length: 10\r\n\r\nabc")
- assert_equal(parse_request(partial, used, req), NEED_MORE)
- var no_end = bytes_of("GET / HTTP/1.1\r\nHost: h\r\n")
- assert_equal(parse_request(no_end, used, req), NEED_MORE)
- var garbage = bytes_of("hello\r\n\r\n")
- assert_equal(parse_request(garbage, used, req), BAD)
- var chunked = bytes_of("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n")
- assert_equal(parse_request(chunked, used, req), BAD)
- var huge = bytes_of("POST / HTTP/1.1\r\nContent-Length: 99999999\r\n\r\n")
- assert_equal(parse_request(huge, used, req), TOO_LARGE)
-
-
-def test_http10_closes() raises:
- var req = Request("GET", "/")
- var used = 0
- var buf = bytes_of("GET / HTTP/1.0\r\n\r\n")
- assert_equal(parse_request(buf, used, req), PARSED)
- assert_false(req.keep_alive)
-
-
-def test_content_length_counts_bytes() raises:
- var r = Response(200, "text/plain", "日本")
- var out = List[UInt8]()
- r.write_to(out, False, True)
- var s = String(from_utf8_lossy=out)
- assert_true("Content-Length: 6\r\n" in s)
-
-
-# render ------------------------------------------------------------------------
-
-
-def test_escape() raises:
- assert_equal(escape("<img src=x onerror=\"a\"> & 'q'"), "&lt;img src=x onerror=&quot;a&quot;&gt; &amp; &#39;q&#39;")
- assert_equal(escape("naïve"), "naïve")
-
-
-# store ---------------------------------------------------------------------------
-
-
-def test_store() raises:
- var s = Store(":memory:")
- s.add("A", "one")
- s.add("A", "two")
- s.add("B", "theirs")
- var a = s.todos("A")
- assert_equal(len(a), 2)
- assert_equal(a[0].title, "one")
- assert_true(s.toggle("A", a[0].id))
- assert_false(s.toggle("B", a[0].id)) # someone else's id
- assert_true(s.todos("A")[0].completed)
- s.toggle_all("A")
- assert_true(s.todos("A")[1].completed)
- s.toggle_all("A")
- assert_false(s.todos("A")[0].completed)
- assert_false(s.todos("A")[1].completed)
- assert_true(s.rename("A", a[1].id, "deux"))
- assert_equal(s.todos("A")[1].title, "deux")
- _ = s.toggle("A", a[1].id)
- s.clear_completed("A")
- assert_equal(len(s.todos("A")), 1)
- assert_equal(len(s.todos("B")), 1)
- assert_true(s.destroy("A", a[0].id))
- assert_equal(len(s.todos("A")), 0)
-
-
-# routes --------------------------------------------------------------------------
-
-
-def test_health_and_version() raises:
- var app = new_app()
- assert_equal(get(app, "/healthz", "").status, 200)
- var v = get(app, "/api/version", "")
- assert_equal(v.status, 200)
- assert_equal(v.header("Set-Cookie"), "")
-
-
-def test_first_visit_sets_cookie() raises:
- var app = new_app()
- var r = get(app, "/", "")
- assert_equal(r.status, 200)
- var c = r.header("Set-Cookie")
- assert_true(c.startswith("todo_list="))
- assert_true("; HttpOnly" in c and "; SameSite=Lax" in c and "; Path=/" in c)
- assert_false("Secure" in c)
- assert_equal(c[byte=10:].find(";"), 64)
- assert_true('<section class="main" hidden>' in r.body)
- assert_true('<footer class="footer" hidden>' in r.body)
-
-
-def test_garbled_cookie_gets_fresh_list() raises:
- var app = new_app()
- var r = get(app, "/", "%%%")
- assert_equal(r.status, 200)
- assert_true(r.header("Set-Cookie").startswith("todo_list="))
- assert_equal(get(app, "/", TOKEN_A).header("Set-Cookie"), "")
-
-
-def test_add_and_filters() raises:
- var app = new_app()
- var r = post(app, "/todos", "title=++milk++&filter=active")
- assert_equal(r.status, 303)
- assert_equal(r.header("Location"), "/active")
- _ = post(app, "/todos", "title=+++&filter=all") # blank: nothing added
- _ = post(app, "/todos", "title=eggs&filter=bogus")
- var page = get(app, "/").body
- assert_equal(count(page, "<li data-id="), 2)
- assert_true("<label>milk</label>" in page)
- assert_true("<strong>2</strong> items left" in page)
- assert_true('<a href="/" class="selected">All</a>' in page)
- assert_true("<button class=\"clear-completed\" hidden>" in page)
- _ = post(app, "/todos/1/toggle", "filter=completed")
- page = get(app, "/").body
- assert_true('<li data-id="1" class="completed">' in page)
- assert_true("<strong>1</strong> item left" in page)
- assert_equal(count(get(app, "/active").body, "<li data-id="), 1)
- var done = get(app, "/completed").body
- assert_equal(count(done, "<li data-id="), 1)
- assert_true('<a href="/completed" class="selected">Completed</a>' in done)
-
-
-def test_edit_view_and_rename() raises:
- var app = new_app()
- _ = post(app, "/todos", "title=a&filter=all")
- var page = get(app, "/active?edit=1").body
- assert_true('<li data-id="1" class="editing">' in page)
- assert_true('<form method="post" action="/todos/1/edit">' in page)
- assert_true('href="/active?edit=1"' in page)
- var r = post(app, "/todos/1/edit", "title=+b+&filter=active")
- assert_equal(r.status, 303)
- assert_equal(r.header("Location"), "/active")
- assert_true("<label>b</label>" in get(app, "/").body)
- _ = post(app, "/todos/1/edit", "title=+&filter=all") # empty: deleted
- assert_equal(count(get(app, "/").body, "<li data-id="), 0)
-
-
-def test_toggle_all_and_clear() raises:
- var app = new_app()
- _ = post(app, "/todos", "title=a")
- _ = post(app, "/todos", "title=b")
- assert_equal(post(app, "/todos/toggle-all", "filter=all").status, 303)
- var page = get(app, "/").body
- assert_equal(count(page, 'class="completed"'), 2)
- assert_true('class="toggle-all" type="checkbox" checked' in page)
- _ = post(app, "/todos/toggle-all", "")
- assert_equal(count(get(app, "/").body, 'class="completed"'), 0)
- _ = post(app, "/todos/2/toggle", "")
- assert_equal(post(app, "/todos/clear-completed", "filter=all").status, 303)
- page = get(app, "/").body
- assert_equal(count(page, "<li data-id="), 1)
- assert_true("<label>a</label>" in page)
-
-
-def test_html_is_escaped() raises:
- var app = new_app()
- _ = post(app, "/todos", "title=%3Cimg+src%3Dx%3E+%26+%22q%22")
- var page = get(app, "/").body
- assert_true("<label>&lt;img src=x&gt; &amp; &quot;q&quot;</label>" in page)
- assert_false("<img" in page)
-
-
-def test_foreign_origin_is_refused() raises:
- var app = new_app()
- assert_equal(post(app, "/todos", "title=evil", origin="https://evil.example").status, 403)
- assert_equal(post(app, "/todos", "title=evil", origin="null").status, 403)
- assert_equal(count(get(app, "/").body, "<li data-id="), 0)
- assert_equal(post(app, "/todos", "title=fine", origin="").status, 303)
-
-
-def test_other_lists_ids_are_404() raises:
- var app = new_app()
- _ = post(app, "/todos", "title=mine")
- assert_equal(post(app, "/todos/1/toggle", "", token=TOKEN_B).status, 404)
- assert_equal(post(app, "/todos/1/edit", "title=x", token=TOKEN_B).status, 404)
- assert_equal(post(app, "/todos/1/destroy", "", token=TOKEN_B).status, 404)
- assert_equal(post(app, "/todos/99/toggle", "").status, 404)
- assert_equal(post(app, "/todos/abc/toggle", "").status, 404)
- var page = get(app, "/").body
- assert_true('<li data-id="1">' in page)
- assert_true("<label>mine</label>" in page)
-
-
-def test_fetch_header_returns_page() raises:
- var app = new_app()
- var req = Request("POST", "/todos")
- req.headers.append(Header("cookie", "todo_list=" + TOKEN_A))
- req.headers.append(Header("x-requested-with", "fetch"))
- req.body = "title=x&filter=active"
- var r = app.handle(req)
- assert_equal(r.status, 200)
- assert_true("<label>x</label>" in r.body)
- assert_true('<a href="/active" class="selected">Active</a>' in r.body)
-
-
-def test_static_and_unknown() raises:
- var app = new_app()
- var css = get(app, "/static/index.css", "")
- assert_equal(css.status, 200)
- assert_true(css.header("Content-Type").startswith("text/css"))
- assert_equal(get(app, "/static/nope.css", "").status, 404)
- assert_equal(get(app, "/nope").status, 404)
- assert_equal(post(app, "/", "").status, 405)
- assert_equal(get(app, "/todos").status, 405)
-
-
-def main() raises:
- test_url_decode()
- test_form_value()
- test_parse_request()
- test_parse_request_incomplete_and_bad()
- test_http10_closes()
- test_content_length_counts_bytes()
- test_escape()
- test_store()
- test_health_and_version()
- test_first_visit_sets_cookie()
- test_garbled_cookie_gets_fresh_list()
- test_add_and_filters()
- test_edit_view_and_rename()
- test_toggle_all_and_clear()
- test_html_is_escaped()
- test_foreign_origin_is_refused()
- test_other_lists_ids_are_404()
- test_fetch_header_returns_page()
- test_static_and_unknown()
- print("ok: 19 tests passed")
diff --git a/builders/extra/mojo.md b/builders/extra/mojo.md
deleted file mode 100644
index d9963e7..0000000
--- a/builders/extra/mojo.md
+++ /dev/null
@@ -1,9 +0,0 @@
-Read the Mojo row and notes in `docs/toolchains.md` before anything else.
-Mojo builds and runs for arm64 inside the arm64 root, but every Mojo
-binary needs `libKGENCompilerRTShared.so` beside it, and Carloku's binary
-kind takes a single file. Look for a supported way to make a
-self-contained Mojo binary. If there isn't one, still build the app and
-pass the shared suite locally, then stop with
-`BLOCKED platform: single-file binaries only (Mojo needs libKGENCompilerRTShared.so)`.
-Don't bundle, embed or self-extract libraries to get around the
-single-file rule.
diff --git a/builders/extra/nim.md b/builders/extra/nim.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md b/docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md
index f95940b..20ff12b 100644
--- a/docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md
+++ b/docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md
@@ -12,7 +12,7 @@
## Global Constraints
-- Contestant slugs, in report order: `go`, `go-rastrillo`, `rust`, `zig`, `ocaml`, `haskell`, `swift`, `mojo`, `spinel`.
+- Contestant slugs, in report order: `go`, `go-rastrillo`, `rust`, `zig`, `ocaml`, `haskell`, `swift`, `nim`, `spinel` (`nim` replaced `mojo` on 2026-10-24).
- Carloku account `bfj`. App names `todo-<slug>`. Hosts `todo-<slug>.bfj.oncarlos.com`. Report app `bakeoff` at `bakeoff.bfj.oncarlos.com`.
- Process contract: `<bin> --socket <path> --db <path>`; `GET /healthz` → 200; `GET /api/version` → body is the build version; reads `CARLOS_HOST`, never parses the socket path; SQLite in WAL mode; additive migrations; clean exit on SIGTERM; built for `linux-arm64`, static or libc-only; `carlos vet --boot` passes before the first deploy; the app is created with `--build-probe /api/version`.
- For local tests every binary also accepts `--listen 127.0.0.1:<port>` instead of `--socket`.
diff --git a/docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md b/docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md
index 08bfdca..7bb1e34 100644
--- a/docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md
+++ b/docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md
@@ -24,7 +24,8 @@ same account compares them.
5. OCaml
6. Haskell
7. Swift
- 8. Mojo
+ 8. Nim (replaced Mojo, Paul's call 2026-10-24: Mojo can't make a
+ single-file binary; see `docs/toolchains.md`)
9. Spinel (a Ruby compiler that produces native binaries)
- Every app passes the same behaviour tests with JavaScript on and with
it off.
diff --git a/docs/toolchains.md b/docs/toolchains.md
index 42fd42d..83995b6 100644
--- a/docs/toolchains.md
+++ b/docs/toolchains.md
@@ -17,7 +17,7 @@ Checked 2026-10-23.
| Swift | Swift 6.2 + Static Linux SDK | `swift build --swift-sdk aarch64-swift-linux-musl` | ok, static |
| OCaml | opam 2.6.1, OCaml 5.3.0 (switch `bakeoff`) | Build **inside the arm64 root**: OCaml 5.3.0, ocamlfind and opam 2.3 for arm64, link with `-ccopt -static` | ok, static |
| Haskell | GHC 9.10.3, cabal 3.16 (ghcup) | Build **inside the arm64 root**: GHC 9.6.6 and cabal 3.10 for arm64, link with `-static -optl-static -optl-pthread` | ok, static |
-| Mojo | Mojo 1.2.0 (pixi) | Build **inside the arm64 root** with `/root/.pixi/bin/mojo build`, which takes about 16 s for hello-world | **builds and runs, but isn't one file**: every Mojo binary needs `libKGENCompilerRTShared.so` beside it, and Mojo ships no static version. Cross-linking from x86 fails |
+| Nim | Nim 2.2.12 | `nim c --cpu:arm64 --os:linux --cc:clang` with `--clang.exe` and `--clang.linkerexe` set to `scripts/zigcc-aarch64-musl` (zig cc, musl) and `--passL:-static` | ok, static |
| Spinel | spinel `abf54280` (tep's pin), tep, spinelkit, Ruby 4.0.9 + prism (pixi) | Either build Spinel **inside the arm64 root** and compile with `--cc="cc -static"`, or follow lpay's zig cross-build (`paulca/lpay` `spinel/BUILD-arm64.md`) | ok, static (arm64 root route) |
## The arm64 root
@@ -49,20 +49,15 @@ To run any arm64 binary on docs-t1:
- **Static glibc** (OCaml, Haskell, Spinel via the arm64 root) warns about
`getaddrinfo` and `dlopen`. That's fine for an app that doesn't resolve
hostnames itself.
-- **Mojo:** the platform's binary kind takes a single file
- (`carlos deploy --target`, "binary kind, single file"). If the Mojo
- builder can't find a supported way to make a self-contained binary, the
- app is `BLOCKED platform: single-file binaries only (Mojo needs
- libKGENCompilerRTShared.so)`. Don't work around it.
-- **Mojo and "standalone binaries":** in 2023 Mojo's authors said it
- compiles to fully standalone binaries
- ([news.ycombinator.com/item?id=35797485](https://news.ycombinator.com/item?id=35797485)).
- In Mojo 1.2.0 every executable, even an 18 KB hello-world, calls the
- runtime's start-up functions (`KGEN_CompilerRT_SetArgV`,
- `KGEN_CompilerRT_DestroyGlobals`, …) from `libKGENCompilerRTShared.so`
- (1.3 MB), which in turn needs `libstdc++`, `libMSupportGlobals.so` and
- `libAsyncRTRuntimeGlobals.so`. No static runtime ships with it, so
- "standalone" now means no Mojo install is needed, not one file.
+- **Mojo was removed from the bakeoff** (Paul's call, 2026-10-24). Mojo
+ 1.2.0 can't make a single-file binary: every executable needs
+ `libKGENCompilerRTShared.so` (plus `libstdc++`, `libMSupportGlobals.so`
+ and `libAsyncRTRuntimeGlobals.so`), and no static runtime ships. In 2023
+ its authors described fully standalone binaries
+ ([news.ycombinator.com/item?id=35797485](https://news.ycombinator.com/item?id=35797485));
+ that no longer holds. The finished app is kept on the `app/mojo` branch,
+ and the platform Idea is
+ [amadan.net/carlos/platform discussion 360](https://amadan.net/carlos/platform/discussions/360).
- **Fallback:** if a route above fails for a real app, the builder writes
`BLOCKED needs-arm64-host`, and the orchestrator arranges a native
arm64 machine.
diff --git a/harness/apps.go b/harness/apps.go
index c1a6f61..4559cf3 100644
--- a/harness/apps.go
+++ b/harness/apps.go
@@ -12,7 +12,7 @@ var Apps = []App{
{"ocaml", "OCaml"},
{"haskell", "Haskell"},
{"swift", "Swift"},
- {"mojo", "Mojo"},
+ {"nim", "Nim"},
{"spinel", "Spinel"},
}
diff --git a/results/go-rastrillo.json b/results/go-rastrillo.json
index b2a9c9d..984313c 100644
--- a/results/go-rastrillo.json
+++ b/results/go-rastrillo.json
@@ -17,26 +17,144 @@
"Skipped": 3
},
"TTFBEmpty": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 50.697,
+ "P95": 57.54,
+ "P99": 58.387,
+ "Min": 46.991,
+ "Max": 58.387
},
"TTFB50": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 55.036,
+ "P95": 65.028,
+ "P99": 66.947,
+ "Min": 43.397,
+ "Max": 66.947
},
"Wake": null,
- "Load": null,
+ "Load": [
+ {
+ "Scenario": "read",
+ "Concurrency": 1,
+ "RPS": 57.99192633501968,
+ "P50Ms": 16.477262,
+ "P95Ms": 23.438197000000002,
+ "P99Ms": 29.363121999999997,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 2,
+ "RPS": 107.86552509952294,
+ "P50Ms": 17.736047000000003,
+ "P95Ms": 26.824119,
+ "P99Ms": 38.105382,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 4,
+ "RPS": 97.68322319946515,
+ "P50Ms": 39.531777,
+ "P95Ms": 51.049011,
+ "P99Ms": 65.773552,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 8,
+ "RPS": 183.5710623717267,
+ "P50Ms": 41.418768,
+ "P95Ms": 55.891783000000004,
+ "P99Ms": 76.985568,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 16,
+ "RPS": 313.91875611810264,
+ "P50Ms": 47.616062,
+ "P95Ms": 74.380572,
+ "P99Ms": 96.741354,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 32,
+ "RPS": 325.3577657313955,
+ "P50Ms": 86.209065,
+ "P95Ms": 193.47635899999997,
+ "P99Ms": 293.914985,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 64,
+ "RPS": 323.75368667899147,
+ "P50Ms": 162.148961,
+ "P95Ms": 451.165504,
+ "P99Ms": 639.8625810000001,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 1,
+ "RPS": 25.795839394223275,
+ "P50Ms": 37.065559,
+ "P95Ms": 42.996107,
+ "P99Ms": 46.49059,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 2,
+ "RPS": 135.48058487349635,
+ "P50Ms": 14.165082,
+ "P95Ms": 19.361393,
+ "P99Ms": 24.168968999999997,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 4,
+ "RPS": 223.86000523862205,
+ "P50Ms": 17.025084,
+ "P95Ms": 23.403494000000002,
+ "P99Ms": 28.705041,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 8,
+ "RPS": 301.2533089605215,
+ "P50Ms": 24.607858,
+ "P95Ms": 41.447399000000004,
+ "P99Ms": 56.895905,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 16,
+ "RPS": 314.15104797842065,
+ "P50Ms": 46.084265,
+ "P95Ms": 96.995763,
+ "P99Ms": 129.784737,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 32,
+ "RPS": 312.74472903060655,
+ "P50Ms": 88.91874299999999,
+ "P95Ms": 222.08951199999998,
+ "P99Ms": 300.757584,
+ "ErrorRate": 0
+ }
+ ],
"Weight": {
- "HTMLBytes": 0,
- "JSBytes": 0
+ "HTMLBytes": 43128,
+ "JSBytes": 3325
},
"Code": {
"Server": 0,
diff --git a/results/go.json b/results/go.json
index fd2fe8f..85a20ca 100644
--- a/results/go.json
+++ b/results/go.json
@@ -17,26 +17,171 @@
"Skipped": 3
},
"TTFBEmpty": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 95.135,
+ "P95": 102.337,
+ "P99": 104.155,
+ "Min": 81.477,
+ "Max": 104.155
},
"TTFB50": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 100.244,
+ "P95": 107.626,
+ "P99": 108.269,
+ "Min": 87.536,
+ "Max": 108.269
},
"Wake": null,
- "Load": null,
+ "Load": [
+ {
+ "Scenario": "read",
+ "Concurrency": 1,
+ "RPS": 26.397101096754653,
+ "P50Ms": 37.415341999999995,
+ "P95Ms": 43.973933,
+ "P99Ms": 51.416360000000005,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 2,
+ "RPS": 52.694969200751416,
+ "P50Ms": 37.250367,
+ "P95Ms": 44.205175,
+ "P99Ms": 52.51072,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 4,
+ "RPS": 99.18649964618396,
+ "P50Ms": 38.063772,
+ "P95Ms": 49.921562,
+ "P99Ms": 110.464989,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 8,
+ "RPS": 200.77325069872686,
+ "P50Ms": 38.678756,
+ "P95Ms": 49.328532,
+ "P99Ms": 63.743903,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 16,
+ "RPS": 327.5620385405477,
+ "P50Ms": 45.270411,
+ "P95Ms": 72.645807,
+ "P99Ms": 105.352213,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 32,
+ "RPS": 334.8053083538595,
+ "P50Ms": 83.184358,
+ "P95Ms": 190.878471,
+ "P99Ms": 350.47273,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 64,
+ "RPS": 362.73355073202134,
+ "P50Ms": 162.90021700000003,
+ "P95Ms": 323.304439,
+ "P99Ms": 407.480976,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 1,
+ "RPS": 29.5974394633574,
+ "P50Ms": 32.721875999999995,
+ "P95Ms": 39.37764,
+ "P99Ms": 47.072869000000004,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 2,
+ "RPS": 169.86405481256503,
+ "P50Ms": 10.855027999999999,
+ "P95Ms": 16.067277,
+ "P99Ms": 26.746589,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 4,
+ "RPS": 308.6614894171468,
+ "P50Ms": 12.359297,
+ "P95Ms": 17.707689,
+ "P99Ms": 20.827004,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 8,
+ "RPS": 564.2460116545043,
+ "P50Ms": 14.158867,
+ "P95Ms": 18.978194000000002,
+ "P99Ms": 23.111986,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 16,
+ "RPS": 986.2311519011463,
+ "P50Ms": 14.932877000000001,
+ "P95Ms": 22.105383999999997,
+ "P99Ms": 33.804784999999995,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 32,
+ "RPS": 1736.243961646239,
+ "P50Ms": 16.271177,
+ "P95Ms": 32.25958,
+ "P99Ms": 44.864916,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 64,
+ "RPS": 2279.229991854429,
+ "P50Ms": 24.351043,
+ "P95Ms": 54.125499,
+ "P99Ms": 72.407609,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 128,
+ "RPS": 2352.1752401964277,
+ "P50Ms": 48.026381,
+ "P95Ms": 101.027269,
+ "P99Ms": 191.64778199999998,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 256,
+ "RPS": 2366.197606911559,
+ "P50Ms": 100.61768000000001,
+ "P95Ms": 186.897535,
+ "P99Ms": 321.48467600000004,
+ "ErrorRate": 0.0005548678987579496
+ }
+ ],
"Weight": {
- "HTMLBytes": 0,
- "JSBytes": 0
+ "HTMLBytes": 46619,
+ "JSBytes": 4689
},
"Code": {
"Server": 0,
diff --git a/results/mojo.json b/results/mojo.json
deleted file mode 100644
index dc46f14..0000000
--- a/results/mojo.json
+++ /dev/null
@@ -1,102 +0,0 @@
-{
- "Slug": "mojo",
- "Language": "Mojo",
- "Status": "BLOCKED platform: single-file binaries only (Mojo needs libKGENCompilerRTShared.so)",
- "URL": "",
- "Version": "",
- "Stack": "",
- "Source": "",
- "BehaviourJSOn": {
- "Passed": 0,
- "Failed": 0,
- "Skipped": 0
- },
- "BehaviourJSOff": {
- "Passed": 0,
- "Failed": 0,
- "Skipped": 0
- },
- "TTFBEmpty": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
- },
- "TTFB50": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
- },
- "Wake": null,
- "Load": null,
- "Weight": {
- "HTMLBytes": 0,
- "JSBytes": 0
- },
- "Code": {
- "Server": 1007,
- "Templates": 0,
- "JS": 62,
- "Tests": 251,
- "Deps": 1
- },
- "Build": {
- "ColdSeconds": 0,
- "WarmSeconds": 0,
- "EditSeconds": 0,
- "Bytes": 0,
- "Host": ""
- },
- "TokensBuild": {
- "Input": 170,
- "Output": 83318,
- "CacheRead": 9923207,
- "CacheWrite": 167410,
- "Turns": 86,
- "Seconds": 1242.5520000000001,
- "CostUSD": 4.9909614000000015
- },
- "TokensFix": {
- "Input": 0,
- "Output": 0,
- "CacheRead": 0,
- "CacheWrite": 0,
- "Turns": 0,
- "Seconds": 0,
- "CostUSD": 0
- },
- "TokensSecurity": {
- "Input": 0,
- "Output": 0,
- "CacheRead": 0,
- "CacheWrite": 0,
- "Turns": 0,
- "Seconds": 0,
- "CostUSD": 0
- },
- "TokensReview": {
- "Input": 0,
- "Output": 0,
- "CacheRead": 0,
- "CacheWrite": 0,
- "Turns": 0,
- "Seconds": 0,
- "CostUSD": 0
- },
- "Readability": {
- "Naming": 4,
- "Structure": 4,
- "Traceability": 4,
- "Errors": 4,
- "Comments": 4,
- "Overall": 8,
- "Notes": "Strongest: clean module split (routes, store, HTTP parsing, libc shims, render) with module docstrings; why-comments where it matters (fixed TERM_PIPE_FD and signal-handler constraints, SQLITE_TRANSIENT, migrations, the fetch same-round-trip page). SQLite errors surface with sqlite3_errmsg and handle() logs and returns 500. Weakest: _action dispatches on len(parts) of a split path, so the URL shape is implicit rather than stated; _list returns a token but reads like it returns todos; Stmt.int/text shadow builtins; several external_call results discarded with `_ =` and the JS swallows every failure into location.reload(); page() is one long string-concatenation function that is hard to scan against the template."
- },
- "ReviewFindings": null,
- "Notes": null
-}
diff --git a/results/ocaml.json b/results/ocaml.json
index 25c2a64..45e5ba4 100644
--- a/results/ocaml.json
+++ b/results/ocaml.json
@@ -17,26 +17,26 @@
"Skipped": 3
},
"TTFBEmpty": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 96.057,
+ "P95": 101.822,
+ "P99": 102.331,
+ "Min": 86.162,
+ "Max": 102.331
},
"TTFB50": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 95.203,
+ "P95": 106.555,
+ "P99": 113.121,
+ "Min": 88.32,
+ "Max": 113.121
},
"Wake": null,
"Load": null,
"Weight": {
- "HTMLBytes": 0,
- "JSBytes": 0
+ "HTMLBytes": 42587,
+ "JSBytes": 3685
},
"Code": {
"Server": 0,
diff --git a/results/rust.json b/results/rust.json
index 13a1d99..4f30503 100644
--- a/results/rust.json
+++ b/results/rust.json
@@ -17,26 +17,180 @@
"Skipped": 3
},
"TTFBEmpty": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 92.325,
+ "P95": 105.63,
+ "P99": 160.783,
+ "Min": 85.641,
+ "Max": 160.783
},
"TTFB50": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 93.96,
+ "P95": 101.951,
+ "P99": 107.206,
+ "Min": 86.482,
+ "Max": 107.206
},
"Wake": null,
- "Load": null,
+ "Load": [
+ {
+ "Scenario": "read",
+ "Concurrency": 1,
+ "RPS": 28.185612101864606,
+ "P50Ms": 35.462399,
+ "P95Ms": 39.121629999999996,
+ "P99Ms": 50.12424,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 2,
+ "RPS": 38.39254057891703,
+ "P50Ms": 36.419970000000006,
+ "P95Ms": 72.873043,
+ "P99Ms": 282.45779999999996,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 4,
+ "RPS": 237.96649874315383,
+ "P50Ms": 15.607902,
+ "P95Ms": 26.506323,
+ "P99Ms": 41.046028,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 8,
+ "RPS": 513.0003530672191,
+ "P50Ms": 15.382689999999998,
+ "P95Ms": 20.712524,
+ "P99Ms": 26.247308,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 16,
+ "RPS": 901.761816814653,
+ "P50Ms": 15.933392000000001,
+ "P95Ms": 27.924761,
+ "P99Ms": 50.763132000000006,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 32,
+ "RPS": 1289.131957753337,
+ "P50Ms": 21.488584000000003,
+ "P95Ms": 46.228396999999994,
+ "P99Ms": 67.645938,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 64,
+ "RPS": 1366.7087002798423,
+ "P50Ms": 42.768637999999996,
+ "P95Ms": 83.634758,
+ "P99Ms": 111.480954,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 128,
+ "RPS": 1354.4764813961515,
+ "P50Ms": 89.141547,
+ "P95Ms": 144.245797,
+ "P99Ms": 253.83596400000002,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 1,
+ "RPS": 29.191697163211032,
+ "P50Ms": 32.976131,
+ "P95Ms": 37.089355,
+ "P99Ms": 57.698962,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 2,
+ "RPS": 57.59188585616497,
+ "P50Ms": 33.514239,
+ "P95Ms": 41.247702999999994,
+ "P99Ms": 48.346012,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 4,
+ "RPS": 114.18116860225956,
+ "P50Ms": 33.755286000000005,
+ "P95Ms": 41.985884,
+ "P99Ms": 66.76484500000001,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 8,
+ "RPS": 486.2910784332009,
+ "P50Ms": 15.267147,
+ "P95Ms": 25.780468,
+ "P99Ms": 44.980184,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 16,
+ "RPS": 983.9711746498187,
+ "P50Ms": 15.495027,
+ "P95Ms": 24.780343,
+ "P99Ms": 35.839623,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 32,
+ "RPS": 1733.262851949785,
+ "P50Ms": 16.226934,
+ "P95Ms": 30.03119,
+ "P99Ms": 62.253621,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 64,
+ "RPS": 2221.0739811919975,
+ "P50Ms": 24.848551,
+ "P95Ms": 56.078465,
+ "P99Ms": 82.48298,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 128,
+ "RPS": 2218.212308943644,
+ "P50Ms": 50.257066,
+ "P95Ms": 120.648697,
+ "P99Ms": 187.86395100000001,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 256,
+ "RPS": 1975.5429685760184,
+ "P50Ms": 97.572354,
+ "P95Ms": 387.185339,
+ "P99Ms": 599.826717,
+ "ErrorRate": 0
+ }
+ ],
"Weight": {
- "HTMLBytes": 0,
- "JSBytes": 0
+ "HTMLBytes": 36949,
+ "JSBytes": 4596
},
"Code": {
"Server": 0,
diff --git a/results/zig.json b/results/zig.json
index be8d517..88ea7bc 100644
--- a/results/zig.json
+++ b/results/zig.json
@@ -17,26 +17,162 @@
"Skipped": 3
},
"TTFBEmpty": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 95.268,
+ "P95": 102.395,
+ "P99": 102.948,
+ "Min": 81.977,
+ "Max": 102.948
},
"TTFB50": {
- "N": 0,
- "P50": 0,
- "P95": 0,
- "P99": 0,
- "Min": 0,
- "Max": 0
+ "N": 30,
+ "P50": 94.987,
+ "P95": 106.136,
+ "P99": 106.341,
+ "Min": 87.479,
+ "Max": 106.341
},
"Wake": null,
- "Load": null,
+ "Load": [
+ {
+ "Scenario": "read",
+ "Concurrency": 1,
+ "RPS": 29.39574477781065,
+ "P50Ms": 34.249815000000005,
+ "P95Ms": 37.864086,
+ "P99Ms": 41.005205000000004,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 2,
+ "RPS": 56.59066082060851,
+ "P50Ms": 35.540171,
+ "P95Ms": 39.507695000000005,
+ "P99Ms": 44.439894,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 4,
+ "RPS": 283.35657268666307,
+ "P50Ms": 14.016504000000001,
+ "P95Ms": 18.088931,
+ "P99Ms": 21.902407,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 8,
+ "RPS": 539.0283164568906,
+ "P50Ms": 14.725098000000001,
+ "P95Ms": 19.244354,
+ "P99Ms": 24.920043,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 16,
+ "RPS": 940.7968704115435,
+ "P50Ms": 15.535354,
+ "P95Ms": 27.759521999999997,
+ "P99Ms": 43.640426000000005,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 32,
+ "RPS": 775.7980624619867,
+ "P50Ms": 37.371939,
+ "P95Ms": 60.208033,
+ "P99Ms": 109.217875,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "read",
+ "Concurrency": 64,
+ "RPS": 809.3032064490328,
+ "P50Ms": 63.381702999999995,
+ "P95Ms": 179.77062899999999,
+ "P99Ms": 252.835274,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 1,
+ "RPS": 29.491687847407768,
+ "P50Ms": 33.419122,
+ "P95Ms": 36.965662,
+ "P99Ms": 40.05267,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 2,
+ "RPS": 60.48882212538628,
+ "P50Ms": 32.054846999999995,
+ "P95Ms": 37.797934999999995,
+ "P99Ms": 41.654875000000004,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 4,
+ "RPS": 120.27733757258152,
+ "P50Ms": 32.573297000000004,
+ "P95Ms": 38.51065,
+ "P99Ms": 46.633263,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 8,
+ "RPS": 234.6660418994756,
+ "P50Ms": 33.676768,
+ "P95Ms": 39.092808,
+ "P99Ms": 42.925565999999996,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 16,
+ "RPS": 1122.2904086748506,
+ "P50Ms": 13.571422,
+ "P95Ms": 20.309955000000002,
+ "P99Ms": 27.69751,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 32,
+ "RPS": 1909.8778647203133,
+ "P50Ms": 14.917698,
+ "P95Ms": 27.771401,
+ "P99Ms": 46.223282,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 64,
+ "RPS": 2526.0084045366834,
+ "P50Ms": 21.814139,
+ "P95Ms": 50.538366,
+ "P99Ms": 72.851228,
+ "ErrorRate": 0
+ },
+ {
+ "Scenario": "toggle",
+ "Concurrency": 128,
+ "RPS": 1760.7469737135646,
+ "P50Ms": 43.446754999999996,
+ "P95Ms": 259.093322,
+ "P99Ms": 379.538323,
+ "ErrorRate": 0.37794555021734155
+ }
+ ],
"Weight": {
- "HTMLBytes": 0,
- "JSBytes": 0
+ "HTMLBytes": 36058,
+ "JSBytes": 3854
},
"Code": {
"Server": 0,
diff --git a/scripts/measure-wake.sh b/scripts/measure-wake.sh
new file mode 100755
index 0000000..5f5594f
--- /dev/null
+++ b/scripts/measure-wake.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# Collects wake-from-sleep samples: every 2 minutes, any app whose instance
+# the platform reports asleep gets one first request timed. Runs until
+# every app has <samples> samples (default 3). Nothing else may touch the
+# apps meanwhile, or they never fall asleep.
+# scripts/measure-wake.sh [samples] [slug...]
+set -uo pipefail
+cd "$(dirname "$0")/.."
+want=${1:-3}; shift || true
+slugs=("$@")
+[ ${#slugs[@]} -gt 0 ] || slugs=(go go-rastrillo rust zig ocaml haskell swift nim spinel)
+have() { python3 -c "import json,sys; print(len(json.load(open('results/$1.json')).get('Wake') or []))"; }
+while :; do
+ left=0
+ for s in "${slugs[@]}"; do
+ n=$(have "$s")
+ [ "$n" -ge "$want" ] && continue
+ left=1
+ bakeoff wake --slug "$s" && echo "$(date +%H:%M) $s sample $((n+1))"
+ done
+ [ $left = 0 ] && break
+ sleep 120
+done
+echo "wake: done"
diff --git a/scripts/smoke.sh b/scripts/smoke.sh
index adebbcc..405d715 100755
--- a/scripts/smoke.sh
+++ b/scripts/smoke.sh
@@ -2,7 +2,7 @@
# Proves a toolchain can make a runnable linux-arm64 binary: builds the
# hello-world in scripts/smoke/<lang>, checks it is aarch64 and static (or
# libc-only), and runs it under qemu. Prints "<lang>: ok" on success.
-# scripts/smoke.sh <go|rust|zig|ocaml|haskell|swift|mojo|spinel>
+# scripts/smoke.sh <go|rust|zig|ocaml|haskell|swift|nim|spinel>
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
source "$here/env.sh"
@@ -22,6 +22,9 @@ case $lang in
haskell) cp "$src/Main.hs" "$out/" && (cd "$out" && "$here/arm64.sh" ghc -O -static -optl-static -optl-pthread Main.hs -o smoke >/dev/null) ;;
mojo) cp "$src/main.mojo" "$out/" && (cd "$out" && "$here/arm64.sh" /root/.pixi/bin/mojo build main.mojo -o smoke 2>/dev/null) ;;
spinel) "$here/smoke-spinel.sh" "$src/main.rb" "$bin" ;;
+ nim) (cd "$out" && nim c --hints:off -d:release --cpu:arm64 --os:linux --cc:clang \
+ --clang.exe="$here/zigcc-aarch64-musl" --clang.linkerexe="$here/zigcc-aarch64-musl" \
+ --passL:-static --nimcache:"$out/cache" -o:"$bin" "$src/main.nim" >/dev/null) ;;
*) echo "unknown language $lang" >&2; exit 2 ;;
esac
desc=$(file -b "$bin")
diff --git a/scripts/smoke/nim/main.nim b/scripts/smoke/nim/main.nim
new file mode 100644
index 0000000..8006cb4
--- /dev/null
+++ b/scripts/smoke/nim/main.nim
@@ -0,0 +1 @@
+echo "ok"
diff --git a/scripts/toolchains.sh b/scripts/toolchains.sh
index a44249f..99a1ca3 100755
--- a/scripts/toolchains.sh
+++ b/scripts/toolchains.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Installs every bakeoff toolchain under $HOME (no sudo), idempotently.
-# scripts/toolchains.sh [section...] sections: base qemu zig rust swift ocaml haskell mojo spinel arm64root
+# scripts/toolchains.sh [section...] sections: base qemu zig rust swift ocaml haskell nim spinel arm64root
# Then `source scripts/env.sh` puts them on PATH. Versions land in docs/toolchains.md.
set -euo pipefail
export PIXI_NO_TELEMETRY=1
@@ -87,6 +87,15 @@ sec_mojo() { # Mojo through pixi, as a global environment
pixi global install --environment mojo -c https://conda.modular.com/max -c conda-forge mojo
}
+sec_nim() { # Nim (prebuilt x86-64 release); arm64 via zig cc as the C compiler
+ if ! [ -x "$TC/nim/bin/nim" ]; then
+ v=$(curl -fsSL https://nim-lang.org/channels/stable | head -1)
+ curl -fsSL "https://nim-lang.org/download/nim-$v-linux_x64.tar.xz" | tar -xJ -C "$dl"
+ rm -rf "$TC/nim" && mv "$dl/nim-$v" "$TC/nim"
+ fi
+ for b in nim nimble; do ln -sf "$TC/nim/bin/$b" "$HOME/.local/bin/$b"; done
+}
+
sec_spinel() { # spinel + tep + spinelkit at the pins lpay uses (paulca/lpay spinel/)
src="$TC/spinel-src"; mkdir -p "$src"
[ -d "$src/lpay" ] || gh repo clone paulca/lpay "$src/lpay" -- --depth 1
@@ -114,7 +123,7 @@ sec_arm64root() { # a Debian arm64 root filesystem, run through proot + qemu (no
}
sections=("$@")
-[ ${#sections[@]} -gt 0 ] || sections=(base qemu zig rust swift ocaml haskell mojo spinel arm64root)
+[ ${#sections[@]} -gt 0 ] || sections=(base qemu zig rust swift ocaml haskell nim spinel arm64root)
for s in "${sections[@]}"; do
echo "== $s"
"sec_$s"
diff --git a/scripts/zigcc-aarch64-musl b/scripts/zigcc-aarch64-musl
new file mode 100755
index 0000000..0733762
--- /dev/null
+++ b/scripts/zigcc-aarch64-musl
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+# A C compiler for static linux-arm64 (musl) binaries, for toolchains that
+# take a C compiler command (Nim's --clang.exe, Spinel's --cc, ...).
+exec zig cc -target aarch64-linux-musl "$@"