skip to content

What are the limits and soundness risks of contract-based smart casts? When can a contract still fail to smart-cast, and how can a bad contract cause a runtime crash?

level: seniorimportance: should knowfreq 30%

answer

  1. Unchecked: bad contract -> NPE/CCE at a 'safe' site
  2. Stability still required: var/custom getter/other-module -> no cast
  3. Bind unstable props to a local val
  4. Conditions limited to null/type/boolean on params
  5. Experimental opt-in; contract first; inline for callsInPlace

basics

~20 s

Contracts are promises the compiler trusts but never checks. If your function body doesn't really enforce the claim, you get an unsound smart cast that can crash at runtime. Smart casts also still fail on mutable or externally-changeable variables.

solid answer

~40 s

Key limits: (1) Contracts are unchecked — the compiler trusts returns()/implies without verifying the body, so a wrong contract yields an unsound smart cast and an NPE/ClassCastException at a 'safe' call site. (2) Even a correct contract can't override the usual smart-cast preconditions: the value must be stable. A var, an open/custom-getter property, or a property of another module can change between the call and use, so the compiler won't smart-cast it. (3) Conditions are limited to null/type/boolean checks on parameters/receiver. (4) Contracts are an experimental API needing opt-in, so signatures can shift. (5) The contract block must be first and the function generally inline for callsInPlace lambda effects. Mitigation: keep contract claims trivially matched by the body, prefer val/local copies, and test the failure path.

code

kotlin · 5 lines
kotlin
class Box { val v: String? get() = compute() }
fun f(b: Box) {
    val v = requireNotNull(b.v) // bind return value: stable local val
    println(v.length)           // safe; relying on b.v smart-cast may be refused
}

go deeper

for a junior

Recognizes that you sometimes still need !! or a local val and that contracts aren't magic.

for a middle

Explains that contracts are unchecked and that custom getters / var break smart casts.

for a senior

Articulates the unsound-smart-cast crash mechanism plus stability rules and binds to a local val as mitigation.

for a principal

Sets team policy on authoring contract helpers, the experimental-API risk, and testing the enforcement path to prevent drift.

## Soundness: the compiler trusts, it does not verify A contract is an **unchecked promise**. The compiler reads `returns() implies (x != null)` and acts on it without confirming the body honors it. ```kotlin @OptIn(ExperimentalContracts::class) fun looksValidated(x: String?) { contract { returns() implies (x != null) } // LIE: body never enforces it } fun caller(s: String?) { looksValidated(s) println(s.length) // compiler believes s != null -> NPE at runtime if s was null } ``` The call site looks null-safe but throws `NullPointerException`. The same hazard applies to `implies (x is Foo)` producing a `ClassCastException`. **Rule:** the body must make the implied condition genuinely true (throw, return early, etc.). ## Smart-cast stability still applies A contract conveys a fact; it cannot bypass Kotlin's requirement that the variable be **stable** between the assertion and the use: - **`var` that could change**, captured mutable variables, or values modified by another thread — no smart cast. - **Properties with custom getters or `open` properties** — the value isn't guaranteed identical on the next read, so even after `requireNotNull(obj.prop)` the compiler may refuse to smart-cast `obj.prop`. Copy to a local `val` first. - **Properties from another module** — same conservative rule. ```kotlin class Box { val v: String? get() = compute() } fun f(b: Box) { requireNotNull(b.v) // returns the value, but b.v has a custom getter // println(b.v.length) // may NOT smart-cast: getter could return different value val v = requireNotNull(b.v) // safe: bind to local val println(v.length) } ``` ## Other limits - **Allowed conditions only**: null checks, `is`/`!is`, and boolean combinations on **parameters/receiver** — not arbitrary expressions or locals. - **Experimental API**: needs `@OptIn(ExperimentalContracts::class)`; the DSL has been stable in practice but is formally experimental, so library authors weigh the opt-in cost. - **Placement**: `contract { }` must be the first statement; `callsInPlace` (lambda invocation effects) requires the function to be `inline`. - **No verification tooling**: there's no compiler check that your contract matches the body, so review/tests are the only safety net. ## Practical guidance - Make contracts trivially honored (one throwing/early-return line that matches the claim). - Prefer binding to a `val` (the helper's return value) over relying on in-place smart cast of unstable references. - Cover the throwing path with a test so a future edit that breaks the invariant fails loudly.

  • Why might requireNotNull(obj.prop) still not let you smart-cast obj.prop afterward?
    If prop has a custom getter, is open, or comes from another module, the next read could differ, so the value isn't stable and the compiler won't smart-cast it. Bind requireNotNull's return value to a local val.
  • How can a contract cause a runtime crash that the compiler didn't warn about?
    Contracts are unchecked. If the body doesn't enforce the implied condition, the compiler emits an unsound smart cast and you get an NPE or ClassCastException at the call site it believed was safe.

saying these in an interview costs you the question

  • Believing the compiler verifies the body matches the contract
  • Asserting a contract overrides smart-cast stability rules for var/custom getters
  • Trusting an in-place smart cast on a property with a custom getter
  • Treating contracts as zero-risk because 'the stdlib uses them'
  • Ignoring the experimental opt-in / API stability concern

context