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 useentry(withsrc/main.cplusas the default), and C-ABI libraries use[library].executor::block_on/executor::runare removed. Useasync fn main,.wait(), orfuture::wait_or_cancel.- A
guard letcomplement iselse Pattern, without pipe delimiters. facet/runtimeandfacet/agentmoved to thefacet_runtimeandfacet_agentpackages. The application and navigation APIs changed with the split; the detailed migration table is linked below.- The raw SQLite import is
sqlite/raw;sqlite_ffiis no longer a separate package.static-arenais nowstatic_arena, andterminal/appkitis nowterminal/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 mainand async#[test]functions are driven by the compiler. Synchronous code consumes a future through.wait()or the cancellablefuture::wait_or_cancelpath; 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 throughApp::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 andruntime::Window, global content navigation, andnav::Showmodes. See the migration table.
Lifecycle
- A component is told when it is on screen, whatever moved the tree.
component::Attach::Foregroundandcomponent::Detach::Backgroundnow 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 ancestorDisplay::Noneor invisible — and announces the transitions; no mutating verb announces anything, soswitch_toparking a pane,set_shownhiding 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 throughcore::frames_settled, where band rules have resolved intodisplay). 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::Mountagain on every restore, and nothing at all on the way out. The node's single attach slot carries no reason, socomponent::childcould say one word; the reason now rides the node's presentation record, the repairscreen::ScreenBoxalready had.Mountkeeps 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 setson_attachon it.
Toolchain
Thisin 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 resolvedThisagainst whicheverimplblock came last. With two blocks in one file,let t: This = this;in the first one typedtas 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
staticthat nothing inside the package reads is reached from nowhere — so it was in no member oflib<name>.aand no export of the dylib, though it is madeweak_odrfor 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>.aused to hold a single object, so resolving any symbol from a package linked every module it had — a facet app on Android that importsstdlib/veclinkedprocessandpty, and with them aposix_spawnAPI 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=1restores 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)withoutfacet/gestures: twenty errors insidevendor/, the real one last).
Packages
- A bundled archive named
lib<package>.aappears 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_cppregenerated against llama.cpp b11003 (0.4.1), and regenerated from upstream's own headers from now on. The package used to carry a hand-curatedupstream/llama_cplus.hmirroring llama.cpp's ABI; it had drifted badly — still declaringllama_model_params::use_mmap,use_direct_ioanduse_mlockafter upstream deleted them, never having heard ofload_mode,lazy_mode,load_mtporllama_context_params::n_outputs_max_per_seq. Every field pastn_gpu_layersread the wrong bytes.build.shnow runscpc-bindgendirectly oninclude/llama.handtools/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/seedonload, greedy at temperature 0),Session::tokens_for,describe,reset,train_context_size,parameter_count,embedding_size,vocabulary_size,bitmap_audio, andBitmapdimension accessors. Three fixes rode along:sampleno longer callsllama_sampler_acceptafterllama_sampler_sample(which already accepts — the double count skews every stateful sampler), model paths cross to C as a NUL-terminatedCStringrather than a barestrpointer, andgenerateclears the KV cache unless asked to continue. llama_cppgained an e2e suite (src/test_main.cplus) that loads a real GGUF and drives real inference, gated onLLAMA_CPP_TEST_MODELand loud when it skips. The unit tests never reach allama_*symbol, so this is the only half that can catch ABI drift — restoring the stalellama_model_paramskills 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/membersremain 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 ascpc query complete, as thecomplete_atMCP tool, and as the LSP'stextDocument/completion— all three call one function in core.cpc lspis 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 typecpc mcpuses, now shared fromcplus-core::sessionrather than duplicated. Measured on a 185-module project: 3.7 s for the first answer, 3.7 ms for every one after it.cpc lspresolves 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 waycpc builddoes, and diagnostics for such a project are the program's own instead of a fabricated missing import.- The graph is indexed.
CodeGraphcarriesby_id/by_name(symbol id and bare name to node indices), built once with it, sodef,members,symbols,callable_idsand the completion walks stop scanning the whole node vector. Measured on a 43k-node project:find_members1.63 → 0.29 ms, 2.67 → 0.43 ms on a 107-member type,file_symbols0.12 → 0.07 ms. A symbol id can repeat (twoimplblocks, 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 lspalso 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
stringValueis 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 throughperformKeyEquivalent:, 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_oknow sendsendEditingFor:before reading, which is what every native dialog does. - The routing itself is now tested rather than left to hands. A real
NSEventthroughperformKeyEquivalent: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. SettingkeyEquivalentand 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
wireditself. It read appkit_ext's agent-click SLOT for every non-control — a slot facet never fills — so every gesture-bound container reportedwired=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 answerperformClick:, and arms it into aFacetInput00, which does. So answering the selector IS the answer. The exception is appkit_ext's own two classes, whoseperformClick:reads the slot and does nothing when it is empty — for those, and only those, the slot is the question. clickno longer acts and then reportsno_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::runsettles before dropping the primary screen. Two of the three paths that free a screen did not.pushed_closedcallson_detach, removes the entry and frees the box;presented_closedfrees 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_pushedruns beforeApp::run's settle, so a wait at the caller would still have been too late for pushed screens. - The bailout says so.
spins > 100000turns "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 silentbreak.
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
choosesheet 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. promptopens on a value.initial:— the ledger'sinitialValue, 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_DROPSsays "facet says it asruntime::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'scancelanddestruction— 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_menuis built on iOS:UIContextMenuInteraction+UIMenu/UIAction, inmenus.cplus. A right-click and a touch-and-hold are one affordance — Apple's own framing — so the ledger'sContextFlyoutmaps 1:1 and facet's contract never says "right click"; each backend picks the gesture its platform has, the wayswipeableis a menu on macOS and a pan on iOS.- The menu is built when it OPENS, not when it is attached. A
UIMenuis 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 andenabledand the titles are current with no refresh path at all. - A UIAction is the sender.
UIActionHandlerisvoid (^)(UIAction *), so one block type serves every item.UIAccessibilityIdentificationis not adopted byUIMenuElement, so the key ridesUIAction.identifier— the slot that means the same thing — and the payload rides an associated object.component::key_of/item_ofanswer 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
UIMenuOptionsDisplayInlinesub-menus, which is what draws the divider. - Inside a table the TABLE answers —
tableView:contextMenuConfiguration ForRowAtIndexPath:point:— because UITableView intercepts the long press to arbitrate it against scrolling. The same split that made acontext_menuin 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_copykeeps 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 bytools/run_ios_tests.sh— 78 passed, 0 failed. Disabling the two lines inwants_view/createturns 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 declaresCommandParameter— button, checkbox, icon_button, refreshable, menu_item, context_menu_item, swipe_item, toolbar_item. It sits on the shared band besidekey, 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 isfn(sender, ctx)and a bound method fillsctxwith 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.
CommandParameterstopped being dropped as MVVM. It shared one regex and one reason string withCommand— butCommandisICommand, which IS the view-model and is rightly dropped, whileCommandParameteris the ARGUMENT the handler needs. facet had already rebuilt the concept from first principles ascore::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:
MenuFlyoutSeparatordeclares 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:
MenuFlyoutSubItemaliased toMenuFlyout(a submenu is not a flyout),MenuBartoMenuBarItem(the item is not the bar),SwipeItemViewtoSwipeItem. Six types now print each regen with the platform answer named: MenuBar, MenuFlyoutSeparator, MenuFlyoutSubItem, ShapeView, SwipeItemMenuItem, SwipeItemView. - The check runs from
gen_contract, not fromledger_spec, because that is where it RUNS:ledger_spec.pyhas not been able to regenerate the spec for some time (check_type_closurefails onMatrix,NavigationProxy,Shape,TableModeland the GIF decoder, none of which have a family rule), soledger-spec.jsonis 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_menuin a tree or list row can now open. A table or outline view handles right-click itself and asks its OWNmenu, 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) whosemenuForEvent:answers with the clicked row's menu, after calling super soclickedRowstill draws the ring.swipeable/swipe_itemwas unreachable the same way and is reachable for the same reason. - A
context_menuunder 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_menujoinshas_gesturesandwants_painting. Direct children only, so the rule agrees withattach_context_menu. run_jobanswers whether it started, and refuses a second flight. Two workers inside one job'srun()assign the same fields; where those areTextorVecthe 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_flightis the question five services were each answering with a private flag.- The agent surface tells a wired control from a dead one.
describe_uireportswiredbesideactionableandclickable, andclickanswersno_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_controlinstalls 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.cpluspromises app code type-checks on every target; it had stopped compiling on all of them (Appearance::Systemnever existed) and was missing eight members the real facades carry. Build it withcpc build --target android-arm64— a platform-shadowed file is unbuilt by default on the platform you are standing on. - A
context_menu_itemhandler can tell which item fired it. A handler isfn(sender, ctx)andctxis 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 inviews::createand a menu kind never gets a view.bind_menu_identitygives it the key as itsaccessibilityIdentifierand the item pointer as an associated object, socomponent::key_of/item_ofanswer from a menu action exactly as from a button.swipe_itemgets it too. - A sender that is not a view no longer raises.
key_ofanditem_ofwalksuperviewwhen 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_ofanswers 0 for an object with no superview, which is what the walk's termination condition already means. context_menuon 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
onwas a candidate fors.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
Exprcarries its span — so the0in one impl block never equalled the0in 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)andB::go(ctx, v = 9), the calla.go(ctx: 3)had exactly one candidate that accepted it — B's — and that arrangement was applied to anAreceiver: the labelctxbound to A's parameterv, and B's default9was spliced into A'sctx. 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 ownv. - 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.