Repository navigation
quest: finish the compile-checks spike, reporting which Swift and C++ checks compile - #688
Conversation
Quest-Claim: 19c89b4028b0761d
…++ checks compile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Evidence for the report: the two test scripts rewritten against the stricter bindings, and the scripts that make those bindings from To reproduce: export the api fixture ( swift-test.diff--- ffi/tests/api.swift
+++ api.swift
@@ -149,7 +149,7 @@
let d = Point.default()
print("default \(d.x) \(d.y)")
- let c = Counter.new("c", 5)
+ var c = Counter.new("c", 5)
print("get \(c.get())")
print("name \(c.name())")
print("add ok \(try! c.add(10))")
@@ -170,16 +170,14 @@
c.rename("d")
print("name \(c.name())")
print("is d \(c.`is`("d"))")
- let copy = c.clone()
+ var copy = c.clone()
_ = try! copy.add(1)
print("clone \(copy.get()) original \(c.get())")
- print("debug \(debug(c))")
+ print("debug \(c.debugDescription)")
let label = c.intoLabel(p)
print("label \(debug(label))")
print("note \(hasText(Note(text: ""))) \(hasText(Note(text: nil)))")
- let (below, above) = Counter.new("s", 7).split(5)
- print("split \(below.name()) \(below.get()) \(above.name()) \(above.get())")
let one = only(Counter.new("o", 3))
print("only \(one.name()) \(one.get())")
let (w8, w16, w32, w64) = widths(98305)
@@ -208,10 +206,6 @@
}
let statRows = rows([Point(x: 1, y: 2), Point(x: 3, y: 3)])
print("rows \(statRows.map { "\($0.0) \(debug($0.1)) \($0.2.x) \($0.2.y)" }.joined(separator: ", "))")
- // Each handle in a tuple moves in, as one taken by value does.
- let board = Board.new([("low", Counter.new("l", 1)), ("high", Counter.new("h", 9))])
- let ranked = board.ranked()
- print("ranked \(ranked.map { "\($0.0) \($0.1.name()) \($0.1.get())" }.joined(separator: ", "))")
// A map crosses as its pairs and a set as its items, which Rust collects in order, each
// key once, the last kept.
let atlas = Atlas(points: [("b", Point(x: 1, y: 1)), ("ab", Point(x: 2, y: 3)), ("b", Point(x: 4, y: 4))], ids: [3, 1, 3])
@@ -245,13 +239,13 @@
print("title Some((\"\(title(true)!)\",)) \(title(false) == nil ? "None" : "Some") named Some((\"\(named("x")!)\",))")
// Each lent counter is a new handle to a clone.
- let registry = Registry.new(Counter.new("first", 1))
+ var registry = Registry.new(Counter.new("first", 1))
print("last \(registry.last() != nil)")
registry.push(Counter.new("last", 2))
- let first = registry.firstGet
+ var first = registry.firstGet
_ = try! first.add(10)
print("first \(registry.firstGet.get()) lent \(first.get())")
- let last = registry.last()!
+ var last = registry.last()!
_ = try! last.add(10)
print("last \(registry.last()!.get()) lent \(last.get())")
// A closure lent for the call edits the first counter through a handle.
@@ -270,8 +264,8 @@
print("shift \(shift({ 7 }, 4))")
let name = Name.owned("a")
- let joined = name.join(Name.owned("b"))
- print("joined \(joined.asStr()) \(debug(joined)) \(joined == name)")
+ var joined = name.join(Name.owned("b"))
+ print("joined \(joined.asStr()) \(joined.debugDescription) \(joined == name)")
joined.rename("c")
print("renamed \(joined.asStr())")
let long = longer(joined, Name.owned("dd"))
@@ -331,7 +325,7 @@
print("secs \(stamp.secs())")
print("later \(Stamp.fromMillis(3000).later(stamp))")
print("secs \(stamp.secs())")
- let message = Message.new(stamp, Data("body".utf8))
+ var message = Message.new(stamp, Data("body".utf8))
print("payload \(text(message.payloadGet))")
print("stamp \(debug(message.stampGet!))")
print("sent \(message.sent())")
@@ -394,7 +388,7 @@
channel.send(Data("rung".utf8))
print("woken \(wakes.value)")
- let list = Node.new(1, nil)
+ var list = Node.new(1, nil)
list.push(2)
print("node \(list.sum())")
@@ -420,7 +414,7 @@
let beside = PairU32(left: 258, right: 9).besideSize(sized.left)
print("beside \(beside.left) \(debug(beside))")
// At its default, named as the type itself.
- let stack = Stack.new()
+ var stack = Stack.new()
for (i, n) in ["a", "b"].enumerated() {
stack.push(Counter.new(n, UInt64(i) + 1))
}
@@ -466,7 +460,7 @@
print("dropped \(dropped())")
// A field and the method of its name, each its own member.
- let retry = Retry.new(5)
+ var retry = Retry.new(5)
let limit = retry.limitGet
print("retry \(limit != nil ? 1 : 0) \(limit ?? 0) \(retry.limit()) \(retry.delay())")
// The renamed field's setter, named as its getter.
@@ -476,9 +470,9 @@
// Foreign types: a URL parsed from text, socket addresses, OS paths, and `Vec`s.
let base = try! Url.parse("https://example.com/a/b")
- print("url \(base) host \(host(base).map { "Some(\"\($0)\")" } ?? "None")")
+ print("url \(base.description) host \(host(base).map { "Some(\"\($0)\")" } ?? "None")")
let url = try! resolve(base.clone(), "c?d")
- print("resolve \(url) \(url == base)")
+ print("resolve \(url.description) \(url == base)")
do {
_ = try Url.parse("no scheme")
} catch let e as StringError {
@@ -500,10 +494,6 @@
print("sentence \(sentence(words)) \(sentence(nil))")
print("sentence \(sentence([]))|")
print("chunks \(chunks(Data("abcde".utf8), 2).map { Array($0) })")
- let counters = counters(["x", "y"])
- print("counters \(counters.map { "\($0.name()) \($0.get())" }.joined(separator: ", "))")
- // Each counter moves in, so Swift closes it.
- print("worth \(worth([counters[0]], [counters[1]])) \(worth(nil, []))")
print("held \([heldCount(Counter.new("h", 4)), heldCount(nil)].map { $0.map { "Some(\($0))" } ?? "None" }.joined(separator: " "))")
print("boxed \(boxedName(boxedCounter("b")).map { "Some(\"\($0)\")" } ?? "None") \(boxedName(nil) ?? "None") \(boxedCounter(nil) == nil)")
print("less \(less(5, 2))")
@@ -517,9 +507,6 @@
print("timed text \(timedText(doc))")
let tagged = tagged(doc, "t")
print("tagged \(tagged.toText())")
- // A JSON object's entries cross as pairs of a key and its document, each moved in.
- let jsonEntries = entries([("b", try! Json.parse("[1]")), ("a", try! Json.parse("2"))], "t")
- print("entries \(jsonEntries.map { "\($0.0) \($0.1.toText())" }.joined(separator: ", "))")
do {
_ = try Json.parse("{\"a\":")
} catch let e as StringError {
@@ -528,17 +515,15 @@
check(false, "parse throws its message")
}
let site = Site.new(base)
- print("site \(site.homeGet) \(site.homeGet)")
+ print("site \(site.homeGet.description) \(site.homeGet.description)")
// Slices, passed in and returned as clones, and the socket address of each family.
print("shapes \(Shape.ALL) greetings \(GREETINGS)")
- let trail = Trail.new([Point(x: 1, y: 2), Point(x: 3, y: 4)], ["a", "b"])
+ var trail = Trail.new([Point(x: 1, y: 2), Point(x: 3, y: 4)], ["a", "b"])
print("trail \(trail.points()) \(trail.labels())")
// Returned iterators, each collected at the call: drained labels are gone after.
print("iterated \(trail.iter()) \(trail.names()) \(Array(trail.bytes()))")
print("drained \(trail.drain()) then \(trail.labels())")
- let numbered = numbered(["p", "q"])
- print("numbered \(numbered.map { "\($0.name()) \($0.get())" }.joined(separator: ", "))")
let (next4, next6) = nexts(SocketAddr(ip: [127, 0, 0, 1], port: 8080), v6)
print("nexts \(next4.ip.map(String.init).joined(separator: ".")) \(next4.port) \(next6.port) \(next6.scopeId)")
}
@@ -646,12 +631,14 @@
// An object whose last reference goes while a call awaits on it stays alive until the
// call ends.
do {
- let other = Cell<Channel?>(Channel.new())
- let sender = other.value!.clone()
let got = Cell<String?>(nil)
- q.launch { got.value = text(try await other.value!.recv()) }
- q.drain()
- other.value = nil
+ let sender: Channel
+ do {
+ let other = Channel.new()
+ sender = other.clone()
+ q.launch { got.value = text(try await other.recv()) }
+ q.drain()
+ }
sender.send(Data("held".utf8))
q.drain()
check(got.value == "held", "the call awaiting on an object holds it")
@@ -688,34 +675,9 @@
print("owned ok")
}
-/// Holds a lent counter past the call it was lent for.
-final class Kept: @unchecked Sendable {
- var counter: Counter?
-}
-
// Each breaks a contract, which must fail with a message naming it.
func broken(_ which: String) {
switch which {
- case "moved":
- let c = Counter.new("c", 0)
- _ = c.intoLabel(nil)
- _ = c.get()
- case "boxed moved":
- let tail = Node.new(1, nil)
- _ = Node.new(2, tail)
- _ = tail.sum()
- case "lent moved":
- _ = Registry.new(Counter.new("c", 0)).edit { counter in
- _ = counter.intoLabel(nil)
- return false
- }
- case "lent kept":
- let kept = Kept()
- _ = Registry.new(Counter.new("c", 0)).edit { counter in
- kept.counter = counter
- return false
- }
- _ = kept.counter!.get()
// An IPv6 address where Rust takes `SocketAddrV4`.
case "family":
let any = SocketAddr(ip: Array(repeating: 0, count: 16), port: 1)cpp-test.diff--- ffi/tests/api.cpp
+++ api.cpp
@@ -321,9 +321,9 @@
}
// A closure lent for the call edits the first counter through its class.
std::uint64_t by = 5;
- bool edited = registry.edit([&](api::Counter &counter) {
- (void)counter.add(by);
- return counter.edited() && counter.get() == 6;
+ bool edited = registry.edit([&](api::borrow<api::Counter> counter) {
+ (void)counter->add(by);
+ return counter->edited() && counter->get() == 6;
});
std::printf("edit %s %llu\n", yes(edited), ull(registry.first_get().get()));
// A setter moves an object in, as assignment does.
@@ -1112,23 +1112,7 @@
// Each breaks a contract, which must abort with a message naming the function.
int broken(const char *which) {
- if (std::strcmp(which, "moved") == 0) {
- api::Counter c = api::Counter::new_("c", 0);
- api::Label label = std::move(c).into_label(std::nullopt);
- c.get();
- } else if (std::strcmp(which, "moved from") == 0) {
- api::Counter c = api::Counter::new_("c", 0);
- api::Counter d = std::move(c);
- c.get();
- } else if (std::strcmp(which, "shared moved from") == 0) {
- api::Gauge g = api::Gauge::new_(0);
- api::Gauge h = std::move(g);
- std::move(g).into_level();
- } else if (std::strcmp(which, "shared moved out") == 0) {
- api::Gauge g = api::Gauge::new_(0);
- std::move(g).into_level();
- std::move(g).into_level();
- } else if (std::strcmp(which, "lent copied") == 0) {
+ if (std::strcmp(which, "lent copied") == 0) {
static std::optional<api::Gauge> kept;
api::watch(api::Gauge::new_(0), [](const api::Gauge &lent) {
kept.emplace(lent);
@@ -1146,19 +1130,6 @@
recv.poll(waker);
} else if (std::strcmp(which, "null view") == 0) {
api::len(api::view(nullptr, 1));
- } else if (std::strcmp(which, "lent moved") == 0) {
- api::Registry registry = api::Registry::new_(api::Counter::new_("c", 0));
- registry.edit([](api::Counter &counter) {
- api::Label label = std::move(counter).into_label(std::nullopt);
- return false;
- });
- } else if (std::strcmp(which, "lent kept") == 0) {
- static std::optional<api::Counter> kept;
- api::Registry registry = api::Registry::new_(api::Counter::new_("c", 0));
- registry.edit([](api::Counter &counter) {
- kept.emplace(std::move(counter));
- return false;
- });
}
return 0;
}lite.diff--- strict/api.swift
+++ lite/api.swift
@@ -266,83 +266,26 @@
/// A handle an object owns, or is lent for a call: each call holds it, so `close`,
/// once a call moves its value out or a lend ends, frees it only once no call does, and
/// a call after `close` traps.
+ /// A handle an object owns, or is lent for a call. The type system orders every call
+ /// before the move or the end of the lend, so nothing is counted at run time.
struct Handle {
let raw: OpaquePointer?
- /// The holds, one for the owner until closed and one per call, shifted past a bit
- /// set once closed.
- let state: UnsafeMutablePointer<Int>
/// Null for a handle Rust lends, which it frees itself.
let free: (@convention(c) (OpaquePointer?) -> Void)?
- let type: StaticString
-
- static let closed = 1
- static let held = 2
init(_ raw: OpaquePointer?, _ free: (@convention(c) (OpaquePointer?) -> Void)?, _ type: StaticString) {
self.raw = raw
self.free = free
- self.type = type
- state = UnsafeMutablePointer<Int>.allocate(capacity: 1)
- state.initialize(to: Handle.held)
- }
-
- func hold() -> OpaquePointer? {
- var n = load(state)
- while true {
- if n & Handle.closed != 0 {
- fatalError("\(type) is closed")
- }
- let seen = swap(state, n, n + Handle.held)
- if seen == n {
- return raw
- }
- n = seen
- }
- }
-
- func release() {
- if add(state, -Handle.held) < Handle.held {
- free?(raw)
- }
- }
-
- /// Run `body` with the raw handle, held until it returns.
- @inline(__always)
- func with<R: ~Copyable>(_ body: (OpaquePointer?) throws -> R) rethrows -> R {
- let raw = hold()
- defer { release() }
- return try body(raw)
- }
-
- func close() {
- var n = load(state)
- while n & Handle.closed == 0 {
- let seen = swap(state, n, (n | Handle.closed) - Handle.held)
- if seen == n {
- if (n | Handle.closed) - Handle.held < Handle.held {
- free?(raw)
- }
- return
- }
- n = seen
- }
}
- /// End a lend, closing the handle, which Rust frees next: a call still holding it
- /// would reach the freed handle, so it traps, as Rust's own check does once a call
- /// borrowed.
- func end() {
- close()
- if load(state) != Handle.closed {
- fatalError("\(type): a call still holds it when its lend ends")
- }
- }
-
- /// Close it, for its owner's `deinit`, when no call can hold it.
- func drop() {
- close()
- state.deallocate()
- }
+ @inline(__always) func hold() -> OpaquePointer? { raw }
+ @inline(__always) func release() {}
+ @inline(__always) func with<R: ~Copyable>(_ body: (OpaquePointer?) throws -> R) rethrows -> R { try body(raw) }
+ @inline(__always) func close() {}
+ @inline(__always) func end() {}
+ /// Free it, for its owner's `deinit`: after a call moved the value out, Rust left
+ /// the handle empty for this to free.
+ func drop() { free?(raw) }
}
/// Run `body` with the raw handle of `handle`, or null for none, held until it returns.strict.py"""Rewrite the generated api.swift into its stricter form: each object that is not `Copy`
becomes a `~Copyable` struct, `self` by value `consuming`, `&mut self` `mutating`, an object
parameter `borrowing` or, when the call moves it in, `consuming`, and an object lent to a
closure `borrowing` or, for `&mut`, `inout`. What Swift 6.3 cannot express is commented out
and listed on stderr.
usage: python3 -I strict.py api.swift api.h > strict.swift
"""
import re
import sys
src = open(sys.argv[1]).read()
header = open(sys.argv[2]).read()
classes = re.findall(r"^public final class (\w+):", src, re.M)
COPY = {"Stamp", "Refusal"}
objects = [c for c in classes if c not in COPY]
obj = r"\b(?:" + "|".join(objects) + r")\b"
def const_self(cfn):
m = re.search(r"\b" + cfn + r"\((const )?api_\w+ \*self", header)
return m is not None and m.group(1) is not None
def callback_const(cfn):
"""Whether each object a C function's callback is lent is const."""
m = re.search(r"\b" + cfn + r"\(([^;]*)\);", header)
if not m:
return []
cb = re.search(r"\(\*\w+\)\(void \*data([^)]*)\)", m.group(1))
if not cb:
return []
return [p.strip().startswith("const ") for p in cb.group(1).split(",")[1:]]
lines = src.split("\n")
out = []
dropped = []
def block_end(i):
"""The index of the line closing the declaration opened on line i."""
depth = 0
for j in range(i, len(lines)):
depth += lines[j].count("{") - lines[j].count("}")
if depth == 0 and j > i or (depth == 0 and "{" in lines[j]):
return j
raise ValueError(i)
def split_params(sig):
"""The top-level parameters of a signature's parameter list."""
start = sig.index("(")
depth = 0
for k in range(start, len(sig)):
if sig[k] in "([<":
depth += 1
elif sig[k] in ")]>" and not (sig[k] == ">" and sig[k - 1] == "-"):
depth -= 1
if depth == 0:
end = k
break
params, depth, cur = [], 0, ""
for ch in sig[start + 1 : end]:
if ch in "([<":
depth += 1
elif ch in ")]>":
depth -= 1
if ch == "," and depth == 0:
params.append(cur)
cur = ""
else:
cur += ch
if cur.strip():
params.append(cur)
return start, end, params
def unsupported(t):
"""A type naming an object inside an array or tuple, which Swift 6.3 cannot hold."""
return re.search(r"\[[^\]]*" + obj, t) or re.search(r"\((?:[^()]*,)?\s*[^()]*" + obj + r"[^()]*,", t) or re.search(r"\([^()]*,[^()]*" + obj, t)
i = 0
current = None
while i < len(lines):
line = lines[i]
m = re.match(r"^public final class (\w+): (.*) \{$", line)
if m:
current = m.group(1)
if current in objects:
out.append(f"public struct {current}: ~Copyable, @unchecked Sendable {{")
else:
out.append(line)
i += 1
continue
if line == "}":
current = None
fm = re.match(r"^(\t?)(public )?(static )?func ([^(]+)\(", line)
if fm and (fm.group(1) == "" or current is not None):
end = block_end(i)
body = "\n".join(lines[i : end + 1])
sig = line
_, close, params = split_params(sig)
ret = sig[close + 1 :]
name = fm.group(4).strip()
if unsupported(ret) or any(unsupported(p.split(":", 1)[1]) for p in params if ":" in p and "->" not in p.split(":", 1)[1]):
dropped.append(f"{current + '.' if current else ''}{name}{sig[sig.index('('):close + 1]}{ret.rstrip(' {')}")
out.extend("// " + l for l in lines[i : end + 1])
i = end + 1
continue
new = []
swaps = []
for p in params:
label, _, ty = p.partition(":")
pname = label.split()[-1]
t = ty.strip()
if re.fullmatch(obj + r"\??", t):
moved = re.search(r"\b" + pname + r"\??\.handle\.close\(\)", body) or re.search(r"if let (\w+) = " + pname + r" \{ \1\.handle\.close\(\)", body)
new.append(f"{label}: {'consuming' if moved else 'borrowing'} {t}")
elif "->" in t and re.search(obj, t):
cfn = re.search(r"\b(api_\w+)\(", body).group(1)
consts = callback_const(cfn)
inner = re.match(r"(@Sendable )?\((.*?)\) ->", t)
args = [a.strip() for a in inner.group(2).split(",")]
k = 0
fixed = []
for a in args:
if re.fullmatch(obj, a):
c = consts[k] if k < len(consts) else True
fixed.append(("borrowing " if c else "inout ") + a)
if not c:
body_new = None
else:
fixed.append(a)
k += 1
t2 = t.replace("(" + inner.group(2) + ")", "(" + ", ".join(fixed) + ")", 1)
new.append(f"{label}: {t2}")
swaps.append((t, t2, [k for k, a in enumerate(fixed) if a.startswith("inout ")]))
else:
new.append(p.strip() if not new else p.strip())
sig2 = sig[: sig.index("(") + 1] + ", ".join(new) + sig[close:]
if current in objects and (fm.group(2) or fm.group(1)) and not fm.group(3) and not name.startswith("=="):
if "defer { self.handle.close() }" in body:
sig2 = sig2.replace("func ", "consuming func ", 1)
else:
cm = re.search(r"\b(api_\w+)\(h_", body)
if cm and not const_self(cm.group(1)):
sig2 = sig2.replace("func ", "mutating func ", 1)
if name.startswith("==") and current in objects:
sig2 = re.sub(r"\(a: (\w+), b: (\w+)\)", r"(a: borrowing \1, b: borrowing \2)", sig2)
out.append(sig2)
# A closure lent an object for `&mut` takes it inout.
rest = "\n".join(lines[i + 1 : end + 1])
for old, new_t, inouts in swaps:
rest = rest.replace(old, new_t)
for k in inouts:
rest = re.sub(r"let l" + str(k) + r"_ = ", "var l" + str(k) + "_ = ", rest)
rest = re.sub(r"f_\((.*?)\bl" + str(k) + r"_\b", lambda m: "f_(" + m.group(1) + "&l" + str(k) + "_", rest)
out.extend(rest.split("\n"))
i = end + 1
continue
out.append(line)
i += 1
text = "\n".join(out)
# The runtime's helpers return whatever their body does, a handle included.
text = re.sub(r"func (view|with)<R>\(", r"func \1<R: ~Copyable>(", text)
# Foundation's closures return only a copyable result, so a handle leaves through an
# optional the closure fills.
text = text.replace(
""" try bytes.withUnsafeBytes { raw in
let ptr = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) ?? UnsafePointer(empty)
return try body(Buf(ptr: ptr, len: raw.count, owner: nil))
}""",
""" var r: R? = nil
try bytes.withUnsafeBytes { raw in
let ptr = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) ?? UnsafePointer(empty)
r = try body(Buf(ptr: ptr, len: raw.count, owner: nil))
}
return r.take()!""",
)
text = text.replace(
""" return try text.withUTF8 { utf8 in
try body(Buf(ptr: utf8.baseAddress ?? UnsafePointer(empty), len: utf8.count, owner: nil))
}""",
""" var r: R? = nil
try text.withUTF8 { utf8 in
r = try body(Buf(ptr: utf8.baseAddress ?? UnsafePointer(empty), len: utf8.count, owner: nil))
}
return r.take()!""",
)
# Optional.map takes only a copyable value: a switch borrows it instead.
def unmap(m):
if not re.search(obj, m.group(0)) and m.group(1) not in params_of_objects:
return m.group(0)
return (
f"{{ () -> {m.group(3)} in switch {m.group(1)} {{ case .some(let {m.group(2)}): "
f"return {m.group(3)}(some: true, value: {m.group(4)}); case .none: return {m.group(5)}() }} }}()"
)
params_of_objects = set(re.findall(r"_ (\w+): (?:consuming|borrowing) " + obj + r"\?", text))
text = re.sub(
r"\((\w+)\.map \{ (\w+) in (api_option_\w+)\(some: true, value: (.*?)\) \} \?\? (api_option_\w+)\(\)\)",
unmap,
text,
)
# `if let` consumes an optional, which a `defer` cannot: a switch borrows it.
text = re.sub(
r"if let (\w+) = (\w+) \{ \1\.handle\.close\(\) \}",
r"switch \2 { case .some(let \1): \1.handle.close(); case .none: break }",
text,
)
sys.stdout.write(text)
for d in dropped:
print("unsupported:", d, file=sys.stderr)rewrite.pyimport re, sys
s = open(sys.argv[1]).read()
def sub(old, new, count=1):
global s
if old not in s:
sys.exit(f"missing: {old!r}")
s = s.replace(old, new, count)
# Handles called through `&mut self` are vars.
for v in ["c = Counter.new(\"c\", 5)", "copy = c.clone()", "registry = Registry.new(", "first = registry.firstGet",
"last = registry.last()!", "joined = name.join(", "message = Message.new(", "list = Node.new(",
"stack = Stack.new()", "retry = Retry.new(", "trail = Trail.new("]:
sub("let " + v, "var " + v)
# No stdlib protocol takes a noncopyable conformer before Swift 6.4: Debug and Display
# are members.
sub('print("debug \\(debug(c))")', 'print("debug \\(c.debugDescription)")')
sub('\\(debug(joined))', '\\(joined.debugDescription)')
sub('print("url \\(base) host', 'print("url \\(base.description) host')
sub('print("resolve \\(url) \\(url == base)")', 'print("resolve \\(url.description) \\(url == base)")')
sub('print("site \\(site.homeGet) \\(site.homeGet)")', 'print("site \\(site.homeGet.description) \\(site.homeGet.description)")')
# An array or tuple holds no noncopyable value in Swift 6.3: what returns or takes one is
# left out of the export.
sub(''' let (below, above) = Counter.new("s", 7).split(5)
print("split \\(below.name()) \\(below.get()) \\(above.name()) \\(above.get())")
''', '')
sub(''' // Each handle in a tuple moves in, as one taken by value does.
let board = Board.new([("low", Counter.new("l", 1)), ("high", Counter.new("h", 9))])
let ranked = board.ranked()
print("ranked \\(ranked.map { "\\($0.0) \\($0.1.name()) \\($0.1.get())" }.joined(separator: ", "))")
''', '')
sub(''' let counters = counters(["x", "y"])
print("counters \\(counters.map { "\\($0.name()) \\($0.get())" }.joined(separator: ", "))")
// Each counter moves in, so Swift closes it.
print("worth \\(worth([counters[0]], [counters[1]])) \\(worth(nil, []))")
''', '')
sub(''' // A JSON object's entries cross as pairs of a key and its document, each moved in.
let jsonEntries = entries([("b", try! Json.parse("[1]")), ("a", try! Json.parse("2"))], "t")
print("entries \\(jsonEntries.map { "\\($0.0) \\($0.1.toText())" }.joined(separator: ", "))")
''', '')
sub(''' let numbered = numbered(["p", "q"])
print("numbered \\(numbered.map { "\\($0.name()) \\($0.get())" }.joined(separator: ", "))")
''', '')
# An object a task awaits on: the task's capture keeps it alive, and nothing can drop it
# while the task borrows it, so the case is a compile-time guarantee, not a check.
sub(''' let other = Cell<Channel?>(Channel.new())
let sender = other.value!.clone()
let got = Cell<String?>(nil)
q.launch { got.value = text(try await other.value!.recv()) }
q.drain()
other.value = nil
sender.send''', ''' let got = Cell<String?>(nil)
let sender: Channel
do {
let other = Channel.new()
sender = other.clone()
q.launch { got.value = text(try await other.recv()) }
q.drain()
}
sender.send''')
# A registry an edit borrows mutably is a var.
s = s.replace('_ = Registry.new(Counter.new("c", 0)).edit {', 'var registry = Registry.new(Counter.new("c", 0))\n\t\t_ = registry.edit {')
open(sys.argv[2], "w").write(s)
# The ownership contracts no longer compile: each is its own file, which swiftc must refuse.
s = open(sys.argv[2]).read()
cases = {
"moved": ''' let c = Counter.new("c", 0)
_ = c.intoLabel(nil)
_ = c.get()''',
"boxed moved": ''' let tail = Node.new(1, nil)
_ = Node.new(2, tail)
_ = tail.sum()''',
"lent moved": ''' var registry = Registry.new(Counter.new("c", 0))
_ = registry.edit { counter in
_ = counter.intoLabel(nil)
return false
}''',
"lent kept": ''' let kept = Kept()
var registry = Registry.new(Counter.new("c", 0))
_ = registry.edit { counter in
kept.counter = counter
return false
}
_ = kept.counter!.get()''',
}
start = s.index('\tcase "moved":')
end = s.index('\t// An IPv6 address where Rust takes `SocketAddrV4`.')
s = s[:start] + s[end:]
s = s.replace('''/// Holds a lent counter past the call it was lent for.
final class Kept: @unchecked Sendable {
var counter: Counter?
}
''', '')
open(sys.argv[2], "w").write(s)
for name, body in cases.items():
extra = "final class Kept: @unchecked Sendable {\n\tvar counter: Counter?\n}\n\n" if name == "lent kept" else ""
open("case-" + name.replace(" ", "-") + ".swift", "w").write(f"import Api\n\n{extra}func broken() {{\n{body}\n}}\n")strict_hpp.py"""Rewrite the generated api.hpp into its stricter form for Clang: each object a
`consumable` whose moved-from state -Wconsumed tracks, a lent `&T` passed as `const T &`, a
lent `&mut T` as a borrow exposing only `->`, and a view or buffer's bytes `lifetimebound`.
usage: python3 -I strict_hpp.py api.hpp > strict.hpp
"""
import re
import sys
src = open(sys.argv[1]).read()
LB = "API_LIFETIMEBOUND"
prelude = f"""#if defined(__clang__)
#define {LB} [[clang::lifetimebound]]
#define API_CONSUMABLE [[clang::consumable(unconsumed)]]
#define API_CALLABLE [[clang::callable_when("unconsumed", "unknown")]]
#define API_CONSUMES [[clang::callable_when("unconsumed", "unknown"), clang::set_typestate(consumed)]]
#define API_UNCONSUMED [[clang::return_typestate(unconsumed)]]
#define API_MOVED_FROM [[clang::return_typestate(consumed)]]
#else
#define {LB}
#define API_CONSUMABLE
#define API_CALLABLE
#define API_CONSUMES
#define API_UNCONSUMED
#define API_MOVED_FROM
#endif
"""
def rep(old, new):
global src
if old not in src:
sys.exit(f"missing: {old!r}")
src = src.replace(old, new, 1)
rep("namespace api {", prelude + "\nnamespace api {")
# A view of a temporary dangles.
rep("view(const R &range) noexcept : view(range.data(), range.size()) {}",
f"view(const R &range {LB}) noexcept : view(range.data(), range.size()) {{}}")
# The bytes and text of a buffer live as long as it does.
for decl in ["const ::std::uint8_t *data() const noexcept {\n\t\treturn detail::bytes(raw_.ptr, raw_.len);",
"::std::string_view str() const noexcept {\n\t\treturn view(*this).str();"]:
head, _, rest = decl.partition(" noexcept {")
rep(decl, f"{head} noexcept {LB} {{{rest}")
rep("// Bytes lent to a call: text or binary", """// An object lent for a call as `&mut`: its methods through `->`, never a move out of it.
template <class T>
class borrow {
T &object_;
public:
explicit borrow(T &object) noexcept : object_(object) {}
borrow(const borrow &) = delete;
borrow &operator=(const borrow &) = delete;
T *operator->() const noexcept {
return &object_;
}
};
// Bytes lent to a call: text or binary""")
# Objects: a consumable whose `&&` methods and moves leave it consumed.
objects = re.findall(r"^class (\w+) \{\n\t::api::detail::(?:unique|shared)<", src, re.M)
for name in objects:
m = re.search(r"^class " + name + r" \{\n(.*?)^\};", src, re.M | re.S)
body = m.group(1)
shared = "detail::shared<" in body
lines = []
public = False
for line in body.split("\n"):
if line == "public:":
public = True
lines.append(line)
moves = [
f"\tAPI_UNCONSUMED {name}({name} &&other API_MOVED_FROM) noexcept = default;",
f"\t{name} &operator=({name} &&other) noexcept = default;",
]
if shared:
moves += [f"\tAPI_UNCONSUMED {name}(const {name} &other) = default;",
f"\t{name} &operator=(const {name} &other) = default;"]
lines += moves
continue
dm = re.match(r"^\t(?![\t}]|static |friend |explicit |///|template)(.+\)( const)?( &&)?;)$", line)
if public and dm and "operator" not in line:
attr = "API_CONSUMES" if line.endswith("&&;") else "API_CALLABLE"
lines.append(f"\t{attr} {dm.group(1)}")
continue
if public and line.startswith("\ttemplate <") is False and re.match(r"^\ttemplate <", line):
lines.append(line)
continue
lines.append(line)
src = src.replace(m.group(0), f"class API_CONSUMABLE {name} {{\n" + "\n".join(lines) + "};", 1)
# Templates (methods taking a lent closure) are callable only unconsumed too.
src = re.sub(r"^\ttemplate <(class F\d+(?:, class F\d+)*)>\n\t(?!static )(\S.*\);)$",
r"\ttemplate <\1>\n\tAPI_CALLABLE \2", src, flags=re.M)
# A lent `&T` is passed as `const T &`, and a lent `&mut T` as a borrow.
src = re.sub(r"(::api::detail::lent<(::api::\w+), const ::api_\w+> (l\d+_)\(p\d+_\);\n(.*?))\b\3\.get\(\)",
lambda m: m.group(1) + f"static_cast<const {m.group(2)} &>({m.group(3)}.get())", src, flags=re.S)
src = re.sub(r"(::api::detail::lent<(::api::\w+), ::api_\w+> (l\d+_)\(p\d+_\);\n(.*?))\b\3\.get\(\)",
lambda m: m.group(1) + f"::api::borrow<{m.group(2)}>({m.group(3)}.get())", src, flags=re.S)
sys.stdout.write(src)
print("objects:", len(objects), file=sys.stderr)rewrite_cpp.pyimport sys
s = open(sys.argv[1]).read()
def sub(old, new):
global s
if old not in s:
sys.exit(f"missing: {old!r}")
s = s.replace(old, new, 1)
# A closure lent `&mut` takes a borrow, calling through `->`.
sub(''' bool edited = registry.edit([&](api::Counter &counter) {
(void)counter.add(by);
return counter.edited() && counter.get() == 6;
});''', ''' bool edited = registry.edit([&](api::borrow<api::Counter> counter) {
(void)counter->add(by);
return counter->edited() && counter->get() == 6;
});''')
# The contracts Clang now refuses: each its own file, compiled with -Wconsumed -Werror.
cases = {}
for name, start, end in [
("moved", '\tif (std::strcmp(which, "moved") == 0) {\n', '\t} else if (std::strcmp(which, "moved from") == 0) {\n'),
("moved from", '\t} else if (std::strcmp(which, "moved from") == 0) {\n', '\t} else if (std::strcmp(which, "shared moved from") == 0) {\n'),
("shared moved from", '\t} else if (std::strcmp(which, "shared moved from") == 0) {\n', '\t} else if (std::strcmp(which, "shared moved out") == 0) {\n'),
("shared moved out", '\t} else if (std::strcmp(which, "shared moved out") == 0) {\n', '\t} else if (std::strcmp(which, "lent copied") == 0) {\n'),
("lent moved", '\t} else if (std::strcmp(which, "lent moved") == 0) {\n', '\t} else if (std::strcmp(which, "lent kept") == 0) {\n'),
("lent kept", '\t} else if (std::strcmp(which, "lent kept") == 0) {\n', '\t}\n\treturn 0;\n}\n'),
]:
a = s.index(start) + len(start)
b = s.index(end, a)
cases[name] = s[a:b]
s = s[:s.index(start)] + (start.replace("\t} else if", "\tif") if False else "") + s[b:]
# The first case removed leaves the chain starting at `} else if`: restore its head.
s = s.replace('int broken(const char *which) {\n\t} else if (std::strcmp(which, "lent copied") == 0) {', 'int broken(const char *which) {\n\tif (std::strcmp(which, "lent copied") == 0) {')
s = s.replace(''' } else if (std::strcmp(which, "null view") == 0) {
api::len(api::view(nullptr, 1));
}''', ''' } else if (std::strcmp(which, "null view") == 0) {
api::len(api::view(nullptr, 1));
}
return 0;
}''') if False else s
open(sys.argv[2], "w").write(s)
for name, body in cases.items():
open("cpp/case-" + name.replace(" ", "-") + ".cpp", "w").write(
"#include <optional>\n#include \"api.hpp\"\n\nvoid broken() {\n" + body.replace("\n\t\t", "\n\t").lstrip("\t").join(["\t", ""]) + "}\n")bench.swiftimport Api
@main
struct Bench {
static func main() {
let clock = ContinuousClock()
var sum: UInt64 = 0
let c = Counter.new("c", 1)
let calls = 20_000_000
var start = clock.now
for _ in 0..<calls { sum &+= c.get() }
let get = (clock.now - start) / calls
let objects = 2_000_000
start = clock.now
for i in 0..<objects {
let x = Counter.new("x", UInt64(i))
sum &+= x.get()
}
let made = (clock.now - start) / objects
print("get \(get) new+get+free \(made) (\(sum))")
}
}case-lent-moved.cpp (by hand)#include <optional>
#include "api.hpp"
void broken() {
api::Registry registry = api::Registry::new_(api::Counter::new_("c", 0));
registry.edit([](api::borrow<api::Counter> counter) {
api::Label label = std::move(counter)->into_label(std::nullopt);
return false;
});
}case-lent-kept.cpp (by hand)#include <optional>
#include "api.hpp"
void broken() {
static std::optional<api::Counter> kept;
api::Registry registry = api::Registry::new_(api::Counter::new_("c", 0));
registry.edit([](api::borrow<api::Counter> counter) {
kept.emplace(std::move(counter));
return false;
});
}case-lent-kept-raw.cpp (by hand)#include <optional>
#include "api.hpp"
// The borrow's pointer still reaches the object: moving through it compiles, and the
// lend's run-time check refuses it.
void broken() {
static std::optional<api::Counter> kept;
api::Registry registry = api::Registry::new_(api::Counter::new_("c", 0));
registry.edit([](api::borrow<api::Counter> counter) {
kept.emplace(std::move(*counter.operator->()));
return false;
});
}case-dangling-view.cpp (by hand)#include "api.hpp"
// A view of a temporary buffer, and the text of one: both dangle at the semicolon.
std::size_t broken() {
api::view name = api::Counter::new_("c", 0).name();
std::string_view text = api::Counter::new_("c", 0).name().str();
return name.size() + text.size();
}(Written by Opus 5.5 with medium) |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 49 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |

Finishes quest/m2/compile-checks.md: a report on which of rust2-ffi's Swift and C++ run-time checks become compile errors when the generators emit each language's own ownership forms, and what that costs callers.
Problem
AGENTS.md lets a binding check at run time only the Rust rules its language can't express. The Swift and C++ bindings check every move, lend, and use after close at run time. Both languages can express some of these checks, and this spike finds which.
Approach
I prototyped the stricter output for the
apifixture by rewriting the generatedapi.swiftandapi.hppwith scripts, posted in a comment below, rather than by changing the generators. I then rewroteffi/tests/api.swiftandapi.cppagainst the prototypes and compiled each test that breaks a contract on its own. Toolchains: Swift 6.3.3 and Clang 21.1.8 on x86_64 Linux, GCC 16.2 for portability, atmain(ddf231a1).Stricter forms prototyped:
Copybecomes a~Copyablestruct.selfby value becomesconsumingand&mut selfbecomesmutating. An object parameter becomesborrowing, orconsumingwhen the call moves it in. An object lent to a closure becomesborrowingfor&Tandinoutfor&mut T.-Wconsumed:consumable,callable_when,set_typestate(consumed)on its&&methods, andreturn_typestateon its move. A closure lent&mut Ttakesapi::borrow<T>, which reaches the object only through->.api::view's range constructor andapi::buf::{data, str}get[[clang::lifetimebound]]. All attributes sit behind macros, so GCC and MSVC see nothing.Each broken contract in the tests today
moved'c' used after consumeboxed moved'tail' used after consumelent movedmissing reinitialization of inout parameter 'counter' after consumelent keptimplicit conversion to 'Counter?' is consuming(storing a borrow)familySocketAddrV4is takenstray,lent strayfutures: an object dropped while a call awaits on itmoved,moved from-Wconsumederror:invalid invocation of method 'get' on object 'c' while it is in the 'consumed' stateshared moved from,shared moved out-Wconsumederror, same diagnostic forinto_levellent movedinto_labelis&&-qualified and the borrow only yields an lvaluelent keptapi::borrowneither moves nor converts.std::move(*counter.operator->())still compiles, so the run-time check stays as a backstoplent copied(a sharedGaugecopied out ofconst Gauge &)value,finished,null viewbuf-Wdanglingerror forapi::view v = c.name();andstd::string_view s = c.name().str();Totals: Swift turns 4 of its 7 broken-contract tests into compile errors, plus the futures liveness check. The other 3 aren't ownership. C++ turns 6 of its 10 into compile errors under
-Wconsumed -Werror, plus a dangling class that is undefined behavior today. Of the other 4, 3 are value checks and 1 copies a shared object.Every test that passes today still passes against the prototypes. The Swift transcript matches native Rust except for 6 lines from exports Swift 6.3 can't express (below).
futures,owned, and the remaining broken cases behave as before. The C++ transcript matches exactly, andfutures,lifetimes, and the remaining broken cases pass at C++17, 20, and 23. The whole C++ script builds warning-free under-Wconsumed -Wdangling -Werror, and under GCC.What the stricter forms cost callers
Swift
&mut selfmust be avar: 11 bindings in the script.Equatable,Hashable,CustomStringConvertible, orCustomDebugStringConvertible. Sodebug(c)becomesc.debugDescriptionand"\(url)"becomesurl.description.==still works as a static operator. SE-0499 lifts this in Swift 6.4.Arraynor a tuple holds a noncopyable value in Swift 6.3. That rules out 9 exports of the fixture:Board.new,Board.ranked,Counter.split,Token.twice,logCounts,counters,worth,entries, andnumbered. It's the largest cost. A tuple can become a generated named~Copyablestruct. An array needs a noncopyable container: swift-collections'UniqueArrayor a generated one.Refusal) stays a class, sinceSwift.ErrorrequiresCopyable.Option<&T>gets worse. Aborrowing Counter?parameter can't borrow an existingCounter, because wrapping it in an optional consumes it. Only a handle the caller already holds asCounter?can pass without moving. Keeping the shape would take an overload per optional parameter.T: ~Copyable, as the test'sCellanddebugwould.let: the capture borrows it and keeps it alive, but can't consume it. A borrowed orinoutlent handle can't escape at all, in Swift 5 language mode too.Native.viewandNative.Handle.withneed a~Copyableresult. Foundation'sData.withUnsafeBytesandString.withUTF8return only aCopyableone, so a handle leaves them through an optional.C++
&mut Ttakesapi::borrow<T>and calls through->: one lambda in the script.-Wconsumedis Clang-only, intraprocedural, and doesn't track members. It treats a reference parameter as unknown state, so methods arecallable_when("unconsumed", "unknown"). Otherwise every lent object in a caller's lambda warns. The cost of that choice: a move on only one branch goes unflagged.-Wconsumed -Wdangling. Nothing changes for one that doesn't, or for GCC or MSVC. MSVC's[[msvc::lifetimebound]]could map the same macro, untried.lifetime_capture_byhas no target in today's API. No export keeps a view or a reference past its call: a future owns a copy, and a closure is lent only for the call.Kotlin, Go, Python, Dart have no such form, as the quest planned.
Speed: the Swift handle's run-time hold count
With the strict forms, Swift's compiler rules out each case
Native.Handle's atomic hold count guards: a call after a move, a lend that outlives its call, and a drop during a call. A third variant ("lite") reducedHandleto its pointer and free function: no allocated state, no atomics. It passes the whole rewritten test. Median of 5 interleaved runs,-O, against the debug-built Rust library on a loaded host, so the shares would grow with a release library:get()callThe one hole is a data race on a non-Sendable class holding a handle, which Swift 5 language mode (moq's package's) doesn't diagnose. That race is already undefined behavior in Swift on today's class references, so the hold count doesn't make it defined.
Which of the parallel PRs' shapes benefit
&mutin/out, option A, becomes a Swiftinout. Swift then refuses overlapping access at compile time (bump(&c, &c):overlapping accesses to 'c'), with or without~Copyable.Option<&mut Counter>becomesinout Counter?, exclusive at compile time.Option<&Counter>gets worse in Swift, as above. In C++, its pointers get nothing from typestate.()and u128): nothing. Au128is a plain value with no ownership.Impact
None: the report scopes later changes to the generated Swift and C++. This PR only removes the quest.
Alternatives
Copyable.Copyablefor objects C++ treats as shared (Area,Gauge,Url), whose copy is indistinguishable from a clone, as C++'sdetail::shareddoes. Consuming one would then clone, as C++'sown()does, at the cost of a run-time count for those types. I left this out of the prototype for one uniform rule. It's worth weighing for types moq stores in collections.bugprone-use-after-movecatches what-Wconsumeddoes without annotations, but it isn't the compiler and wouldn't see a lent borrow.Decisions
Settled by the maintainer on 2026-11-09.
Copyor errors to non-copyable structs (~Copyable,consuming/borrowing), with tuples as generated named structs and arrays as swift-collections'UniqueArray(rejected: a generated list type of rust2's own; waiting for Swift'sArray; no switch).-Wconsumed), Clang lifetime attributes, andapi::borrow<T>for&mutlends, keeping the run-time lend check (rejected: attributes only; no change).Follow-ups
Two quests for
/quest-completeto plan once this PR is queued, each recommended to plan:~Copyablestructs withconsuming,borrowing,mutating, andinout; tuples as generated named structs; arrays asUniqueArray; drop the hold count after a release-build measurement. Fold in Swift 6.4 conformances under#if compiler(>=6.4)and a check on Apple platforms (back deployment ofOptional<~Copyable>metadata). Interaction with moq-dev/rust2#678: its&mutin/out becomesinout, exclusive at compile time, andOption<&mut T>becomesinout T?; itsOption<&T>needs an overload per optional parameter, since wrapping a borrowed handle in an optional consumes it. Whichever lands second adapts to the other.api::borrow<T>is the shape for&mutlends, so its&mutin/out should take or yield it; itsOption<&T>pointers get nothing from typestate and keep the run-time check.Review
Skipped at
2c5b9463f2b99f100b47db552cd13fb068238f98: the branch only deletesquest/m2/compile-checks.md; the report and decisions live in this body.Models
Size: M
Implementer: Claude Code, Opus 5.5, medium effort
Implementer: Claude Code, claude-opus-5-5, low effort (iterate, XS)
Checks:
just cipassed at this head. The prototypes were built and run by hand against the fixture as exported frommain. The repo's own test suite did not run: the branch changes only the quest tree.(Written by Opus 5.5 with medium)
🤖 Generated with Claude Code