Blog › ICP guides
Whiley developer on retainer: flow-sensitive types, record copy semantics, method verification, loop invariants, and Whiley extended static checking on monthly retainer
November 20, 2026 · ~15 min read
A Whiley program implementing a 2D spatial index was producing five wrong position values per computation cycle. The program defined a Point record type type Point is {int x, int y} representing coordinates. A translate method accepted a point and two integer offsets and was supposed to return the translated point: method translate(Point p, int dx, int dy) -> (Point r). Inside the body, the developer wrote p.x = p.x + dx; p.y = p.y + dy; return p. In Whiley, records are value types with copy semantics: every assignment of a record value to a variable, every parameter pass, and every return creates an independent copy. The p parameter inside translate was an independent copy of the caller’s point; modifying p.x and p.y inside the method modified the local copy; and returning p correctly returned the modified local copy. The bug was at the call site: the developer wrote translate(origin, 10, 20) without capturing the return value, expecting origin to be mutated in place (as it would be if Point were a reference type in Java, C#, or Python). Whiley’s pass-by-value semantics meant the translation was computed inside the method, returned as a value, and then immediately discarded because no binding captured the return. The origin point was never updated. Five wrong position values per computation cycle. The Whiley developer on retainer diagnosed the record copy-semantics misunderstanding: audited all call sites for record-returning methods in the codebase and restructured each discarded-return call to capture the result: origin = translate(origin, 10, 20). Wrong position values per cycle: 5 → 0.
The work log entry read “fixed wrong point coordinates, 13h.” It names the symptom and duration. It cannot explain to a client why Whiley’s record copy semantics are total — there are no reference types for user-defined records in Whiley; p inside a method is always a local copy of the value; the only way to “update” a binding in the caller’s scope is to return the new value and let the caller rebind (Whiley’s value model makes every mutation an explicit rebinding, not a silent in-place modification). It cannot explain why Whiley’s flow-sensitive type system relies on copy semantics to make type narrowing sound (if records were reference types, narrowing OptPoint where type OptPoint is {int|null x, int|null y} to a configuration where p.x is definitely int after an if p.x is int check would be unsound, because another execution path could rebind p.x to null through a shared reference before the narrowed code runs; copy semantics eliminate the aliasing threat, so narrowing after a check is always sound: the narrowed copy is independent and cannot be modified through a shared alias). It cannot explain why adding ensures r.x == p.x + dx && r.y == p.y + dy to the translate postcondition — combined with a subsequent assert origin.x == expected_x at the call site — would have surfaced the discarded-return bug at compile time via the Whiley verifier (the verifier checks each assertion by proving it follows from the method postconditions and local assignments; after a call to translate(origin, dx, dy) with a discarded return, the verifier has no evidence that origin.x changed, so the assertion fails to verify, pinpointing the missing capture). The 13 hours of call site audit, copy-semantics discipline design across a codebase where some developers expected reference semantics, and return-value capture enforcement at all record-returning method calls are invisible in the diff beyond the added origin = assignments.
Whiley record types, copy semantics, field updates, and record width subtyping
Whiley record types are declared with type Name is {T1 f1, T2 f2, ...} and are always value types. Every instance of a Whiley record is an independent value: passing a record to a method creates a copy; assigning a record to a new binding creates a copy; returning a record from a method copies the value out. There are no Whiley reference types for user-defined records — record values do not have identity, only content. This is the same semantics as records in Haskell, structs in Rust (without ownership transfer), or value types in OCaml. It is the opposite of class instances in Java, C#, or Python, where a method parameter receives a reference to the same object in the caller’s heap.
Field assignment in Whiley method bodies creates a locally modified copy. When a method parameter p is assigned p.x = new_value, the assignment creates a new Point value identical to the previous p except with x set to new_value, and rebinds p to this new value. The original point value that was passed in is unaffected; the caller’s binding still holds the original value. The explicit record update expression {p with x: p.x + dx} makes this semantics visible: it creates a new record value with all fields of p except x, which is set to the new expression. This form is preferred when updating a record for readability: the intent (create a new record with modified field) matches the syntax, whereas the assignment form p.x = ... can mislead developers who expect Java-style mutation. The retainer rule: prefer {p with field: new_value} over p.field = new_value in Whiley code written by teams with Java or Python backgrounds; the update expression makes copy semantics explicit and reduces the discarded-return call site bug rate.
Whiley supports record width subtyping: a record type with more fields is a subtype of a record type with fewer fields. A value of type {int x, int y, int z} can be assigned to a binding of type {int x, int y}; the extra field z is dropped. This is used for flexible method signatures: a method that accepts {int x, int y} can be called with any record type that includes at least x and y fields. The subtlety: width subtyping + copy semantics means the dropped fields are lost at assignment; if a method receives a {int x, int y, int z} argument typed as {int x, int y}, it receives a copy with only two fields; the third field is inaccessible. Retainer pattern: check the declared parameter type of each method called with a wide record argument to confirm the narrowing is intentional; unintentional field dropping (where the method needs access to the third field but its parameter type only declares two) is a silent bug under Whiley’s width subtyping.
Whiley flow-sensitive type system: union types, is-checks, null narrowing, and integer subranges
Whiley’s type system is flow-sensitive: the type of a binding can be different at different program points based on the control flow between them. A binding declared as int|null x has type int|null at the declaration point. After a conditional check if x is int, the binding has type int inside the true branch and retains type int|null inside the false branch. This narrowing is unconditionally sound in Whiley because records (and all values) have copy semantics: the binding x inside the true branch is an independent copy of the value that was checked, not a reference to shared state; no other code can change x’s value through an alias between the check and the first use in the narrowed branch.
Narrowing invalidation is a common source of Whiley verifier rejections in large methods. A narrowing established by an if x is int check is valid from the check point until the first reassignment that could widen the type: x = maybeNull() where maybeNull() has return type int|null reassigns x and invalidates the narrowing. Any code after the reassignment no longer has the benefit of the narrowing, even if the reassignment is inside the narrowed branch. The retainer pattern: when a verifier rejects code that appears correct, check whether a reassignment inside a narrowed region has invalidated the narrowing; move the reassignment outside the narrowed region or add an additional if x is int check after the reassignment to re-establish the narrowing.
Integer subrange types allow declaring values with constrained ranges: type Index is (int i) where i >= 0 && i < 100. The Whiley verifier checks at every assignment to an Index binding that the assigned value satisfies the range constraint. Arithmetic on subrange integers produces results that may not satisfy the subrange: adding two Index values could produce a result larger than 99; the result type is widened to int rather than Index, and an explicit cast or assertion is needed to narrow it back. This is intentional: Whiley’s verifier is conservative, and widening on arithmetic prevents silent overflow-into-range bugs. Retainer pattern: when arithmetic on subrange integers produces a result that the developer expects to still be in range (because domain knowledge says it is), add a where clause to the method’s postcondition or a local assertion stating the range constraint on the result; the verifier can then use the assertion to narrow the type for subsequent code.
Whiley method verification, loop invariants, array safety, and David Pearce’s extended static checking design
Whiley methods support precondition and postcondition annotations as first-class language features, not just documentation. A requires clause states a condition the caller must satisfy before the method can be called; the Whiley verifier checks every call site to confirm the precondition holds, reporting a verification failure if the call site does not satisfy it. An ensures clause states a condition the method body must guarantee to hold on all return paths; the verifier checks the method body, reporting a failure on any return path that does not satisfy the postcondition. Together, these annotations form a contract that the verifier enforces mechanically. The value of this contract: when a downstream method calls translate and the verifier accepts the call, the verifier has proved that translate’s postcondition holds at the call site; the caller can use the postcondition as a fact in its own verification without re-proving it.
Array access safety is one of the most impactful applications of Whiley’s verifier. For every array access a[i], Whiley’s verifier must prove that 0 <= i && i < |a| (where |a| is a.length). In straight-line code with a bounded index, the verifier can usually prove safety automatically. In loops, the verifier needs a loop invariant that asserts the index is in bounds on every iteration. The invariant must be: (1) true before the loop starts (establishable by the pre-loop state); (2) maintained by each loop iteration (if it holds entering iteration N, it still holds entering iteration N+1); (3) strong enough to prove the array access safe. A missing loop invariant produces a verifier failure on the array access inside the loop, even when the access is obviously safe to a human reader. Retainer work on array-access invariants: write the invariant as invariant 0 <= i && i < |a| inside the while loop body; if the verifier rejects this invariant (cannot prove it is maintained), strengthen it by adding additional constraints that capture why the index stays in bounds (for example, if the index is incremented by 1 each iteration and starts at 0 and the loop condition is i < |a|, the invariant 0 <= i && i <= |a| with post-condition checks at the access site is often sufficient).
David Pearce at Victoria University of Wellington designed Whiley as a research language to explore practical extended static checking (ESC) with a flow-sensitive type system that eliminates common runtime errors at compile time without requiring a full formal specification language. Whiley is open-source and available via GitHub; the whileyc compiler translates Whiley source to Java bytecode (via Java source), JavaScript, and an experimental native backend. The Whiley verifier is bounded: it uses a combination of flow analysis and a constraint solver (typically backed by Z3 or a similar SMT solver) to discharge verification conditions; the verifier may time out on complex method bodies or fail to prove correct programs that require non-linear arithmetic reasoning. When the verifier fails on a program the developer believes is correct, the retainer pattern is annotation strengthening: add more intermediate assertions, loop invariants, or postconditions to give the verifier intermediate facts that reduce the size of the verification problem at each step. Whiley’s closest conceptual relatives are Haskell for the functional value-type model and OCaml for record semantics, but its flow-sensitive type system and mechanical ESC discipline make its retainer work distinct from both.
How HourTab tracks Whiley developer retainer hours
Whiley retainer work shares the invisible-work problem of all verification-language retainers, compounded by Whiley’s record copy semantics — the developer who expects mutable records writes code that compiles and runs without errors or exceptions, and the values before and after the “mutation” are identical (the caller’s binding still holds the pre-call value) rather than wrong in a detectable way; there is no null-pointer exception, no array-out-of-bounds crash, no type error — just silently stale values that match the pre-call state, which can look like an initialization bug, a stale-cache bug, or a missed update depending on the context. A record copy-semantics call site repair is a diff with added assignments at each call site that previously discarded the return value; the value is correct values at all bindings that were never actually modified by the discarded-return pattern, a call site audit documenting which record-returning methods had discarded return values throughout the codebase, and a copy-semantics discipline applied at every record-returning method call site to verify the return value is captured and rebound when the intent was to update the local binding.
HourTab gives Whiley developers a public retainer-hours URL they send to clients — typically research organizations using Whiley for correct-by-construction safety-critical systems, academic groups exploring practical ESC in production code, and teams maintaining Whiley systems where the verifier contract suite requires ongoing extension as the codebase evolves. For Whiley retainers, each work log entry should name the mechanism (record copy-semantics discipline; return-value capture at record-returning method call sites; field update patterns with explicit reassignment; record width subtyping and field-dropping analysis; union type narrowing with is-checks; null-check flow narrowing; narrowing invalidation analysis; integer subrange declaration and arithmetic widening; array access safety via loop invariants; precondition requires clause writing; postcondition ensures clause writing; verifier annotation strengthening; verifier timeout investigation; whileyc compiler invocation), the specific record types, methods, call sites, narrowing sites, and verification conditions involved in the bug, and the before/after metric. Whiley retainers are often compared to Haskell developer retainers for the shared functional value-type model and OCaml developer retainers for the similar immutable record semantics, but Whiley’s mechanical ESC verifier and flow-sensitive type narrowing make the retainer work distinct in verification annotation engineering and loop invariant design. HourTab’s work log makes the call site audit, copy-semantics discipline design, and return-value capture enforcement visible to clients who would otherwise see only the symptom — five wrong position values per cycle — and not understand why the fix required understanding Whiley’s value-type record model, auditing every record-returning method call site for the discarded-return pattern, and adding explicit binding captures at each site where the caller expected in-place mutation.
Track Whiley developer retainer hours without the status emails
HourTab gives Whiley developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Whiley developer retainers
What does a Whiley developer on retainer typically do?
A Whiley developer on monthly retainer covers four principal service areas: record and value-type semantics design (record copy-semantics discipline; return-value capture at record-returning method call sites; field update patterns using explicit reassignment vs {p with field: value} form; record width subtyping and field-dropping analysis); flow-sensitive type system engineering (union type narrowing with is-checks; null-check flow narrowing for int|null fields; narrowing invalidation analysis; integer subrange type declaration; arithmetic widening on subrange operations; array length-based flow narrowing for safe indexing); method verification with ESC (precondition requires and postcondition ensures clause writing; verifier annotation strengthening; integer subrange arithmetic verification; array access safety via loop invariants; verifier timeout investigation and problem decomposition); and Whiley toolchain (whileyc compiler invocation; Whiley-to-Java compilation; Whiley standard library types and operations).
What Whiley work is most commonly underlogged in a retainer?
Record copy-semantics call site repair (translate(Point, int, int) returned modified copy; caller discarded return value; origin never updated; 5 wrong position values per cycle; added origin = translate(origin, dx, dy) at all call sites; wrong values: 5/run → 0; 11–18 hrs invisible in call site audit and copy-semantics discipline enforcement); flow-sensitive narrowing invalidation (int|null binding narrowed to int; reassignment inside narrowed branch widened type back; verifier rejected subsequent code; 4–8 hrs invisible in narrowing invalidation analysis and reassignment restructuring); and loop invariant writing for array access safety (array access in loop; verifier could not prove 0 <= i < |a| without explicit invariant; 6–12 hrs invisible in invariant design and verifier interaction).
What are typical Whiley developer retainer rates?
Entry-level Whiley developers (1–2 years, record types, basic flow-sensitive narrowing, method precondition and postcondition annotation) bill at $60–$110/hr. Mid-level Whiley engineers (2–4 years, record copy-semantics discipline, union type narrowing, integer subrange verification, loop invariant writing, verifier annotation strengthening) bill at $100–$180/hr. Senior Whiley architects (4–8 years, full ESC pipeline, recursive type invariants, complex multi-method verification chains, Whiley-to-Java compilation debugging, verifier timeout analysis) bill at $150–$270/hr. Monthly retainer ranges: $1,600–$4,400/mo advisory (15–25 hrs), $6,000–$16,000/mo for full Whiley system development engagements.
What should a Whiley developer retainer agreement include?
A Whiley developer retainer agreement should specify: record and value-type semantics scope (record copy-semantics discipline; return-value capture at record-returning method call sites; field update patterns; record width subtyping); flow-sensitive type system scope (union type narrowing with is-checks; null-check flow narrowing; integer subrange declaration and arithmetic widening; array length-based safe indexing); method verification scope (precondition requires and postcondition ensures clause writing; verifier annotation strengthening; integer subrange arithmetic verification; array access safety via loop invariants); Whiley toolchain scope (whileyc compiler; Whiley-to-Java compilation; Whiley standard library); and hour logging format (advisory category, before/after wrong-value metric, Whiley version, whether fix required return-value capture addition, narrowing invalidation repair, subrange annotation strengthening, loop invariant addition, or postcondition clause addition).
How should Whiley developer retainer hours be logged?
Log each Whiley retainer session with: advisory category (record copy-semantics discipline; return-value capture at record-returning call sites; field update patterns; record width subtyping; union type narrowing with is-checks; null-check flow narrowing; narrowing invalidation analysis; integer subrange declaration; arithmetic widening on subrange operations; array access safety via loop invariants; precondition requires clause writing; postcondition ensures clause writing; verifier annotation strengthening; verifier timeout investigation; whileyc compiler invocation); the specific record types, methods, call sites, narrowing sites, and verification conditions involved (translate(Point, int, int) returned modified copy; caller discarded return; origin never updated; 5 wrong values per cycle; added origin = translate(origin, dx, dy) at all call sites; wrong values: 5/run → 0); and the before/after observable metric. Include Whiley version and whether fix required return-value capture addition, narrowing invalidation repair, subrange annotation strengthening, loop invariant addition, or postcondition clause addition for verifier feedback.