Skip to content

Commit acc4217

Browse files
committed
Drive local effects from resolved HIR
1 parent 841b77a commit acc4217

4 files changed

Lines changed: 244 additions & 60 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ It currently implements:
2525
- Per-function HIR body views that group bindings, calls, fields, effects, and returns
2626
- AST-driven mode and call checks for local-only features, named arguments, data effects, and retaining APIs
2727
- AST-driven body checks for local moves, early-exit-aware `fresh` returns, resource escape, resolved handle-field `take`, and managed closure captures
28+
- Local ownership effect application now indexes binding types and move/retain events from resolved HIR body trees
2829
- Focused check modules for mode, calls, body semantics, and forbidden operator behavior
2930
- Fixture-based pass/fail scenario tests under `tests/fixtures`
3031

@@ -61,7 +62,7 @@ Non-goals for this stage:
6162

6263
Near-term roadmap:
6364

64-
1. Start moving body checkers from syntax AST plus lookup tables onto the resolved HIR statement/expression tree.
65+
1. Continue moving body checkers from syntax AST plus lookup tables onto the resolved HIR statement/expression tree.
6566
2. Extend CleanLocal dataflow from structured statements into a dedicated CFG representation.
6667
3. Expand `rss review` local/manage boundary diffs from summaries into path-aware risk explanations.
6768

src/checks/body.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ fn apply_stmt_effects(
192192
}
193193
}
194194
if let Some(value) = &stmt.value {
195-
if let Some(type_name) = hir_binding_type(analyzer, &stmt.span)
195+
if let Some(type_name) = local_analysis
196+
.binding_type(&stmt.span)
197+
.map(str::to_string)
196198
.or_else(|| infer_expr_type(analyzer, value, state))
197199
{
198200
state.record_type(stmt.name.clone(), type_name);
@@ -216,13 +218,6 @@ fn apply_stmt_effects(
216218
}
217219
}
218220

219-
fn hir_binding_type(analyzer: &Analyzer<'_>, span: &crate::diagnostic::Span) -> Option<String> {
220-
analyzer
221-
.hir
222-
.binding(span)
223-
.and_then(|binding| binding.type_name.clone())
224-
}
225-
226221
fn apply_expr_effects(local_analysis: &LocalAnalysis, expr: &Expr, state: &mut BodyState) {
227222
match expr {
228223
Expr::Manage { value, span } => {

src/checks/local.rs

Lines changed: 219 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::collections::{HashMap, HashSet};
22

33
use crate::diagnostic::Span;
4-
use crate::hir::{HirBinding, HirBindingKind, HirEffectEvent, HirEffectEventKind, HirFunctionBody};
4+
use crate::hir::{
5+
HirBinding, HirBindingKind, HirBlock, HirEffectEvent, HirEffectEventKind, HirExpr,
6+
HirFunctionBody, HirStmt,
7+
};
58

69
use super::body::Flow;
710

@@ -16,12 +19,26 @@ pub(crate) struct BodyState {
1619

1720
pub(crate) struct LocalAnalysis {
1821
body: Option<HirFunctionBody>,
22+
events_by_span: HashMap<Span, Vec<HirEffectEvent>>,
23+
binding_types_by_span: HashMap<Span, String>,
1924
}
2025

2126
impl LocalAnalysis {
2227
pub(crate) fn new(body: Option<&HirFunctionBody>) -> Self {
28+
let body = body.cloned();
29+
let events_by_span = body
30+
.as_ref()
31+
.and_then(|body| body.block.as_ref())
32+
.map_or_else(HashMap::new, index_events_from_block);
33+
let binding_types_by_span = body
34+
.as_ref()
35+
.and_then(|body| body.block.as_ref())
36+
.map_or_else(HashMap::new, index_binding_types_from_block);
37+
2338
Self {
24-
body: body.cloned(),
39+
body,
40+
events_by_span,
41+
binding_types_by_span,
2542
}
2643
}
2744

@@ -41,21 +58,161 @@ impl LocalAnalysis {
4158
state.apply_retention_events(self.effect_events(span));
4259
}
4360

61+
pub(crate) fn binding_type(&self, span: &Span) -> Option<&str> {
62+
self.binding_types_by_span.get(span).map(String::as_str)
63+
}
64+
4465
fn effect_events(&self, span: &Span) -> &[HirEffectEvent] {
45-
self.body
46-
.as_ref()
47-
.and_then(|body| events_at_span(&body.effect_events, span))
48-
.unwrap_or(&[])
66+
self.events_by_span.get(span).map_or(&[], Vec::as_slice)
4967
}
5068
}
5169

52-
fn events_at_span<'a>(events: &'a [HirEffectEvent], span: &Span) -> Option<&'a [HirEffectEvent]> {
53-
let start = events.iter().position(|event| event.span == *span)?;
54-
let end = events[start..]
55-
.iter()
56-
.position(|event| event.span != *span)
57-
.map_or(events.len(), |offset| start + offset);
58-
Some(&events[start..end])
70+
fn index_events_from_block(block: &HirBlock) -> HashMap<Span, Vec<HirEffectEvent>> {
71+
let mut events = HashMap::new();
72+
collect_block_events(block, &mut events);
73+
events
74+
}
75+
76+
fn collect_block_events(block: &HirBlock, events: &mut HashMap<Span, Vec<HirEffectEvent>>) {
77+
for statement in &block.statements {
78+
collect_stmt_events(statement, events);
79+
}
80+
}
81+
82+
fn collect_stmt_events(statement: &HirStmt, events: &mut HashMap<Span, Vec<HirEffectEvent>>) {
83+
match statement {
84+
HirStmt::Let {
85+
value: Some(value), ..
86+
}
87+
| HirStmt::Return {
88+
value: Some(value), ..
89+
}
90+
| HirStmt::Expr(value) => collect_expr_events(value, events),
91+
HirStmt::With { resource, body, .. } => {
92+
collect_expr_events(resource, events);
93+
collect_block_events(body, events);
94+
}
95+
HirStmt::If {
96+
condition,
97+
then_body,
98+
else_body,
99+
..
100+
} => {
101+
collect_expr_events(condition, events);
102+
collect_block_events(then_body, events);
103+
if let Some(else_body) = else_body {
104+
collect_block_events(else_body, events);
105+
}
106+
}
107+
HirStmt::Loop {
108+
condition, body, ..
109+
} => {
110+
if let Some(condition) = condition {
111+
collect_expr_events(condition, events);
112+
}
113+
collect_block_events(body, events);
114+
}
115+
HirStmt::Let { value: None, .. }
116+
| HirStmt::Return { value: None, .. }
117+
| HirStmt::Break(_)
118+
| HirStmt::Continue(_)
119+
| HirStmt::Unknown(_) => {}
120+
}
121+
}
122+
123+
fn collect_expr_events(expr: &HirExpr, events: &mut HashMap<Span, Vec<HirEffectEvent>>) {
124+
match expr {
125+
HirExpr::Call {
126+
args,
127+
events: expr_events,
128+
span,
129+
..
130+
} => {
131+
record_expr_events(events, span, expr_events);
132+
for arg in args {
133+
collect_expr_events(&arg.value, events);
134+
}
135+
}
136+
HirExpr::Effect {
137+
value,
138+
events: expr_events,
139+
span,
140+
..
141+
}
142+
| HirExpr::Manage {
143+
value,
144+
events: expr_events,
145+
span,
146+
..
147+
} => {
148+
record_expr_events(events, span, expr_events);
149+
collect_expr_events(value, events);
150+
}
151+
HirExpr::Field { base, .. } => collect_expr_events(base, events),
152+
HirExpr::Closure { body, .. } => collect_block_events(body, events),
153+
HirExpr::Ident { .. }
154+
| HirExpr::Number { .. }
155+
| HirExpr::String { .. }
156+
| HirExpr::Unknown(_) => {}
157+
}
158+
}
159+
160+
fn record_expr_events(
161+
events: &mut HashMap<Span, Vec<HirEffectEvent>>,
162+
span: &Span,
163+
expr_events: &[HirEffectEvent],
164+
) {
165+
if expr_events.is_empty() {
166+
return;
167+
}
168+
events
169+
.entry(span.clone())
170+
.or_default()
171+
.extend(expr_events.iter().cloned());
172+
}
173+
174+
fn index_binding_types_from_block(block: &HirBlock) -> HashMap<Span, String> {
175+
let mut types = HashMap::new();
176+
collect_block_binding_types(block, &mut types);
177+
types
178+
}
179+
180+
fn collect_block_binding_types(block: &HirBlock, types: &mut HashMap<Span, String>) {
181+
for statement in &block.statements {
182+
collect_stmt_binding_types(statement, types);
183+
}
184+
}
185+
186+
fn collect_stmt_binding_types(statement: &HirStmt, types: &mut HashMap<Span, String>) {
187+
match statement {
188+
HirStmt::Let {
189+
type_name: Some(type_name),
190+
span,
191+
..
192+
} => {
193+
types.insert(span.clone(), type_name.clone());
194+
}
195+
HirStmt::With { body, .. } => collect_block_binding_types(body, types),
196+
HirStmt::If {
197+
then_body,
198+
else_body,
199+
..
200+
} => {
201+
collect_block_binding_types(then_body, types);
202+
if let Some(else_body) = else_body {
203+
collect_block_binding_types(else_body, types);
204+
}
205+
}
206+
HirStmt::Loop { body, .. } => collect_block_binding_types(body, types),
207+
HirStmt::Let {
208+
type_name: None, ..
209+
}
210+
| HirStmt::Return { .. }
211+
| HirStmt::Break(_)
212+
| HirStmt::Continue(_)
213+
| HirStmt::Expr(_)
214+
| HirStmt::Unknown(_) => {}
215+
}
59216
}
60217

61218
impl BodyState {
@@ -263,6 +420,8 @@ fn merge_fallthrough_states(base: &BodyState, left: &BodyState, right: &BodyStat
263420
#[cfg(test)]
264421
mod tests {
265422
use super::*;
423+
use crate::hir::{CallResolution, HirBlock, HirExpr, HirStmt};
424+
use crate::syntax::ast::Callee;
266425

267426
fn span(line: usize) -> Span {
268427
Span {
@@ -304,6 +463,23 @@ mod tests {
304463

305464
#[test]
306465
fn local_analysis_seeds_params_and_applies_events_by_span() {
466+
let retain_event = HirEffectEvent {
467+
function_name: "run".to_string(),
468+
kind: HirEffectEventKind::Retain {
469+
callee: "Cache.store".to_string(),
470+
param: "value".to_string(),
471+
},
472+
binding_name: "cached".to_string(),
473+
span: span(20),
474+
value_span: span(20),
475+
};
476+
let manage_event = HirEffectEvent {
477+
function_name: "run".to_string(),
478+
kind: HirEffectEventKind::Manage,
479+
binding_name: "image".to_string(),
480+
span: span(21),
481+
value_span: span(21),
482+
};
307483
let body = HirFunctionBody {
308484
function_name: "run".to_string(),
309485
bindings: vec![HirBinding {
@@ -313,25 +489,36 @@ mod tests {
313489
span: span(1),
314490
type_name: Some("ResourcePool<File>".to_string()),
315491
}],
316-
effect_events: vec![
317-
HirEffectEvent {
318-
function_name: "run".to_string(),
319-
kind: HirEffectEventKind::Retain {
320-
callee: "Cache.store".to_string(),
321-
param: "value".to_string(),
492+
block: Some(HirBlock {
493+
statements: vec![
494+
HirStmt::Let {
495+
kind: HirBindingKind::LocalLet,
496+
name: "cached".to_string(),
497+
value: None,
498+
type_name: Some("Image".to_string()),
499+
span: span(2),
322500
},
323-
binding_name: "cached".to_string(),
324-
span: span(20),
325-
value_span: span(20),
326-
},
327-
HirEffectEvent {
328-
function_name: "run".to_string(),
329-
kind: HirEffectEventKind::Manage,
330-
binding_name: "image".to_string(),
331-
span: span(21),
332-
value_span: span(21),
333-
},
334-
],
501+
HirStmt::Expr(HirExpr::Call {
502+
callee: Callee::Name("store".to_string()),
503+
args: Vec::new(),
504+
resolution: CallResolution::Unknown,
505+
events: vec![retain_event],
506+
type_name: None,
507+
span: span(20),
508+
}),
509+
HirStmt::Expr(HirExpr::Manage {
510+
value: Box::new(HirExpr::Ident {
511+
name: "image".to_string(),
512+
type_name: Some("Image".to_string()),
513+
span: span(21),
514+
}),
515+
events: vec![manage_event],
516+
type_name: Some("Image".to_string()),
517+
span: span(21),
518+
}),
519+
],
520+
span: span(1),
521+
}),
335522
..HirFunctionBody::default()
336523
};
337524
let local_analysis = LocalAnalysis::new(Some(&body));
@@ -340,6 +527,7 @@ mod tests {
340527
state.bind_local("image");
341528

342529
assert_eq!(state.value_type("pool"), Some("ResourcePool<File>"));
530+
assert_eq!(local_analysis.binding_type(&span(2)), Some("Image"));
343531

344532
local_analysis.apply_retention_events(&span(20), &mut state);
345533
local_analysis.apply_move_events(&span(21), &mut state);

0 commit comments

Comments
 (0)