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
16 changes: 16 additions & 0 deletions crates/moonbit/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# MoonBit Bindings Context

MoonBit emits core wasm and `wasm-tools` adapts it into a component. Generated
bindings expose WIT imports and adapt MoonBit exports to the component ABI.

Async design references:

- [Design contract](docs/async-design.md)
- [Terminology](docs/async-glossary.md)
- [FFI-boundary conversion decision](docs/adr/0001-async-ffi-boundary-conversion.md)
- [Local Future/Promise decision](docs/adr/0002-local-future-promise.md)

Lowercase `future` and `stream` refer to Component Model types. Uppercase
`Future` and `Stream` refer to local MoonBit types. Generated code converts
between them only at concrete WIT positions whose intrinsic names come from
`wit-parser`.
43 changes: 43 additions & 0 deletions crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Keep local async values separate from component endpoints

Status: accepted; implemented

## Context

MoonBit `Future[T]` and `Stream[T]` must support arbitrary MoonBit values. A
Component Model `future<T>` or `stream<T>` is instead a transferable readable
endpoint whose operations and payload representation belong to a concrete WIT
function position.

The previous implementation exposed generic component endpoint wrappers backed
by operation vtables. That tied local construction and recursive conversion to
position-specific ABI machinery.

## Decision

`Future[T]`, `Promise[T]`, `Stream[T]`, and `Sink[T]` are local coordination
types and never contain component handles or operation tables.

For each endpoint occurrence, generated FFI code obtains intrinsic names from
`wit-parser`, owns the raw endpoint, and recursively lifts or lowers the payload.
Nested endpoints are converted one layer at a time when their containing value
crosses or is read at the boundary.

## Consequences

- `Future::new()` creates a local Future/Promise pair; `Stream::new()` creates a
local Stream/Sink pair.
- Incoming endpoints become lazy generated sources. Outgoing local values are
bridged through newly created component endpoint pairs.
- Lowering uses explicit prepare, commit, and reject paths so resources and
partial stream prefixes transfer or clean exactly once.
- Once a component future readable end is exposed, its writer must eventually
write a real value or observe that the reader was dropped. The binding cannot
fabricate a default value or close it without a value.
- Position-specific endpoint traversal remains generator data, not a MoonBit
runtime vtable.
- Producing a component future or stream requires an active component async task
scope. Scope-free synchronous lowering remains unsupported.

Detailed API and lifecycle invariants live in the
[async design contract](../async-design.md).
43 changes: 43 additions & 0 deletions crates/moonbit/docs/adr/0002-local-future-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Add a local Future/Promise pair

Status: accepted; implemented in the current branch

MoonBit needs a producer-facing one-shot primitive for local control-flow cycles
that cannot be expressed as `Future::from(async () -> T)`. The motivating case
is `wasi:http@0.3.0` request handling: `consume-body` takes a Future reporting
processing completion before it returns the request body whose later processing
determines that completion.

`Future::new()` returns `(Future[T], Promise[T])` and
`Future::new_with_cleanup(cleanup)` adds explicit value-discard cleanup. The
Promise can complete with a value, fail with a MoonBit error, or close without a
value. It is local coordination state only. It does not create, wrap, or own a
component `future` endpoint, and it works for arbitrary MoonBit `T`.

## Consequences

- `Promise::complete(value)` returns `true` only when the reader accepts
ownership. If the Future was already dropped, it returns `false` and the
caller retains `value`.
- `Future::drop()` after accepted completion runs the cleanup supplied to
`new_with_cleanup`. The plain `new()` constructor is appropriate only when
discarded `T` needs no explicit cleanup.
- `Promise::fail(error)` makes local `Future::get()` raise that error.
`Promise::close()` makes it raise `PromiseClosed`.
- Settlement is one-shot. Repeating `complete`, `fail`, or `close` after a
successful settlement is a programmer error.
- Dropping or otherwise abandoning a still-pending Promise does not implicitly
close its Future because MoonBit has no generic deterministic destructor. The
producer must explicitly complete, fail, or close it.
- Task cancellation that reaches a waiting reader before settlement drops the
reader, so later completion returns `false`. Once completion assigns the value
and wakes the reader, completion wins a simultaneous cancellation race and the
reader receives the value.
- Explicit `Future::drop()` follows the same race rule while `get()` is pending:
dropping before settlement wakes the reader with `Cancelled`, while an
already-assigned value or error remains owned by the waiting reader.
- A local failure or close cannot settle an already-exposed component future
without a value. If such an outcome is expected across WIT, it belongs in the
payload type, for example `Future[Result[V, E]]`.
- Generated FFI-boundary code remains solely responsible for creating concrete
component future pairs and satisfying their writable-end settlement rules.
Loading
Loading