C+
Introduction · View as Markdown

C+ 0.0.28 — 2026-09-18

This release spans the language and toolchain, package management, the agent surface, Facet's application model, and native Windows, Linux, and Android support.

Upgrade notes

  • [[bin]] / [lib] are removed. Applications use entry (with src/main.cplus as the default), and C-ABI libraries use [library].
  • executor::block_on / executor::run are removed. Use async fn main, .wait(), or future::wait_or_cancel.
  • A guard let complement is else Pattern, without pipe delimiters.
  • facet/runtime and facet/agent moved to the facet_runtime and facet_agent packages. The application and navigation APIs changed with the split; the detailed migration table is linked below.
  • The raw SQLite import is sqlite/raw; sqlite_ffi is no longer a separate package. static-arena is now static_arena, and terminal/appkit is now terminal/backend.
  • Agent-surface reads and actions take an explicit capability grant, and multiple UI selection is represented as a set.

Async and concurrency

  • async fn main and async #[test] functions are driven by the compiler. Synchronous code consumes a future through .wait() or the cancellable future::wait_or_cancel path; the old executor entry points are gone.
  • Dropping a future destroys its suspended frame tree, the reactor owns detached spawned work through teardown, and thread scopes request cancellation before joining.
  • Async/thread bridges, sockets, and platform reactors gained deadline-aware operations. Windows socket and pipe waits are cooperatively cancellable.

Facet applications and navigation

  • Applications register windows with app.window(name, factory, chrome:) and start through App::run. Window instances are addressed by name and key.
  • Each window owns routes, default slots, and retained Back/Forward history. Screens receive instance arguments and navigation-state hooks. Window opening never falls back to content navigation on another platform.
  • Removed app.screen, Screen::chrome, the standalone runtime hosts and runtime::Window, global content navigation, and nav::Show modes. See the migration table.

Lifecycle

  • A component is told when it is on screen, whatever moved the tree. component::Attach::Foreground and component::Detach::Background now reach a boxed component (component::child) and a presented one (mount::set_content) from the tree itself, not only from the app event stream. One pass computes the answer — in a mounted window, and neither the node nor any ancestor Display::None or invisible — and announces the transitions; no mutating verb announces anything, so switch_to parking a pane, set_shown hiding one, an ancestor hidden twelve levels up and a band rule collapsing a column all arrive the same way. It is not occlusion: scrolled out of view, behind another window or under a sheet are not known and are not claimed.
  • The pass runs at the end of the frame (mount::sync, and again after layout through core::frames_settled, where band rules have resolved into display). A pane parked and restored inside one tick nets out to nothing, and everything leaving is told before anything arriving, so a device handed between screens is released on one side before the other asks for it.
  • A parked pane used to hear Attach::Mount again on every restore, and nothing at all on the way out. The node's single attach slot carries no reason, so component::child could say one word; the reason now rides the node's presentation record, the repair screen::ScreenBox already had. Mount keeps meaning what it meant — the application put this in the tree — and is said once per attachment. A component's own root is also no longer silently robbed of its lifecycle when the application sets on_attach on it.

Toolchain

  • This in a method body is the block's own type, whatever else the file impls. The self-type was bound while method signatures were collected and left set afterwards, so every method BODY in the build resolved This against whichever impl block came last. With two blocks in one file, let t: This = this; in the first one typed t as the second one's target: a compiler panic when that type lacks the field named next, and IR clang rejects when it has it. Signatures were unaffected, and swapping the two blocks made the same program build. Bodies now bind the self-type for themselves, structs, enums and blessed builtin impls alike.
  • A package's public static stays in its own archive. Splitting the library into one object per module placed data by reachability, and a static that nothing inside the package reads is reached from nowhere — so it was in no member of lib<name>.a and no export of the dylib, though it is made weak_odr for the express purpose of being kept. A global that its linkage says must survive the link is now defined in its own module's piece whether or not anything reaches it, while one the linkage says is discardable still travels only where it is used. Verified against the single-object build: the two now export the same data symbols.
  • A package archives one object per module. lib<name>.a used to hold a single object, so resolving any symbol from a package linked every module it had — a facet app on Android that imports stdlib/vec linked process and pty, and with them a posix_spawn API floor of 28 for code it could not reach. The library pipeline now partitions the emitted IR one piece per source file, compiles the pieces in parallel and archives all of them; a consumer links the modules it reaches. The Android gallery links at API 24 again. CPC_ONE_OBJECT=1 restores the single object.
  • A call to an extension method whose module is not imported reports one error: E0388 at the call site, naming the import to add. It used to be preceded by an E0312 per defaulted parameter, each located inside the extending module's file (.gesture(on_click: h) without facet/gestures: twenty errors inside vendor/, the real one last).

Packages

  • A bundled archive named lib<package>.a appears once on the link line. The default prebuild setting no longer causes the same author-supplied archive to be classified and appended a second time.
  • llama_cpp regenerated against llama.cpp b11003 (0.4.1), and regenerated from upstream's own headers from now on. The package used to carry a hand-curated upstream/llama_cplus.h mirroring llama.cpp's ABI; it had drifted badly — still declaring llama_model_params::use_mmap, use_direct_io and use_mlock after upstream deleted them, never having heard of load_mode, lazy_mode, load_mtp or llama_context_params::n_outputs_max_per_seq. Every field past n_gpu_layers read the wrong bytes. build.sh now runs cpc-bindgen directly on include/llama.h and tools/mtmd/mtmd.h, so layouts come from clang's record layout of the real header and the mirror is gone.
  • The facade gained a sampler chain (temperature/top_k/top_p/min_p/ seed on load, greedy at temperature 0), Session::tokens_for, describe, reset, train_context_size, parameter_count, embedding_size, vocabulary_size, bitmap_audio, and Bitmap dimension accessors. Three fixes rode along: sample no longer calls llama_sampler_accept after llama_sampler_sample (which already accepts — the double count skews every stateful sampler), model paths cross to C as a NUL-terminated CString rather than a bare str pointer, and generate clears the KV cache unless asked to continue.
  • llama_cpp gained an e2e suite (src/test_main.cplus) that loads a real GGUF and drives real inference, gated on LLAMA_CPP_TEST_MODEL and loud when it skips. The unit tests never reach a llama_* symbol, so this is the only half that can catch ABI drift — restoring the stale llama_model_params kills the test binary with SIGSEGV, which is what it is there to do.

Platforms and capabilities

  • Windows gained a complete host toolchain path, the standard-library platform layer, a broad native Facet backend, the agent/inspector transport, WinHTTP and filesystem watching, Media Foundation camera capture, Credential Manager storage, file picking, notifications, permissions, haptics, and related capability backends.
  • Linux gained a rebuilt GTK Facet backend and agent surface plus native process/PTY, terminal, HTTP, inotify, camera/V4L2 preview, location, secure storage, file-picker, and inspector paths.
  • Android gained a native Facet runtime and agent surface, recycler-backed controls, rich rendering and input, notifications, application links, secure storage, system file picking, and Maven/AAR dependency resolution.
  • iOS gained a much broader UIKit surface, device/simulator checks, multiwindow and keyboard behavior, context menus, agent access, notifications, application links, and camera/device capability integrations.
  • The capability layer now covers notifications, application links, camera, location, sensors, biometrics, secure storage, permissions, file picking, haptics, sharing, HTTP, and filesystem watching with native implementations or explicit unsupported results per platform.

Completion — one composed answer, three front doors

  • complete FILE:LINE:COL — the caret question, answered whole. The graph decides whether a caret follows a . (the receiver's fields and methods), a :: (a module's items or a type's methods and variants), or neither (everything in scope), filters by the word already typed, and ranks nearest-binding-first. scope-at / type-at / members remain the primitives; deciding which of them a caret is asking is C+'s rules, so it lives in the compiler rather than in each caller. Available as cpc query complete, as the complete_at MCP tool, and as the LSP's textDocument/completion — all three call one function in core.
  • cpc lsp is resident. It used to rebuild the whole project on every request (~2 s each on a large one), which is why it had no completion. The graph is now built once per project root and kept warm, with open buffers overlaid and rebuilds on a worker — the same session type cpc mcp uses, now shared from cplus-core::session rather than duplicated. Measured on a 185-module project: 3.7 s for the first answer, 3.7 ms for every one after it.
  • cpc lsp resolves dependencies. It loaded projects without their declared [dependencies], so every vendored import reported E0401 imported file not found, the graph never built, and the editor silently fell back to single-file behaviour on any real project. It now resolves the way cpc build does, and diagnostics for such a project are the program's own instead of a fabricated missing import.
  • The graph is indexed. CodeGraph carries by_id / by_name (symbol id and bare name to node indices), built once with it, so def, members, symbols, callable_ids and the completion walks stop scanning the whole node vector. Measured on a 43k-node project: find_members 1.63 → 0.29 ms, 2.67 → 0.43 ms on a 107-member type, file_symbols 0.12 → 0.07 ms. A symbol id can repeat (two impl blocks, one method name), so both maps hold a list — and answers stay in declaration order, so an indexed query answers identically to the scan it replaced. cpc lsp also stopped canonicalizing every file in the project on every request.

facet — Return in a prompt commits what you TYPED

  • The key equivalents landed and Return still "did not work", because the sheet committed the value it OPENED with. An NSTextField does not hold the text you are typing — the window's shared FIELD EDITOR does, and stringValue is only refreshed from it when editing ENDS. Clicking a button ends editing on the way, because the click moves first responder. Return does not: it arrives through performKeyEquivalent:, which fires the button before anything touches the responder chain. So a Rename kept the old name and a New File got an empty one. prompt_ok now sends endEditingFor: before reading, which is what every native dialog does.
  • The routing itself is now tested rather than left to hands. A real NSEvent through performKeyEquivalent: is what a window does with a key-down before the responder chain sees it, so driving it is the routing and not a stand-in for it. Setting keyEquivalent and asserting the property — which is all the previous change did — asserts nothing about whether AppKit delivers the key.
  • Also pinned, and NOT facet's doing: a prompt opens with the caret in the field and the old name selected. A selectText: call was added here on the assumption that it did not, and removed again when measurement showed it changed nothing — AppKit's initial-key-view behaviour already does both. The test stays, because a future change to the tree could take it away.

agent — wired was false for a gesture, and click acted then denied it

  • A regression from wired itself. It read appkit_ext's agent-click SLOT for every non-control — a slot facet never fills — so every gesture-bound container reported wired=false. That is the one CORRECT way to make a non-control clickable, and the very form the flag was added to tell apart from a dead button.
  • A framework swaps in a class that receives input only for a node that HAS input: facet leaves a plain container as a FlexFlippedView, which does not answer performClick:, and arms it into a FacetInput00, which does. So answering the selector IS the answer. The exception is appkit_ext's own two classes, whose performClick: reads the slot and does nothing when it is empty — for those, and only those, the slot is the question.
  • click no longer acts and then reports no_handler. The first version sent the click anyway on the reasoning that a report is not a refusal. That was worse than the bug it replaced: a verb that performs the action and says it did not cannot be trusted in either direction, and an agent retrying does the thing twice. It refuses without sending now, so a disagreement is inert.

facet_runtime — a screen is not freed while its jobs are running

  • App::run settles before dropping the primary screen. Two of the three paths that free a screen did not. pushed_closed calls on_detach, removes the entry and frees the box; presented_closed frees the tree — neither waited. A worker still inside that memory keeps writing, the next screen is allocated into it, and the fault surfaces in whatever was allocated next, which is why this family of crash never looked like the screen that caused it.
  • Both settle now, and the wait lives at the FREE rather than at a caller: close_all_pushed runs before App::run's settle, so a wait at the caller would still have been too late for pushed screens.
  • The bailout says so. spins > 100000 turns "teardown would wedge" into "teardown frees anyway", which is the same crash by another route. It is now a line on stderr rather than a silent break.

facet — the dialogs answer the keyboard, and open on a value

  • Return and Escape. A native alert gives a window a default button and a cancel button for free; a facet sheet is built out of facet controls — which is what makes it keyed and agent-drivable — and the cost is that everything the platform did for nothing has to be said. It was not said, so typing a name into a prompt and pressing Return did nothing, and the only way out of any sheet was the mouse. The primary button now carries keyEquivalent "\r" and the secondary "\x1b", which also draws the default accented. Return fires while the text field has focus, because a window's default button answers regardless of first responder.
  • A choose sheet binds neither, deliberately: its buttons are N options and none is "the obvious one", so binding Return would act on the user's behalf without being asked.
  • prompt opens on a value. initial: — the ledger's initialValue, which is NOT the placeholder: one is the hint shown while the field is empty, the other is what the field starts with and what a person then edits. Only the hint had reached facet's signature, so Rename opened on nothing with the old name greyed out behind the caret. Appended last, on the neutral base and both facades.
  • Guard 8: an implementation CLAIM accounts for the row's parameters. METHOD_DROPS says "facet says it as runtime::prompt" and that was checked at the METHOD level only — the verb exists, so the row is answered. A claim on a row with three or more parameters now has to say what each one is carried as, or that it is absent and why. It found two more the moment it ran: DisplayActionSheet's cancel and destruction — a choose sheet has no cancel button and no way to mark an option destructive, and neither absence had been decided. Nine unaccounted parameters print on every regen.

facet_uikit — a context menu, as the platform's own long press

  • context_menu is built on iOS: UIContextMenuInteraction + UIMenu / UIAction, in menus.cplus. A right-click and a touch-and-hold are one affordance — Apple's own framing — so the ledger's ContextFlyout maps 1:1 and facet's contract never says "right click"; each backend picks the gesture its platform has, the way swipeable is a menu on macOS and a pan on iOS.
  • The menu is built when it OPENS, not when it is attached. A UIMenu is immutable and arrives from a provider block, where AppKit mutates an NSMenu built at attach time — so the provider re-reads the nodes each time and enabled and the titles are current with no refresh path at all.
  • A UIAction is the sender. UIActionHandler is void (^)(UIAction *), so one block type serves every item. UIAccessibilityIdentification is not adopted by UIMenuElement, so the key rides UIAction.identifier — the slot that means the same thing — and the payload rides an associated object. component::key_of / item_of answer off a menu action exactly as off a row, and reading a sender that is not a view answers empty instead of raising, the same guard AppKit needed.
  • A separator is an inline group. UIKit has no separator element; the runs either side of a titleless item become UIMenuOptionsDisplayInline sub-menus, which is what draws the divider.
  • Inside a table the TABLE answerstableView:contextMenuConfiguration ForRowAtIndexPath:point: — because UITableView intercepts the long press to arbitrate it against scrolling. The same split that made a context_menu in an NSOutlineView row unreachable on AppKit. Both paths end in one function.
  • The blocks are hand-built with a static descriptor rather than taken from the generated binding: Block_copy keeps the descriptor POINTER, and a UIAction is retained by UIKit for the life of the menu, so a descriptor on the frame that built the action is read after that frame is gone.
  • Six checks in selftest.cplus, run on a real simulator by tools/run_ios_tests.sh — 78 passed, 0 failed. Disabling the two lines in wants_view / create turns two of them red, which is what makes them checks rather than decoration.

facet — the payload half of a handler

  • item: at construction, on the eight controls whose ledger type declares CommandParameter — button, checkbox, icon_button, refreshable, menu_item, context_menu_item, swipe_item, toolbar_item. It sits on the shared band beside key, and the pair is the point: key is the ADDRESS a node answers to, item is the PAYLOAD its handler receives.
  • It had to be a constructor parameter rather than only core::set_item. A handler is fn(sender, ctx) and a bound method fills ctx with its RECEIVER (E0824) — the same pointer for every row — so the payload is the only thing that can say which one, and it has to be settable in the same expression that sets the handler. Reached only through a setter it forces a builder call into three statements, which is why an application that needed it encoded the payload into the KEY and parsed it back out instead.
  • Appended LAST in each signature, so nothing a caller already passes positionally moves; every existing call site compiles unchanged.
  • CommandParameter stopped being dropped as MVVM. It shared one regex and one reason string with Command — but Command is ICommand, which IS the view-model and is rightly dropped, while CommandParameter is the ARGUMENT the handler needs. facet had already rebuilt the concept from first principles as core::set_item / item_of, whose own comment reads "an item IS the payload half of a handler" — this row's definition. The map now points at where it lives instead of contradicting the code.

facet — the contract closes over TYPES, not only rows

  • Guard 7: a type the ledger renders must be decided. A row could not leave the ledger without a reason — an unmapped one fails the run — and a TYPE could. The existing type closure builds its work list from the rows a type declares, so a type whose surface is entirely INHERITED was never asked about, which is exactly what a marker type looks like: MenuFlyoutSeparator declares one member, its constructor.
  • The floor is the ledger's own definition of "this renders": every type it puts on screen has an I<Name>Handler. Each must be extracted, aliased, dropped by family, or UNBUILT — a fourth outcome the other three could not express, because ALIAS claims "already covered, nothing refused".
  • Three were claiming exactly that and were wrong: MenuFlyoutSubItem aliased to MenuFlyout (a submenu is not a flyout), MenuBar to MenuBarItem (the item is not the bar), SwipeItemView to SwipeItem. Six types now print each regen with the platform answer named: MenuBar, MenuFlyoutSeparator, MenuFlyoutSubItem, ShapeView, SwipeItemMenuItem, SwipeItemView.
  • The check runs from gen_contract, not from ledger_spec, because that is where it RUNS: ledger_spec.py has not been able to regenerate the spec for some time (check_type_closure fails on Matrix, NavigationProxy, Shape, TableModel and the GIF decoder, none of which have a family rule), so ledger-spec.json is a frozen artifact and a floor living only there would never fire. Repairing that tool is its own job.

facet — six gaps found building iris

  • A context_menu in a tree or list row can now open. A table or outline view handles right-click itself and asks its OWN menu, which facet never set — so a menu nested in a row was built, attached to the row's view, and unreachable. Both row hosts are facet subclasses now (FacetTableView / FacetOutlineView) whose menuForEvent: answers with the clicked row's menu, after calling super so clickedRow still draws the ring. swipeable/swipe_item was unreachable the same way and is reachable for the same reason.
  • A context_menu under an unkeyed container is no longer dropped. A node carrying one asks for a view exactly as one with a gesture or a background does — wants_menu joins has_gestures and wants_painting. Direct children only, so the rule agrees with attach_context_menu.
  • run_job answers whether it started, and refuses a second flight. Two workers inside one job's run() assign the same fields; where those are Text or Vec the second frees what the first is writing. It shipped as a SIGSEGV. A 64-slot table of job addresses, claimed before the worker spawns and released when the apply is done. job_in_flight is the question five services were each answering with a private flag.
  • The agent surface tells a wired control from a dead one. describe_ui reports wired beside actionable and clickable, and click answers no_handler — because an NSControl is driven by its target/action and ONLY that, so a gesture band on a button never fires. Made honest at the source: controls::arm_control installs an action when there is something on the other end and clears it when there is not, so lldb and Accessibility Inspector read the same fact.
  • The neutral runtime facade compiles, and keeps the facade's surface. runtime.cplus promises app code type-checks on every target; it had stopped compiling on all of them (Appearance::System never existed) and was missing eight members the real facades carry. Build it with cpc build --target android-arm64 — a platform-shadowed file is unbuilt by default on the platform you are standing on.
  • A context_menu_item handler can tell which item fired it. A handler is fn(sender, ctx) and ctx is the component — the same pointer for every row — so identity is read off the sender, and a menu item was the one control kind carrying neither half, because both bindings live in views::create and a menu kind never gets a view. bind_menu_identity gives it the key as its accessibilityIdentifier and the item pointer as an associated object, so component::key_of / item_of answer from a menu action exactly as from a button. swipe_item gets it too.
  • A sender that is not a view no longer raises. key_of and item_of walk superview when the first hop finds nothing, and an NSMenuItem does not answer it — so the natural line sent an unrecognised selector from inside a menu action. input::superview_of answers 0 for an object with no superview, which is what the walk's termination condition already means.
  • context_menu on iOS is an audible debt. Being a non-view kind was answered first and stopped the question, so declaring one produced no menu and no warning. It warns once now, like every other not-yet-built kind. facet_gtk's README carries the same decision in writing.

Language — a label names a parameter of the receiver's own method

  • Two types may share a labeled method name. Named arguments are rewritten into positional order by the LOWERING pass, which runs before types exist and so keys method candidates by bare name — every type declaring on was a candidate for s.on(v: 5), and the call was E1002 whenever those candidates disagreed. It now settles only what every candidate agrees on and leaves the rest to sema, which knows the receiver's type and arranges the call from that type's own parameter list. reload(then:, then_ctx:) on two stores no longer breaks either one's callers, and no method needs renaming to make a labeled call resolve.
  • Two identical declarations are one arrangement. The dedup that decides whether two candidates would lower a call the same way compared the spliced default expressions, and an Expr carries its span — so the 0 in one impl block never equalled the 0 in another, and any call omitting a shared default was reported as ambiguous between two declarations a reader cannot tell apart. Defaults now compare by VALUE.
  • A label that fits only another type's method was a MISCOMPILE, not an error. Given A::go(v, ctx = 0) and B::go(ctx, v = 9), the call a.go(ctx: 3) had exactly one candidate that accepted it — B's — and that arrangement was applied to an A receiver: the label ctx bound to A's parameter v, and B's default 9 was spliced into A's ctx. It compiled, and returned a value, for a call whose required argument was never given. Accepting is not belonging: with more than one candidate, a candidate rejecting the call is itself type-dependent information, so the decision goes to sema. a.go(ctx: 3) is now E0308, naming A's own v.
  • E1002 is left for the callees that really have no parameter names: a fn-pointer value (its type records parameter types, not names — a handler field included), and a method reached through a generic receiver, which is not one type until it is instantiated. Its message says which, instead of describing method-name matching that no longer decides anything.