Check lambda bodies in the safety checker - #41
Open
wtholliday wants to merge 1 commit into
Open
Conversation
`check_expr` had no `Expr::Lambda` arm, so lambdas fell into the catch-all
and their bodies were never visited: array bounds, divisor non-zero, and
`require` clauses at call sites were all skipped inside one. Since safety
here is entirely static, that meant nothing was checking at all — an
out-of-bounds store through a lambda parameter was silent on every backend.
Add `check_lambda_body`, which binds the parameters and checks the body:
* Param types come from the annotation when present, otherwise from the
solved `Type::Func` domain of the lambda expression, so `u32` params
still get `min = 0`.
* Params shadow captured variables of the same name; other captures keep
the constraints they have at the definition site.
* State is saved/restored around the body, then `invalidate_assigned`
runs — a lambda that writes a captured variable conservatively drops
what we knew about it, since the write happens at an unknown call time.
`Expr::Call` checks an immediately-invoked lambda literal against its
actual argument intervals (inheriting the arguments' symbolic `len`
bounds), so `(|i| arr[i])(2)` is still accepted while `(|i| arr[i])(1000)`
is rejected. A lambda that is stored, passed to a higher-order function,
or nested is checked with unconstrained parameters — the same rule as a
function body — so it has to guard itself.
Fixes #39
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #39.
SafetyChecker::check_exprhad noExpr::Lambdaarm, so lambdas fell into the catch-all and their bodies were never visited — array bounds, divisor non-zero, andrequireclauses at call sites were all skipped inside one. Since safety here is entirely static, nothing was checking at all: an out-of-bounds store through a lambda parameter was silent on every backend.What changed
New
check_lambda_bodyhelper binds the parameters and checks the body:Type::Funcdomain of the lambda expression (lambda params are usually unannotated), sou32params still getmin = 0.invalidate_assignedruns — a lambda that writes a captured variable conservatively drops what was known about it, since the write happens at an unknown call time.Expr::Callchecks an immediately-invoked lambda literal against its actual argument intervals (inheriting the arguments' symboliclenbounds), so(|i| arr[i])(2)is still accepted while(|i| arr[i])(1000)is rejected.A lambda that is stored in a variable, passed to a higher-order function, or nested is checked with unconstrained parameters — the same rule as a function body — so it has to guard itself:
Relating call-site arguments back to a lambda bound to a variable (
var g = (|i| arr[i]); g(2)) is deliberately not attempted: it needs the binding to be provably unreassigned and non-escaping, which is a design decision rather than part of closing the hole.Tests
All four repros from #39 now fail
--checkwith the same diagnostics the identical non-lambda code produces.safety_checker.rs(unconstrained/guarded index, write, divisor, capture, shadowing, direct call safe/unsafe, nested, capture invalidation,u32param).tests/cases/lambdas/— 6 rejection cases (read, write, divide,require, nested, HOF) and 2 acceptance cases.cargo test --workspacepasses: 369 lib + 4 golden (jit/vm/asm/stack) + 21 lsp. No existing test changed behavior.cargo fmt --checkis clean for the touched file.Unrelated bug found while writing the tests
A lambda that captures an array misbehaves on every non-JIT backend, independent of this change (the safety checker only emits diagnostics):
JIT prints
12; the VM panics atsrc/vm.rs:1283(attempt to subtract with overflow, fromself.ip - 1inside the out-of-bounds-access panic message); asm and stack exit 139. That is whylambda_guarded_ok.lyteis--check-only, with a comment saying so. Worth its own issue.🤖 Generated with Claude Code