⚠️ ADVISORY CORRECTION — 2026-09-03, supersedes the advisory on the v0.34.0 releaseThe v0.34.0 advisory said blanket
deny <Effect>gates were not affected. Retract that. It was
true of the four defects then in hand, it was never a property of the engines, and it is falsified in
all four. In those first cases the caller vanished while the callee stayed independently reported,
so an unscoped deny caught the effect incidentally. That rescue is not a guarantee. In the findings
below, isolated reproductions have nothing anywhere in the report carrying the effect, and the blanket
gate exits 0.None of this was introduced by 0.34.0. Everything named predates that tag, so downgrading does not
help. All of it is fixed in 0.35.0 unless stated otherwise.In candor-swift, fixed in 0.35.0. A stored optional closure invoked through any unwrap binder was
absent from the report:if let,guard let,.map,switch case .some,if case let,??, over
both atypealiasand a plainly-spelled field, and for a function-typed parameter or local — fourteen
spellings in all, while the optional-chaining callcb?()was honest throughout. Found live in
Alamofire, where the public entry points of the closure-based request interceptors were affected.Also fixed here: a method call whose receiver was a module-scope
letorvar, multiple executable
targets' top-level code collapsing into one entry so a pure target inherited another's effects, a
factory-initialised public global, and a closure-typed field that resolved to whichever default the
scan happened to see. Still open: a closure reached through a subscript, an Optional-returning member,
or a tuple element is not yet treated as a source.What to do
- Upgrade to 0.35.0. None of these fixes are available any other way.
- Re-baseline. Do not diff future scans against a 0.34.0 report; re-scan on 0.35.0 and start there.
- Diff in both directions. An upgrade can also remove a charge that was never real, and the
baseline diff inAS-EFF-005flags gains only, by design. A disappearing row is not automatically
good news or bad; check losses separately if one matters to you.- Re-run your scoped gates specifically. This note exists because policies naming a function, a
path,callers,path,gainsortourwere blind in cases an unscopeddenystill caught.
Passing on 0.34.0 did not mean the rule was checking what you thought.This is a snapshot, not the register.
SOUNDNESS.mdin candor-spec is the register; read it for
anything not summarised here and for whatever closes after this note. The fix list is not a
completeness claim — the finding rate has not saturated.
[0.35.0] — 2026-09-03
AGENTS.mdnow enumerates the full eleven-effect vocabulary includingLlm, and the embedded
--agentscontract is regenerated from it (SOUNDNESS R155).
⚠ R178 — a stored optional closure invoked through an UNWRAP BINDER was ABSENT, in 14 spellings
A PUBLISHED cardinal sin, byte-identical on the v0.34.0 build and on this branch before the fix.
if let c = cb { c() } and cb.map { $0() } over a stored optional closure reported the INVOKING
function absent from functions[] — no effects, no Unknown, no row — while cb?() one line away
disclosed Unknown/unresolved: true/callback:computed. pure, deny Fs and deny Unknown over
Holder.fireIfLet all exited 0 with "policy rule matched NO function", which is this class's
signature: the caller was never judged at all. Ground truth EXECUTED.
Fourteen spellings, measured before anything was written, and they shared no branch — if let,
if let self., guard let, the shorthand if let cb, .map { $0() }, .map { f in f() },
switch case .some(let c), if case let .some(c), if case let c?, let c = cb ?? {}, over both a
typealias-spelled and a plainly-spelled field, plus a fn-typed optional parameter and local. What they
share is upstream: nothing in the collector could answer "the value being unwrapped is a FUNCTION". One
predicate answers it now and each binder position asks.
A second, wider defect sat under it. typeName returns (name: nil, isFunction: true) for a function
type, so typealias Cb = () -> Void was recorded in NO alias table and every callable spelled through it
read as a plain nominal type — an alias-typed FIELD, PARAMETER and LOCAL were each invisible as callables
while the plainly-spelled twin one line away disclosed. The Driver now completes the three indexes that
carry the isFunction flag with exactly what DeclCollector would have written had it known, so the two
spellings are byte-identical downstream rather than two paths that happen to agree.
REAL-CODE RECALL: Alamofire's Adapter.adapt (both overloads) and Retrier.retry — the public entry
points of its closure-based RequestInterceptors, whose bodies do nothing but invoke a caller-supplied
AdaptHandler/RetryHandler — were ABSENT on the published v0.34.0 build and now read
['Unknown'] dispatch:Adapter.adaptHandler. deny Unknown Adapter.adapt bound nothing before and is a
violation now.
A/B, WIDE KEY (every disclosure channel, not just inferred), 5 corpora, 15,861 analyzed units:
ADDED 3 · REMOVED 0 · CHANGED 62. Zero rows lost an effect; zero lost a call edge; 4 lost an unknownWhy
reason and every one is a ground-truthed owner CORRECTION (dispatch:HTTPServerProtocolUpgrader. shouldUpgrade → dispatch:NIOWebSocketServerUpgrader.shouldUpgrade, and the protocol does not declare
that member — the concrete type does). Branch reach was counted first with an instrumented binary:
swift-nio 36 hits, Alamofire 50, swift-argument-parser 5, swift-collections 0, swift-algorithms 0 —
those two arms are safety-only and their zero-diff says nothing either way.
The A/B caught two regressions the fix now carries the answer to: a callable field whose name is also a
METHOD on the same type is left alone (swift-nio's ClientBootstrap declares both a
channelInitializer property and a channelInitializer(_:) builder, and completing the field turned a
real method edge into a hedge), and a binder that re-binds a fn-typed parameter under its OWN name adds
nothing, because the existing callback-flow deferral answers it better.
The reason carries the owner: SPEC §4 ⟨0.7⟩ makes dispatch:owner.member the one normative detail, so a
field unwrap answers dispatch:Holder.cb and a parameter/local unwrap — which genuinely has no owner —
answers callback:c. And an unwrapped callable is not always opaque: let cb: Cb? = { … } has a visible
closure unit, so the binder spelling resolves it EXACTLY (['Fs'], no reason owed) through R96's single
authority, a var gets R96's union (the visible default AND Unknown), and only a field with no visible
unit hedges — the binder spelling is never less precise than obj.cb() for the same program.
R180 — ⟨0.35⟩ at a protocol dispatch site: NO CHANGE, and the reasoning is pinned by tests
Filed as "swift fails ⟨0.35⟩(b)'s third conjunct — Widget.fire is Unknown + unresolved: true with
no unknownWhy". The observation is exact; the conclusion does not follow, and emitting the reason
would have violated the spec rather than satisfied it.
SPEC §2 requires unknownWhy on a fn that introduces Unknown DIRECTLY, and requires it absent
when purely inherited. That row has direct: [] and calls: [ClosureTask.go, Repaint.go] — it
RESOLVED the dispatch, taking ⟨0.35⟩'s branch (a), to a conformer whose own effect happens to be
Unknown because that conformer invokes a stored closure field; ClosureTask.go carries
dispatch:ClosureTask.f. Claiming the inheriting caller as a source is the one thing blindspots exists
to prevent. path Widget.fire Unknown already answers Widget.fire → ClosureTask.go [Unknown source].
Instantiating the toggle the clause actually names — one conformer whose effect is VISIBLE, then one
unrelated pure conformer added and nothing else changed — the caller carries ['Fs'] in both arms. The
java/ts vanishing does not reproduce here. The engine's branch-(b) machinery is proven able to fire in
the same test file, on a genuinely incomplete candidate set (a requirement satisfied by an inherited
superclass method), where it emits ['Unknown'] unresolved:true dispatch:Task.go.
⚠ R73 — a module-scope global RECEIVER resolved to its own bare identifier, not to the method
Ships here; the published 0.34.0 advisory promised it and this is the entry it never got.
final class Worker { func doWork() { …writes a file… } } · let worker = Worker() ·
func invoke() { worker.doWork() } — five lines, no protocol, no closure. On v0.34.0 invoke was
ABSENT from functions[] and deny Fs invoke exited 0, policy ✓; here invoke reads ['Fs']
and the same rule exits 1. The call-graph sidecar showed the mechanism plainly: invoke → ["worker"],
worker → [], with Worker.doWork disconnected — the edge pointed at the global, which has no effects.
BOUNDED, and the bound is the honest part: an UNSCOPED deny Fs still exited 1 on v0.34.0, because
Worker.doWork is itself in the report. What this defeated was CALLER ATTRIBUTION — scoped and layered
policies, path, gains, tour and fix-gate, i.e. the reachability surface. Same class as R65/R66/R71.
⚠ R74 — top-level code is keyed by its MODULE, so one target no longer inherits another's effects
Ships here, found while fixing R73, and it is the opposite direction: a FABRICATION. A package with two
executable targets collapsed every target's top-level code into ONE functions[] entry keyed by the
literal qual "<main>", carrying the UNION of both.
Measured on a two-target fixture where each target's top-level code performs a DIFFERENT effect —
v0.34.0: one row, <main> ['Env', 'Fs'], located at the PURE target's file. Here: two rows, <main>
['Fs'] at the target that writes and <main>#1 ['Env'] at the target that reads the environment.
Say exactly what closed. On a fixture where one target is genuinely pure there is still only ONE
<main> row after the fix — not because the merge survived, but because a pure <main> carries no
effects and every engine omits pure functions; what moved is that the surviving row's loc is now the
target that performs the effect instead of the one that does not. WHAT DID NOT CLOSE: the suffix is
POSITIONAL, assigned alphabetically over the top-level modules, so <main>#1 is not a stable name for a
particular target — adding or renaming a target renumbers it, and a policy still cannot name one
target's top-level code by a meaningful identifier.
This also invalidated the swift-nio arm of R73's own A/B, which was re-run after this fix rather than
trusted.
⚠ R135 — flock is a raw syscall AND a struct, so building a lock record read Unknown
R130 added flock to the native: disclosure allowlist in the same commit whose "DELIBERATELY STILL
ABSENT" block excludes stat/statfs because they are also structs. struct flock is the fcntl
advisory-lock record, so var fl = flock() — a zero-argument construction that touches no fd and no path
— was charged Unknown + unknownWhy ["native:flock"], and deny Unknown describeLock exited 1 over
it. Never published; introduced and closed inside the same unreleased block.
GROUND TRUTH EXECUTED on both platforms — the fixture is an SPM package that was built and RUN
(describeLock() = 3 on macOS, = 1 under swift:6.1; F_WRLCK differs, the program does not).
The fix is ARITY, not deletion. Dropping the name would have traded the fabrication for silence on the
real flock(fd, LOCK_EX), which exists in shipped code (swift-tools-support-core
Sources/TSCBasic/Lock.swift:138,140,160). A C function that requires an argument cannot be what a bare
name() bound to, so a zero-argument call to an allowlisted name is suppressed — except for the names
whose C prototype really is nullary, which are exempted by name in NATIVE_DISCLOSURE_C_NULLARY_FNS.
!argLabelled, R130's other narrowing, could not have caught this: a struct construction carries no
labels either, so flock() and flock(fd, LOCK_EX) are identical on every field that branch reads except
the count.
THE AUDIT, because the trigger was one name. All 84 allowlist entries were probed with
func p(_ x: <name>) {} and _ = <name>() under swiftc -typecheck, on macOS and on Linux under
swift:6.1:
flockis the only name on the list that is also a TYPE — on both platforms (glibc declares
struct flockexactly as Darwin does), so the Linux half is measured rather than inferred.forkandvforkare the only nullary entries,pid_t fork(void)in both platforms'unistd.h.
A blanket "zero arguments ⇒ not a C call" would have madefork()silent — a cardinal sin swapped in
for a fabrication. Executed underswift:6.1: afork()+waitpidfixture really forks and reaps.
ptrace/setresuid/setresgidare declared on neither Swift overlay and were read from the headers
instead; that part is analysis, and is labelled as such in the source.- No allowlisted name resolves with only the Swift stdlib in scope (84/84 "cannot find in scope"), so
there is no stdlib-collision class; a project's own declaration of one of these spellings resolves
several arms earlier in the Driver and never reaches this branch. - CORRECTION to R130's own comment: its stated reason covers
statandstatfsand not
lstat/fstat, which are functions only. All four are now admissible under the arity gate — left out
deliberately, as a stated under-report awaiting its own over-charge control, because widening the
disclosure surface is a separate change from removing a fabrication.
A/B — 17 packages, 20,929 common rows, keyed on EVERY field (inferred, direct, incomplete,
declared, invisible, unknownWhy, unresolved, netClass, fs, paths, hosts, commands,
ambiguous): ADDED 0 · REMOVED 0 · CHANGED 0. Pre-image: a release binary built from 29f317a in a
separate worktree, proven pre-fix by reporting native:flock on the fixture the post binary reports
functions: 0 for.
BRANCH HITS (§E1), and this A/B is SAFETY-ONLY. A third, instrumented binary counted every arrival at
the changed branch: 83 arrivals, 83 PASS (argc > 0), 0 suppressions, 0 nullary rescues. The corpus
contains no instance of the shape, so the byte-identical result is an over-charge control and not
evidence the fix fires. What fires it is the executed fixture and the revert test. A recall hunt over the
17 packages plus swift-package-manager, swift-system, swift-corelibs-foundation and swift-nio-extras found
34 textual zero-argument calls to an allowlisted name and every one resolves to a local declaration
(swift-collections' own func remove(), swift-nio's FileDescriptor.opendir()); SwiftPM's apparent
flock(fd, LOCK_EX) sites are inside a generated-source STRING LITERAL, which candor reads correctly as
one unit — a near-miss, recorded rather than counted.
REVERT TEST, run twice rather than reasoned about. Reverting the gate turns three rows red. Replacing it
with the tempting blanket argc > 0 turns a different row red — spawnChild comes back with
["native:waitpid"] alone, fork() gone silent. A suite that only reverts the whole change cannot tell a
correct narrowing from an over-wide one, because both fix the bug in front of them.
⚠ R130b — the Foundation file routes that are neither FileManager nor raw C
The other half of "does the Fs rule cover only the ordinary spelling". Surveying 27 Swift-level routes
against the pre-fix binary, one executed fixture each, found a second family reading silent-pure —
absent from the report entirely, which under ⟨0.21⟩ is a positive purity claim:
| route | pre-R130b | why it matters |
|---|---|---|
NSString(contentsOfFile:), NSDictionary(contentsOfFile:), NSArray(contentsOfFile:) (and the contentsOf: forms) |
ABSENT | String(contentsOfFile:) beside them was charged. NSDictionary(contentsOfFile:) is how a decade of plist-reading Swift is written. |
NSData.write(toFile:) |
ABSENT | the bridged twin of Data.write(to:), charged since ⟨0.29⟩ |
URL.checkResourceIsReachable(), .checkPromisedItemIsReachable(), .resourceValues(forKeys:), .setResourceValues(_:), .bookmarkData(), .resolvingSymlinksInPath() |
ABSENT | URL had no κ entry at all, because nearly all of it is pure path algebra. checkResourceIsReachable() is a stat and the sibling of FileManager.fileExists, always Fs. |
OutputStream(toFileAtPath:), OutputStream(url:), InputStream(fileAtPath:), InputStream(url:) |
ABSENT | kappaFree is keyed on (name, argCount) and InputStream(fileAtPath:) / InputStream(data:) are both one argument — only the LABEL separates a file open from an in-memory stream, so the arm had to move beside chargeContentsCtor where the syntax node is in hand |
NSURLConnection.sendSynchronousRequest etc. |
ABSENT (Net) |
the pre-URLSession class, still all over legacy code |
Ground truth EXECUTED for the four Fs rows: NSDictionary(contentsOfFile:) read back one key from a
plist it had just written; checkResourceIsReachable() returned false then true across a file creation;
OutputStream(toFileAtPath:) left 8 bytes readable by an independent read; NSData.write(toFile:) left 7.
Pre-fix each was ABSENT and all five policy forms exited 0. The NSURLConnection row is ANALYSIS-ONLY and
is labelled so in its test.
The additions are verb-precise and each carries its own fabrication control: URL's path algebra
(appendingPathComponent/standardized/pathComponents), the in-memory streams
(OutputStream(toMemory:), InputStream(data:)), and NSURLConnection.canHandle(_:) must all stay
uncharged, and are pinned.
A/B, same 13 packages and same wide key: ADDED 1 · REMOVED 0 · CHANGED 14. All 15 audited against
source: 13 genuine recall gains (Kingfisher's disk cache ×8 through resourceValues/checkResourceIsReachable,
Alamofire's writeEncodedData gaining its write direction through OutputStream(url:append:),
swift-argument-parser's executeCommand + 2 callers through checkResourceIsReachable); 1 precision
gain that reads as a loss on the narrow key — Alamofire's MultipartFormData.append(_:withName:…) drops
Unknown + dispatch:URLConvertible.checkPromisedItemIsReachable and keeps Fs, and a grep of the whole
package confirms no conformer declares that member, so the CHA entry was noise now answered precisely;
1 over-charge riding a pre-existing defect — Kingfisher's MemoryStorage.Backend.isCached gains Fs
because its only recorded call edge is literally DiskStorage.Backend.value(String,ExpirationExtending),
a same-simple-name nested-type mis-resolution that predates R130 (it already carried that edge's Unknown).
Filed separately rather than folded in.
⚠ R130 — the raw-syscall disclosure was gated on import Darwin, and Foundation re-exports Darwin
R61 discloses Unknown + native:<name> for a raw C call on a curated allowlist (system, unlink,
symlink, chmod, dlopen, …), but only in a file whose imports named Darwin/Glibc/Musl/WinSDK.
That second condition was a silent under-report over the whole allowlist at once, because the imports
that put the C surface in scope are not the ones that name it: Foundation re-exports Darwin on Apple
platforms, and swift-corelibs-foundation re-exports Glibc on Linux (checked under swift:6.1 in Docker,
not assumed).
Two SPM packages differing in ONE line, both built and RUN, each creating a real symlink verified by
lstat + S_IFLNK in the program's own output, scanned over a BARE directory so no manifest exclusion
could mask the verdict:
| arm | report | deny Fs |
deny Unknown |
deny Fs Unknown |
deny Fs doWork |
pure doWork |
|---|---|---|---|---|---|---|
import Darwin |
Unknown, native:symlink |
0 | 1 | 1 | 0 | 0 |
import Foundation |
functions: 0 |
0 | 0 | 0 | 0 | 0 |
The Foundation arm carried no Unknown, no invisible, no incomplete — a positive purity claim over an
executed syscall. The gate is now the NAME allowlist alone, plus one narrowing that is a fact about the
language rather than a guess about intent: a C function imported into Swift has no argument labels, so
remove(at: i) cannot bind to libc's remove.
Half 2 — the siblings that list never asked about. R61's names were the ones its own repro used. The
*at twins of five names already on it (unlinkat/renameat/mkdirat/symlinkat/linkat/fchmodat/
fchownat), plus openat/chroot/chdir/mkfifo/mknod/creat/opendir/readdir/closedir/
flock/fsync/mmap/umask/waitpid/copyfile/sendfile/chflags/utimes, the execl* family and
the remaining set*id setters were silent under BOTH imports — one executed fixture per name.
execve/execvP/fexecve/posix_spawnp were in NEITHER table and are now concretely Exec beside
execv/execvp/posix_spawn, and establishing, so a runtime-built command through them can no
longer mask an allow Exec allowlist while the same command through posix_spawn fails closed.
stat/lstat/fstat/statfs are deliberately still absent and now say why in the table: var s = stat()
constructs the Foundation STRUCT, so that spelling appears in essentially every file that stats anything.
A/B over 13 real packages, 11,008 common rows, keyed on every field (inferred, direct, incomplete,
declared, invisible, unknownWhy, unresolved, netClass, fs, paths, hosts, commands,
ambiguous), pre-image a release binary built from the parent commit in a separate worktree and proven
pre-fix on the fixture: ADDED 2 · REMOVED 0 · CHANGED 39 (6 on inferred alone). All 41 audited
against source with no sampling: 40 correct — swift-nio's Syscalls.swift wrappers and its
dlsym(dlopen(nil, RTLD_NOW), …) under import Atomics, swift-nio-ssl's opendir/readdir/closedir
directory iterator, swift-tools-support-core's flock/unlink/waitpid, and 22 rows gaining
incomplete: ["Fs"] → ["Exec","Fs"] because Process.launch spawns a runtime command through
posix_spawnp. One false: swift-format's DebugOptions.set calls remove(element) on an OptionSet,
a bare one-argument call syntactically identical to a libc remove(path), and is now disclosed Unknown.
That residual is stated rather than narrowed away — suppressing it would trade a disclosure for silence on
the C call.
⚠ R125 — a higher-order function lost its OWN callback: disclosure as soon as anything called it
A function whose fn-typed parameter is invoked (func hof(_ body: (Int) -> Void) { body(1) }) discloses
Unknown / callback:body — unless something in the scan calls it. Then the ⟨0.34⟩ per-caller callback
flow wrote the Unknown to each caller and never to the callee, so the callee dropped out of the report
and deny Unknown <that function> exited 0 over a value it provably cannot address. Two byte-identical
methods, one called and one not, answered differently; the fixture was built and RUN, and the closure the
called one receives really does delete a file.
Present in the published 0.34.0 artifacts (introduced by the ⟨0.34⟩ per-caller fix, not by anything since).
Across five real packages the callee now keeps its own disclosure on 93 functions that previously
certified clean — Mutex.withLockUnchecked, Deque._Storage.update, _HTable.find_Large(tester:),
NIOAsyncTestingEventLoop.executeInContext, sockaddr_storage.withMutableSockAddr and the rest of the
with* family, plus _BTree.forEach in swift-collections.
The callee's copy is added only when not one caller resolved the deferral, so every caller already
carries the identical Unknown and nothing propagates anywhere new: A/B over 7,876 common rows is
ADDED 59 / REMOVED 0 / CHANGED 274 on every field, and every moved row is one of the 406 functions the
changed branch fired on. ⟨0.34⟩'s precision is untouched — a HOF whose callers all pass named functions
still carries nothing of its own, and no caller inherits another caller's target.
Known residual, pinned by a test rather than left to drift: when one caller resolves and another does
not, the callee is still left silent. Marking it would push Unknown into the caller that resolved
precisely, which is the fabrication ⟨0.34⟩ removed; closing it needs a per-caller node. That arm occurred
zero times in the five corpora (406 branch hits, 0 mixed).
R126 — RETRACTED: there is no nested-func/closure/property-receiver conjunction
A reported loss of the callback: disclosure "inside a closure inside a nested func whose receiver is an
instance property" was an artefact of R125 above. The four arms it rested on differed in two variables:
only the property-receiver arm was ever called, and having a caller was the entire effect. A 20-arm
sweep holding "has a caller" constant — instance let/var, static, computed, subscript, tuple element,
local copy, nested-func parameter, nested-type property, literal receivers; direct, one closure, two
closures, nested func, nested func in a closure, closure in two nested funcs, defer, a stored local
closure, and a by-reference pass to a sync invoker — is ABSENT for all twenty before the R125 fix and
discloses Unknown / callback:body for all twenty after it. No position-specific hole exists.
⚠ R124 — vars is not in ShadowSave, so every raw vars[…] = in a scoped visitor leaked
leaveShadowScope gives back seven name-keyed FLAG maps. The TYPE indexes come back through a
different channel, typeScopes, written only by scopeBindingType — and of the eight sites that write
vars, two used it. The other six wrote the map raw, so a binder's type outlived the construct that
bound it, for the rest of the function body.
That is wrong in BOTH directions, and both were measured with rename controls and with the fixture
EXECUTED:
- Silent under-report.
func f(_ fm: FileManager) { if let fm = o { }; fm.removeItem(…) }was
ABSENT from the report over a file deletion the program is observed to perform.deny Fs,deny Fs Unknown,deny Unknownandpureall exited 0 on it — silent, not coarse. Nine binder forms. - Fabrication. The mirror: an inner binder typed
FileManagershadowing an inert outer parameter
chargedFsto a call that provably deletes nothing.
Two of the nine were introduced since 0.34.0, by R96/R97/R98, and seven were pre-existing. Both new
ones came from this release's own work and one of them was asserted safe in a comment written by the
change that introduced it ("the write lands INSIDE the closure's own save … so it cannot leak past
the closure" — vars is not in that save):
visit(ClosureExprSyntax)— R97's annotated closure parameter ({ (fm: FileManager) in … }).visit(VariableDeclSyntax)— R98's rewrite to.skipChildrenhand-enumerates what to walk and
node.attributeswas not on the list, so a property-wrapper attribute ARGUMENT
(@Tagged(effTag()) var n) was never visited at all. A different mechanism reaching the same
silence.
The pre-existing seven: typeClosureParams' annotated and element arms, both of
typeEnumCaseBinding's arms, visitPost(OptionalBindingConditionSyntax), and the plain and
tuple binders of visit(VariableDeclSyntax). They were found by widening the audit past the two sites
it was handed.
Every site now routes through scopeBindingType (or, for typeClosureParams, registers against the
closure's own node id, because it runs before the closure's scope is open). A/B over five real Swift
corpora (swift-collections, swift-nio, swift-argument-parser, swift-algorithms, Alamofire), 7,876
common rows keyed on EVERY field: ADDED 0, REMOVED 0, CHANGED 16, inferred changes 0. Regression
suite BinderTypeScopeProcessTests, revert-tested site by site.
⚠ R96 — a reassignable (var) closure FIELD is resolved from whatever default the scan saw
README.md:44 promises: "A function-typed value invoked (let f: () -> Void param, a closure-typed
field d.f()) reads Unknown — never silent purity." The engine kept that only for a field with no
visible closure initializer. A public var f: () -> Void = { }, reassigned by anyone holding the
object, was resolved to the empty default and certified PURE — deny Unknown <that unit>, README's own
named strictness knob, exited 0 over a file deletion proved by running the program, and so did blanket
deny Unknown. The discriminator was "is there a closure literal here?"; var versus let was never
consulted.
The fix consults REASSIGNABILITY. A let stored property with an initializer cannot be assigned
anywhere in Swift, so it stays exactly resolved — no hedge, no flood. A var gets the UNION: the
visible default's effects (still genuinely reachable) AND the §4 Unknown, named as
dispatch:<Type>.<field>. Replacing rather than unioning would have traded this silent under-report
for a lost deny <E> on an effectful default. pure <scope> still passes on Unknown — that is
README.md:47 / AS-EFF-003, and unverified --strict now names the scope and prints the upgrade.
Four call sites (f(), obj.f(), map(f), map(obj.f)) each spelled this rule themselves; they now
share one authority, closurePropertyInvocation.
⚠ R97 — 22 of 23 binder arms dropped the type; one dealias call was doing all the work
typealias FM = FileManager; let fm = FM.default; try fm.removeItem(…) was absent from the report
while the plain-spelled twin one line away was charged Fs. Of the 23 sites that bind a name to a
type, exactly one called dealias. Eighteen binder spellings were silent — parameter annotation,
annotated local, inferred local, for…in annotation, if let, guard let, as!, as?, return type,
stored property, module-scope global, alias-of-alias, module-qualified alias — plus three siblings that
were broken with no alias involved at all: switch case let x as T, if case let x as T =, an
explicitly annotated closure parameter outside call-argument position, and for x in [Host.singleton]
over an inline array literal. All exit 0 on every policy form over a program whose only purpose is
deleting a file.
Fixed by three authorities rather than 22 more calls: rootOf dealiases its ANSWER once (so vars,
fields, globalTypes, returns and tupleElem are alias-transparent by construction, including
producers not yet written, and a module qualifier is stripped there too); typeCastBinder is the one
routine that types a case let x as T binder for all three grammars that spell it; and
leaveShadowScope performs the type restore for every scope, so a binder can be typed without each
statement kind owning a save.
A seventh spelling was found by attacking this change's OWN "alias-transparent by construction" comment
rather than by a fixture: the callAsFunction arm reads vars[name] directly instead of through
rootOf, so typealias C = Caller; func b(_ c: C) { c() } was silent while the Caller-spelled twin
charged. Fixed, and the comment now says what it covers and what it does not.
RESIDUAL, measured and executed: let c: (FileManager) -> Void = { fm in … } — an UNANNOTATED closure
parameter typed only by the closure variable's annotation — is still silent, and is pinned as such.
⚠ R98 — a binder that shadows its receiver's name loses the effect in its own initializer
rust R92's ordering bug, one language over. The initializer or sequence expression is a CHILD of the
binder's syntax node, so a visitor that cleared or rebound the name and then descended resolved that
expression against a receiver it had already destroyed. Holding everything constant but the binder's
name: if let w = w.kill() ABSENT vs if let q = w.kill() ['Fs'], and the same pair for
guard let, while let, for…in, plain let and if case let w?.
One rule in three places: the expression that produces the value is walked BEFORE the name it binds is
touched. OptionalBindingConditionSyntax and MatchingPatternConditionSyntax defer the binding to
visitPost; ForStmtSyntax and VariableDeclSyntax walk the sequence/initializers explicitly and then
skip children. catch let and switch case let were ALREADY correct — their producing expression is
not a child of the binder's node — and are pinned so the list is closed.
-
R79 (SOUNDNESS.md), partial — the false "nothing hidden" clean bill over a ≥1-Unknown graph.
candor: nothing hidden — every effect sits where its name says it should.is an ABSOLUTE claim, and
the scan-note (emitSurface) andtourshared one formula — "is at least ⅓ of the graph Unknown" —
that licensed the unqualified sentence over any report whose Unknown count fell below that line,
however small a fraction of the codebase it was. A handful of unresolved-dispatch callers in an
otherwise large, mostly-resolved real package never crossed ⅓, so the unqualified claim kept printing
over a report that demonstrably had something hidden.unknown == 0is now the ONLY gate for the
unqualified sentence; any nonzero count gets a qualified one (a NEW, lighter tier below the existing
⅓ line, alongside the pre-existing dense-graph one). Both callers now share oneunknownDensity
function (CandorCore/Surface.swift) rather than two independent copies of the same formula.
PARTIAL: this closes the false-disclosure half of R79. The silent under-report half — a caller
through a cross-module/cross-package global receiver vanishing fromfunctions[]entirely — remains
OPEN; see SOUNDNESS.md R79 for the measured reason a disclosure-only fix at that exact call site was
attempted and reverted (it broke 8 unrelated, pre-existing tests spanning keypath charging, fn-ref
shadowing, singleton-field typing, accessor joins, global initializers and protocol-shadow
resolution — the terminal member-access fallback is shared far more widely than R79's own shape).
A/B over swift-collections/algorithms/argument-parser/nio (fresh clones): byte-identical
functions[]before/after (this change touches only stderr/stdout prose, never report content). -
⚠ R85 (SOUNDNESS.md) — a FACTORY-initialized or DESTRUCTURED public global drops the caller
silently, the same cardinal sin R79 closed for a direct constructor call, one binder shape over.
public let sharedWorker = makeWorker()(a factory call rather than= Worker()) and
public let (a, b) = (Worker(), 42)(tuple-destructured) both left the CALLER absent from
functions[]entirely — not merelyUnknown, absent — while the callee itself stayed correctly
classified and the engine's unqualified "nothing hidden" clean bill printed over the gap regardless.
Ground truth EXECUTED: changing one line from= Worker()to= makeWorker(), everything else held
constant, took the report from<main>/runcarryingFsto onlyWorker.doWork— both callers
gone.deny Fs runanddeny Fs <main>both went exit 0 over provable file I/O;deny Unknownalso
went exit 0 (no disclosure either — a true silent gap, not a coarser answer).ROOT CAUSE, two independent binder shapes, one question:
Driver.swiftpopulated
publicGlobalTypesByModuleonly from the per-file merge ofglobalTypes/globalPublic; a factory
global's TYPE resolves in a separate, LATER pass (once the project-widereturnsindex exists) that
wrote only into the module-localglobalTypesByModuleand never revisited the cross-module-visible
table. Separately,DeclCollectorran its type-inference logic ONLY for the plain-identifier binder
shape — a tuple-destructure pattern's elements never enteredglobalTypes/globalPublicat all, for
any module, so a destructured global was untyped full stop, not merely invisible cross-module.FIX: both binder shapes now feed the SAME authority the direct-constructor shape always used, rather
than gaining a third write site that could drift the same way again.DeclCollector.inferGlobalType
is one function shared by the plain-identifier binder (the whole initializer) and each element of a
tuple-destructure binder (its own positional sub-expression), covering explicit annotation / ctor
call / factory call / singleton access identically for both, and markingglobalPublicon every
branch including the deferred factory one (R79 left that branch out ofglobalPublicentirely, which
was the other half of the gap — a factory global's public/open FACT is known at parse time,
independent of when its TYPE resolves). InDriver.swift,publicGlobalTypesByModuleis no longer
written at per-file-merge time; it is DERIVED once, after every pass that can populate the underlying
globalTypesByModule(the merge AND the later factory-resolution pass) has run, from that table plus
a newglobalPublicByModulename set. Two upstream tables, one filter, computed once — not two
separate write sites that can answer a different question about the same name.CONTROLS, all executed: the two sin fixtures (factory, destructured) both flip caller ABSENT ->
carries the callee's realFs. A genuinely PURE factory/destructured cross-module global gains
NOTHING (over-charge control). A non-publicfactory/destructured global still does NOT resolve
(access control — resolving it would be fabrication dressed as a fix). R79's own direct-constructor
case and its three original controls still pass unchanged. Revert test: stashing the fix turns
exactly the two fix-pinning tests red (FactoryAndDestructuredGlobalReceiverProcessTests); the four
control tests in that file pass with or without the fix — not vacuous.ALSO FIXED, same round: R79's own fourth control,
testAmbiguousCrossModuleGlobalNameResolvesNothing
(two imported modules both declaringpublic let sharedWorker), asserted behaviour for a program
shape that does not compile —swift buildon that exact fixture fails witherror: ambiguous use of 'sharedWorker', unconditionally, in every language mode tried. No reachable Swift program can trigger
themoduleCount > 1exclusion branch it existed to pin, so it was evidence of nothing. Replaced with
testInternalGlobalInOneModuleDoesNotPoisonAPublicSameNameInAnother: a REACHABLE, executed sibling —
one imported module declares the colliding nameinternal, the otherpublic— that genuinely
compiles and runs, and exercises the real candidate-counting logic (an internal declaration must never
enterpublicGlobalTypesByModuleand must not inflate the ambiguity count against the real public
candidate).1006/1006 tests (1000 + 6 new),
smoke.sh148/148,fuzz.py25/25,fabrication_probe.py28/28,
soundness/realworld/recall/recall.sh5/5,ci/self-gate.shOK;soundness/realworld/run.shand
disclosure_recall.shSELFSKIP on Darwin (Linux+strace only), reported UNRUN not green.