Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .release-notes/fix-generic-recover-sendable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Fix non-sendable data seen as sendable in generic recover blocks

The compiler incorrectly allowed accessing generic fields inside `recover` blocks when the type parameter had an unconstrained capability or used `#alias`. For example, this unsound code was accepted without error:

```pony
class Foo[T]
let _t: T
new create(t': T) => _t = consume t'
fun box get(): T^ =>
recover _t end
```

When `T` is unconstrained (`#any`), concrete instantiations like `Foo[String ref]` would allow recovering a `ref` field to `val` — violating reference capability safety. The compiler now correctly rejects this with "can't access non-sendable field of non-sendable object inside of a recover expression".

Fields constrained to `#send` or `#share` are unaffected — all their concrete instantiations are sendable, so access inside `recover` remains valid.
119 changes: 113 additions & 6 deletions src/libponyc/type/alias.c
Original file line number Diff line number Diff line change
Expand Up @@ -638,14 +638,121 @@ bool sendable(ast_t* type)

case TK_ARROW:
{
ast_t* upper = viewpoint_upper(type);
AST_GET_CHILDREN(type, left, right);

if(upper == NULL)
return false;
// Determine if the left side has a generic cap that needs
// case-splitting. For generic caps, viewpoint_upper computes a single
// bound that can incorrectly classify non-sendable types as sendable
// (e.g. this->(T #any !) with box receiver upper-bounds to tag, which
// is sendable, but ref->(T #any !) = T #alias which is not).
//
// Case-splitting enumerates the concrete capabilities the left side
// could be and checks sendability for each, matching the approach used
// by viewpoint_reifythis and viewpoint_reifytypeparam for subtyping.
token_id concrete_caps[6];
int num_caps = 0;

switch(ast_id(left))
{
case TK_THISTYPE:
concrete_caps[0] = TK_REF;
concrete_caps[1] = TK_VAL;
concrete_caps[2] = TK_BOX;
num_caps = 3;
break;

bool ok = sendable(upper);
ast_free_unattached(upper);
return ok;
case TK_NOMINAL:
case TK_TYPEPARAMREF:
{
ast_t* l_cap = cap_fetch(left);

switch(ast_id(l_cap))
{
case TK_CAP_READ:
concrete_caps[0] = TK_REF;
concrete_caps[1] = TK_VAL;
concrete_caps[2] = TK_BOX;
num_caps = 3;
break;

case TK_CAP_SEND:
concrete_caps[0] = TK_ISO;
concrete_caps[1] = TK_VAL;
concrete_caps[2] = TK_TAG;
num_caps = 3;
break;

case TK_CAP_SHARE:
concrete_caps[0] = TK_VAL;
concrete_caps[1] = TK_TAG;
num_caps = 2;
break;

case TK_CAP_ALIAS:
concrete_caps[0] = TK_REF;
concrete_caps[1] = TK_VAL;
concrete_caps[2] = TK_BOX;
concrete_caps[3] = TK_TAG;
num_caps = 4;
break;

case TK_CAP_ANY:
concrete_caps[0] = TK_ISO;
concrete_caps[1] = TK_TRN;
concrete_caps[2] = TK_REF;
concrete_caps[3] = TK_VAL;
concrete_caps[4] = TK_BOX;
concrete_caps[5] = TK_TAG;
num_caps = 6;
break;

default:
break;
}
break;
}

default:
break;
}

if(num_caps == 0)
{
// Single concrete cap: use viewpoint_upper directly.
ast_t* upper = viewpoint_upper(type);

if(upper == NULL)
return false;

bool ok = sendable(upper);
ast_free_unattached(upper);
return ok;
}

// Generic cap: check that ALL concrete instantiations are sendable.
for(int i = 0; i < num_caps; i++)
{
ast_t* temp_left = ast_from(left, concrete_caps[i]);

BUILD(temp_arrow, type,
NODE(TK_ARROW,
TREE(temp_left)
TREE(ast_dup(right))));

ast_t* upper = viewpoint_upper(temp_arrow);
ast_free_unattached(temp_arrow);

if(upper == NULL)
return false;

bool ok = sendable(upper);
ast_free_unattached(upper);

if(!ok)
return false;
}

return true;
}

case TK_NOMINAL:
Expand Down
106 changes: 106 additions & 0 deletions test/libponyc/recover.cc
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,112 @@ TEST_F(RecoverTest, CantAutoRecover_CtorAssignmentWithNonSendableArg)
TEST_ERRORS_1(src, "right side must be a subtype of left side");
}

// Issue #4459 - generic field with #any in recover
TEST_F(RecoverTest, CantRecover_GenericFieldAny)
{
const char* src =
"class Foo[T]\n"
" let _t: T\n"
" new create(t': T) => _t = consume t'\n"
" fun box get(): T^ =>\n"
" recover _t end\n";

TEST_ERRORS_1(src,
"can't access non-sendable field of non-sendable object inside of a "
"recover expression");
}

// Issue #4459 - generic field with #read in recover
TEST_F(RecoverTest, CantRecover_GenericFieldRead)
{
const char* src =
"class Foo[T: Any #read]\n"
" let _t: T\n"
" new create(t': T) => _t = consume t'\n"
" fun box get(): T^ =>\n"
" recover _t end\n";

TEST_ERRORS_1(src,
"can't access non-sendable field of non-sendable object inside of a "
"recover expression");
}

// Issue #4459 - generic field with #alias in recover
TEST_F(RecoverTest, CantRecover_GenericFieldAlias)
{
const char* src =
"class Foo[T: Any #alias]\n"
" let _t: T\n"
" new create(t': T) => _t = consume t'\n"
" fun box get(): T^ =>\n"
" recover _t end\n";

TEST_ERRORS_1(src,
"can't access non-sendable field of non-sendable object inside of a "
"recover expression");
}

// Issue #4459 - generic field with #send in recover: field access is allowed
// (all instantiations of #send are sendable) even though the recovery
// expression itself may not fully type-check for unrelated reasons.
TEST_F(RecoverTest, CanRecover_GenericFieldSend)
{
const char* src =
"class Foo[T: Any #send]\n"
" let _t: T\n"
" new create(t': T) => _t = consume t'\n"
" fun box check(): None =>\n"
" recover\n"
" _t\n"
" None\n"
" end\n";

TEST_COMPILE(src);
}

// Issue #4459 - generic field with #share in recover should compile
TEST_F(RecoverTest, CanRecover_GenericFieldShare)
{
const char* src =
"class Foo[T: Any #share]\n"
" let _t: T\n"
" new create(t': T) => _t = consume t'\n"
" fun box get(): T^ =>\n"
" recover _t end\n";

TEST_COMPILE(src);
}

// Regression guard: concrete val field in recover still works
TEST_F(RecoverTest, CanRecover_ConcreteValField)
{
const char* src =
"class Inner\n"
"class Foo\n"
" let _t: Inner val\n"
" new create() => _t = Inner\n"
" fun box get(): Inner val =>\n"
" recover _t end\n";

TEST_COMPILE(src);
}

// Regression guard: concrete box field in recover still rejected
TEST_F(RecoverTest, CantRecover_ConcreteBoxField)
{
const char* src =
"class Inner\n"
"class Foo\n"
" let _t: Inner box\n"
" new create(t': Inner box) => _t = t'\n"
" fun box get(): Inner val =>\n"
" recover val _t end\n";

TEST_ERRORS_1(src,
"can't access non-sendable field of non-sendable object inside of a "
"recover expression");
}

TEST_F(RecoverTest, CantAutoRecover_CtorParamToComplexTypeWithNonSendableArg)
{
const char* src =
Expand Down
Loading