Skip to content

build a result unwrap_or's fallback lazily#592

Merged
kacy merged 1 commit into
mainfrom
result-unwrap-or-fallback-leak
Jul 26, 2026
Merged

build a result unwrap_or's fallback lazily#592
kacy merged 1 commit into
mainfrom
result-unwrap-or-fallback-leak

Conversation

@kacy

@kacy kacy commented Jul 26, 2026

Copy link
Copy Markdown
Owner

summary

result unwrap_or evaluated its fallback before branching on ok/err, so on the
ok path the fallback was built and then discarded — a leak of one object per
call for any heap default. r.unwrap_or(bytes.empty()) is the everyday shape,
and a loop over an ok result grew ~64 bytes an iteration without bound.

the optional unwrap_or already builds its default inside the none arm; the
result path now matches. on the ok path the fallback expression is never
evaluated, which is leak-free and the semantics a lazy-looking default implies.

measured

a 200k-iteration loop through make(i).unwrap_or(bytes.empty()).len(), ok
every time: VmHWM ~21 mb before, ~2.5 mb after (flat).

what was tested

make bootstrap-verify green, make memcheck clean under valgrind including
the new case. tests/cases/test_result_unwrap_or_fallback_leak drives the ok
path 200k times and asserts the footprint stays flat; it is in MEMCHECK_CASES.

notes

the test uses an inline (owned) operand deliberately. unwrap_or on a borrowed
operand — a named result local, or a T! parameter — has a separate,
pre-existing ownership bug: it shell-releases an operand whose owner also
releases it, a use-after-free that PITH_STRUCT_FREELIST=0 valgrind exposes.
that is a distinct fix (it is shared with ! and catch on a borrowed operand)
and is being handled on its own; this PR isolates the fallback leak so the two
don't tangle.

result `unwrap_or` evaluated its fallback before branching on ok/err. on the ok
path the fallback was never used, so a heap default — `r.unwrap_or(bytes.empty())`
is the everyday shape — was allocated and dropped every call, leaking one
object per iteration. a loop over an ok result grew ~64 bytes an iteration
without end.

the optional `unwrap_or` already builds its default inside the none arm; the
result path now does the same. on the ok path the fallback expression is never
evaluated, which is both leak-free and the semantics you'd expect from a
lazy-looking default.

tests/cases/test_result_unwrap_or_fallback_leak drives 200k ok iterations
through a heap-fallback unwrap_or and checks the footprint stays flat. it uses
an inline (owned) operand on purpose: `unwrap_or` on a borrowed operand — a
named result local, or a `T!` param — has a separate, pre-existing ownership
bug (it shell-releases an operand its owner also releases) that is being fixed
on its own; this test isolates the fallback.
@kacy
kacy merged commit f600e93 into main Jul 26, 2026
2 checks passed
@kacy
kacy deleted the result-unwrap-or-fallback-leak branch July 26, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant