# MSR round 2026-07-22-r1 — findings

8/8 tasks: MSR 100% (`scores/2026-07-22-r1.json`). All initial implementations
passed and all survived their modification. Two modification stages each hit
a real, reproducible compiler bug along the way (worked around, not just
spec difficulty) — these are the round's actual signal, since a 100% MSR
number alone hides how much friction there was getting there.

## Major findings (issue-worthy)

### 1. `let* <bind> = <Option-expr>` followed by an `if`/`else` as the block's tail expression traps at runtime

Found while implementing `intermediate/02_expr_eval`'s modification (`eval`
returning `Option[Int]`, using the `let*` Option-railway sugar documented in
`docs/cheatsheet.md`). The agent's first (idiomatic, spec-matching) attempt
compiled cleanly but trapped with `RuntimeError: unreachable` at runtime,
even in the smallest possible repro — no enums, no nesting:

```vibe
fn half(x: Int) -> Option[Int] {
  if x % 2 == 0 { Some(x / 2) } else { None }
}

fn f(x: Int) -> Option[Int] {
  let* rv = half(x)
  if rv == 0 { None } else { Some(rv) }
}

test "letstar tail if" {
  assert(f(4) == Some(2))
}
```

Independently re-confirmed (2026-07-22, committed seed): `FAIL ... failing
test: letstar ... trap: RuntimeError: unreachable`. Wrapping the `if/else`
in an intermediate `let` did not help; only moving the `if/else` into a
separate helper function (so `let*`'s own tail is a plain call) avoided it.
Filed as issue #1064.

**Impact**: `let*` is presented in `docs/cheatsheet.md` as the idiomatic
Option/Result railway; this shape (bind, then branch on the bound value) is
about as common as `let*` usage gets. Any solution using it this way silently
compiles to a trapping program — no diagnostic, just a runtime crash.

### 2. `Array::find` (closure-predicate higher-order call) traps at runtime; likely the same class of bug as a closure+RC heap-corruption found in `advanced/02_package_module`

Independently reproduced (2026-07-22, committed seed):

```vibe
test "array find" {
  let xs = [1, 2, 3]
  let r = Array::find(xs, (x) -> x == 2)
  assert(r == Some(2))
}
```

`FAIL ... trap: RuntimeError: unreachable`. Separately, `Array::contains`
does not exist at all (compile error `undefined variable (local):
Array::contains @call`) — a plain stdlib gap, not a crash, lower priority
but worth fixing/documenting (`docs/cheatsheet.md`'s Array section doesn't
list what IS available, so this had to be discovered by trial and error).

The `advanced/02_package_module` implementing agent independently found a
related, more severe issue while writing `store.vibe`'s modification (the
internal-representation change): a test file containing **two different
closures**, each capturing a `String` parameter and doing tuple/struct field
access, defined in **different functions**, exercised across multiple
`test { }` blocks in one file, reliably **corrupts the heap**
(`RuntimeError: memory access out of bounds` at `__rt_rc_alloc`, on a later,
unrelated `set` call) — minimized down to a 2-test/2-closure case by that
agent, but not preserved as a standalone fixture (the agent worked around it
by rewriting `store.vibe` with zero closures — see
`eval/msr/attempts/2026-07-22-r1/advanced_02_package_module/modified/store.vibe`
for the closure-free version, and the closure-based version would need to be
reconstructed from the description above to get a minimal fixture). Given
this project's own history of subtle RC/closure-capture double-free and
heap-corruption bugs (#706–#720 in the issue tracker), and that `Array::find`
independently traps with a similarly shaped closure-over-captured-param
call, these may share a root cause worth investigating with the
`selfhost-miscompile-bisect` methodology. Filed as issue #1065.

**Impact**: closures capturing a parameter, combined with HOF-style Array
calls, are an extremely common vibe idiom (used throughout this repo's own
stdlib and compiler). A silent heap-corruption bug here is more dangerous
than a plain trap — the agent's report explicitly notes it surfaced later,
at an unrelated call site, which matches this project's own documented
experience that these bugs "never corrupt... at worst leak" is NOT actually
holding in this case; this is real corruption, not a benign leak.

## Minor findings (documentation/UX friction, not filed as issues)

- **`basic/01_string_utils`**: `String::is_empty` (defined as an `export fn`
  in `lib/@vibe/prelude/string.vibe`) is not directly callable unqualified
  the way `String::length`/`String::split`/`String::concat` are — calling it
  gave `unknown name: String::is_empty` with a misleading source location
  (pointed at line 1, not the actual call site). Worked around with
  `String::length(s) == 0`. `docs/cheatsheet.md` doesn't distinguish "true
  compiler builtins" from "prelude functions that need an explicit import"
  for the String API surface, so this has to be discovered by trial and
  error each time.
- **`intermediate/01_shopping_cart`**: dot-call syntax (`it.qty.to_double()`)
  does not work for the builtin `Int::to_double` — must use the
  function-call form `Int::to_double(it.qty)`. Not documented as a
  restriction anywhere obvious; only surfaced as a compile error.
- **`advanced/01_generic_stack`**: explicit type-argument syntax
  (`Stack::new[Int]()`, analogous to `Box[Int]::{...}` for generic structs)
  does NOT work for generic **functions** — gives `unknown name: Int`. Had
  to rely entirely on type inference (annotate the *binding*, e.g. `let s0:
  Stack[Int] = Stack::new()`, rather than the call). `docs/cheatsheet.md`
  documents explicit struct type-args but not this generic-function
  distinction.
- **`advanced/02_package_module`**: `Store::new()` (an associated function
  on a struct with no `self`-like receiver) needed the `fn Store::new()`
  declaration form on the first attempt's guess (a bare `fn new()` inside no
  enclosing scope failed) — matches the `TypeName::method` convention used
  elsewhere in the codebase (e.g. `lib/@vibex/deque`) once found, but
  wasn't obvious a priori from `docs/cheatsheet.md` alone.

## Cross-cutting observation

Every "major" friction point in this round was a **runtime trap or
corruption from valid-looking, spec-compliant code**, not a rejected
ill-typed program — i.e., these are exactly the failure mode
`eval/lang-review`'s `type_soundness` dimension is scored on ("ill-typed
programs silently miscompile / garbage bindings"), except here the programs
are *well-typed* and the unsoundness is at the RC/codegen layer, not the
type checker. Worth flagging to whoever owns the type-soundness dimension
score for the next `eval/lang-review` round.
