Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.

Commit 1727fbe

Browse files
committed
Re-check the fresh-clone constraint, which S1 had ten chances to break
"bb check passing locally does not mean it passes on a clone" is a gotcha this repo earned by failing it once, with empty directories git never tracked. S1 added ten WIT/WAT artifacts since it was last checked, two of them deps/ trees -- dev/resources/rnd/ and dev/resources/args/ -- which is exactly the shape that failed before. Cloned into /tmp and ran it: 29 tests, 212 assertions, green in 5.6 s. Then the spikes the design notes cite as their evidence, because a note whose artifact does not reproduce is a claim rather than a measurement: spike-reflect prints echo's fourteen exports with their WIT signatures spike-import re-entry from a host callback traps, as 0014's amendment says spike-hres run(42) = 42, dropped [[1 42]], table empty All three from the clone, none from this working tree. doc/status.md's Verified entry now says when it was last true and what it survived, rather than "a fresh clone passes bb check" with no date on it. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent d169c01 commit 1727fbe

2 files changed

Lines changed: 119 additions & 2 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# 0018 — Host-defined resources in `cljwit.host`
2+
3+
**Status:** proposed · 2026-07-30 · the mechanism is `bb spike-hres`; this is
4+
the API over it, and no code exists
5+
6+
## The question
7+
8+
`0017` A keys imports by `iface#func`. A **resource is a type, not a
9+
function**, so that map has nowhere to put one — and every `wasi:io` interface
10+
declares one, so this is not an edge case.
11+
12+
What is settled, measured 2026-07-30:
13+
14+
- **The round trip works** (`bb spike-hres`): a guest mints a host resource,
15+
reads it through a `borrow`, and drops it; the host's destructor fires with
16+
the right rep.
17+
- **A host resource is a `u32` type tag plus a destructor upcall**, and the
18+
destructor returns `wasmtime_error_t *`, not `void`. Getting that wrong
19+
crashes the JVM *after* the callback has run correctly.
20+
- **A handle carries only a `u32` rep.** `resource_host_new(owned, rep, ty)`
21+
mints one; `any_to_host` + `host_rep` reads it back. There is nowhere to put
22+
a Clojure value, so the host must keep the mapping.
23+
- **The marshaller can tell a host resource from a guest one by type.** A
24+
valtype's union carries `own`/`borrow` → `resource_type_t *`, and
25+
`resource_type_equal` compares it against the types the host registered.
26+
27+
## The decision
28+
29+
### A. Resources are declared in their own map, keyed by the same WIT name
30+
31+
```clojure
32+
(host/instantiate
33+
art {:resources {"local:hres/host@0.1.0#token" {:drop (fn [v] (close! v))}}
34+
:imports {"local:hres/host@0.1.0#mint" (fn [v] (open-thing v))
35+
"local:hres/host@0.1.0#peek" (fn [t] (:n t))}})
36+
```
37+
38+
The identity stays the exact WIT name (`0014` B, `0017` A). What changes is
39+
that a resource is a different *kind* of thing, so it gets a different key in
40+
the options map rather than a different value shape under the same one.
41+
42+
### B. A host resource is any Clojure value; the host owns the rep table
43+
44+
The import that returns one returns an ordinary value. `cljwit.host` assigns it
45+
a `u32` rep, keeps `rep → value` for the instance, and mints the handle. A
46+
`borrow` parameter of that type arrives as the value, not as a rep or a handle.
47+
48+
The guest's drop removes the entry and calls `:drop` with the value, if given.
49+
50+
### C. The table is per-instance and dies with it
51+
52+
Reps are only meaningful inside one store. The table lives beside the handle
53+
registry `0016` D already keeps, and `Instance/close` clears it — after
54+
closing outstanding guest handles, before deleting the store.
55+
56+
### D. `:drop` may not throw
57+
58+
It runs inside wasmtime's destructor trampoline, which is the same place `0017`
59+
D's rule applies: a JVM exception unwinding through native frames exits the VM.
60+
Anything thrown is caught and converted, and the instance is marked dead, as an
61+
import's exception already is.
62+
63+
## Why
64+
65+
**A, because a resource is not a function and pretending otherwise costs
66+
later.** `0017` F recorded the shape; the alternative — one map whose values
67+
are sometimes functions and sometimes maps — makes a typo in the value shape
68+
into a silent misregistration.
69+
70+
**B, because the rep is a `u32` and nothing else fits through it.** Any design
71+
where the Clojure value crosses the boundary is impossible; the only question
72+
is who keeps the table, and the host is the only party that sees both sides.
73+
74+
**D, because the failure is the worst one available**, and it is the same
75+
failure `0017` D already pays a `try` to prevent.
76+
77+
## Alternatives rejected
78+
79+
- **One `:imports` map with a value shape that says "resource".** See A.
80+
- **Exposing the rep to Clojure.** Smallest, and it makes a capability an
81+
integer anyone can forge — the same argument `0016` A made against integer
82+
handles for guest resources.
83+
- **A global rep table.** Reps are store-scoped; a global one would collide
84+
between instances and leak.
85+
- **Letting `:drop` propagate.** It cannot: the frame it would unwind through
86+
is native.
87+
- **Requiring the user to mint handles explicitly**, with a `host/resource`
88+
constructor called inside the import. Honest about what happens, and it
89+
makes every `wasi:io`-shaped import carry ceremony that the type already
90+
determines.
91+
92+
## What would falsify this
93+
94+
- **A resource type used by two instances of the same artifact.** The type is
95+
registered on the *linker*, which `0014` C creates per instantiate, so each
96+
instance has its own — unverified, and if linkers turn out shareable the
97+
table's scope is wrong.
98+
- **`borrow` parameters of a host type arriving as something other than a
99+
handle to a live rep** — the spike read one, but only inside the call that
100+
minted it. A borrow held across two guest calls is untested.
101+
- **The type comparison being too coarse.** `resource_type_equal` is the only
102+
discriminator; if two distinct host types compare equal, values would be
103+
handed to the wrong `:drop`.
104+
- **`own` parameters** — a guest handing a host resource *back*. The spike only
105+
covers the guest receiving one.
106+
- **The cost.** A rep table lookup per borrow and a `swap!` per mint, on a call
107+
that `0013` prices at ~0.4 µs. Unmeasured.
108+
109+
## Resources
110+
111+
- `bb spike-hres` and `dev/resources/hres.{wit,wat}` — the mechanism.
112+
- `0017` — the import API this extends, and the exception rule D inherits.
113+
- `0016` — the guest-resource side, and the argument against integer handles.

‎doc/status.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,12 @@ minimum, set for WASI 0.3, is also the component minimum; do not lower it.
130130

131131
## Verified
132132

133-
- **A fresh clone from GitHub passes `bb check`** — including the test that
134-
shells out to `wasm-tools`.
133+
- **A fresh clone passes `bb check` and reproduces every spike** — re-checked
134+
2026-07-30 after S1 added ten WIT/WAT artifacts, two of them `deps/`
135+
directories that git could have dropped: 29 tests, 212 assertions, 5.6 s,
136+
and `spike-reflect`, `spike-import` and `spike-hres` all print what their
137+
design notes quote. This is the constraint that has failed here before —
138+
empty directories git never tracked.
135139
- **The benchmark driver fails on a wrong answer, not just a slow one**, and
136140
refuses an `n` whose expected value an empty loop would also produce. Both
137141
confirmed by breaking the benchmark on purpose.

0 commit comments

Comments
 (0)