Skip to content

Commit 7e3a7ee

Browse files
committed
Reject fresh returns of non-struct types
1 parent 72570cf commit 7e3a7ee

4 files changed

Lines changed: 74 additions & 1 deletion

File tree

src/checks/body.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::analyzer::Analyzer;
22
use crate::diagnostic::{Diagnostic, code};
33
use crate::hir::{HirBindingKind, HirBlock, HirExpr, HirStmt, HirTypeKind};
4-
use crate::syntax::ast::{Callee, FunctionDecl, Item};
4+
use crate::syntax::ast::{Callee, FunctionDecl, Item, TypeRef};
55

66
use super::local::{
77
BodyState, FreshReturnIssue, FreshReturnIssueKind, LocalAnalysis, ManagedToLocalUse, MovedUse,
@@ -249,6 +249,7 @@ fn check_fresh_returns(
249249
if !function.returns_fresh {
250250
return;
251251
}
252+
check_fresh_return_type(analyzer, function);
252253
for issue in local_analysis.fresh_return_issues() {
253254
match &issue.kind {
254255
FreshReturnIssueKind::NotClean { name } => {
@@ -262,6 +263,28 @@ fn check_fresh_returns(
262263
}
263264
}
264265

266+
fn check_fresh_return_type(analyzer: &mut Analyzer<'_>, function: &FunctionDecl) {
267+
let Some(return_ty) = &function.return_ty else {
268+
return;
269+
};
270+
let target = fresh_return_target_type(return_ty);
271+
match analyzer.hir.type_kind(&target.name) {
272+
Some(HirTypeKind::Struct) | None => {}
273+
Some(HirTypeKind::Class) | Some(HirTypeKind::Resource) => {
274+
invalid_fresh_return_type_diagnostic(analyzer, function, target);
275+
}
276+
}
277+
}
278+
279+
fn fresh_return_target_type(return_ty: &TypeRef) -> &TypeRef {
280+
if matches!(return_ty.name.as_str(), "Result" | "Option")
281+
&& let Some(first_arg) = return_ty.args.first()
282+
{
283+
return first_arg;
284+
}
285+
return_ty
286+
}
287+
265288
fn managed_to_local_diagnostic(analyzer: &mut Analyzer<'_>, managed_to_local: ManagedToLocalUse) {
266289
analyzer.diagnostics.push(
267290
Diagnostic::error(
@@ -631,6 +654,30 @@ fn freshness_unknown_diagnostic(
631654
);
632655
}
633656

657+
fn invalid_fresh_return_type_diagnostic(
658+
analyzer: &mut Analyzer<'_>,
659+
function: &FunctionDecl,
660+
target: &TypeRef,
661+
) {
662+
analyzer.diagnostics.push(
663+
Diagnostic::error(
664+
code::INVALID_FRESH_RETURN_TYPE,
665+
format!(
666+
"function `{}` declares `fresh {}` but `{}` is not a struct.",
667+
function.name, target.name, target.name
668+
),
669+
target.span.clone(),
670+
"invalid fresh type",
671+
)
672+
.with_cause("RSScript `fresh` is a shallow guarantee for newly created struct shells.")
673+
.with_fix(
674+
"use_struct_fresh_type",
675+
"Return a struct type as fresh, or remove `fresh` from this return contract.",
676+
"manual",
677+
),
678+
);
679+
}
680+
634681
fn trusted_fresh_ident(analyzer: &Analyzer<'_>, name: &str) -> bool {
635682
analyzer.hir.type_kind(name) == Some(HirTypeKind::Struct)
636683
|| analyzer

src/diagnostic.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod code {
1818
pub const LOCAL_VALUE_RETAINED: &str = "RS0501";
1919
pub const FRESH_RETURN_NOT_CLEAN: &str = "RS0601";
2020
pub const FRESHNESS_UNKNOWN: &str = "RS0602";
21+
pub const INVALID_FRESH_RETURN_TYPE: &str = "RS0603";
2122
pub const RESOURCE_FIELD: &str = "RS0701";
2223
pub const RESOURCE_ESCAPE: &str = "RS0702";
2324
pub const LOCAL_CAPTURED_BY_MANAGED_CLOSURE: &str = "RS0801";
@@ -274,6 +275,11 @@ static DIAGNOSTIC_EXPLANATIONS: &[DiagnosticExplanation] = &[
274275
title: "freshness unknown",
275276
explanation: "The MVP checker could not prove the returned value is fresh. Current proof support trusts clean locals, struct constructors, and known fresh calls.",
276277
},
278+
DiagnosticExplanation {
279+
code: code::INVALID_FRESH_RETURN_TYPE,
280+
title: "invalid fresh return type",
281+
explanation: "`fresh` may only be used with struct types. Classes and resources are not fresh values.",
282+
},
277283
DiagnosticExplanation {
278284
code: code::RESOURCE_FIELD,
279285
title: "resource field",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// expect: RS0603
2+
mode: managed
3+
4+
class User {
5+
name: String
6+
}
7+
8+
fn load_user() -> Result<fresh User, UserError> {
9+
return User(name: "Ada")
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// expect: RS0603
2+
mode: managed
3+
4+
class User {
5+
name: String
6+
}
7+
8+
fn current_user() -> fresh User {
9+
return User(name: "Ada")
10+
}

0 commit comments

Comments
 (0)