Skip to content
CINCH/OverviewDRAFT

Design conversation context

Exported 22 October 2026Source: original page

This is the visible saved conversation returned by the conversation reader, included as historical context. Current documentation is the exported page set, which may contain later refinements. Links to old sandbox attachments are historical and are not bundled targets.

User — 2026-10-22T13:17:48.157949Z

Original message

I want to figure out how to write a new program language where the code has proofs or gradual proofs required to actually compile. Must also be memory safe like rust, strict, functionally typed. If there other areas of safety we could improve, would be good too.


Assistant — 2026-10-22T13:18:13.808659Z

Original message

Yes. I’d design it as a strict functional language with ownership, refinement types, and an effect system, where compilation must account for every proof obligation.

The important distinction is between gradually specifying more properties and deferring proof obligations to runtime. Both are useful, but they give different guarantees.

I’m interpreting “functionally typed” as algebraic data types, pattern matching, first-class functions, parametric polymorphism, and pure functions by default.

The basic design

Layer What it guarantees How it gets checked
Ordinary types Correct data shapes, exhaustive matches, no null confusion Conventional type checking
Ownership and borrowing No use-after-free, invalid aliasing, or data races in safe code Static ownership checking
Effects Which operations may fail, mutate, perform I/O, or diverge Effect inference and checking
Refinements Bounds, numeric constraints, relationships between values Automated reasoning plus explicit proofs
Specifications Algorithms implement their stated behavior Contracts, invariants, lemmas
Build policy Required guarantees have actually been established Reject unresolved obligations or forbidden dependencies

You shouldn’t have to write proofs of everything manually. Type inference, ownership checking, and automated arithmetic reasoning already discharge many proofs.

Make “gradual” precise

I would support two complementary mechanisms:

  1. Gradual specification: Ordinary typed code compiles with baseline safety guarantees. Adding stronger contracts creates stronger obligations. A sorting function initially promises List a -> List a; later it promises a sorted permutation of its input.
  2. Explicit runtime discharge: An executable predicate can be checked at runtime, producing evidence on the successful branch. Failure must be represented in the type or effects.

For example, in hypothetical syntax:

type Fin(n: Nat) = { i: Nat | i < n }
fn get(xs: &Vec(a, n), i: Fin(n)) -> a
where a: Copy
=
xs[i]

There is no bounds-failure possibility in this interface: callers must supply an index with evidence that it is valid.

That evidence can come from static reasoning:

fn first(xs: &Vec(a, n)) -> a
requires n > 0
where a: Copy
=
get(xs, 0)

Or from a runtime decision:

fn try_get(xs: &Vec(a, n), i: Nat) -> Option(a)
where a: Copy
=
match check(i < n) {
Yes(proof) => Some(get(xs, refine(i, proof)))
No => None
}

The second function still has a fully justified implementation. Its contract acknowledges that the index may be invalid.

An unresolved proposition must never silently become an assumption. For the initial language, I would prefer explicit check boundaries over inserting runtime checks whenever the solver times out. That keeps error behavior and performance predictable.

Actual gradual verification research goes further, combining partial specifications with generated runtime checks; Sound Gradual Verification with Symbolic Execution is a particularly relevant foundation. genui{“citation”:{“ref”:“turn1academia28”}}

There is also a hard limit: not every proposition has a usable runtime checker. Bounds checks do; arbitrary termination claims and equivalence of functions generally don’t. Those require a static proof, a weaker interface, or an explicitly recorded trusted assumption.

Ownership should make proofs easier

I would use:

  • Immutable values by default.
  • Affine ownership for resources: a value can be consumed at most once.
  • Shared immutable borrows and exclusive mutable borrows.
  • Explicit sharing through immutable structures or synchronized abstractions.
  • Closures whose captured resources determine whether the closure is reusable or single-use.

Exclusive access makes reasoning about mutation much simpler: while you own a mutable borrow, another alias cannot invalidate your invariant.

For example, a proof that i < xs.length must not survive a mutation that could shorten xs. Refinements about mutable objects need to be tied to the relevant state and ownership permissions.

Flux is directly relevant here: it combines refinement types with Rust’s ownership mechanisms to verify properties of pointer-manipulating code. genui{“citation”:{“ref”:“turn0search2”}}

Memory safety alone doesn’t require Rust-style ownership; garbage collection can provide it too. Ownership is especially valuable if you also want predictable resource management, controlled mutation, and no mandatory tracing collector.

Separate executable code from logic

I would distinguish three kinds of code:

spec // mathematical definitions and contracts
proof // erased evidence and lemmas
fn // executable code

Verus uses this division explicitly. genui{“citation”:{“ref”:“turn1search6”}}

The logic must remain sound: an infinitely recursive “proof” cannot establish an arbitrary proposition. Proof computation must terminate or be constrained by a sound logical system.

Executable code can still run servers and event loops. Track possible divergence in its effects, while requiring termination where the code is used as a logical definition. Also distinguish partial correctness—“if this returns, its result satisfies P”—from total correctness, which additionally establishes termination.

Other safety improvements worth building in

Area Proposed mechanism Important limit
Arithmetic Distinct checked, wrapping, saturating, and mathematical arithmetic Proofs must match machine arithmetic semantics
Error handling Typed errors; no implicit panic in ordinary APIs Allocation failure and foreign code need explicit policies
Resource protocols Typestate: Socket<Connected>, Transaction<Open> Protocol compliance doesn’t establish external success
Resource completion Linear obligations for resources that must be consumed Cancellation and unwinding need defined behavior
Authority Capabilities for filesystem, network, clock, randomness Effects describe behavior; capabilities authorize it
Concurrency Ownership plus structured concurrency Data-race freedom does not establish deadlock freedom
Confidentiality Information-flow labels and controlled declassification Timing and other side channels require additional models
Resource bounds Optional cost annotations or fuel budgets These require a defined cost model

An effect system could give signatures such as:

parse : Bytes -> Result(ParseError, Message)
load : (&FileReadCap, Path) -> {IO} Result(ReadError, Bytes)
serve : Server -> {IO, Async, Diverge} Unit

Effects should be inferred locally and visible at public boundaries. Koka is a useful reference: its effect types distinguish exceptions, divergence, and other behavior, with inference and polymorphism. genui{“citation”:{“ref”:“turn1search2”}}

For a first version, I would avoid unrestricted multi-shot effect handlers: resuming a continuation multiple times interacts badly with captured resources that may only be consumed once.

The compiler architecture

I’d build around a small explicitly typed core:

  1. Infer ordinary types and effects.
  2. Check ownership and borrowing.
  3. Elaborate contracts and refinements into verification conditions.
  4. Discharge easy conditions automatically.
  5. Accept explicit lemmas for the remaining conditions.
  6. Reject obligations that the selected build policy does not permit.
  7. Erase ghost code and compile executable code.

There is a consequential trust decision here:

  • Practical prototype: Trust the verification-condition generator and SMT solver.
  • Stronger architecture: Require proof certificates checked by a small kernel.

Certificate checking reduces trust in proof search, but doesn’t automatically verify your compiler, runtime, FFI, or the translation from program semantics to logical obligations.

I’d also make build artifacts report their guarantees and assumptions transitively. A module cannot honestly claim a fully proved contract if its proof depends on an unchecked foreign-function contract hidden in a dependency.

What I would study and prototype first

Reference What to borrow
F* Strict functional programming with dependent/refinement types, effects, and automated proofs
Flux Practical integration of ownership and refinements
Verus Explicit specifications, ghost code, and proof structure
Koka Ergonomic effect inference
Gradual verification research Sound interaction between static obligations and runtime checks

F* is particularly close to the overall language you describe: it is call-by-value and functional, with a proof-oriented type-and-effect system. genui{“citation”:{“refs”:[“turn0search12”,“turn0search4”]}}

My recommended first prototype would be strict ML-style syntax, ADTs, affine ownership, scoped borrowing, effect rows, and integer/length refinements. Start with automatic proofs for bounds and checked arithmetic, then add contracts and erased lemmas. Defer unrestricted dependent types and shared-memory concurrency until that core is coherent.

The central design goal should be: every exported guarantee has evidence, and every remaining uncertainty is visible in the interface. That is concrete enough to enforce at compilation, while still letting people write useful programs before proving their full functional correctness.


User — 2026-10-22T13:20:18.689680Z

Original message

What else could we do to make it better than those and feel more like Haskell and typescript


Assistant — 2026-10-22T13:20:27.312635Z

Original message

I’d aim for Haskell’s composability with TypeScript’s immediacy, with proofs integrated into ordinary programming. The opportunity is making verified code pleasant to write, refactor, and explore.

A few design choices could make that work especially well.

1. Make control flow construct proofs automatically

TypeScript-style narrowing is an excellent interface for refinement checking:

function first<A>(xs: Vec<A>): Option<A> {
if (xs.length === 0) return None;
return Some(xs[0]);
}

On the second branch, the compiler knows xs.length > 0. Indexing is justified without an annotation or proof block.

Extend that to:

  • Pattern matching that introduces constructor invariants.
  • Exhaustiveness checking over refined values.
  • Early returns that establish preconditions.
  • Successful parsing that produces validated types.
  • Ownership transitions that establish resource state.

Mutation must invalidate affected facts; the ownership system should make that predictable.

The everyday proof language should mostly be ordinary code. Explicit lemmas are the escape hatch for harder reasoning.

2. Combine structural records with nominal domain types

Use structural typing for ordinary data, rows for extensibility, and explicit nominal types for distinctions that must not disappear:

type Named = { name: String, ... };
opaque type UserId = UUID;
opaque type InvoiceId = UUID;
function greet<A extends Named>(value: A): String {
return `Hello, ${value.name}`;
}

You get TypeScript-like object ergonomics without treating every structurally identical domain value as interchangeable.

I would also provide exact records at serialization boundaries, explicit optional fields, and no implicit undefined. A missing field and a field containing None should be distinguishable.

3. Keep Haskell’s higher-kinded abstractions

Make these first-class rather than something users have to encode:

  • Higher-kinded types.
  • Typeclasses with coherent instance resolution.
  • Associated types.
  • Newtypes with negligible runtime overhead.
  • Deriving for routine instances.
  • Expression-oriented syntax and excellent pattern matching.

Then add law-bearing interfaces:

class Monoid a where
empty : a
append : a -> a -> a
law leftIdentity : append empty x == x
law rightIdentity : append x empty == x
law associativity :
append (append x y) z == append x (append y z)

An implementation could expose operations before supplying all the laws, but a generic algorithm that relies on associativity would require the corresponding evidence. An unproved instance must not unlock law-dependent optimizations.

That turns familiar algebraic abstractions into useful verification boundaries.

4. Make effect polymorphism feel like ordinary polymorphism

A higher-order function should preserve the effects of its callback:

map : (a ->{e} b) -> List a ->{e} List b

Mapping a pure function remains pure. Mapping a function that can fail propagates that failure effect.

Use familiar do notation or direct-style syntax for sequencing. Keep first-class monadic abstractions available, but don’t require transformer plumbing for every combination of effects.

Public signatures should show effects; local code should usually infer them.

5. Make ownership mostly invisible for functional code

A functional language becomes frustrating if every combinator turns into a lifetime puzzle.

I’d want:

  • Ordinary immutable values to be easy to share.
  • Explicit unique ownership for mutable buffers and resources.
  • Borrow inference for local operations.
  • Closure capture inferred, with useful diagnostics.
  • Persistent collections as a serious part of the standard library.
  • Explicit operations when copying could be expensive.

The key tradeoff is memory management. For application programming, I’d favor managed immutable data plus owned resources and mutable regions. For a systems language, require more explicit control. Trying to promise both unrestricted sharing and universally predictable reclamation would obscure real costs.

6. Make “parse, don’t validate” effortless

Generate executable validators from the executable subset of refinements:

type Port = Int where 1 <= self && self <= 65535;
const port: Result<ValidationError, Port> =
Port.parse(input);

A successful parse produces an ordinary Port; downstream functions don’t repeat the check.

Support the same mechanism for JSON, database rows, CLI arguments, and network messages. Validation errors should identify the exact field and failed condition.

Not every specification is executable, so distinguish checkable predicates from general propositions explicitly.

7. Treat proof diagnostics as a product feature

Instead of “SMT obligation failed,” show something like:

Cannot establish: index < items.length
Known here:
index >= 0
items.length >= 0
Missing:
an upper bound on index
Possible changes:
• branch on index < items.length
• accept Fin(items.length)
• return Option<Item>

Distinguish a genuine counterexample, insufficient information, and a solver timeout. Don’t present a solver model as a concrete failing execution unless it has been validated.

Add typed holes that show available values, effects, ownership permissions, and missing evidence. Proof holes can support editor exploration, while remaining forbidden in the final build.

8. Make proofs survive refactoring

This could matter more than adding another powerful type feature.

  • Verify callers against published contracts, not implementation details.
  • Keep unfolding explicit and controlled.
  • Cache proofs against their actual dependencies.
  • Pin solver versions and verification settings.
  • Let developers turn successful automated reasoning into reusable lemmas.
  • Explain exactly which contract change invalidated a proof.

Proofs that randomly break after an unrelated edit will kill adoption.

9. Ship a standard library whose guarantees compose

The most valuable proofs will often already exist in libraries:

sort : Vec a n -> SortedPermutation a n
zipExact : Vec a n -> Vec b n -> Vec (a, b) n
decode : Bytes -> Result DecodeError ValidatedMessage
commit : Transaction Open -> IO (Result CommitError CommitOutcome)

Specify failure and cancellation paths as carefully as successful ones. For example, a failed commit may have an unknown external outcome; its type should preserve that uncertainty.

I’d prioritize flow-sensitive refinements, higher-kinded types, row-polymorphic records and effects, and exceptionally good diagnostics. Those give the language a recognizable Haskell/TypeScript feel immediately. Full dependent types can come later; the initial experience should be that ordinary, readable code establishes surprisingly strong guarantees.


User — 2026-10-22T13:22:15.037328Z

Original message

Oh no I’d want fully dependent types, and I also would want something like haskell’s rewrite rules and stream fusion


Assistant — 2026-10-22T13:22:49.792618Z

Original message

Then I’d revise the foundation substantially: a fully dependently typed, strict functional language with quantitative ownership and proof-checked, library-defined optimizations. My earlier suggestion to defer dependent types was too conservative for what you want.

Dependent types and rewrite rules actually strengthen each other: libraries can provide abstractions, their laws, and verified ways to compile those abstractions away.

1. Start with a dependent core that accounts for resource usage

I’d investigate quantitative type theory as the foundation. Idris 2 uses it to track erased, linear, and unrestricted arguments—making both erasure and resource usage part of typing. genui{“citation”:{“ref”:“turn2search35”}}

Your core should support:

  • Dependent functions and pairs.
  • Indexed inductive types and dependent pattern matching.
  • Universe polymorphism.
  • Equality proofs and explicit transport.
  • Erased arguments and proofs.
  • Resource usage information.
  • A total logical fragment alongside effectful, potentially nonterminating programs.

For example:

append :
Vec a n ->
Vec a m ->
Vec a (n + m)
filter :
(a -> Bool) ->
Vec a n ->
(m : Nat ** (LTE m n, Vec a m))

Here filter returns an existential length and evidence that it cannot exceed the input length.

The compiler should erase evidence that execution does not require. But dependent types do not mean that every index can disappear: if code needs a length at runtime, it must obtain it from retained data.

Quantitative typing is a foundation, not a complete Rust replacement. Borrow lifetimes, mutable aliasing, destruction, and concurrency still need explicit design.

2. Make rewrite rules verified declarations

GHC checks that rewrite-rule sides have matching types, but does not establish semantic equivalence or termination of rewriting. Your language could improve this directly. genui{“citation”:{“ref”:“turn3view0”}}

Hypothetical syntax:

theorem mapFusion :
forall (f : b -> c) (g : a -> b) (xs : Vec a n).
map f (map g xs) = map (f . g) xs
proof
induction xs
...
rewrite "map/map"
map f (map g xs)
==> map (f . g) xs
justified by mapFusion

The theorem establishes validity. The rewrite declaration specifies orientation and when the optimizer should consider it.

I would distinguish three mechanisms:

Mechanism Purpose
Definitional reduction Computation used by the type checker to recognize equal types
Propositional rewriting Explicit equality proofs used in programs and proofs
Optimization rewriting Semantics-preserving transformations of executable code

Do not automatically add optimizer rules to definitional equality. A library optimization should not change whether unrelated types unify or cause type checking to loop.

For expressions with effects, the required theorem is generally observational equivalence under a defined semantics, rather than ordinary equality of returned values.

3. Strictness makes the rule conditions important

For total, pure functions over finite collections, ordinary map fusion is straightforward.

But consider:

map f (map g xs)

With strict collections, the original runs all applications of g before running applications of f. Fusion interleaves them.

That can change:

  • Which error occurs first.
  • Observable effects.
  • Termination behavior.
  • Resource acquisition and cleanup order.

Consequently, the rule above should initially apply to total, pure callbacks, with resource constraints checked too. Broader rules need stronger evidence.

Effect rows alone are insufficient: two functions both having an IO effect does not mean their operations commute. You would need a proof of the particular reordering.

This is an excellent reason to make totality and effects visible in function types.

4. Make streams an explicit, optimizable abstraction

Strict evaluation does not prevent demand-driven streams. A stream can be an explicit state machine:

data Step s a
= Done
| Skip s
| Yield a s
-- Conceptual representation:
Stream a = exists s. {
initial : s,
step : s -> Step s a
}

map, filter, zip, and folds transform or consume these state machines. Specialization and simplification can eliminate intermediate states and constructors.

That is the central approach behind stream fusion; the original work covers zips, left folds, and nested-list operations. There is also a formalization of much of the framework in Isabelle/HOLCF. genui{“citation”:{“refs”:[“turn2search34”,“turn3view1”]}}

I’d support two related APIs:

  • Pure streams: reusable descriptions where their captured values permit reuse.
  • Resource streams: single-use state machines with effects and guaranteed cleanup paths.

The second needs ownership-aware stepping and finalization, particularly when consumption stops early. A file stream cannot be modeled adequately by just adding IO to the simple representation above.

Explicit stream pipelines also establish their evaluation order from the start, avoiding the semantic ambiguity of transforming arbitrary eager collection operations.

5. Use dependent types to express stronger fusion facts

Length information can guide both verification and code generation:

map :
(a -> b) -> SizedStream a n -> SizedStream b n
zip :
SizedStream a n ->
SizedStream b n ->
SizedStream (a, b) n
collect :
SizedStream a n -> Vec a n

Here SizedStream a n must mean a stream with evidence that it actually produces exactly n elements—not merely an attached number.

For filtering, track an upper bound or an existential output size. For arbitrary streams, retain an unsized interface.

Those distinctions let the compiler justify exact allocation, bounds-check elimination, and appropriate loop shapes. Infinite or potentially diverging streams need different guarantees.

6. Let libraries ship verified lowering strategies

This is where the language could become particularly interesting.

A library could expose a clear mathematical interface and provide several implementations:

matrix expression
→ fused scalar loop
→ vectorized loop
→ specialized backend call

Each lowering carries its assumptions and correctness evidence. The compiler chooses among applicable implementations using a cost model.

Keep two questions separate:

  • Is this transformation correct? Checked evidence.
  • Is this transformation beneficial? Heuristics, profiling, or explicit policy.

Floating-point reassociation, for example, cannot use a theorem about exact real arithmetic to justify changed machine results. Approximate equivalence would need an explicit error contract.

You could eventually use proof-producing equality saturation for selected pure regions, but bounded, directed rewriting is a simpler starting point. Correct rules can still cause endless optimization or explosive code growth.

7. Make optimization outcomes inspectable—and optionally required

I would want source-level assertions such as:

@requireFusion
@noIntermediateCollections
fn sumSquares(xs: &Vec Int n) -> Int {
xs.stream().map(square).fold(0, checkedAdd)
}

These are proposed compiler features, not mathematical guarantees inferred from those names. Each needs a precise scope and definition.

The compiler should explain failures:

Fusion stopped at `normalize`:
its implementation is unavailable for specialization.
Remaining intermediate allocation:
Vec<Float> at pipeline stage 3

That would make performance far less dependent on discovering whether a particular rule happened to fire.

The architecture I’d pursue is a small dependent proof kernel, a strict ownership-and-effect-aware executable core, and a typed optimizer that checks evidence for library-defined rewrites. Haskell-like abstractions and TypeScript-like narrowing can sit above that. The distinctive feature would be that users can extend both what the language expresses and how efficiently it executes, with the compiler checking the connection between the two.


User — 2026-10-22T13:28:32.876212Z

Original message

I think tla+ also has proofs about infinite streams or smth right? Coinductive vs inductive proofs?


Assistant — 2026-10-22T13:29:08.093999Z

Original message

Yes—TLA+ reasons about infinite executions, and coinduction is closely related, but they’re different tools. Your language could use both.

The important correction to our earlier discussion is: a program can run forever and still have strong correctness proofs. Termination is only one possible property.

Induction versus coinduction

Induction Coinduction
Typical objects Finite lists, trees, natural numbers Infinite streams, ongoing processes
Reasoning Establish base cases and preservation through constructors Establish observable behavior and preservation into subsequent states
Typical obligation Recursive work decreases Recursive production remains productive
Example Reversing a finite list twice restores it Two stream generators produce the same observations forever

For stream equivalence, a coinductive proof usually supplies a bisimulation: related streams have equal heads, and their tails remain related. Agda’s documentation demonstrates precisely this approach. genui{“citation”:{“ref”:“turn4search0”}}

For example:

map f (map g xs) ≈ map (f . g) xs

For productive streams and total, pure callbacks:

  1. Both sides produce f (g (head xs)).
  2. Their tails have the same relationship, now applied to tail xs.

That establishes agreement for arbitrarily many observations. It is not permission to assume any theorem recursively: the proof must satisfy the appropriate guardedness or coinduction principle.

What TLA+ adds

TLA+ models a behavior as an infinite sequence of states. Temporal logic expresses properties of that whole execution. genui{“citation”:{“ref”:“turn4search36”}}

For your language, the useful categories would be:

Property Example
Safety A completed request never receives a second response
Liveness Every accepted request eventually receives a response
Recurrence The service emits heartbeats infinitely often
Stability Eventually, the leader stops changing

Typical temporal notation:

□ P P always holds
◇ P P eventually holds
□◇ P P occurs infinitely often
◇□ P eventually P holds forever
□ (accepted(id) ⇒ ◇ responded(id))

The last property needs suitable assumptions: perhaps the scheduler is fair, the service does not permanently crash, and dependencies eventually respond.

TLA+ makes fairness central to these arguments. Without it, an allowed execution might simply never schedule the action required for progress. genui{“citation”:{“ref”:“turn4search1”}}

Infinite executions don’t imply that every proof uses coinduction. A state invariant can be proved inductively over reachable states and therefore hold throughout an infinite execution. Liveness often requires progress arguments, well-founded measures, and fairness assumptions. Coinduction is especially useful for ongoing behavior and equivalence.

For your language, distinguish termination, productivity, and liveness

These are three different guarantees:

Termination:
This computation finishes.
Productivity:
Every requested finite prefix can be produced in finite computation.
Liveness:
Under stated environmental assumptions, a desired event eventually occurs.

A productive stream never finishes, but it keeps producing. A server waiting for input may be perfectly correct without continuously producing output.

That suggests separate abstractions:

Vec a n -- finite data with known length
Stream a -- productive infinite data
Process e a -- ongoing effectful behavior; may wait or diverge

In a strict language, Stream would use explicit delayed tails or guarded corecursion. You don’t need to make the entire language lazy.

There’s an important consequence for fusion

Consider:

filter isEven (repeat 1)

The input is productive. The filtered output never produces an element.

Therefore, an unrestricted function with this total signature would be wrong:

filter : (a -> Bool) -> Stream a -> Stream a

You would need either:

  • A result abstraction that permits unbounded searching.
  • Evidence that qualifying elements continue to occur.
  • A stronger bound on the search between outputs.

This also exposes a subtlety in the earlier Skip | Yield | Done representation: terminating individual steps does not guarantee productive output. A machine can take Skip steps forever.

Fusion proofs must preserve the relevant behavior, including divergence, effects, and cleanup. Matching emitted values alone is insufficient if one implementation can get stuck forever before emitting the next one.

The design I’d choose

Include all three proof facilities:

  • Inductive types and proofs for finite structures, invariants, and terminating computations.
  • Coinductive types and guarded proofs for productive streams and behavioral equivalence.
  • Temporal specifications for processes, protocols, concurrency, and environmental assumptions.

Then allow a stream library to ship fusion rules justified by bisimulation, while a service library exposes temporal contracts justified by transition-system proofs.

The particularly valuable feature would be connecting these levels: proving that the actual executable implementation refines the protocol model. Otherwise, a correct TLA+-style model and a memory-safe implementation can still disagree.


User — 2026-10-22T13:33:06.597676Z

Original message

Yeah and we also need something like ocaml modules or type classes to carry abstractions so that eg filter can work for all of them but carry different proofs and guarantees


Assistant — 2026-10-22T13:33:23.517103Z

Original message

Exactly. The abstraction should carry its operations, semantic model, and laws—and its result type should depend on which guarantees the implementation can provide.

I’d combine first-class dependent modules with typeclass-style implicit resolution. Modules are the underlying mechanism; typeclasses provide convenient syntax for finding and passing them.

Separate “can filter” from “filter preserves this guarantee”

A single signature like:

filter : (a -> Bool) -> f a -> f a

is too restrictive. It assumes filtering preserves the container abstraction, which fails for productive infinite streams.

Instead, let a filtering implementation specify its result family:

interface Filterable (C : Type -> Type) where
Result : (a : Type) -> (p : a -> Bool) -> C a -> Type
filter :
(p : a -> Bool) ->
(xs : C a) ->
Result a p xs

This is schematic syntax. Because Result depends on the predicate and input, implementations can expose different guarantees:

Input abstraction Possible result
List a A finite list
Vec a n A vector with existential length m, with m ≤ n
Productive Stream a A process that may search indefinitely between outputs
Productive stream plus evidence of recurring matches Another productive stream
Resource-owning source A filtered source preserving ownership and cleanup obligations

You could additionally refine output elements with evidence that they satisfy p.

But associated result types alone aren’t enough

If every implementation chooses an arbitrary result, generic code knows almost nothing about it. The interface also needs a shared semantic contract.

For finite sequences, a module might expose:

interface FiniteSequence (C : Type -> Type) where
contents : C a -> List a -- erased semantic model
filter : (a -> Bool) -> C a -> C a
law filterCorrect :
contents (filter p xs)
= List.filter p (contents xs)

One implementation might use linked lists, another packed arrays, another trees. Their representations differ; generic proofs use contents.

For streams and processes, the semantic model must also account for ongoing computation. I would use trace or transition-system semantics rather than force everything into a finite list model.

Build a hierarchy of capabilities and laws

Something like:

Filterable
operation with an implementation-specific result
FiniteFilterable
finite sequence semantics and exact filtering correctness
ProductiveFilterable
filtering under evidence that output remains productive
EffectfulFilterable
filtering with specified effect ordering and resource behavior

These should be composable interfaces, not necessarily a rigid inheritance tree.

A generic algorithm requests precisely what it needs:

fn selectAll
using F : FiniteFilterable C
...
fn consumeForever
using F : ProductiveFilterable C
...

The name filter can stay uniform. Uniform syntax need not imply identical result types or guarantees.

Make stronger guarantees available through explicit evidence

For productive streams:

filter :
(p : a -> Bool) ->
(xs : Stream a) ->
Process a
filterProductive :
(p : a -> Bool) ->
(xs : Stream a) ->
MatchesRecur p xs ->
Stream a

MatchesRecur p xs must provide constructive evidence sufficient to find successive matches—not merely an opaque assertion about classical existence.

You could overload the same surface name for both, but I’d keep the selected capability visible in editor tooling and avoid having proof search unpredictably choose runtime behavior.

Use modules for explicit choice, implicits for convenience

For example:

module Packed = PackedVector(Int)
module Linked = LinkedSequence(Int)
Packed.filter isEven values

Or:

filter isEven values

when the relevant dictionary is unambiguous.

This gives you:

  • Abstract representation types and private constructors.
  • Dependent fields whose laws mention earlier fields.
  • Functors parameterized by whole implementations.
  • Explicit alternatives for the same underlying type.
  • Convenient implicit arguments for ordinary use.

I would require coherent default resolution and explicit selection when implementations differ. A proof about one dictionary must never justify optimization using another.

This connects directly to verified rewrite rules

A module can export a law and an optimization based on it:

law filterFusion :
filter p (filter q xs)
≈ filter (\x -> q x && p x) xs

The relevant equivalence and assumptions belong to that module:

  • Finite pure collections: equality of contents.
  • Productive streams: suitable bisimulation, with productivity evidence.
  • Effectful processes: equivalence preserving relevant effects and termination behavior.

For eager effectful filtering, that rule is not generally valid, because fusion can reorder predicate effects. Such an implementation simply would not supply that law without additional conditions.

The resulting abstraction is richer than an operation dictionary: it packages representation, capabilities, semantics, evidence, and verified optimizations. Generic code gets exactly the guarantees it requests, while implementations remain free to use very different execution models.


User — 2026-10-22T13:35:34.619278Z

Original message

Also need something like template Haskell that can solve its staging restrictions, support being hygienic, and work across modules


Assistant — 2026-10-22T13:35:52.474152Z

Original message

Yes—typed, hygienic, explicitly staged metaprogramming should be part of the core design, including generated definitions, proofs, and rewrite rules.

One qualification: staging restrictions reflect real dependencies. If a macro executes during compilation, its implementation must already be executable. We can remove arbitrary file-order and module restrictions by making those dependencies explicit.

I’d distinguish three facilities:

Facility Purpose
Syntax macros Introduce syntax, declarations, patterns, and DSLs
Typed quotations and splices Construct and combine well-typed programs
Compile-time computation Execute generators, derive instances, search for proofs

They should share infrastructure, but syntax expansion necessarily happens before all types are known.

Typed quotations should carry context

A code value should record more than its result type. Conceptually:

Code<stage, context, effects, result>

The context includes available bindings and their usage constraints. That matters with dependent types and ownership: generated code must not refer to unavailable variables, duplicate a linear resource, or smuggle an erased proof into runtime computation.

Schematic syntax:

stage fn composeCode
(f : Code (b -> c))
(g : Code (a -> b))
: Code (a -> c)
=
quote {
\x -> splice(f) (splice(g) x)
}

Inside the quotation, x is a fresh binding. Spliced code cannot accidentally capture it through a matching textual name.

Hygiene should use binding identities

Every identifier should carry a stable binding identity and scope information.

That gives you:

  • Fresh generated binders automatically.
  • Definition-site resolution for helpers referenced by macros.
  • Preserved bindings for syntax passed in by callers.
  • Source locations tracing generated code back through expansions.
  • Explicit APIs for deliberate capture and public-name generation.

A macro that generates a public encode function needs to expose that name intentionally. Hygiene should prevent accidental capture without making declaration-generating macros awkward.

Across modules, export executable generators and typed code

A module could export:

module DeriveCodec where
stage derive :
(t : TypeInfo) ->
Generator (CodecDeclarations t)

Another module could use it:

import stage DeriveCodec
data Message = ...
derive Codec for Message

The compiled module interface would include the generator’s portable compile-time executable representation, its signature, and the metadata it is authorized to inspect.

I’d support same-module use too:

stage fn deriveSomething(...) = ...
deriveSomething(...)

The compiler schedules this from the compile-time dependency graph, rather than requiring the generator to live in another source file. A genuine cycle—where compiling the generator requires executing that same generator—still gets rejected, with the dependency chain shown.

Cross-stage persistence must distinguish values from resources

Moving something into generated code should require one of these mechanisms:

  • Serialize a value through a Lift instance.
  • Reference an exported runtime definition by its stable identity.
  • Embed an explicitly supported constant.
  • Transport types and erased evidence under the staging rules.

You cannot embed a compile-time file handle, pointer, or socket as a runtime resource. Likewise, duplicating a quotation that captures an owned runtime value must respect its usage constraints.

Generated proofs should go through the same kernel

Metaprogramming becomes particularly valuable in the language we’ve been designing:

stage deriveVectorOps(n) = {
definitions = ...
laws = ...
rewrites = ...
}

A generator can produce an optimized operation, its correctness proof, and a rewrite declaration. The compiler checks the resulting evidence before accepting the rule.

Proof search can be complicated, heuristic, or externally assisted. Its output must still be checked. A macro receives no special authority to assert a theorem.

For dependent quotations, binding a generated value must also make it available in generated types:

quote {
\(n : Nat) -> \(xs : Vec Int n) -> ...
}

That requires a genuine context-aware quotation system; a string-based AST builder would be a poor foundation.

Make compile-time execution reproducible

Generators should be pure by default, with explicit capabilities for inputs:

ReadDeclaredFile
InspectPublicType
EmitDeclarations
ReportDiagnostic

Declared inputs become cache dependencies. Arbitrary network, clock, and environment access should require explicit build configuration.

Public type reflection should expose constructors and representation only where the module permits it. Otherwise, macros could bypass abstraction boundaries and couple callers to private implementations.

Finally, provide two separate extension paths:

  • Generators produce code and proof obligations.
  • Optimizer extensions propose transformations with checked evidence.

That lets a library generate specialized streaming code across modules and supply verified fusion rules, while preserving hygiene, ownership, and module abstraction. The key design is to make staging another checked dimension of the language—alongside types, effects, and resource usage.


User — 2026-10-22T13:40:16.843104Z

Original message

I do want to make sure we can load files at compile time etc or connect to a db to generate schema bindings, but yeah,


Assistant — 2026-10-22T13:40:33.268740Z

Original message

Absolutely—compile-time I/O should be fully supported, including file access, database introspection, subprocesses, and network requests. The requirement should be to make those dependencies explicit and manageable.

For example, in schematic syntax:

stage function generateBindings(
db: Capability<Database.ReadSchema>,
): Declarations {
const schema = db.introspect();
return deriveBindings(schema);
}
generate {
const db = stage.connectDatabase(secret("SCHEMA_DATABASE_URL"));
emit(generateBindings(db));
}

I’d provide two build modes:

Mode Behavior
Refresh Executes external queries and records their results as versioned inputs
Replay Generates code from recorded inputs; fails if a required input is missing

Local files can be tracked by content hash automatically. Database introspection needs an explicit snapshot because the same query and URL can produce different results tomorrow. Secrets authorize access; they should never be embedded in generated code or recorded snapshots.

That gives a useful workflow:

Terminal window
lang generate --refresh # introspect database, update schema snapshot
lang build # compile using recorded schema
lang generate --check # compare live schema against the snapshot

Direct live introspection during a build should also be available when requested. The build should record that it depended on external state and avoid silently treating cached results as current.

There’s an important proof boundary here: generated bindings can be proved correct relative to the captured schema; that does not prove the deployed database still has that schema. We can bridge that with a runtime compatibility check:

connect :
DatabaseConfig ->
IO (Result SchemaMismatch (Connection schema))

Only successful validation produces Connection schema, which typed queries then require. Maintaining that guarantee also needs a migration policy—such as preventing incompatible schema changes while those connections are active.

So yes: powerful compile-time I/O, with capabilities controlling access, snapshots enabling reproducibility, and explicit evidence connecting generated assumptions to runtime reality.


User — 2026-10-22T13:41:52.303666Z

Original message

Ok I want to make our language able to support code hot swapping as long as the new code is compatible with the old across relevant fronts


Assistant — 2026-10-22T13:42:08.411404Z

Original message

Yes—hot swapping should require evidence that the replacement is compatible with the running system. The unit of replacement should be a versioned module with an explicit contract, rather than an arbitrary function pointer.

Compatibility has several independent dimensions:

Dimension Required evidence
Interface Existing callers can still supply arguments and consume results
Behavioral contract The replacement preserves guarantees callers rely on
Effects and authority It stays within the permitted effects and capabilities
State Existing state remains valid or can be migrated
Ownership References, borrows, closures, and resources remain valid
Protocol In-flight operations can continue legally
Temporal behavior Required progress and fairness properties remain supported
Representation Layouts and calling conventions match, or adapters handle differences

Compatibility should mean refinement, not identical behavior.

For ordinary function contracts, a replacement must accept at least the inputs previously accepted and provide at least the guarantees previously promised. It can fix bugs, improve performance, and strengthen results without preserving every implementation detail.

For stateful modules, I’d model an update package roughly like this:

record Upgrade (old new : Module) where
migrate :
old.State ->
Result MigrationError new.State
preservesInvariant :
...
preservesContract :
...
preservesResources :
...
preservesProtocol :
...

The actual obligations depend on the module’s declared contract. A pure utility and a running payment processor should not require the same update machinery.

State migration needs a relation between versions

Suppose a cache changes from a map to a sharded representation. There may be no equality between their physical states, but there can be a relation:

Related(oldState, newState)

A migration establishes that relation. Subsequent operations must preserve the relevant observable contract.

You also need to specify migration failure behavior. A migration that consumes sockets or mutates external storage cannot automatically promise rollback. The update protocol must distinguish reversible preparation from the point where ownership transfers.

Make the treatment of in-flight code explicit

I’d support three strategies:

Strategy Behavior
Drain and replace Stop admitting work, let existing calls finish, then migrate
Version coexistence Existing calls and closures retain the old version; new work uses the new version
Explicit continuation migration Move suspended computations at declared upgrade points

Drain-and-replace is easiest to reason about. Version coexistence gives better availability. Continuation migration is substantially harder and should require deliberately designed checkpoints.

For infinite processes, checkpoints matter especially:

loop state = do
event <- receive
next <- handle state event
upgradePoint next
loop next

An upgrade point is a declared boundary where the runtime knows which state and resources are live. It must also account for outstanding messages and external operations.

Ownership becomes an advantage here

The runtime should only retire old code when nothing still depends on it:

  • No active stack frames.
  • No closures pointing into it.
  • No outstanding callbacks.
  • No live values requiring its destructors or method tables.
  • No borrowed state whose representation would change.

A closure is therefore not just a function pointer: it retains the code version and environment layout it expects. Old versions remain loaded until those dependencies disappear.

For state migration, exclusive ownership—or a proven synchronization protocol—prevents another thread from observing a half-migrated representation.

Dependent types make abstraction boundaries essential

If callers depend on a module’s public type-level definitions, changing those definitions may invalidate their compiled assumptions.

I would distinguish:

  • Stable public definitions: callers may reduce them during type checking; changing them requires compatibility evidence or rebuilding dependents.
  • Abstract types and contracts: implementations can change behind the interface.
  • Version-indexed state: incompatible representations remain distinct until explicitly migrated.

A runtime update cannot simply replace the meaning of a type-level function underneath already-checked code.

Verified optimizations also create dependencies

This connects directly to our rewrite rules and fusion design. If a caller has inlined a function or optimized using one of its laws, updating the original function does not update that caller.

We should offer two boundaries:

replaceable import
calls go through a stable dispatch boundary
specialized import
implementation may be inlined or fused;
caller becomes part of the dependent update set

The compiler records dependencies on implementations, representations, and optimization laws. An update either preserves those assumptions or replaces affected dependents together.

That lets users choose where dynamic replacement matters and where maximum specialization matters.

Liveness needs an update policy, too

Even compatible individual replacements can destroy progress if the system continually restarts work. Temporal contracts should therefore cover the update mechanism itself: pending obligations survive migration, and updates cannot indefinitely postpone them.

Likewise, preserving exactly-once handling requires tracking accepted work and external outcomes—not merely proving that the new handler has the same type.

I’d make the deployable artifact a checked upgrade package containing the replacement code, migration, compatibility evidence, and activation strategy. The runtime then checks operational preconditions—such as reaching a safe point—and atomically activates the package. Static proofs establish that the transition is valid; runtime coordination establishes that the running system is actually in a state where that transition applies.


User — 2026-10-22T13:43:53.754789Z

Original message

Ok I want you to write up the language specifications as we have discussed so far, a high level overview, a series of tutorial pages, and some examples


User — 2026-10-22T14:03:23.809569Z

Original message

I’d like to see us have platform specific proofs or rewrite rules so we could do platform specific simd rewrites


Assistant — 2026-10-22T14:12:31.105105Z

Original message

Created the language documentation set, including:

  • High-level overview.
  • Four specification chapters.
  • Ten tutorials.
  • Eight example files, including expected rejection cases.
  • Open decisions and implementation priorities.

The platform and SIMD specification covers target-specific proofs, feature detection, scalar fallbacks, memory safety, and hot-swap compatibility.

Download everything: language-design-draft-0.1.zip.

Syntax and unresolved choices are marked as provisional; examples aren’t compiler-tested.


User — 2026-10-22T15:02:02.005000Z

Original message

i guess i’d like to see some FFI considerations for using external libraries, make sure wasm and wasi are well supported, make sure we have ideas around concurrency etc


User — 2026-10-22T15:15:08.348533Z

Original message

did you finish?


User — 2026-10-22T15:15:56.399508Z

Original message

can you just share the files independently instead of providing a zip, or turn this all into a documentation site instead


User — 2026-10-22T15:21:39.248821Z

Original message

can you formalize the grammar. i would like to see if we could do more of a zig-style runtime (maybe implicit?) parameter or make async stuff an effect so that it’s not a special kind of function coloring?


User — 2026-10-22T15:22:27.715720Z

Original message

oh i also want to say that I’d really like the syntax to be significantly more Haskell-like e.g. do notations etc rather than rust-like


User — 2026-10-22T15:25:31.994847Z

Original message

i hope we don’t have to do qualified forms. i’d really rather we had a trait or typeclass system or something backpack-like