carlos / todomvc-bakeoff Public
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>
34 files changed,
+685
−2210
apps/mojo/.gitignore+0 −1apps/mojo/AGENTS.md+0 −28apps/mojo/DEPS+0 −1apps/mojo/Makefile+0 −123apps/mojo/README.md+0 −109apps/mojo/STATUS.md+0 −57apps/mojo/scripts/embed.py+0 −38apps/mojo/src/app.mojo+0 −179apps/mojo/src/db.mojo+0 −189apps/mojo/src/http.mojo+0 −237apps/mojo/src/libc.mojo+0 −183apps/mojo/src/main.mojo+0 −196apps/mojo/src/render.mojo+0 −190apps/mojo/static/app.css+0 −59apps/mojo/static/app.js+0 −69apps/mojo/tests/smoke.sh+0 −33apps/mojo/tests/test_main.mojo+0 −312builders/extra/mojo.md+0 −9builders/extra/nim.md+0 −0docs/superpowers/plans/2026-10-23-todomvc-bakeoff.md+1 −1docs/superpowers/specs/2026-10-23-todomvc-bakeoff-design.md+2 −1docs/toolchains.md+10 −15harness/apps.go+1 −1results/go-rastrillo.json+133 −15results/go.json+160 −15results/mojo.json+0 −102results/ocaml.json+14 −14results/rust.json+169 −15results/zig.json+151 −15scripts/measure-wake.sh+24 −0scripts/smoke.sh+4 −1scripts/smoke/nim/main.nim+1 −0scripts/toolchains.sh+11 −2scripts/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 = "&" |
| - elif c == 60: |
| - rep = "<" |
| - elif c == 62: |
| - rep = ">" |
| - elif c == 34: |
| - rep = """ |
| - elif c == 39: |
| - rep = "'" |
| - 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 <b></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'"), "<img src=x onerror="a"> & 'q'") |
| - 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><img src=x> & "q"</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 "$@" |