Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Prose references a version as `v0.X.Y`; headings stay bare `[0.X.Y]`.

### Added

- RFC process. `spec/rfcs/` directory with a README documenting the lifecycle (draft → accepted → implemented / superseded / rejected), a `0000-template.md` for new RFCs, and the five existing RFCs migrated into structured files with YAML frontmatter (number, title, status, authors, tracking issue, created date):
- `0001-memory-model.md` (tracking #42)
- `0002-extern-host-abi.md` (tracking #55)
- `0003-project-metadata.md` (tracking #45)
- `0004-cross-engine-state.md` (tracking #46)
- `0005-third-party-protocols.md` (tracking #65)

GitHub issues stay as the discussion thread; RFC files are the source of truth for proposal text. `spec/README.md` updated with a pointer to the RFC index and the lifecycle. Closes #66.
- Subsystem keys in the chain manifest accept arbitrary identifiers, not just the five stdlib axes (`consensus`, `gas`, `state`, `exec`, `da`). A chain can declare any axis it cares about as a first-class subsystem: `privacy: GrothProver<curve=BN254>`, `mev: FlashbotsBundler`, etc. Lifts the parser-layer gatekeeping that was blocking third-party protocol extensions at the chain layer. Closes #64.
- Parser test reworked: `test_error_unknown_subsystem_key` (which asserted "unknown:" was rejected) replaced by `test_arbitrary_subsystem_key_parses` (asserts `privacy: ...` works) plus `test_subsystem_key_can_still_be_stdlib_keyword` (regression guard that `consensus:`, `state:`, etc. still parse the same way).
- `spec/grammar.ebnf` updated: `SubsystemKey = IDENT` instead of the enumerated five names; comment explains the stdlib axes are still conventions, just not parser-level requirements.
Expand Down
9 changes: 5 additions & 4 deletions spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ The spec evolves alongside the compiler. Sections marked **stable** match what t
| [Protocols: State](protocols/state.md) | draft (issue [#8](https://github.com/cleave-lang/cleave/issues/8)) | First full write-up landed; iterating |
| [Protocols: DataAvailability](protocols/da.md) | draft (issue [#16](https://github.com/cleave-lang/cleave/issues/16)) | First full write-up landed; iterating |
| [Effect system](effects.md) | draft (issue [#9](https://github.com/cleave-lang/cleave/issues/9)) | First full write-up landed; iterating |
| Module bodies (state, gas, fn, effect) | planned | Parser does not yet handle these |
| Type system | planned | No type checker exists yet |
| ABI (WASM hostcalls) | planned | Codegen lands in v0.3 |
| Standard library reference | planned | First impls land in v0.2 |
| [ABI (WASM hostcalls)](abi/wasm.md) | stable | Hostcall surface that codegen targets and the runtime implements |
| [RFCs (active proposals)](rfcs/README.md) | varies | See `rfcs/` for active design proposals; structure + lifecycle documented in the directory README |
| Module bodies (state, gas, fn, effect) | stable | Parser, type checker, codegen all handle these now |
| Type system | stable (basic) | Primitive types + fn types + opaque generics; sum types gated on RFC #42 |
| Standard library reference | planned | First impls land in v0.5+ (#53 consensus, #54 state) |

## Versioning

Expand Down
72 changes: 72 additions & 0 deletions spec/rfcs/0000-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
rfc: 0000
title: "RFC template (copy this for new RFCs)"
status: draft
authors: ["Your Name"]
tracking: https://github.com/cleave-lang/cleave/issues/NNN
created: YYYY-MM-DD
---

# Summary

One paragraph: what does this RFC propose? Stated as a noun phrase ("A type-level effect system for...") rather than a verb ("We should add..."). Someone reading just this paragraph should be able to decide whether the RFC is relevant to them.

# Motivation

Why does this need to happen? What's broken / missing / suboptimal without it? Concrete examples beat abstract claims. Link to issues, prior discussions, real bugs that this would prevent.

# Design

The proposed change. Long-form. Sections as needed.

## Sub-design topics

Break into subsections when there's structure: API surface, type rules, runtime semantics, error messages, etc. Show code examples in the language being designed.

## What changes externally

What does a developer using Cleave see differently? New syntax? New error messages? Different runtime behavior? Different gas costs?

## What changes internally

What does the compiler / runtime have to do differently? Touch which files? What's the migration path for existing code?

# Alternatives

What other designs were considered? Why is this one preferred?

- **Alternative A**: brief description, pros, cons
- **Alternative B**: same shape
- **Do nothing**: what happens if we don't do this? (Always worth considering.)

# Drawbacks

The cost of saying yes. There always is one. Be honest:

- Implementation complexity
- Audit surface
- Developer-facing complexity
- Risk of being wrong (and the cost to revert)
- Interaction with other planned work

# Open questions

Things the author can't decide alone or doesn't know yet. Each is a bullet, ideally with an open-ended question mark. Resolved questions move to the Design section; rejected directions move to Alternatives.

# Reversibility

How hard is this to undo if it turns out wrong?

- **High**: a flag flip, a backward-compatible deprecation
- **Medium**: a breaking compiler version + migration tool
- **Low**: every contract on every chain has to remigrate; in practice we don't reverse it

State this honestly. Low-reversibility decisions deserve longer review windows.

# Related work

Prior art in other languages / chains / academic papers. Cite specifically; vague references don't help.

# Implementation roadmap

If accepted, what happens next? Sub-issues, ordering, dependencies. Keep this light; the real plan emerges from implementation discussion.
120 changes: 120 additions & 0 deletions spec/rfcs/0001-memory-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
rfc: 0001
title: "Memory model for Cleave (ownership, GC, escape hatches)"
status: draft
authors: ["Cleave Labs"]
tracking: https://github.com/cleave-lang/cleave/issues/42
created: 2026-05-23
---

**Status:** draft. Open for discussion before any implementation work.

Cleave has no memory model today. The current compiler / runtime sidesteps the question entirely because the language surface is so small (only integers, bool, str, char, type params, record literals; no pointers, references, or allocation primitives). That has to change before we can:

- Implement standard-library collections (\`Vec<T>\`, \`Map<K, V>\`, \`HashMap<K, V>\`) which the protocol RFCs (#6, #7, #8, #16) reference
- Compile \`Result<T>\` / \`Option<T>\` (codegen #17 deliberately rejected these as out of scope)
- Self-host the compiler (#20, the v0.4 milestone)
- Land any non-trivial application module

This RFC picks the memory model. Once picked, every contract on every chain built with Cleave depends on the choice; changing it later breaks the world. So it's worth being deliberate.

## Hard constraints

Blockchain languages have constraints that rule out most of the conventional design space:

1. **Deterministic execution.** Two nodes running the same transaction against the same state must produce byte-identical post-state. Rules out non-deterministic GC pause patterns.
2. **Gas metering.** Every allocation must charge gas with bounded worst-case cost. Rules out implementations whose cost is opaque to the gas accountant.
3. **Bounded execution.** A transaction has finite gas. Memory growth must be metered against that budget so a malicious contract cannot exhaust node memory.
4. **Persistent state round-trips.** Anything written to chain state must serialize. Rules out in-memory cycles in stored data structures.
5. **No security footguns at the language layer.** Memory corruption in shared chain state is catastrophic. One bug, billions lost. The cost of a memory bug here is not comparable to a bug in a normal program.

## Design space

| Approach | Determinism | Gas accounting | Dev ergonomics | Risk |
|---|---|---|---|---|
| **GC (mark + sweep, RC, generational)** | hard to make deterministic across nodes; pause times must be metered | gas charge per allocation is doable; gas charge per collection cycle is awkward | familiar from JS/Python/Go | implementation complexity, audit surface |
| **Linear / affine ownership (Rust, Move)** | trivially deterministic; no GC | every alloc has a known site, gas is straightforward | steeper learning curve; lifetime annotations | borrow-checker frustration; UX cost |
| **Per-transaction arena** (auto-free at tx end) | trivially deterministic | one arena = one gas allocation pool | very simple model | cannot share heap across txs; collections that outlive a tx are awkward |
| **All-by-value with copy-on-write** | trivially deterministic | gas scales with data size on every copy | simple to teach | quadratic cost on large structures |
| **Reference-counted, cycles statically prohibited** | deterministic; RC ops are bounded | gas per inc/dec | middle ground | type system has to forbid cycles, which is a non-trivial restriction |

## Recommended approach

**Linear / affine ownership, no opt-out, with a typed \`extern host\` escape hatch for raw performance.**

The reasoning:

### Why ownership, not GC

The Sui Move + Aptos Move ecosystem has demonstrated that linear / affine type systems work for smart contracts at scale, and the model maps cleanly onto deterministic gas. GC is theoretically possible (the JVM family proved that determinism is achievable with care), but it adds an audit surface that we should not pay for at v0. We are not Solidity; we do not need to inherit the EVM's "everything is a 32-byte word" model. We are not the JVM; we do not need to inherit JIT-friendly GC complexity.

The ergonomic cost of ownership is real, but on a blockchain the cost of memory bugs is far higher than the cost of a stricter type system. Stefan should treat the borrow checker as a feature, not a tax.

### Why no opt-out

\"Let me manage memory myself\" almost always translates to \"I want raw pointer arithmetic to be fast.\" In a gas-metered environment, the upper bound on that speedup is whatever gas allows. The downside is severe: memory corruption in shared chain state means an exploit pattern that no other system in the world has to defend against, because no other system has \"a million people send signed transactions to your VM\" as its threat model.

Other languages that exposed memory escape hatches:

- **C**: every CVE-2024-* shows the cost.
- **Rust \`unsafe\`**: works because Rust has external auditors and a culture of writing wrapper crates. We do not yet have that culture.
- **Solidity inline assembly**: the source of approximately every published smart-contract exploit since 2019.

The pattern that does work in a blockchain context: type-safe escape hatches at well-defined boundaries.

### What the escape hatch should be

\`\`\`cleave
extern host fn blake3(input: bytes) -> [u8; 32]
extern host fn bls12_381_aggregate(sigs: Vec<Signature>) -> Signature
\`\`\`

A declaration that says \"this function is provided by the host runtime, runs outside the WASM gas meter, has a known typed signature, and the host is responsible for implementing it safely.\" Cryptographic primitives, hash functions, signature verification, pairing operations, big-integer arithmetic. All the things people would want unsafe memory ops for in conventional languages are better served as native host functions in a blockchain.

This pattern is established: Substrate calls them \"host functions,\" Solana calls them \"syscalls,\" Cosmos SDK has \"keepers.\" Cleave should adopt it under the \`extern host\` name (or similar; bikeshed-able).

## Open questions

1. **Stack vs heap distinction.** Do small types (u64, bool, fixed-size arrays) live exclusively on the WASM operand stack? Larger types in linear memory? Cleave-managed heap on top of linear memory?
2. **String representation.** UTF-8 byte slice? Length-prefixed? Null-terminated? Borrowed-by-default vs owned?
3. **Collection types in the stdlib.** Are \`Vec<T>\`, \`HashMap<K, V>\`, \`Map<K, V>\` part of the language (built-in syntax) or stdlib types (defined in \`.cv\` once self-hosting lands)?
4. **Move vs copy semantics.** Does \`let x = y\` move \`y\` (Rust default) or copy \`y\` (most languages)? Move-by-default is the rigorous choice; copy-by-default is the gentle one.
5. **References / borrows.** Do we want a borrow checker (Rust-style) or are linear types enough (Move-style)? Linear is simpler and works for most contract patterns.
6. **Drop semantics.** When does memory get freed? End of scope? End of transaction? At an explicit \`drop()\` call?
7. **Persistent state types.** What's the contract between in-memory values and \`state\` slots? Do all serializable types go through a single trait?
8. **Cross-engine memory.** When a Cleave WASM module reads a value written by a Solidity contract, how does the type system understand the byte layout?
9. **Failure modes.** What happens on out-of-memory? Trap? Allocate-result-Result?
10. **\`extern host\` security model.** Who can declare host functions? Are they per-chain (manifest declares allowed extensions) or per-module? How is the host-function ABI versioned?

## Implementation roadmap

If we land on \"ownership + extern host,\" the work breaks into:

1. **Type system: linearity tracking.** Extend the type checker (#34 surface) with use-counting per binding. Diagnostic when a moved value is used again. Pure mechanical pass; no codegen changes.
2. **AST + grammar: \`drop\` keyword, move/borrow annotations** (if we add them). Maybe \`&T\` for borrows; maybe Move-style implicit. RFC-level decision.
3. **Codegen: heap allocation.** \`bump-allocate\` primitive in linear memory; \`drop\` emits a free or a no-op depending on allocator. Most likely choice: per-call arena that resets between calls, plus per-instance arena for state slots.
4. **Stdlib: \`Vec<T>\`, \`String\`, \`Bytes\`.** Built on the allocator. Maybe defined in \`.cv\` once self-hosting lands; until then, defined as compiler intrinsics.
5. **\`extern host\` syntax + ABI.** Grammar addition. Codegen lowers to WASM imports under a new namespace (e.g. \`host\` instead of \`env\`). Runtime exposes a registration API.

Estimate: each of (1) through (5) is its own issue, each comparable in size to v0.3's parser-extension PR. Total: ~3000-5000 LoC, several weeks of work, plus design churn from this RFC.

## Reversibility

**Low.** Once contracts ship against a memory model, changing it requires either:

- A breaking compiler version bump and a chain hard fork (every deployed contract recompiles, every state slot remigrates)
- Multi-version support in the runtime (load v1 and v2 contracts side by side, each with its own runtime semantics)

This is why the RFC matters more than feature issues. Worth absorbing several weeks of discussion before committing.

## Related work to read before commenting

- Move's \"Resource\" type and Sui's \"object-centric\" extension
- Rust's RFC #1444 (linear types attempt) and the reasoning for why borrow-checking won
- The \"What every systems programmer should know about concurrency\" line of thinking applied to multi-validator consensus
- Solidity's inline assembly + the post-mortems on every assembly-related exploit since 2019
- Substrate's host function ABI design (\`pallet-contracts\` runtime API)

## Discussion

Comment thread on this issue. RFC stays draft for at least two weeks before any \"recommended approach\" gets promoted to a binding decision.
57 changes: 57 additions & 0 deletions spec/rfcs/0002-extern-host-abi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
rfc: 0002
title: "extern host ABI for native function declarations"
status: draft
authors: ["Cleave Labs"]
tracking: https://github.com/cleave-lang/cleave/issues/55
created: 2026-05-25
---

Promised in the memory-model RFC (#42) as the escape hatch for raw-performance primitives (crypto, hashing, signature verification) that should not run inside the WASM gas meter.

## Status

Draft. Open for discussion.

## Context

Smart-contract platforms universally need a host-function mechanism for expensive primitives. Substrate calls them "host functions," Solana calls them "syscalls," EVM has "precompiles," Cosmos SDK has "keepers." Each ecosystem has reinvented this with subtly different semantics.

Cleave needs one too. The memory-model RFC argues it should be the ONLY escape from the safe-language envelope (no `unsafe` block, no raw pointer ops). That puts more weight on getting the ABI right.

## Strawman

```cleave
extern host fn blake3(input: bytes) -> [u8; 32]
extern host fn bls12_381_aggregate(sigs: Vec<Signature>) -> Signature
extern host fn ec_recover(message: [u8; 32], v: u8, r: [u8; 32], s: [u8; 32]) -> Address
```

- `extern host fn` is a declaration, not a definition. The function body lives in the host (Rust / C / etc.).
- Codegen lowers calls to WASM imports under a `host` namespace (vs the `env` namespace used by `state_get` / `state_set`).
- The runtime crate exposes a registration API: `runtime.register_host_fn("blake3", impl)`.
- Per-chain manifest declares which host functions are allowed: `host_functions: [blake3, bls12_381_aggregate]`.

## Questions

1. **Who declares the allow-list?** Per-chain (chain manifest) or per-module (module declaration)?
2. **Versioning.** If `blake3`'s implementation changes, how does the runtime know it's the new version?
3. **Determinism.** Host functions must be deterministic across nodes. How do we enforce that the registered implementation actually is?
4. **Gas accounting.** Host functions run outside the WASM gas meter. Do they have their own gas surface? Charged per-call? Per byte of input?
5. **Type marshaling.** WASM linear memory vs Cleave values. Vec<T>, String, bytes need ABI conventions.
6. **Trust model.** Host functions can do anything the host can do. Who audits them? Is there a "stdlib host function set" that's blessed?
7. **Composition.** Can a host function call back into the runtime to read state? (Probably no, but worth being explicit.)

## Reversibility

Medium. Once contracts call `host::blake3(...)`, the ABI for `blake3` is frozen. Adding new host functions is easy; changing existing ones requires a chain hard fork.

## Related work

- Substrate `pallet-contracts` runtime API
- Solana SBPF syscalls (`sol_log`, `sol_keccak256`, `sol_invoke_signed`)
- EVM precompiles (the 0x01..0x0a address space)
- WebAssembly Component Model (the longer-term industry direction)

Discussion: comment thread. RFC stays draft for at least two weeks before any binding decision.

Loading
Loading