Skip to content

Commit ae6dc62

Browse files
committed
Validate ResourcePool resource type arguments
1 parent 7e3a7ee commit ae6dc62

5 files changed

Lines changed: 226 additions & 6 deletions

File tree

src/analyzer.rs

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::checks;
22
use crate::diagnostic::{Diagnostic, code};
33
use crate::hir::{DuplicateSymbolKind, Hir, HirTypeKind};
44
use crate::lexer::{Token, lex};
5-
use crate::syntax::ast::{EffectDecl, Item};
5+
use crate::syntax::ast::{Callee, EffectDecl, Expr, Item, Stmt, TypeRef};
66
use crate::syntax::parse_source;
77

88
pub fn analyze_source(file: &str, source: &str) -> Vec<Diagnostic> {
@@ -32,6 +32,7 @@ impl Analyzer<'_> {
3232
self.check_duplicate_declarations();
3333
self.check_signature_explicitness();
3434
self.check_resource_fields();
35+
self.check_resource_pool_type_arguments();
3536
checks::mode::check(self);
3637
checks::calls::check(self);
3738
checks::body::check(self);
@@ -167,6 +168,157 @@ impl Analyzer<'_> {
167168
}
168169
}
169170
}
171+
172+
fn check_resource_pool_type_arguments(&mut self) {
173+
let items = self.syntax_program.items.clone();
174+
for item in &items {
175+
match item {
176+
Item::Type(decl) => {
177+
for field in &decl.fields {
178+
self.check_resource_pool_type_ref(&field.ty);
179+
}
180+
}
181+
Item::Function(function) => {
182+
for param in &function.params {
183+
self.check_resource_pool_type_ref(&param.ty);
184+
}
185+
if let Some(return_ty) = &function.return_ty {
186+
self.check_resource_pool_type_ref(return_ty);
187+
}
188+
self.check_resource_pool_calls_in_block(&function.body);
189+
}
190+
}
191+
}
192+
}
193+
194+
fn check_resource_pool_type_ref(&mut self, ty: &TypeRef) {
195+
if ty.name == "ResourcePool" {
196+
match ty.args.first() {
197+
Some(arg) => self.check_resource_pool_arg(&arg.name, &arg.span),
198+
None => self.invalid_resource_pool_type_diagnostic(
199+
"ResourcePool must declare a resource type argument.",
200+
ty.span.clone(),
201+
),
202+
}
203+
}
204+
for arg in &ty.args {
205+
self.check_resource_pool_type_ref(arg);
206+
}
207+
}
208+
209+
fn check_resource_pool_calls_in_block(&mut self, block: &crate::syntax::ast::Block) {
210+
for statement in &block.statements {
211+
self.check_resource_pool_calls_in_stmt(statement);
212+
}
213+
}
214+
215+
fn check_resource_pool_calls_in_stmt(&mut self, statement: &Stmt) {
216+
match statement {
217+
Stmt::Let(stmt) => {
218+
if let Some(value) = &stmt.value {
219+
self.check_resource_pool_calls_in_expr(value);
220+
}
221+
}
222+
Stmt::Return(stmt) => {
223+
if let Some(value) = &stmt.value {
224+
self.check_resource_pool_calls_in_expr(value);
225+
}
226+
}
227+
Stmt::Expr(value) => self.check_resource_pool_calls_in_expr(value),
228+
Stmt::With(stmt) => {
229+
self.check_resource_pool_calls_in_expr(&stmt.resource);
230+
self.check_resource_pool_calls_in_block(&stmt.body);
231+
}
232+
Stmt::If(stmt) => {
233+
self.check_resource_pool_calls_in_expr(&stmt.condition);
234+
self.check_resource_pool_calls_in_block(&stmt.then_body);
235+
if let Some(else_body) = &stmt.else_body {
236+
self.check_resource_pool_calls_in_block(else_body);
237+
}
238+
}
239+
Stmt::Loop(stmt) => {
240+
if let Some(condition) = &stmt.condition {
241+
self.check_resource_pool_calls_in_expr(condition);
242+
}
243+
self.check_resource_pool_calls_in_block(&stmt.body);
244+
}
245+
Stmt::Break(_) | Stmt::Continue(_) | Stmt::Unknown(_) => {}
246+
}
247+
}
248+
249+
fn check_resource_pool_calls_in_expr(&mut self, expr: &Expr) {
250+
match expr {
251+
Expr::Call { callee, args, span } => {
252+
if let Callee::Qualified { namespace, name } = callee
253+
&& namespace == "ResourcePool"
254+
&& name == "new"
255+
{
256+
self.invalid_resource_pool_type_diagnostic(
257+
"ResourcePool.new must be called as ResourcePool<T>.new with resource T.",
258+
span.clone(),
259+
);
260+
} else if let Callee::Qualified { namespace, .. } = callee
261+
&& let Some(arg) = resource_pool_namespace_arg(namespace)
262+
{
263+
self.check_resource_pool_arg(arg, span);
264+
}
265+
for arg in args {
266+
self.check_resource_pool_calls_in_expr(&arg.value);
267+
}
268+
}
269+
Expr::Binary { left, right, .. } => {
270+
self.check_resource_pool_calls_in_expr(left);
271+
self.check_resource_pool_calls_in_expr(right);
272+
}
273+
Expr::Effect { value, .. } | Expr::Manage { value, .. } => {
274+
self.check_resource_pool_calls_in_expr(value);
275+
}
276+
Expr::Field { base, .. } => self.check_resource_pool_calls_in_expr(base),
277+
Expr::Closure { body, .. } => self.check_resource_pool_calls_in_block(body),
278+
Expr::Ident(_, _) | Expr::Number(_, _) | Expr::String(_, _) | Expr::Unknown(_) => {}
279+
}
280+
}
281+
282+
fn check_resource_pool_arg(&mut self, type_name: &str, span: &crate::diagnostic::Span) {
283+
match self.hir.type_kind(type_name) {
284+
Some(HirTypeKind::Resource) | None => {}
285+
Some(HirTypeKind::Class) | Some(HirTypeKind::Struct) => {
286+
self.invalid_resource_pool_type_diagnostic(
287+
format!(
288+
"ResourcePool can only hold resources, but `{type_name}` is not a resource."
289+
),
290+
span.clone(),
291+
);
292+
}
293+
}
294+
}
295+
296+
fn invalid_resource_pool_type_diagnostic(
297+
&mut self,
298+
summary: impl Into<String>,
299+
span: crate::diagnostic::Span,
300+
) {
301+
self.diagnostics.push(
302+
Diagnostic::error(
303+
code::INVALID_RESOURCE_POOL_TYPE,
304+
summary,
305+
span,
306+
"invalid ResourcePool type",
307+
)
308+
.with_cause("`ResourcePool<T>` is the privileged container for long-lived resource values, so `T` must be a resource.")
309+
.with_fix(
310+
"use_resource_type",
311+
"Use a resource type argument or a non-resource container for ordinary values.",
312+
"manual",
313+
),
314+
);
315+
}
316+
}
317+
318+
fn resource_pool_namespace_arg(namespace: &str) -> Option<&str> {
319+
namespace
320+
.strip_prefix("ResourcePool<")
321+
.and_then(|rest| rest.strip_suffix('>'))
170322
}
171323

172324
fn effect_name(effect: &EffectDecl) -> &str {

src/diagnostic.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub mod code {
2121
pub const INVALID_FRESH_RETURN_TYPE: &str = "RS0603";
2222
pub const RESOURCE_FIELD: &str = "RS0701";
2323
pub const RESOURCE_ESCAPE: &str = "RS0702";
24+
pub const INVALID_RESOURCE_POOL_TYPE: &str = "RS0703";
2425
pub const LOCAL_CAPTURED_BY_MANAGED_CLOSURE: &str = "RS0801";
2526
pub const TAKE_HANDLE_FIELD: &str = "RS0901";
2627
pub const OPERATOR_OVERLOAD_ATTEMPT: &str = "RS1001";
@@ -290,6 +291,11 @@ static DIAGNOSTIC_EXPLANATIONS: &[DiagnosticExplanation] = &[
290291
title: "resource escape",
291292
explanation: "A resource introduced by `with` must not escape the block through return, manage, retention, or managed closure capture.",
292293
},
294+
DiagnosticExplanation {
295+
code: code::INVALID_RESOURCE_POOL_TYPE,
296+
title: "invalid ResourcePool type",
297+
explanation: "`ResourcePool<T>` is only valid when `T` is a resource type.",
298+
},
293299
DiagnosticExplanation {
294300
code: code::LOCAL_CAPTURED_BY_MANAGED_CLOSURE,
295301
title: "local captured by managed closure",

src/syntax/parser.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -610,13 +610,14 @@ fn find_top_level_binary_operator(
610610
second: &str,
611611
) -> Option<(usize, BinaryOp)> {
612612
let mut depth = 0usize;
613-
for index in (start..end.saturating_sub(1)).rev() {
613+
let mut found = None;
614+
for index in start..end.saturating_sub(1) {
614615
let token = &tokens[index];
615-
if token.symbol(")") || token.symbol("}") || token.symbol("]") || token.symbol(">") {
616+
if token.symbol("(") || token.symbol("{") || token.symbol("[") || token.symbol("<") {
616617
depth += 1;
617618
continue;
618619
}
619-
if token.symbol("(") || token.symbol("{") || token.symbol("[") || token.symbol("<") {
620+
if token.symbol(")") || token.symbol("}") || token.symbol("]") || token.symbol(">") {
620621
depth = depth.saturating_sub(1);
621622
continue;
622623
}
@@ -634,10 +635,10 @@ fn find_top_level_binary_operator(
634635
} else {
635636
BinaryOp::LogicalAnd
636637
};
637-
return Some((index, op));
638+
found = Some((index, op));
638639
}
639640
}
640-
None
641+
found
641642
}
642643

643644
fn parse_call_expr(tokens: &[Token], start: usize, end: usize) -> Option<Expr> {
@@ -897,6 +898,44 @@ fn is_trivia_boundary(token: &Token) -> bool {
897898
token.symbol(",") || token.symbol(";")
898899
}
899900

901+
#[cfg(test)]
902+
mod tests {
903+
use super::*;
904+
905+
#[test]
906+
fn parses_generic_qualified_resource_pool_call() {
907+
let program = parse_source(
908+
"test.rss",
909+
r#"
910+
mode: uses-local
911+
912+
fn run() -> Unit {
913+
local pool = ResourcePool<Image>.new(
914+
create: || Image.load(path: read path),
915+
max_size: 4,
916+
)
917+
}
918+
"#,
919+
);
920+
let Item::Function(function) = &program.items[0] else {
921+
panic!("expected function");
922+
};
923+
let Stmt::Let(stmt) = &function.body.statements[0] else {
924+
panic!("expected let");
925+
};
926+
let Some(Expr::Call { callee, .. }) = &stmt.value else {
927+
panic!("expected call, got {:?}", stmt.value);
928+
};
929+
assert_eq!(
930+
callee,
931+
&Callee::Qualified {
932+
namespace: "ResourcePool<Image>".to_string(),
933+
name: "new".to_string(),
934+
}
935+
);
936+
}
937+
}
938+
900939
fn ident_name(token: &Token) -> Option<&str> {
901940
match &token.kind {
902941
TokenKind::Ident(value) => Some(value),
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// expect: RS0703
2+
mode: uses-local
3+
4+
struct Image {
5+
pixels: Buffer
6+
}
7+
8+
fn bad_pool(path: read Path) -> Unit {
9+
local pool = ResourcePool<Image>.new(
10+
create: || Image.load(path: read path),
11+
max_size: 4,
12+
)
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// expect: RS0703
2+
mode: uses-local
3+
4+
struct Image {
5+
pixels: Buffer
6+
}
7+
8+
fn bad_pool(pool: mut ResourcePool<Image>) -> Unit {
9+
return Unit
10+
}

0 commit comments

Comments
 (0)