commit 6d73c1e5ebb79e9c76814b37342f5bdae0b96626
parent 977cc8c48aacd19bd104c05e46851cd5724acce5
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 13 Jul 2026 10:26:46 -0700
Document contract language design influences
Diffstat:
2 files changed, 508 insertions(+), 0 deletions(-)
diff --git a/doc/ideas/CONTRACT_LANG.md b/doc/ideas/CONTRACT_LANG.md
@@ -0,0 +1,507 @@
+# Contract-oriented language notes
+
+This is a reference sketch for a possible kit-hosted language centered on
+design by contract, local reasoning, explicit effects, and typed communication.
+It is not a plan of record. The purpose is to preserve the design influences and
+the lessons worth carrying forward before any syntax or implementation work
+starts.
+
+## Goal
+
+The language should make module boundaries unusually clear:
+
+- Interfaces state types, contracts, effects, and communication protocols.
+- Implementations hide representation and authority.
+- Effects are explicit and mockable through interpreters or capabilities.
+- Stateful protocols are expressed with typed channels/session endpoints.
+- Modules can be implemented and tested in parallel against stable interfaces.
+- Local reasoning is the default user experience; formal proof is optional, not
+ the entry cost.
+
+The design should favor ordinary modular engineering over proof theater. Stronger
+static checking is useful where it improves separation and confidence, but the
+first-class user story is "I can read this interface and know what clients and
+implementations owe each other."
+
+## Influence map
+
+| Area | Languages / systems | Lesson |
+|------|---------------------|--------|
+| Design by contract | Eiffel, D contracts, Racket contracts, Spec#, JML, ACSL | Put preconditions, postconditions, invariants, and blame at boundaries. Keep contracts executable before requiring proof. |
+| Safety-critical modularity | Ada, SPARK | Package specs/bodies are a strong model for interface/implementation separation. SPARK's flow/contracts/proof model is valuable, but the language should not require proof culture for normal use. |
+| Module systems | SML, OCaml, Modula-2/3, Mesa, CLU | Signatures, opaque types, and functors are the best precedent for decoupled modules, parallel implementation, and trivial substitution in tests. |
+| Local reasoning and authority | Rust, Cyclone, Clean, ATS, Pony | Ownership, borrowing, uniqueness, and reference capabilities are useful for bounding aliasing and mutation. Use the parts that keep authority visible without making all code annotation-heavy. |
+| Algebraic effects | Koka, Effekt, Frank, Unison, OCaml 5 research | Effects belong in computation types and should be handled by ordinary user-defined interpreters. Effect polymorphism is mandatory for ergonomics. |
+| Capability security | E, Joe-E, Pony, object-capability systems | Authority should be passed explicitly. If a function cannot reach a capability, it cannot use it. Confinement is mostly about reference flow. |
+| Channels and processes | CSP, occam, Newsqueak, Alef, Limbo, Go, Concurrent ML | Channels are a useful primitive, but untyped channel protocols drift into comments. CML-style composable events are worth studying. |
+| Session types | Scribble, Links, Session C, Linear Haskell libraries | Protocol state should be part of endpoint types: send, receive, choose, offer, and close advance the channel state. |
+| Coroutine state machines | C# async, Rust async/generators, Kotlin coroutines | Coroutines lower to state machines. The source language should make protocol state visible where useful while keeping generated state-machine plumbing opaque. |
+| Actors and supervision | Erlang, Elixir, Pony, Orleans | Isolation and supervision are valuable. Untyped mailboxes are not enough for local reasoning; prefer typed endpoints/protocols. |
+| Mocking and dependency substitution | ML functors, Go interfaces, Rust traits, Swift protocols, Haskell typeclasses | Testability comes from depending on narrow interfaces or capabilities, not globals. ML functors and effect handlers are especially clean substitution mechanisms. |
+
+## Main takeaways
+
+### Eiffel gives vocabulary, not the whole shape
+
+Eiffel's `require`, `ensure`, and invariants are the obvious design-by-contract
+reference. The useful part is the vocabulary and the idea that contracts are
+part of the callable interface. The less useful part is tying contracts deeply to
+inheritance. Contract inheritance gets subtle, and runtime-only contracts do not
+give enough modular leverage by themselves.
+
+Carry forward:
+
+- Preconditions on callers.
+- Postconditions on implementers.
+- Type and module invariants.
+- `old(...)` values in postconditions.
+- Runtime checking as the first implementation path.
+
+Avoid:
+
+- Making inheritance the main reuse or contract mechanism.
+- Treating contracts as comments.
+- Requiring whole-program verification before contracts are useful.
+
+### Ada/SPARK is the best systems-language precedent
+
+Ada's package spec/body split maps directly onto the desired separation between
+interface and implementation. SPARK adds contracts, flow analysis, absence of
+runtime errors, and proof-oriented tooling. It is a strong influence for module
+discipline even if this language starts with lighter-weight checks.
+
+Carry forward:
+
+- Explicit package/module interfaces.
+- Separate implementation bodies.
+- Contracts attached to specs.
+- Optional ghost/spec-only code.
+- Flow/effect summaries that help readers understand dependencies.
+
+Avoid:
+
+- Making the proof subset feel like a separate language too early.
+- Letting annotation burden dominate normal code.
+
+### SML/OCaml should heavily influence modules
+
+SML signatures and functors are probably the strongest direct model for the
+module layer:
+
+- A signature says what a module provides.
+- The implementation can hide representation behind opaque types.
+- A functor parameterizes one module over another.
+- Tests and mocks are just alternate modules satisfying the same signature.
+
+This is a better foundation for parallel implementation than inheritance or
+ambient dependency injection.
+
+### Effects should track computation, not object reachability
+
+The important ergonomic rule from Koka, Effekt, Unison, Haskell, and related
+systems is that effects are attached to computations, not to mere possession of
+values.
+
+A function should not become `uses IO` just because it accepts a struct that
+transitively contains an I/O capability. Passing, storing, comparing, or reading
+ordinary fields of that struct can still be pure. The effect appears when the
+function invokes an operation that uses the capability, delegates it, or leaks it
+in a way the type system cares about.
+
+Distinguish three facts:
+
+- Possession: a value contains or can reach authority.
+- Use: a computation performs an operation through that authority.
+- Escape: a computation stores, returns, sends, captures, or delegates that
+ authority.
+
+Most ergonomic systems track use by default. Security-sensitive systems also
+track escape.
+
+### Go channels are a warning, not the target
+
+Go shows that channels can be pleasant, but untyped protocols do not preserve
+local reasoning. A `chan Msg` says little about the legal sequence of messages.
+For protocol-heavy code, the type should describe the session:
+
+- Which side sends first.
+- What choices are available.
+- Which state follows each message.
+- When the endpoint is closed.
+
+Session types are the right reference. Go-like syntax may be ergonomic, but the
+semantics should be closer to typed protocol endpoints.
+
+### Coroutines need typed cancellation and cleanup
+
+Coroutine state machines interact with resources and channels. The design needs
+explicit answers for:
+
+- Cancellation propagation.
+- Cleanup/finalization of linear resources.
+- Protocol closure on early return.
+- Which effects may suspend.
+- Whether handlers can resume once, many times, or not at all.
+
+These are interface concerns, not runtime afterthoughts.
+
+## Effects, capabilities, and data structures
+
+The central ergonomic problem:
+
+> If a struct contains a capability, must every function accepting that struct be
+> marked with the capability's effect?
+
+The answer should be no.
+
+Effects should track what the computation can do through the operations it
+actually performs, not every authority reachable inside every value it receives.
+
+For example:
+
+```text
+struct Server {
+ cfg: Config,
+ log: Logger,
+ store: Store,
+ metrics: Metrics
+}
+```
+
+This should be pure:
+
+```text
+fn server_name(s: Server) -> String
+{
+ s.cfg.name
+}
+```
+
+This should carry effects because it invokes effectful operations:
+
+```text
+fn handle(s: Server, req: Request) -> Response
+ uses { Log, Store }
+{
+ s.log.info("request")
+ s.store.get(req.key)
+}
+```
+
+The type system should not punish coarse-grained structs by marking every
+consumer effectful. Instead, it should encourage narrower views when a function
+only needs part of a value:
+
+```text
+interface HasConfig {
+ fn config(self) -> Config
+}
+
+interface HasStore {
+ fn get(self, key: Key) -> Value? uses Store
+}
+```
+
+### Authority facts worth separating
+
+The language may eventually want separate summaries for different authority
+flows:
+
+```text
+uses { Store } // may perform Store operations during this call
+captures { Store } // may retain Store authority after this call
+returns { Store } // may return Store authority to the caller
+sends { Store } // may transfer Store authority over a channel
+```
+
+Only `uses` needs to be in the first design. The others are useful concepts for
+security-critical modules, plugin sandboxes, and confinement checks.
+
+### Recommended default rule
+
+Default effect checking:
+
+- Passing a capability-bearing value is pure.
+- Loading a pure field is pure.
+- Calling an operation with `uses E` gives the caller `uses E`.
+- Calling a generic callback inherits the callback's effects.
+- Returning or storing a capability is not a use effect, but may be checked by a
+ stricter authority-flow mode.
+- Sending a capability over a channel is a delegation event, not just ordinary
+ I/O.
+
+That keeps ordinary code ergonomic while leaving room for stricter boundary
+audits.
+
+### Effect polymorphism is non-negotiable
+
+Without effect polymorphism, every higher-order helper becomes either unusable or
+over-annotated.
+
+```text
+fn map<A, B, e>(xs: List<A>, f: fn(A) -> B uses e) -> List<B>
+ uses e
+```
+
+The same applies to resource helpers:
+
+```text
+fn with_lock<T, e>(m: Mutex, body: fn() -> T uses e) -> T
+ uses e
+```
+
+The helper should add only its own effects. It should not erase or manually
+restate the callback's effects.
+
+### Capability narrowing is the main ergonomic tool
+
+Large environment structs are useful at composition roots, but most functions
+should depend on narrow capabilities:
+
+```text
+interface Clock {
+ fn now(self) -> Instant uses Clock
+}
+
+interface Logger {
+ fn info(self, msg: String) -> Unit uses Log
+}
+
+interface Store<K, V> {
+ fn get(self, key: K) -> V? uses Store
+ fn put(self, key: K, value: V) -> Unit uses Store
+}
+```
+
+Tests replace these interfaces with in-memory interpreters. Production passes
+real interpreters. No global patching is needed.
+
+## Contracts
+
+Contracts should live in interfaces whenever they describe client-visible
+behavior:
+
+```text
+interface AccountStore {
+ abstract Account
+
+ fn transfer(src: Account, dst: Account, amount: Money) -> Unit
+ requires amount > 0
+ requires balance(src) >= amount
+ ensures balance(src) == old(balance(src)) - amount
+ ensures balance(dst) == old(balance(dst)) + amount
+ uses { Store }
+}
+```
+
+Implementation-private contracts are still useful, but they should not be the
+only place where behavior is specified. The caller should not need to read the
+implementation to know the obligations.
+
+### Contract levels
+
+Useful levels, from cheapest to strongest:
+
+- Documentation-level contracts: parsed and attached to symbols, even before
+ checking.
+- Runtime contracts: checked at debug/test boundaries.
+- Static local checks: obvious precondition discharge, exhaustiveness,
+ initialization, protocol state, purity.
+- Optional proof: ghost values, lemmas, solver integration, or external tools.
+
+The language should start by making the first two pleasant and preserving a path
+to the latter two.
+
+### Blame matters
+
+Racket's contract system is worth studying because it assigns blame across
+module boundaries. When a precondition fails, the caller is wrong. When a
+postcondition fails, the implementation is wrong. This is important for parallel
+module work: failures should point to the side that broke the interface.
+
+## Modules
+
+A module interface should be the main unit of decoupling:
+
+```text
+module interface Store {
+ abstract Key
+ abstract Value
+
+ effect Store
+
+ fn get(k: Key) -> Value? uses Store
+ fn put(k: Key, v: Value) -> Unit uses Store
+}
+```
+
+An implementation supplies representation and an interpreter:
+
+```text
+module MemoryStore : Store {
+ type Key = String
+ type Value = Bytes
+
+ struct State { map: Map<String, Bytes> }
+
+ fn get(k: Key) -> Value? uses Store { ... }
+ fn put(k: Key, v: Value) -> Unit uses Store { ... }
+}
+```
+
+The implementation may store capabilities internally, but clients can only use
+the authority exposed by the interface.
+
+### Desirable module properties
+
+- Private by default.
+- Explicit exports.
+- Opaque types by default across interfaces.
+- Ability to reveal type equality selectively.
+- Parameterized modules/functors for dependency injection.
+- Interface-level contracts and effects.
+- Separate compilation against signatures.
+- Mock modules that satisfy the same signature as production modules.
+
+## Effects and handlers
+
+There are two related but distinct implementation models:
+
+- Capabilities as explicit values with effectful methods.
+- Algebraic effects as named operations handled by interpreters.
+
+The surface can support both if the interface model is disciplined. A capability
+object is often the ergonomic value passed around; the operation it performs is
+what contributes to the effect row.
+
+```text
+effect Log {
+ op info(msg: String) -> Unit
+ op warn(msg: String) -> Unit
+}
+
+handler MemoryLog : Log {
+ ...
+}
+```
+
+Mocking then becomes ordinary interpretation:
+
+```text
+test "logs request id" {
+ let log = MemoryLog.new()
+ let store = MemoryStore.new()
+ handle(log, store, req)
+ assert log.contains("request")
+}
+```
+
+No ambient global logger is required.
+
+## Channels and session types
+
+Channels should carry protocols, not just payload types.
+
+```text
+protocol StoreSession<K, V> =
+ loop {
+ choice {
+ Get(K) -> recv V? -> continue
+ Put(K, V) -> recv Ack -> continue
+ Close -> end
+ }
+ }
+```
+
+The exact syntax is not important yet. The important semantic property is that
+using an endpoint advances its type:
+
+```text
+let ch1 = send ch0, Get(key)
+let (ch2, value) = recv ch1
+```
+
+This is the core local reasoning rule: after a send, the old endpoint state is
+gone. The program cannot send the wrong next message because the endpoint no
+longer has that type.
+
+### Structs containing endpoints
+
+Storing endpoints in structs should be allowed, but use must respect linear or
+affine state:
+
+```text
+struct Client<P> {
+ endpoint: Endpoint<P>
+}
+```
+
+Operations that advance the endpoint must also advance the containing value's
+state, borrow the endpoint linearly, or return an updated client:
+
+```text
+fn get<K, V, P>(c: Client<P>, key: K) -> (Client<P>, V?)
+ where P supports StoreSession<K, V>
+```
+
+The exact mechanics are open. The invariant is not: an endpoint cannot be used
+twice at the same protocol state.
+
+## Local reasoning rules
+
+A reader should be able to inspect a function signature and know:
+
+- Which abstract types and interfaces it depends on.
+- Which contracts callers must satisfy.
+- Which contracts implementations promise.
+- Which effects it may perform.
+- Which capabilities it may retain or delegate, when stricter mode is enabled.
+- Which channel protocols it may advance.
+- Which state is mutable and who can alias it.
+
+This implies a few likely defaults:
+
+- No hidden global state.
+- No ambient I/O.
+- Private representation by default.
+- Explicit capability arguments or module parameters.
+- Effects on operations, not on data possession.
+- Linear or affine treatment for session endpoints.
+- Runtime contracts enabled in tests/debug builds.
+
+## Design hazards
+
+Avoid these traps:
+
+- **Transitive effect pollution.** Marking every function that accepts a
+ capability-bearing struct as effectful makes the system unusable.
+- **Untyped protocol comments.** If a channel protocol lives in prose, it will
+ drift.
+- **Proof-first ergonomics.** Optional proof is useful; mandatory proof blocks
+ adoption.
+- **Inheritance-centered contracts.** Composition, modules, and interfaces fit
+ the goal better.
+- **Implicit global handlers.** They make tests and authority audits harder.
+- **One giant environment type.** Useful at the top level, harmful when it leaks
+ into every helper.
+- **Effect rows without polymorphism.** Annotation burden will explode.
+- **Session types without cleanup rules.** Cancellation and early return must
+ close or transfer endpoints predictably.
+
+## Likely starting point
+
+A first cut should probably be small:
+
+1. Module interfaces with abstract types.
+2. Runtime-checked preconditions, postconditions, and invariants.
+3. Explicit capability values and narrow interfaces.
+4. Computation-level effect summaries with effect polymorphism.
+5. Opaque implementation bodies.
+6. Basic typed channels.
+7. Session typing once linear/affine endpoint ownership is designed.
+
+The first implementation does not need a theorem prover, full algebraic effects,
+or complete session typing. It should establish the boundary discipline:
+interfaces are where contracts, authority, and protocols are declared; bodies are
+where representations and interpreters live.
+
diff --git a/doc/ideas/README.md b/doc/ideas/README.md
@@ -7,5 +7,6 @@ captured idea, not a plan of record.
| Sketch | Scope |
|--------|-------|
+| [CONTRACT_LANG.md](CONTRACT_LANG.md) | Notes for a possible contract-oriented language: design-by-contract influences, effect/capability ergonomics, modules, typed channels, and session-type lessons. |
| [RQL.md](RQL.md) | A typed relational query language (a clean-break SQL replacement): surface syntax, logical algebra/IR, physical plan, algebraic laws, option types instead of NULL. |
| [RSN.md](RSN.md) | A statically-typed scripting language to replace make/shell/Python for kit's build and test orchestration: typed CG-API frontend, staged arena/GC runtime, cooperative fibers. |