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

Commit 1e73dc2

Browse files
committed
0018 C: an own parameter arrives closeable, and 0018 is done
A guest handing a host resource back transfers ownership, and there is no guest drop afterwards -- ever. So lifting an `own` removes the table entry and hands the caller an AutoCloseable over the value rather than the bare value: closing it runs :drop, and the instance's own close does not drop it again because it is no longer in the table. That is what 0016 A decided for the other direction, for the same reason: a transferred capability is not its bytes. A bare value carries no obligation and no way to discover one, which for wasi:http/handler.handle -- `func(request: request)`, an own in exactly this position -- is a leaked request per call. Also stated in the code where it bites: only the resource_host_t from any_to_host is ours to delete. Deleting the `any` aborts the process. dev/resources/hres gains `eat` and `give-back`. Adding `eat` to the WIT broke every other test in the file until each supplied it, which is the import contract 0017 B chose working as intended -- a component's imports are all or nothing and wasmtime names what is missing. Fault injection: handing back the bare value gives one failure and two errors. 133 assertions. 0018 is accepted, and every shape S1's stop condition names is now reached through cljwit.host itself rather than through a spike. What remains is more than one host resource type per instance -- the tag is fixed at 1 -- and 0012's rows that no shipped release can express. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent d67634c commit 1e73dc2

5 files changed

Lines changed: 58 additions & 10 deletions

File tree

‎dev/resources/hres.wat‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
(module
55
(import "local:hres/host@0.1.0" "mint" (func $mint (param i32) (result i32)))
66
(import "local:hres/host@0.1.0" "peek" (func $peek (param i32) (result i32)))
7+
(import "local:hres/host@0.1.0" "eat" (func $eat (param i32) (result i32)))
78
(import "local:hres/host@0.1.0" "[resource-drop]token" (func $drop (param i32)))
89
(memory (export "memory") 1)
910
(func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) (i32.const 0))
11+
(func (export "give-back") (param $v i32) (result i32)
12+
(call $eat (call $mint (local.get $v))))
1013
(global $kept (mut i32) (i32.const 0))
1114
(func (export "leak") (param $v i32) (result i32)
1215
(global.set $kept (call $mint (local.get $v)))

‎dev/resources/hres.wit‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ interface host {
66
resource token { }
77
mint: func(v: u32) -> token;
88
peek: func(t: borrow<token>) -> u32;
9+
/// Takes the resource back: ownership transfers to the host, and there is
10+
/// no guest drop afterwards, ever (`0018` C).
11+
eat: func(t: token) -> u32;
912
}
1013
world w {
1114
import host;
1215
export run: func(v: u32) -> u32;
1316
/// Mints and keeps the handle, so the instance closes with one outstanding.
1417
export leak: func(v: u32) -> u32;
18+
/// Mints and hands it straight back.
19+
export give-back: func(v: u32) -> u32;
1520
}

‎doc/design/0018-host-defined-resources.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# 0018 — Host-defined resources in `cljwit.host`
22

3-
**Status:** A, B, D and E implemented (`src/cljwit/host.clj`, one host resource
4-
type per instance); C still `proposed` · 2026-07-30 · third version. Two adversarial rounds have
3+
**Status:** accepted — A through E implemented (`src/cljwit/host.clj`), with
4+
one host resource type per instance · 2026-07-30 · third version. Two adversarial rounds have
55
each falsified something load-bearing; what they found is kept below, because
66
it is most of the evidence.
77

‎src/cljwit/host.clj‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -622,16 +622,30 @@
622622
;; A host resource in an import's signature is one this instance declared,
623623
;; so `:own`/`:borrow` here mean the rep table rather than a guest handle.
624624
;; One host resource type per instance for now — the tag is fixed at 1.
625-
(let [hres-lift (fn [^MemorySegment seg]
625+
(let [transferred (fn [v]
626+
;; 0018 C: the guest will never drop this, so the caller
627+
;; gets the obligation with the value rather than a bare
628+
;; value and no way to know.
629+
(reify java.lang.AutoCloseable
630+
clojure.lang.IDeref
631+
(deref [_] v)
632+
(close [_] (when-let [d @(:drop rtab)] (d v)))))
633+
hres-lift (fn [own? ^MemorySegment seg]
626634
(let [any ^MemorySegment (.get seg ADDR (long UNION))
627635
out ^MemorySegment (.allocate arena ^java.lang.foreign.MemoryLayout ADDR)]
628636
(ok! api "any_to_host" (invoke (:any-to-hres api) (:ctx rtab) any out))
629637
(let [hr ^MemorySegment (.get out ADDR (long 0))
630638
rep (invoke (:hres-rep api) hr)]
639+
;; Only the host_t is ours to delete; deleting the
640+
;; `any` aborts the process.
631641
(invoke (:hres-delete api) hr)
632-
(first (get @(:table rtab) rep)))))
633-
lifts (mapv (fn [[_ t]] (if (contains? #{:own :borrow} t)
634-
hres-lift
642+
(let [[v] (get @(:table rtab) rep)]
643+
(if own?
644+
(do (swap! (:table rtab) dissoc rep) (transferred v))
645+
v)))))
646+
lifts (mapv (fn [[_ t]] (case t
647+
:own (partial hres-lift true)
648+
:borrow (partial hres-lift false)
635649
(lift-fn t nil)))
636650
(:params sig))
637651
rt (:result sig)

‎test/cljwit/host_test.clj‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,8 @@
530530
:imports {"local:hres/host@0.1.0#mint"
531531
(fn [v] {:n v})
532532
"local:hres/host@0.1.0#peek"
533-
(fn [t] (:n t))}})]
533+
(fn [t] (:n t))
534+
"local:hres/host@0.1.0#eat" (fn [t] (:n @t))}})]
534535
(is (= 42 ((i "run") 42))
535536
"the guest minted, borrowed and dropped a host resource")
536537
(is (= [{:n 42}] @dropped)
@@ -544,7 +545,8 @@
544545
a {:resources {"local:hres/host@0.1.0#token"
545546
{:drop (fn [v] (swap! dropped conj v))}}
546547
:imports {"local:hres/host@0.1.0#mint" (fn [_] same)
547-
"local:hres/host@0.1.0#peek" (fn [t] (:n t))}})]
548+
"local:hres/host@0.1.0#peek" (fn [t] (:n t))
549+
"local:hres/host@0.1.0#eat" (fn [t] (:n @t))}})]
548550
((i "run") 7)
549551
((i "run") 7)
550552
(is (= [same same] @dropped)
@@ -558,7 +560,8 @@
558560
a {:resources {"local:hres/host@0.1.0#token"
559561
{:drop (fn [v] (swap! dropped conj v))}}
560562
:imports {"local:hres/host@0.1.0#mint" (fn [v] {:kept v})
561-
"local:hres/host@0.1.0#peek" (fn [t] (:kept t))}})]
563+
"local:hres/host@0.1.0#peek" (fn [t] (:kept t))
564+
"local:hres/host@0.1.0#eat" (fn [t] (:kept @t))}})]
562565
(is (= 9 ((i "leak") 9)) "the guest mints and keeps the handle")
563566
(is (= [] @dropped) "nothing dropped while it is held")
564567
(.close ^java.lang.AutoCloseable i)
@@ -570,7 +573,8 @@
570573
a {:resources {"local:hres/host@0.1.0#token"
571574
{:drop (fn [_] (throw (ex-info "close! failed" {})))}}
572575
:imports {"local:hres/host@0.1.0#mint" (fn [v] {:kept v})
573-
"local:hres/host@0.1.0#peek" (fn [t] (:kept t))}})]
576+
"local:hres/host@0.1.0#peek" (fn [t] (:kept t))
577+
"local:hres/host@0.1.0#eat" (fn [t] (:kept @t))}})]
574578
(is (= 5 ((i "run") 5))
575579
"the guest's drop still succeeds — 0017 D would have killed the instance")
576580
(is (= 1 (count (host/drop-failures i)))
@@ -579,6 +583,28 @@
579583
"reading them clears them, so close has nothing to rethrow")
580584
(.close ^java.lang.AutoCloseable i)))
581585

586+
(testing "0018 C — an own parameter hands the caller the obligation"
587+
(let [dropped (atom [])
588+
got (atom nil)]
589+
(with-open [i (host/instantiate
590+
a {:resources {"local:hres/host@0.1.0#token"
591+
{:drop (fn [v] (swap! dropped conj v))}}
592+
:imports {"local:hres/host@0.1.0#mint" (fn [v] {:n v})
593+
"local:hres/host@0.1.0#peek" (fn [t] (:n t))
594+
"local:hres/host@0.1.0#eat"
595+
(fn [t]
596+
(reset! got t)
597+
(is (instance? java.lang.AutoCloseable t)
598+
"an own arrives closeable, not bare")
599+
(:n @t))}})]
600+
(is (= 4 ((i "give-back") 4)))
601+
(is (= [] @dropped)
602+
"the guest will never drop it, so nothing has yet")
603+
(.close ^java.lang.AutoCloseable @got)
604+
(is (= [{:n 4}] @dropped) "closing it runs :drop"))
605+
(is (= [{:n 4}] @dropped)
606+
"and the instance's own close does not drop it again")))
607+
582608
(testing "a resource the component does not declare is a typo"
583609
(let [ex (is (thrown? clojure.lang.ExceptionInfo
584610
(host/instantiate

0 commit comments

Comments
 (0)