# 0036: Crystal is the userland language

> One language for apps, tools and integrations, with generated bindings over the stable C ABI.

Source: https://wolos.pages.dev/manual/adr-0036

## Context

WolOS has a stable C ABI (`libwol.so.1`, `crates/wol-abi/include/wol.h`) with generated declarations for three languages (`crates/wol-abi/bindings/{crystal,swift,dotnet}`, generator `crystal()` in `crates/wol-abi/tests/common/bindgen.rs`), and two tools already written in Crystal: the wopkg CLI (`wopkg/src/wopkg/`, over `crates/wopkg/include/wopkg.h`) and wol-hue (`hue/`). Apps have no default language, so every app, tool and integration picks its own and the ecosystem stays thin. The lead wants one language for apps, the AI framework and integrations: libraries after libraries that integrate natively and easily, a repository of libraries that already integrate and update as WolOS releases, a reactive native UI kit that is as easy as Go, and an accelerated GPU interface. The two finalists were Crystal and Swift; Swift was rejected because its toolchain is about 1 GB that WolOS would have to ship, and Apple steers the language's direction.

## Decision

- **Crystal is the WolOS userland language.** Why: the toolchain is small and comes from the base distribution's repositories ([ADR 0016](/manual/adr-0016)), so nothing large ships; the language direction is not owned by a platform vendor; its FFI (`lib ... end`, `@[Link]`) reads a C header almost one to one, so every libwol call is one generated line; the collector can be turned off (`-Dgc_none`) or swapped, so a short CLI pays nothing and an app can tune it. Publishing a library is one step (a git tag on a repository with `shard.yml`; `shards` resolves by git URL and version, with no host to run), which is what a fast-growing library ecosystem needs. Swift is rejected (Context).
- **Where Rust stays:** `crates/core`, `crates/shared`, wold and every daemon, the shell and the compositor side (the Kit host in `crates/shell`, `crates/realtime`), the installer, libwol and libwopkg themselves, and security-critical code: privilege boundaries, parsers of untrusted input, signatures and crypto, xguard. Lua stays the language of sandboxed shell plugins ([ADR 0031](/manual/adr-0031)). A new crate is Rust; a new app is not.
- **Where Crystal is the default:** apps, command-line tools, the AI framework, integrations with outside services, and plugin tooling (generators, checkers and previews that run as the person). A new app or tool in another language is a review question that names its reason.
- **The Crystal ABI surface:** every WolOS verb is reachable from Crystal through generated bindings, never hand-written ones. Low layer: `lib LibWol` from `wol.h` (the Kit parts, "Windows and Kit parts" onward, and the event loop, `wol_context_get_fd` and `wol_context_dispatch`), `lib LibWopkg` from `wopkg.h`, and `lib LibRealtime` from `crates/realtime/include/wol_realtime.h`, all produced by the same generator and checked by `cargo test -p wol-abi --test bindings` (a drift fails the test). High layer: the `wol` shard, idiomatic wrappers (`Wol::Context`, exceptions from `wol_error`, `Bytes` and `String` in place of pointer plus length, blocks in place of callback plus `user_data`). A new ABI version adds its Crystal wrappers in the same change. Callbacks must not unwind (`wol.h`, header rules): every wrapper catches inside the callback and reports through the context.
- **wolkit, the reactive native UI kit for Crystal:** a declarative view tree that maps one to one onto `shared::view` (`crates/shared/src/view.rs`, ADR 0020), sent to the Kit host, which draws it with the shell's toolkit parts; apps never link the GPL Kit. State is signals: `Signal(T)` holds a value, `computed { }` derives one, a view block that reads a signal re-runs when it changes, and wolkit sends the smallest changed subtree. One event loop: a fiber waits on `wol_context_get_fd` and calls `wol_context_dispatch`, so libwol callbacks, timers, sockets and signals all run on the UI fibers with no second loop. Hot reload in development: `wol app run --watch` rebuilds on save, restarts the process and restores signal values from a JSON snapshot while the Kit host keeps the window, so the window does not flicker or move. Go-like ergonomics: a small surface (views, signals, `on_click` blocks, `spawn`), no generics-heavy API, convention over configuration (one `App` entry, `src/views/`, resources by name), and one way to do each thing.
- **GPU: Vulkan, through a Crystal binding generated from the system Vulkan header.** The Kit itself keeps drawing through the compositor-side renderer; an app that needs acceleration asks libwol for a GPU surface (a new `wol_surface` family: a subsurface placed by a `surface` view node, sized and scaled by the host) and renders into it, and the same device gives compute queues to the AI framework. Why Vulkan, in one line: it is already in every driver stack WolOS ships, its header is frozen, and it covers drawing and compute with no runtime of ours to ship. Rejected: a WebGPU C API (its header is not frozen and it adds a large runtime to track); reusing the toolkit's GL or Vulkan renderer (it lives in the GPL Kit host, apps cannot link it, and a draw list over a socket caps throughput). wolkit adds a small 2D canvas over Vulkan so most apps never write a pipeline. The first implementation may land behind a feature flag, so v0.12.x apps keep drawing through the toolkit renderer until it is on by default.
- **Concurrency: execution contexts** (Crystal 1.16 and later; the pinned toolchain decides whether preview flags are needed). One rule for apps: view state is read and written only on the UI context (single-threaded, the one that dispatches libwol); blocking or heavy work runs in `Wol.background`, one shared parallel context, and comes back through a `Channel` or `Signal#set_later`. The `wol` shard enforces it: touching a signal off the UI context raises in development builds.
- **GC policy:** the default collector for apps and long-lived tools, tuned by the `wol` shard at start (initial heap and free-space settings sized for a desktop app) and told to collect when a window hides and when the memory saver asks ([ADR 0026](/manual/adr-0026)). `-Dgc_none` only for short-lived CLIs that exit within seconds (the `cli` template sets it); never for an app, a daemon or anything with a loop. A swapped collector is an ADR.
- **Library ecosystem, "wolos-shards":** a signed, curated index, not a package host. Each row is a git URL, a pinned commit, a version, the verified source hash, the WolOS version range and the license; `index.json` plus an Ed25519 signature, the same form as the plugin index ([ADR 0031](/manual/adr-0031), `crates/wopkg/src/index/plugins.rs`). `wol shard add|update|list|check` (in wopkg) reads the index, verifies the signature and the hash, pins the commit in `shard.yml`, and hands `shards` a lock file, so `shards` never resolves a moving ref. Publishing a WolOS library is: tag the repository, open one pull request adding the index row; CI runs `wol shard check` on it (the plugin index flow). At every WolOS release CI rebuilds every indexed shard against the new toolchain and bindings and moves its version range, so the index updates as WolOS releases. Prebuilt artifacts exist only for finished apps and tools: one static binary, one pacman package each, built in the release chroot ([ADR 0029](/manual/adr-0029)).
- **Vendoring and quality bar:** an app or tool that ships in WolOS vendors its shards (by pinned commit in the package build, never fetched at run time). A shard enters the index with: specs that pass (`crystal spec`), the format check clean (`crystal tool format --check`), docs for every public method, a license compatible with its users, and no upstream project or author names in code (Doctrine §6). Official shards (`wol`, `wolkit`, the AI framework) live under the WolOS organisation and follow `docs/STANDARDS.md`.
- **Lock-in, stated plainly:** WolOS-only integrations (wolkit, signals over the Kit host, GPU surfaces, Search, the AI framework, assistants and helpers) are first-class in Crystal: designed, documented and tested there first. Other languages get the C ABI (`wol.h`, `wopkg.h`) and the generated low-level declarations only; they keep working, but WolOS writes no high layer for them.
- **Agent-first:** `wol app new <id> [--template app|cli|tool|ai]` scaffolds a project that builds and passes its checks; templates ship in the `wolos-crystal` package. An agent skill describes the one way to write a WolOS app (layout, signals, the concurrency rule, the GC rule, the shard index). The rules catalogue gains a Crystal section with its own prefix (`docs/RULES.md` "Catalogues"; `RC-` is taken by CI, so Crystal uses `RCR-nnn`), checked by a test that reads `.cr` sources the way `crates/shared/tests/rules_rust.rs` reads Rust.
- **Toolchain:** `crystal` and `shards` from the base distribution's repositories at a version pinned per WolOS release (bumped only at the start of a release cycle, with every indexed shard rebuilt); apps and tools build in our chroot like every other package. A `wolos-crystal` meta package installs the toolchain, the `wol` and `wolkit` shards, the templates, the skill and the rules checker.

## Consequences

maintainers work in two compiled languages, Rust below the ABI and Crystal above it, and the line between them is `wol.h`. Every new libwol call costs a Crystal wrapper in the same change, which keeps the ABI usable, not merely stable. Apps stay out of the GPL Kit (ADR 0020 unchanged): wolkit is a view-tree builder, not a toolkit binding, so the HIG and the view limits (`MAX_NODES`, `MAX_DEPTH`) bind Crystal apps too, and new parts still go into `shared::view` and `wol.h` together. The GPU surface is the one new ABI family this ADR needs; until it lands wolkit has no canvas. The language's community is small, so WolOS writes and carries the libraries it needs; the index makes that work visible and reusable. Execution contexts are young; the one-rule model keeps apps correct if the runtime changes under them. Swift and .NET declarations stay generated (`crates/wol-abi/bindings/`) but get no high layer. None of this gates v0.12.
