Skip to content

verify: warn when recursive self-call return is discarded#580

Merged
danieljohnmorris merged 3 commits into
mainfrom
fix/verify-discarded-recursive-call
May 21, 2026
Merged

verify: warn when recursive self-call return is discarded#580
danieljohnmorris merged 3 commits into
mainfrom
fix/verify-discarded-recursive-call

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Adds ILO-T043, a verifier warning that fires when a recursive self-call sits at a non-tail position and its return value is silently discarded. Closes the diagnostic gap surfaced 2026-05-21 by the interp1d persona via #5ba investigation (PR #571): the persona wrote a braceless-guard recursive search where the recursive call was followed by a literal fallback, every call discarded the recursive return, and the function always returned the fallback. With no warning emitted, the persona then mis-diagnosed the bug as broken braceless guards.

T043 is narrowly scoped to recursive self-calls (callee name == caller name) with a non-nil declared return type. Bare non-recursive user-fn calls at non-tail position do NOT warn, because the callee may legitimately be side-effecting (logging, file I/O). The narrow surface keeps the false-positive risk near zero; we can broaden later if reruns surface other classes.

Manifesto framing: a missing signal here is paying for itself in agent confusion tokens (the persona spent an entire investigation chasing a phantom braceless-guard bug). Adding the signal once means every future agent who reaches for the same shape gets pointed at the real footgun immediately.

Repro before/after

Before:

find-idx xs:L n target:n i:n>n;>=i len xs (-1);=target at xs i i;find-idx xs target +i 1;-1

ilo check is silent. Function always returns -1 because the recursive call's return is dropped.

After:

warning[ILO-T043]: recursive call to 'find-idx' is at a non-tail position and its return value is discarded
  hint: a call is only in tail position when it's the last statement of the body, an arm of a tail match,
        or the body of a braceless guard. To use the recursive result, restructure as `?h cond {result}
        {fallback}` or `=cond {recursive-call}`. To return early, write `ret <call>`.

Fix shape (examples/recursive-tail-position.ilo):

find-idx xs:L n target:n i:n>n;>=i len xs (-1);=target at xs i i;find-idx xs target +i 1

Recursive call is now the body's last statement (tail position); braceless guards short-circuit above.

What's in the diff

Per-commit:

  1. verify: warn on non-tail recursive self-call return discard - new ILO-T043 check in verify_body, narrowly scoped to recursive self-calls with non-nil return.
  2. test: regression for ILO-T043 recursive discard - cross-engine regression at tests/regression_verify_discarded_recursive.rs (5 cases: warns on non-tail self-call, no-warn on tail, no-warn on non-recursive, no-warn on ret-wrapped, multiple warns).
  3. docs: ILO-T043 registry entry + example + SPEC sync - --explain ILO-T043 now resolves; SPEC.md tail-position section points at the new code; examples/recursive-tail-position.ilo pins the canonical fix shape across every engine via examples_engines.

Test plan

  • cargo test --release --features cranelift --test regression_verify_discarded_recursive (5/5 passing)
  • examples/recursive-tail-position.ilo main returns 3, missing returns -1 (manual run)
  • Full suite: cargo test --release --features cranelift green
  • CI green (main currently red due to PR feat(http): getx/pstx with status + headers + body Ok-map (#5bn) — fresh reimpl #566 token-budget + crypto-primitives panic; only checking my changes don't introduce new failures)

Follow-ups

  • Broaden to all user-fn discards if rerun data surfaces other false-positive classes.
  • Consider extending to detect =cond recursive-call shapes (assignment in a non-tail position with a recursive RHS) - currently Stmt::Let isn't covered.

A recursive self-call followed by another statement in the same body
silently discards its return value, since ilo only uses the tail
expression as the function's return. The interp1d persona hit this
shape (find-idx xs target +i 1; -1) and mis-diagnosed it as a broken
braceless guard because the verifier emitted no signal. Add ILO-T043
to close the gap.

Narrowly scoped to recursive self-calls (callee name == caller name)
with a non-nil declared return type. Bare non-recursive user-fn calls
at non-tail position do not warn; the callee may legitimately be
side-effecting. Broaden later if reruns surface other classes.
Five cases covering the new warning surface: warns on non-tail
recursive self-call (the persona's shape), no-warn on tail recursive
self-call, no-warn on non-recursive user-fn call at non-tail, no-warn
when the call is wrapped in ret, and two warnings emitted when two
non-tail recursive calls appear in a row.

examples/recursive-tail-position.ilo pins the canonical fix shape
under examples_engines so every engine asserts the same output. The
example doubles as the in-context example any future agent reads when
ILO-T043 fires; the hint points at this file.
Adds --explain ILO-T043 reachable entry with the canonical fix walk-
through (tail-position move, ret-wrap, ?h restructure). SPEC.md tail-
call rules section now points at the new warning. ai.txt regenerated
by build.rs picks up the SPEC change.
@danieljohnmorris danieljohnmorris merged commit 56227f0 into main May 21, 2026
1 of 4 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/verify-discarded-recursive-call branch May 21, 2026 20:28
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
7574 1 7573 0
View the top 1 failed test(s) by shortest run time
ilo::skill_md::body_is_thin_bootstrap
Stack Traces | 0.005s run time
thread 'body_is_thin_bootstrap' (65093) panicked at tests/skill_md.rs:254:5:
SKILL.md body is 10925 bytes; bootstrap shape should stay well under 8 KB
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

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