Every entry on this page is grounded in the actual source - not the plan docs. “Supported” means the path is wired end-to-end: the compiler emits, the .ktx wire format carries it, the runtime resolves it, and an adapter consumes it. Audited against HEAD on 2026-05-17.
Fully routed Material3 + Foundation + Coil components, KSP-generated.
Named-arg modifier folding - padding, fill, size, background, clickable.
Network, storage, navigation, platform, dispatchers, Flow operators.
Logic, coroutine, Compose state, control flow, VM lifecycle.
KSP reads adapter-scan-roots.txt, resolves each FQ name against the real Compose / Material3 / Foundation / Coil declarations on the build classpath, and emits typed adapters. Every parameter on every adapter is wired - never a curated subset.
These IDs exist in KBCAdapterIds but aren’t in the scan-roots manifest yet. KBC source referencing them fails with KetoyCompilationError.UnregisteredComposable. Adding one is a one-line change to adapter-scan-roots.txt followed by ./gradlew :ketoy-adapters-material3:kspRelease.
The host registers custom adapters on KBCAdapterRegistry and supplies a matching FQ in its own scan-roots file. Additional adapter catalogs published by other Gradle modules are loaded via composeAdapterCatalogPath.
Every parameter on these adapters is keyed by name, not position. The emitter maps each argument’s param name to the catalog’s sourceIndex and encodes in catalog space, so a newer Compose that adds, reorders, or removes a param no longer shifts your arguments into the wrong slot. A param the catalog doesn’t know is dropped to the Compose default with a build-time advisory - never a silent wrong render. Bump your Compose BOM freely; see the version compatibility update.
Complex Compose value types aren’t encoded inline. The compiler emits a CONSTRUCT_JVM opcode that builds the object via a registered constructor adapter and stores it in a register; the consuming COMPOSABLE_CALL reads it as KBCValue.Register(n).
IDs are reserved for TEXT_STYLE, SPAN_STYLE,PARAGRAPH_STYLE, TEXT_FIELD_VALUE,CUT_CORNER_SHAPE, SHADOW, OFFSET,BORDER_STROKE, ANNOTATED_STRING,VISUAL_TRANSFORM_PASSWORD, TEXT_SELECTION_COLORS,MUTABLE_INTERACTION_SOURCE, plus a coherent 0x0016…0x001Fblock for the remaining Material3 *Defaults factories.
TextStyle in particular needs the classifier to model FontSynthesis,BaselineShift, TextGeometricTransform,LocaleList, Shadow, TextDirection, andTextIndent before it can ship.
The IR walker extracts modifier builder calls at emit time into a KBCModifierDescriptor, pools it into the bundle’s modifier table, and references it via KBCValue.ModifierRef(idx). Argument resolution is named-arg - so K2’s partial-default lowering like padding(top = 48.dp, start = 16.dp) works correctly.
Weight is handled at the layout-scope level by Row / Column adapters via KBCParamSet.getWeightOrNull(idx) rather than during modifier folding.
Shapes - KBCShape covers:RectangleCircleRoundedCorner(size)RoundedCornerPercent(percent)CutCorner(size)
Compose value reads like FontWeight.Bold, Color.Red, 16.dp,Arrangement.SpaceEvenly all flow through a single ComposeTokenRegistryand encode as the matching KBCValue.*Id byte or inline literal.
Thin → Black
Named Compose colors + ARGB literals
Start, Center, SpaceBetween, SpaceEvenly…
Top/Center/Bottom × Start/Center/End
Fit, Crop, Inside, FillBounds…
Text, Email, Number, Password, Phone…
Done, Next, Search, Send, Go…
Full Material typography scale
-keep rulesMaterial icons flow through KBCValue.StringLiteral carrying the canonical FQ. The host registers a MaterialIconsResolver built via thematerialIconsResolver { } DSL with per-style helpers (registerFilled, registerRounded,registerOutlined, registerSharp,registerTwoTone, plus the five registerAutoMirrored* variants).
The compiler’s ComposeTokenRegistry pattern-matches all 10 Material icon style packages and 13 Icons.<get-Style> selectors. Missing-icon fallback is a 24×24 transparent PlaceholderImageVector- surfaces gaps visibly rather than crashing.
The KBC interpreter is register-based with 106 opcodes. The IR-to-KBC lowering handles every IR node listed below; everything else folds statically (Compose runtime infra), routes to an allowlisted intrinsic, or fails compilation with a typed KetoyCompilationError.
Int, Long, Float, Double, Boolean, String, Unit, null, Char. Full arithmetic, comparison, boolean ops, Int↔Long/Float/Double conversions, nullable boxing/unboxing.
Variadic STRING_CONCAT - every +, interpolation, or multi-arg concat collapses to one opcode. Per-bundle deduplicated string pool.
if, when, while, do-while, break, continue, return, throw, and try/catch.
Caveat: catch is currently catch-all - multi-catch collapses to the first handler. Rethrow inside a catch works correctly.
val onClick = { }, list.forEach { }, lambda parameters to adapters. Closures with captured outer-scope locals are fully supported via KBCValue.ClosureRef - capture values snapshot eagerly at slot resolution time.
Audited against 6 capture patterns: single, multi, navigation-onClick, nested two-level, branch, non-capturing baseline.
State-machine lowering with SUSPEND_POINT/RESUME_VALUE chains. Structured concurrency via SupervisorJob. CancellationException is preserved.
listOf, mapOf, mutableListOf intrinsified. Bigger stdlib ops (map, filter, reduce) flow through INVOKE_VIRTUAL against the KBC heap.
Both layers are load-bearing in production. The host owns lifecycle and persistence; KBC owns business logic.
Extends Android ViewModel, owns a KetoyVM per screen, instantiated by KetoyScreen via viewModel(factory).
State map is Map<String, Any?>. Persistable types - primitives + arrays - mirror to SavedStateHandle. Restore priority: SavedState > initialExtras > empty.
Base class developers extend when writing @KetoyViewModel-annotated classes. Exposes four lateinit var properties bound post-construction before init() fires.
open fun init() runs once after binding. Default no-op. Constructor parameters resolve via the host’s KetoyCapabilityProvider - capability IDs only, never Hilt graph access.
@KetoyViewModel
class CounterViewModel : KetoyBaseViewModel() {
override fun init() { setState("count", 0) }
fun increment() {
val current = getState("count") as? Int ?: 0
setState("count", current + 1)
}
}MutableStateFlow / MutableSharedFlow via STATE_FLOW_CREATE / SHARED_FLOW_CREATEviewModelScope.launch { } and async { }suspend functions, withContext(Dispatchers.IO), Flow.collectmap, filter, debounce, flatMapLatest, combine, take, distinctUntilChanged@KetoyCapabilityStub (Room DAO, Retrofit, DataStore)onCleared. Lifecycle is owned by KetoyVirtualViewModel; init() is the only entry. Cleanup is implicit via viewModelScope cancellation.These produce typed diagnostics with the offending FQ name, a rationale, a runnable fix example, and a docs URL.
The bridge between KBC and the host platform. KBC code never sees Hilt, Retrofit, OkHttp, Room DAOs, DataStore, or the Android Context - every interaction routes through a Short-ID-keyed lambda registered on CapabilityRegistry.
There is no special generator. The host writes a normal Room @Entity+ @Dao + @Database, injects the DAO into itsKetoyCapabilityProvider, and registers each operation under a0x4000+ ID via KBCRoomBridge helpers. The KBC side calls a @KetoyCapabilityStub-marked function and the compiler plugin rewrites the call to INVOKE_CAPABILITY_SUSPEND.
Retrofit, DataStore typed flows, Bluetooth, camera, WorkManager - same pattern, same wrap-as-capability flow.
Brotli-compressed, Ed25519-signed binary container. Format v2 - additive, so pre-2.1 readers parse v2 files with one field defaulted. 10 ordered sections.
KetoyBundleSource has 4 variants. Remote loading uses Ktor + OkHttp with on-device ETag cache; 304 serves cache; offline fallback to cache on network failure.
Ed25519 keypair generation via openssl genpkey (raw 32-byte seed). Signing plumbed through KetoyBundleTask. Verification: Ed25519 → KtxReader → BundleValidator inside KetoyRuntime.parseBundle.
DexMaker-backed JIT for whitelisted pure-logic functions - no capabilities, no coroutines, no Compose, no DIV/MOD on Int/Long. Activated on API 26+ when enableJIT = true. Translation failures fall back to the interpreter silently.
Speed gate: ≥1.5× over the interpreter.
Auto-enabled on debug builds (FLAG_DEBUGGABLE). Shows COMPOSABLE_CALL / CONSTRUCT_JVM / capability dispatch logs with human-readable names. Every emit site is gated - zero overhead in release.
FakeAdapterRegistry + FakeConstructorRegistry (record-only fakes), KBCBuilder + ParamBuilder sparse-encoding DSL, buildKBCBundle { } named-entry-point DSL, KetoyTestRuntime with descriptive entry-point errors.
id("dev.ketoy.compiler") from the Gradle plugin portal plus the BOM. Choose the mode that fits your project layout.
A separate library module holds the KetoyBC source. Produces .ktx in build/ketoy-bundles/. The consumer (:app) copies the asset.
ketoy { exportFromAppModule = true; bundleVariant = "release" } on the host APK. The compiler attaches only to the selected variant’s compile<Variant>Kotlin; output lands in src/main/assets/ketoy/<bundleId>.ktx and the plugin auto-wires the merge<Variant>Assets dependency.
./gradlew ketoyBundle produces a signed .ktx against the host-supplied private key. The runtime activation policy (PackageInfo.longVersionCode check, rollback, onBundleAppVersionMismatch callback) is plumbed through the format but not yet enforced at runtime - that’s a Phase 11B/D item.