Skip to content
Open
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
7 changes: 7 additions & 0 deletions .release-notes/fix-lambda-type-this-viewpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Fix wrong code generation for `this->` in lambda type parameters

Using `this->` in a lambda type parameter (e.g., `{(this->A!): Bool}`) and then forwarding that lambda to another function produced wrong code at runtime. Depending on the program, you'd get incorrect results, wrong vtable dispatch, or a segfault. Calling the lambda directly in the same method worked fine — the bug only appeared when the lambda was passed along.

The root cause: lambda types are desugared into anonymous interfaces, and `this->` was copied verbatim into the interface. Once inside the interface, `this` referred to the interface's own receiver instead of the enclosing class's receiver, producing the wrong type for structural subtype checks.

`this->` is now resolved to the enclosing method's receiver capability during desugaring, so `{(this->A!)}` in a `box` method correctly becomes `{(box->A!)}`.
45 changes: 45 additions & 0 deletions src/libponyc/pass/sugar.c
Original file line number Diff line number Diff line change
Expand Up @@ -974,15 +974,60 @@ static ast_result_t sugar_semi(pass_opt_t* options, ast_t** astp)
}


// Replace TK_THISTYPE nodes in an AST with a cap token. This is used
// when desugaring lambda types to resolve this-> to the enclosing method's
// receiver cap, since this-> in the desugared interface would otherwise
// refer to the interface's own receiver instead of the enclosing class's
// receiver. See #4168.
static void replace_thistype_with_cap(ast_t* ast, token_id cap)
{
if(ast == NULL)
return;

if(ast_id(ast) == TK_THISTYPE)
{
ast_setid(ast, cap);
return;
}

for(ast_t* child = ast_child(ast); child != NULL; child = ast_sibling(child))
replace_thistype_with_cap(child, cap);
}


static ast_result_t sugar_lambdatype(pass_opt_t* opt, ast_t** astp)
{
pony_assert(astp != NULL);
ast_t* ast = *astp;
pony_assert(ast != NULL);

// Find the enclosing method's receiver cap before extracting children.
// When a lambda type contains this->, the user intends the enclosing
// method's receiver, not the desugared lambda interface's receiver.
// Replace this-> with the enclosing method's cap to preserve semantics.
// See #4168.
ast_t* method = ast;
while(method != NULL && ast_id(method) != TK_FUN &&
ast_id(method) != TK_NEW && ast_id(method) != TK_BE)
{
method = ast_parent(method);
}

AST_EXTRACT_CHILDREN(ast, apply_cap, apply_name, apply_t_params, params,
ret_type, error, interface_cap, ephemeral);

if(method != NULL)
{
token_id method_cap = ast_id(ast_child(method));

// Default receiver cap for fun is box, for be/new it varies.
if(method_cap == TK_NONE)
method_cap = TK_BOX;

replace_thistype_with_cap(params, method_cap);
replace_thistype_with_cap(ret_type, method_cap);
}

bool bare = ast_id(ast) == TK_BARELAMBDATYPE;

if(bare)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Regression test for issue #4168. When this-> appears in a lambda type parameter
(e.g. {(this->A!)}), it must resolve to the enclosing method's receiver cap,
not the desugared interface's receiver. Without the fix, forwarding the lambda
to another function caused wrong vtable dispatch and segfaults.
"""
use @pony_exitcode[None](code: I32)

class Container[A: Any #read]
let _data: A

new create(a: A) =>
_data = a

fun apply(): this->A =>
_data

// this-> in the lambda type is the key construct being tested.
// It should resolve to box-> since fun defaults to box receiver.
fun update(f: {(this->A!)}) =>
Applier[A, this->Container[A]](this, f)

primitive Applier[A: Any #read, B: Container[A] #read]
fun apply(c: B, f: {(B->A!)}) =>
f(c.apply())

class Counter
var value: I32 = 0

actor Main
new create(env: Env) =>
let c = Container[Counter](Counter)
c.update({(x: Counter box) =>
if x.value == 0 then
@pony_exitcode(1)
end
})
21 changes: 21 additions & 0 deletions test/libponyc/sugar.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,27 @@ TEST_F(SugarTest, LambdaTypeSimpleAliased)
}


TEST_F(SugarTest, LambdaTypeThisViewpoint)
{
// See #4168. this-> in a lambda type should resolve to the enclosing
// method's receiver cap (box for fun), not the desugared interface's
// receiver.
const char* short_form =
"trait Foo[A: F64]\n"
" fun f(x: {(this->A!)})";

const char* full_form =
"use \"builtin\"\n"
"trait ref Foo[A: F64]\n"
" fun box f(x: $T[A]): None\n"

"interface ref $T[A: F64]\n"
" fun box apply(p1: box->A!): None";

TEST_EQUIV(short_form, full_form);
}


TEST_F(SugarTest, UseGuardNormalises)
{
const char* short_form =
Expand Down