Ketoy
Reference · code-verified

Supported features

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.

16

Composable adapters

Fully routed Material3 + Foundation + Coil components, KSP-generated.

27

Modifier operations

Named-arg modifier folding - padding, fill, size, background, clickable.

67

Built-in capabilities

Network, storage, navigation, platform, dispatchers, Flow operators.

106

KBC opcodes

Logic, coroutine, Compose state, control flow, VM lifecycle.

01 · Compose components

Fully catalogued (16)

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.

Text0x0001
15-param canonical String overload (post-1.5; minLines at position 14). AnnotatedString overload and the 16-arg deprecated overload deliberately not picked.
modifiercolorfontSizefontStylefontWeightfontFamilyletterSpacingtextDecorationtextAlignlineHeightoverflowsoftWrapmaxLinesminLines
Column0x0002
Content slot resolves via KBCParamSet.getContentSlot.
modifierverticalArrangementhorizontalAlignment
Row0x0003
Same shape as Column.
modifierhorizontalArrangementverticalAlignment
Box0x0004
Content slot at source-position 3.
modifiercontentAlignmentpropagateMinConstraints
Scaffold0x0007
Full Material3 signature with the trailing PaddingValues content slot.
topBarbottomBarsnackbarHostfloatingActionButtonfabPositioncontainerColorcontentColorcontentWindowInsets
Surface0x0008
Non-clickable overload - the clickable variant is excluded.
modifiershapecolorcontentColortonalElevationshadowElevationborder
Card0x0009
6-param non-clickable overload. Colors and elevation built via CardDefaults object factories.
modifiershapecolorselevationborder
Spacer0x000B
Modifier-only. Width/height live in the modifier chain (Modifier.height(20.dp)).
modifier
Button0x000C
Full Material3 shape with content slot at source position 9.
onClickmodifierenabledshapecolorselevationbordercontentPaddinginteractionSource
IconButton0x000F
Full signature with honour-default enabled.
onClickmodifierenabledcolorsinteractionSource
TextField0x0012
String overload picked explicitly. 22 parameters end-to-end.
valueonValueChangemodifierenabledreadOnlytextStylelabelplaceholderleadingIcontrailingIconprefixsuffixsupportingTextisErrorvisualTransformationkeyboardOptionskeyboardActionssingleLinemaxLinesminLinesinteractionSourceshapecolors
Checkbox0x0014
Full signature.
checkedonCheckedChangemodifierenabledcolorsinteractionSource
Switch0x0016
Full signature with thumbContent slot.
checkedonCheckedChangemodifierthumbContentenabledcolorsinteractionSource
AsyncImage0x0018
Coil 16-arg post-1.x overload (clipToBounds at pos 14).
modelcontentDescriptionmodifierplaceholdererrorfallbackonLoadingonSuccessonErroralignmentcontentScalealphacolorFilterfilterQualityclipToBounds
Icon0x0019
ImageVector overload - picked via nullability-aware disambiguator over Painter / ImageBitmap variants. Resolved at runtime via a host-provided KBCImageVectorResolver.
imageVectorcontentDescriptionmodifiertint
TopAppBar0x001D
Post-1.2 overload with expandedHeight: Dp. Colors/insets/scrollBehavior fall through to Compose defaults. Requires @OptIn(ExperimentalMaterial3Api::class) in KBC source.
titlemodifiernavigationIconactionsexpandedHeight

Reserved but not yet routed (19)

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.

LAZY_COLUMN · 0x0005LAZY_ROW · 0x0006ELEVATED_CARD · 0x000AOUTLINED_BUTTON · 0x000DTEXT_BUTTON · 0x000EFAB · 0x0010EXTENDED_FAB · 0x0011OUTLINED_TEXT_FIELD · 0x0013RADIO_BUTTON · 0x0015SLIDER · 0x0017DIVIDER · 0x001ACIRCULAR_PROGRESS · 0x001BLINEAR_PROGRESS · 0x001CNAVIGATION_BAR · 0x001ETAB_ROW · 0x001FALERT_DIALOG · 0x0020BOTTOM_SHEET · 0x0021LAZY_VERTICAL_GRID · 0x0022HORIZONTAL_PAGER · 0x0023

App-specific composables - range 0x4000+

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.

02 · Constructor parameters

CONSTRUCT_JVM - registered adapters

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).

0x0004KeyboardOptions(…)constructor
0x0005KeyboardActions(…)constructor
0x0007RoundedCornerShape(corner: Dp)factory
0x0021RoundedCornerShape(percent: Int)factory
0x0022RoundedCornerShape(Dp×4)per-corner factory
0x0023RoundedCornerShape(Int×4)percent-per-corner factory
0x0012CardDefaults.cardColors(…)object factory · runs in composition
0x0013CardDefaults.cardElevation(Dp×6)object factory
0x0014ButtonDefaults.buttonColors(Color×4)object factory
0x0015ButtonDefaults.buttonElevation(Dp×5)object factory
0x0020TopAppBarDefaults.topAppBarColors(Color×5)object factory

TextStyle and friends - deferred

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.

03 · Modifier chains

27 modifier operations

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.

Padding & sizing

PaddingPaddingAllPaddingHorizontalPaddingVerticalPaddingValuesRefFillMaxWidthFillMaxHeightFillMaxSizeSizeWidthInHeightInWrapContentWidthWrapContentHeightWrapContentSize

Appearance

BackgroundClipBorderShadowClipToBoundsBlurAlpha

Interaction & layout

ClickableOffsetZIndexTestTagWeight*

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)

04 · Tokens & icons

88 token constants across 13 families

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.

FontWeight

9 entries

Thin → Black

Color

17 entries

Named Compose colors + ARGB literals

Arrangement

7 entries

Start, Center, SpaceBetween, SpaceEvenly…

Alignment

12 entries

Top/Center/Bottom × Start/Center/End

ContentScale

6 entries

Fit, Crop, Inside, FillBounds…

KeyboardType

10 entries

Text, Email, Number, Password, Phone…

ImeAction

7 entries

Done, Next, Search, Send, Go…

TextAlign / Overflow / Decoration / FontStyle / Typography

remaining 20

Full Material typography scale

Material icons - R8-safe, no -keep rules

Material 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.

05 · Kotlin language

What the KBC interpreter handles

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.

2.2

Primitives & arithmetic

Int, Long, Float, Double, Boolean, String, Unit, null, Char. Full arithmetic, comparison, boolean ops, Int↔Long/Float/Double conversions, nullable boxing/unboxing.

ADD_INTSUB_INTMUL_INTDIV_INTMOD_INTNEG_INT_LONG_FLOAT_DOUBLE
2.3

Strings

Variadic STRING_CONCAT - every +, interpolation, or multi-arg concat collapses to one opcode. Per-bundle deduplicated string pool.

STRING_CONCATSTRING_LENGTHSTRING_SUBSTRINT_TO_STRING
2.4

Control flow

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.

2.5

Lambdas & closures

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.

2.6

Coroutines

State-machine lowering with SUSPEND_POINT/RESUME_VALUE chains. Structured concurrency via SupervisorJob. CancellationException is preserved.

LAUNCHASYNCAWAITWITH_CONTEXTFLOW_EMITFLOW_COLLECTCOLLECT_AS_STATE
2.8

Collections

listOf, mapOf, mutableListOf intrinsified. Bigger stdlib ops (map, filter, reduce) flow through INVOKE_VIRTUAL against the KBC heap.

LIST_NEWLIST_ADDLIST_GETLIST_SIZELIST_FOR_EACHMAP_NEWMAP_PUTMAP_GET

Classes & type system

Fulldata classequals · hashCode · toString · copy · componentN
Fullsealed class / interfacewhen exhaustiveness, is, as, as?
Fullenum classvalues() / valueOf() via stdlib intrinsics
Fullobject declarationsCompanions resolved via ComposeTokenRegistry / stdlib allowlist
Fullextension functionsTop-level extensions; receiver becomes register 0
Fulltop-level functionsAuto-included in the closure walk
Fullinline functionsFrontend-inlined; emitter sees inlined body. Reified params work for concrete use
Limitedinheritance / interfacesNo virtual-dispatch table beyond INVOKE_VIRTUAL for stdlib + KBC heap
ErasedgenericsErased at IR; type checks only match erased type
Partialvalue / inline classCompose Dp, Sp, Color, TextUnit via tokens. User-defined value classes reach the emitter as their unboxed primitive form
06 · ViewModel support

Two layers - host-side and KBC-side

Both layers are load-bearing in production. The host owns lifecycle and persistence; KBC owns business logic.

Host side

KetoyVirtualViewModel

Extends Android ViewModel, owns a KetoyVM per screen, instantiated by KetoyScreen via viewModel(factory).

State API

state: StateFlowgetStatesetStatesetStateAllremoveStateobserveStatedispatch

State map is Map<String, Any?>. Persistable types - primitives + arrays - mirror to SavedStateHandle. Restore priority: SavedState > initialExtras > empty.

KBC side

KetoyBaseViewModel

Base class developers extend when writing @KetoyViewModel-annotated classes. Exposes four lateinit var properties bound post-construction before init() fires.

Injected properties

viewModelScopegetStatesetStateobserveState

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.

CounterViewModel.kt
@KetoyViewModel
class CounterViewModel : KetoyBaseViewModel() {
    override fun init() { setState("count", 0) }

    fun increment() {
        val current = getState("count") as? Int ?: 0
        setState("count", current + 1)
    }
}

What you can and can’t put in a KBC ViewModel

You can

  • MutableStateFlow / MutableSharedFlow via STATE_FLOW_CREATE / SHARED_FLOW_CREATE
  • viewModelScope.launch { } and async { }
  • suspend functions, withContext(Dispatchers.IO), Flow.collect
  • Every Flow operator - map, filter, debounce, flatMapLatest, combine, take, distinctUntilChanged
  • Calls to host capabilities through @KetoyCapabilityStub (Room DAO, Retrofit, DataStore)
  • Plain Kotlin - data classes, sealed classes, branches, loops
  • Cross-ViewModel calls within the same bundle (via capability path)

You can’t

  • Inject host dependencies directly. Hilt is host-side only - KBC never sees the graph.
  • Hold non-persistable state across process death. Values outside the allowlist drop at save time, reappear absent on restore.
  • Override onCleared. Lifecycle is owned by KetoyVirtualViewModel; init() is the only entry. Cleanup is implicit via viewModelScope cancellation.
07 · Hard non-goals

Compile-time-rejected by CapabilityValidator

These produce typed diagnostics with the offending FQ name, a rationale, a runnable fix example, and a docs URL.

RejectDirectAndroidApiAccessandroid.* / androidx.* calls outside the catalog
RejectReflectionUsagekotlin.reflect.*, java.lang.Class.get*, java.lang.reflect.*
RejectFileIoUsagejava.io.*, java.nio.*, kotlin.io.*
RejectGlobalScopeUsageGlobalScope.*, runBlocking, raw CoroutineScope() factory
RejectUnregisteredCapability@KetoyCapabilityStub referencing an ID not in the host’s registry JSON
RejectUnregisteredComposable@Composable callee not in the catalog or AdapterFqNameRegistry
RejectNonKbcConstructorCompose-domain constructor without a registered adapter
RejectUnregisteredCallAnything else - includes up to 3 fuzzy-match suggestions
08 · Capabilities

67 built-in IDs across 8 ranges

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.

Network0x0500 – 0x0506 · Ktor + OkHttp
HTTP_GETHTTP_POSTHTTP_PUTHTTP_DELETEHTTP_REQUESTSSE_SUBSCRIBEWEBSOCKET_CONNECT · reserved
6 active
Storage · DataStore KV0x0600 – 0x0605
KV_GETKV_SETKV_DELETEKV_GET_ALLKV_CLEARKV_OBSERVE
6
Storage · Room bridge0x0610 – 0x0614 + host range 0x4000+
suspendCapabilityflowCapabilitysyncCapabilityobserveListmutatefindOne
DSL + 5 generic
Navigation0x0700 – 0x0708 · wraps Compose NavController
NAV_PUSHNAV_POPNAV_REPLACENAV_POP_TONAV_DEEP_LINKNAV_SET_RESULTNAV_GET_RESULTNAV_CAN_POPNAV_CURRENT_ROUTE
9
Platform0x0900 – 0x090A
ANALYTICS_TRACKTOASTVIBRATECLIPBOARD_SETCLIPBOARD_GETOPEN_URLREQUEST_PERMISSIONCHECK_PERMISSIONDEVICE_LOCALEAPP_VERSIONLOG
11
ViewModel state0x0A00 – 0x0A03
VM_GET_STATEVM_SET_STATEVM_OBSERVE_STATEVM_DISPATCH
4
Coroutine dispatchers0x0B00 – 0x0B03
DISPATCHER_IODISPATCHER_DEFAULTDISPATCHER_MAINDISPATCHER_UNCONFINED
4
Flow operators0x0C00 – 0x0C08
FLOW_MAPFLOW_FILTERFLOW_FLAT_MAP_LATESTFLOW_COMBINEFLOW_TAKEFLOW_DEBOUNCEFLOW_DISTINCT_UNTIL_CHANGEDSTATE_FLOW_CREATESHARED_FLOW_CREATE
9
App-specific0x4000 – 0x7FFF · your domain
Room DAOsRetrofit endpointsDataStore flowsBluetooth / camera / WorkManager / locationAnalytics SDKPayments
16K slots

The Room / Retrofit / DataStore pattern

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.

09 · Bundle & tooling

.ktx format, signing, JIT, and dev tools

.ktx bundle format

Brotli-compressed, Ed25519-signed binary container. Format v2 - additive, so pre-2.1 readers parse v2 files with one field defaulted. 10 ordered sections.

STRING_POOLADAPTER_MANIFESTCONSTRUCTOR_MANIFESTCAPABILITY_MANIFESTMODIFIER_TABLEFUNCTION_TABLECODEDEBUG_INFOENTRY_POINTSBUNDLE_METADATA

Bundle loading

KetoyBundleSource has 4 variants. Remote loading uses Ktor + OkHttp with on-device ETag cache; 304 serves cache; offline fallback to cache on network failure.

Preloaded(bundle)Raw(bytes)Asset(path)Remote(url, headers)

Signing & key management

Ed25519 keypair generation via openssl genpkey (raw 32-byte seed). Signing plumbed through KetoyBundleTask. Verification: Ed25519 → KtxReaderBundleValidator inside KetoyRuntime.parseBundle.

Tier-1 JIT

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.

Dev overlay

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.

Test infrastructure

FakeAdapterRegistry + FakeConstructorRegistry (record-only fakes), KBCBuilder + ParamBuilder sparse-encoding DSL, buildKBCBundle { } named-entry-point DSL, KetoyTestRuntime with descriptive entry-point errors.

10 · Gradle plugin

Two build modes

id("dev.ketoy.compiler") from the Gradle plugin portal plus the BOM. Choose the mode that fits your project layout.

Default

Bundle module mode

A separate library module holds the KetoyBC source. Produces .ktx in build/ketoy-bundles/. The consumer (:app) copies the asset.

ADR-0003 · sample app uses this

In-tree mode

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.

Ready to ship?

The local-asset path is fully supported today. Cloud delivery - push .ktx to a CDN, users get updates without a Play Store release - is in active development.