Skip to content

Commit 3b40058

Browse files
committed
Record field access facts in HIR
1 parent 60cc27e commit 3b40058

3 files changed

Lines changed: 85 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ It currently implements:
1818
- HIR constructor signatures derived from declared type fields
1919
- HIR call-site facts with resolved builtin, user function, constructor, enum variant, and unknown callees
2020
- HIR body binding facts for parameters, managed lets, local lets, and best-effort initial value types
21+
- HIR field-access facts with resolved base type, field type, and handle status where known
2122
- AST-driven mode and call checks for local-only features, named arguments, data effects, and retaining APIs
2223
- AST-driven body checks for local moves, early-exit-aware `fresh` returns, resource escape, resolved handle-field `take`, and managed closure captures
2324
- Focused check modules for mode, calls, body semantics, and forbidden operator behavior

src/checks/body.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,12 @@ fn check_take_of_handle_field(analyzer: &mut Analyzer<'_>, expr: &Expr, state: &
469469
value,
470470
span,
471471
} => {
472-
if let Expr::Field { base, name, .. } = value.as_ref()
473-
&& is_handle_field(analyzer, state, base, name)
472+
if let Expr::Field {
473+
base,
474+
name,
475+
span: field_span,
476+
} = value.as_ref()
477+
&& is_handle_field(analyzer, state, base, name, field_span)
474478
{
475479
analyzer.diagnostics.push(
476480
Diagnostic::error(
@@ -840,7 +844,12 @@ fn is_handle_field(
840844
state: &BodyState,
841845
base: &Expr,
842846
field_name: &str,
847+
field_span: &crate::diagnostic::Span,
843848
) -> bool {
849+
if let Some(field) = analyzer.hir.field_access(field_span) {
850+
return field.is_handle;
851+
}
852+
844853
if let Some(base_type) = infer_expr_type(analyzer, base, state) {
845854
return analyzer
846855
.hir

src/hir.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ pub struct HirBinding {
119119
pub type_name: Option<String>,
120120
}
121121

122+
#[derive(Debug, Clone, PartialEq, Eq)]
123+
pub struct HirFieldAccess {
124+
pub function_name: String,
125+
pub name: String,
126+
pub span: Span,
127+
pub base_type: Option<String>,
128+
pub type_name: Option<String>,
129+
pub is_handle: bool,
130+
}
131+
122132
#[derive(Debug, Default)]
123133
pub struct Hir {
124134
signatures: HashMap<String, FunctionSig>,
@@ -130,6 +140,8 @@ pub struct Hir {
130140
bindings: Vec<HirBinding>,
131141
bindings_by_span: HashMap<Span, HirBinding>,
132142
bindings_by_function: HashMap<String, Vec<HirBinding>>,
143+
field_accesses: Vec<HirFieldAccess>,
144+
field_accesses_by_span: HashMap<Span, HirFieldAccess>,
133145
}
134146

135147
impl Hir {
@@ -220,6 +232,10 @@ impl Hir {
220232
.map_or(&[], Vec::as_slice)
221233
}
222234

235+
pub fn field_access(&self, span: &Span) -> Option<&HirFieldAccess> {
236+
self.field_accesses_by_span.get(span)
237+
}
238+
223239
pub fn resolve_call(&self, callee: &Callee) -> CallResolution {
224240
let call_name = callee_name(callee);
225241
if is_enum_variant_call(call_name) {
@@ -302,15 +318,22 @@ impl Hir {
302318
by_function
303319
},
304320
);
321+
self.field_accesses_by_span = facts
322+
.field_accesses
323+
.iter()
324+
.map(|field| (field.span.clone(), field.clone()))
325+
.collect();
305326
self.call_sites = facts.call_sites;
306327
self.bindings = facts.bindings;
328+
self.field_accesses = facts.field_accesses;
307329
}
308330
}
309331

310332
#[derive(Default)]
311333
struct BodyFacts {
312334
call_sites: Vec<HirCallSite>,
313335
bindings: Vec<HirBinding>,
336+
field_accesses: Vec<HirFieldAccess>,
314337
}
315338

316339
fn collect_function_body_facts(hir: &Hir, function: &FunctionDecl, facts: &mut BodyFacts) {
@@ -418,7 +441,20 @@ fn collect_body_facts_in_expr(
418441
Expr::Effect { value, .. } | Expr::Manage { value, .. } => {
419442
collect_body_facts_in_expr(hir, function_name, value, value_types, facts);
420443
}
421-
Expr::Field { base, .. } => {
444+
Expr::Field { base, name, span } => {
445+
let base_type = infer_hir_expr_type(hir, base, value_types);
446+
let field = base_type
447+
.as_deref()
448+
.and_then(|type_name| hir.type_info(type_name))
449+
.and_then(|type_info| type_info.fields.get(name));
450+
facts.field_accesses.push(HirFieldAccess {
451+
function_name: function_name.to_string(),
452+
name: name.clone(),
453+
span: span.clone(),
454+
base_type,
455+
type_name: field.map(|field| field.type_name.clone()),
456+
is_handle: field.is_some_and(|field| field.is_handle),
457+
});
422458
collect_body_facts_in_expr(hir, function_name, base, value_types, facts);
423459
}
424460
Expr::Closure { body, .. } => {
@@ -1201,4 +1237,40 @@ fn load(path: read Path) -> Unit {
12011237
HirBindingKind::LocalLet
12021238
));
12031239
}
1240+
1241+
#[test]
1242+
fn records_field_access_facts() {
1243+
let source = r#"
1244+
mode: uses-local
1245+
1246+
class Rules {
1247+
}
1248+
1249+
struct Config {
1250+
rules: handle Rules
1251+
}
1252+
1253+
fn take_rules(config: mut Config) -> Unit {
1254+
List.consume(list: take config.rules)
1255+
}
1256+
"#;
1257+
1258+
let program = parse_source("test.rss", source);
1259+
let hir = Hir::from_syntax(&program);
1260+
let field = hir
1261+
.field_accesses
1262+
.first()
1263+
.expect("field access is recorded");
1264+
1265+
assert_eq!(field.function_name, "take_rules");
1266+
assert_eq!(field.name, "rules");
1267+
assert_eq!(field.base_type.as_deref(), Some("Config"));
1268+
assert_eq!(field.type_name.as_deref(), Some("Rules"));
1269+
assert!(field.is_handle);
1270+
assert!(
1271+
hir.field_access(&field.span)
1272+
.expect("field access lookup by span works")
1273+
.is_handle
1274+
);
1275+
}
12041276
}

0 commit comments

Comments
 (0)