Skip to content

Commit b32cd9f

Browse files
committed
Fix false positive in _final send checking for generic classes
The finalisers pass conservatively reports FINAL_MAY_SEND when it encounters a method call on an unknown type. When _final calls a method on a generic class instantiated with concrete type args (e.g., Generic[Prim]), the pass follows into the method body where expressions still reference the generic type parameter A. Since A's constraint is a trait, is_known(A) returns false and the pass reports a false positive. Two changes fix this: 1. Reify the method body with the entity's concrete type arguments before analyzing it. This replaces type parameters with their concrete types so is_known sees the actual type. The recursion guard is set on the original body (not the reified copy) so that recursive lookups correctly detect cycles. 2. Replace ast_get with lookup_try for method resolution. ast_get only searches the entity's own symbol table, missing methods inherited through the provides chain. lookup_try handles the full provides chain. This expands the range of generic code accepted in _final methods. Cases where the concrete type doesn't have the called method in its own scope (e.g., Range calling finite() which exists on the Real trait but not on USize) still fall back to FINAL_MAY_SEND conservatively. Closes #4249
1 parent 6491d1f commit b32cd9f

4 files changed

Lines changed: 142 additions & 88 deletions

File tree

.github/workflows/ponyc-tier3.yml

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: Check for recent commits
3232
id: check
3333
run: |
34-
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
34+
if [ "${{ github.event_name }}" != "schedule" ]; then
3535
echo "has-changes=true" >> "$GITHUB_OUTPUT"
3636
elif git log --since="7 days ago" --oneline | head -1 | grep -q .; then
3737
echo "has-changes=true" >> "$GITHUB_OUTPUT"
@@ -69,7 +69,7 @@ jobs:
6969
- name: Install QEMU
7070
run: |
7171
sudo apt-get update -q
72-
sudo apt-get install -y -q qemu-utils qemu-system-x86 cloud-image-utils
72+
sudo apt-get install -y -q qemu-utils qemu-system-x86 cloud-image-utils expect
7373
sudo chmod 666 /dev/kvm
7474
- name: Download FreeBSD image
7575
run: |
@@ -81,15 +81,16 @@ jobs:
8181
run: |
8282
ssh-keygen -t ed25519 -f vm_key -N ""
8383
PUB_KEY=$(cat vm_key.pub)
84+
85+
# nuageinit seed: SSH key for freebsd, set root password for su access
8486
cat > user-data <<USERDATA
8587
#cloud-config
8688
ssh_authorized_keys:
8789
- ${PUB_KEY}
88-
packages:
89-
- sudo
90-
- rsync
91-
runcmd:
92-
- echo 'freebsd ALL=(ALL) NOPASSWD: ALL' > /usr/local/etc/sudoers.d/freebsd
90+
chpasswd:
91+
expire: false
92+
list:
93+
- root:ciroot
9394
USERDATA
9495
cat > meta-data <<METADATA
9596
instance-id: freebsd-ci
@@ -116,19 +117,30 @@ jobs:
116117
sleep 2
117118
done
118119
'
119-
echo "SSH available, waiting for cloud-init to finish..."
120-
timeout 300 bash -c '
121-
while ! ssh -o StrictHostKeyChecking=no -i vm_key -p 2222 freebsd@localhost "test -f /var/lib/cloud/instance/boot-finished" 2>/dev/null; do
122-
sleep 5
123-
done
124-
'
125-
echo "VM fully provisioned"
120+
echo "SSH available"
126121
- name: Install build dependencies
127122
run: |
128-
ssh -o StrictHostKeyChecking=no -i vm_key -p 2222 freebsd@localhost <<'EOF'
129-
set -e
130-
sudo pkg install -y cmake gmake libunwind git python3
131-
EOF
123+
export PUB_KEY=$(cat vm_key.pub)
124+
expect <<'EXPECT'
125+
set timeout 600
126+
spawn ssh -o StrictHostKeyChecking=no -i vm_key -p 2222 -t freebsd@localhost su -m root
127+
expect {
128+
"Password:" { send "ciroot\r" }
129+
timeout { puts "Timeout waiting for password prompt"; exit 1 }
130+
}
131+
expect {
132+
"#" {}
133+
"su: Sorry" { puts "su failed - wrong password or nuageinit didn't set it"; exit 1 }
134+
timeout { puts "Timeout waiting for root shell"; exit 1 }
135+
}
136+
send "pkg install -y cmake gmake libunwind git python3 rsync\r"
137+
expect {
138+
"#" {}
139+
timeout { puts "Timeout during pkg install"; exit 1 }
140+
}
141+
send "exit\r"
142+
expect eof
143+
EXPECT
132144
- name: Copy source to VM
133145
run: |
134146
rsync -az -e "ssh -o StrictHostKeyChecking=no -i vm_key -p 2222" \
@@ -153,7 +165,7 @@ jobs:
153165
- name: Shutdown VM
154166
if: always()
155167
run: |
156-
ssh -o StrictHostKeyChecking=no -i vm_key -p 2222 freebsd@localhost "sudo shutdown -p now" 2>/dev/null || true
168+
pkill -f qemu-system-x86 || true
157169
- name: Send alert on failure
158170
if: ${{ failure() }}
159171
uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Fix false positive in `_final` send checking for generic classes
2+
3+
The compiler incorrectly rejected `_final` methods that called methods on generic classes instantiated with concrete type arguments. For example, calling a method on a `Generic[Prim]` where `Generic` has a trait-constrained type parameter would produce a spurious "_final cannot create actors or send messages" error, even though the concrete type is known and does not send messages.
4+
5+
The finaliser pass now resolves generic type parameters to their concrete types before analyzing method bodies for potential message sends. This expands the range of generic code accepted in `_final` methods, though some cases involving methods inherited through a provides chain (like `Range`) still produce false positives.

src/libponyc/pass/finalisers.c

Lines changed: 59 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ enum
1414
FINAL_RECURSE = (1 << 2)
1515
};
1616

17-
static int check_body_send(ast_t* ast, bool in_final);
17+
static int check_body_send(pass_opt_t* opt, ast_t* ast, bool in_final);
1818

1919
static void show_send(pass_opt_t* opt, ast_t* ast)
2020
{
@@ -38,62 +38,7 @@ static void show_send(pass_opt_t* opt, ast_t* ast)
3838
}
3939
}
4040

41-
static ast_t* receiver_def(ast_t* type)
42-
{
43-
// We must be a known type at this point.
44-
switch(ast_id(type))
45-
{
46-
case TK_ISECTTYPE:
47-
{
48-
// Find the first concrete type in the intersection.
49-
ast_t* child = ast_child(type);
50-
51-
while(child != NULL)
52-
{
53-
ast_t* def = receiver_def(child);
54-
55-
if(def != NULL)
56-
{
57-
switch(ast_id(def))
58-
{
59-
case TK_PRIMITIVE:
60-
case TK_STRUCT:
61-
case TK_CLASS:
62-
case TK_ACTOR:
63-
return def;
64-
65-
default: {}
66-
}
67-
}
68-
69-
child = ast_sibling(child);
70-
}
71-
72-
break;
73-
}
74-
75-
case TK_NOMINAL:
76-
// Return the def.
77-
return (ast_t*)ast_data(type);
78-
79-
case TK_ARROW:
80-
// Use the right-hand side.
81-
return receiver_def(ast_childidx(type, 1));
82-
83-
case TK_TYPEPARAMREF:
84-
{
85-
// Use the constraint.
86-
ast_t* def = (ast_t*)ast_data(type);
87-
return receiver_def(ast_childidx(def, 1));
88-
}
89-
90-
default: {}
91-
}
92-
93-
return NULL;
94-
}
95-
96-
static int check_call_send(ast_t* ast, bool in_final)
41+
static int check_call_send(pass_opt_t* opt, ast_t* ast, bool in_final)
9742
{
9843
AST_GET_CHILDREN(ast, lhs, positional, named, question);
9944
AST_GET_CHILDREN(lhs, receiver, method);
@@ -118,15 +63,60 @@ static int check_call_send(ast_t* ast, bool in_final)
11863
if(!is_known(type))
11964
return FINAL_MAY_SEND;
12065

121-
ast_t* def = receiver_def(type);
122-
pony_assert(def != NULL);
123-
12466
const char* method_name = ast_name(method);
125-
ast_t* fun = ast_get(def, method_name, NULL);
126-
pony_assert(fun != NULL);
67+
68+
// Use lookup_try to resolve the method, which handles inherited methods
69+
// from the provides chain. ast_get only searches the entity's own scope.
70+
deferred_reification_t* method_ref = lookup_try(opt, NULL, type,
71+
method_name, true);
72+
73+
if(method_ref == NULL)
74+
return FINAL_MAY_SEND;
75+
76+
ast_t* fun = method_ref->ast;
77+
ast_t* def = (ast_t*)ast_data(type);
12778

12879
AST_GET_CHILDREN(fun, cap, id, typeparams, params, result, can_error, body);
129-
int r = check_body_send(body, false);
80+
81+
// If the receiver type has type arguments and the entity has type parameters,
82+
// reify the method body so generic type parameters are replaced with their
83+
// concrete types. Without this, expressions like `_min.lt(_max)` inside
84+
// `Range[USize]` have receiver type `A` (the type parameter), which fails
85+
// is_known() and produces a false positive FINAL_MAY_SEND.
86+
ast_t* reified_body = NULL;
87+
int r;
88+
89+
if(ast_id(type) == TK_NOMINAL)
90+
{
91+
ast_t* typeargs = ast_childidx(type, 2);
92+
ast_t* entity_typeparams = ast_childidx(def, 1);
93+
94+
if((ast_id(typeargs) == TK_TYPEARGS) &&
95+
(ast_id(entity_typeparams) == TK_TYPEPARAMS))
96+
{
97+
reified_body = reify(body, entity_typeparams, typeargs, NULL, true);
98+
}
99+
}
100+
101+
if(reified_body != NULL)
102+
{
103+
// Set the recursion guard on the original body so that recursive calls
104+
// (which look up the original, not the reified copy) see FINAL_RECURSE.
105+
bool already_recursing = ast_checkflag(body, AST_FLAG_RECURSE_1);
106+
if(!already_recursing)
107+
ast_setflag(body, AST_FLAG_RECURSE_1);
108+
109+
r = check_body_send(opt, reified_body, false);
110+
111+
if(!already_recursing)
112+
ast_clearflag(body, AST_FLAG_RECURSE_1);
113+
114+
ast_free_unattached(reified_body);
115+
} else {
116+
r = check_body_send(opt, body, false);
117+
}
118+
119+
deferred_reify_free(method_ref);
130120

131121
if(r == FINAL_NO_SEND)
132122
{
@@ -147,27 +137,27 @@ static int check_call_send(ast_t* ast, bool in_final)
147137
return r;
148138
}
149139

150-
static int check_expr_send(ast_t* ast, bool in_final)
140+
static int check_expr_send(pass_opt_t* opt, ast_t* ast, bool in_final)
151141
{
152142
int send = FINAL_NO_SEND;
153143

154144
if(ast_id(ast) == TK_CALL)
155-
send |= check_call_send(ast, in_final);
145+
send |= check_call_send(opt, ast, in_final);
156146

157147
ast_t* child = ast_child(ast);
158148

159149
while(child != NULL)
160150
{
161151
if(ast_mightsend(child))
162-
send |= check_expr_send(child, in_final);
152+
send |= check_expr_send(opt, child, in_final);
163153

164154
child = ast_sibling(child);
165155
}
166156

167157
return send;
168158
}
169159

170-
static int check_body_send(ast_t* ast, bool in_final)
160+
static int check_body_send(pass_opt_t* opt, ast_t* ast, bool in_final)
171161
{
172162
if(ast_checkflag(ast, AST_FLAG_RECURSE_1))
173163
return FINAL_RECURSE;
@@ -180,7 +170,7 @@ static int check_body_send(ast_t* ast, bool in_final)
180170

181171
ast_setflag(ast, AST_FLAG_RECURSE_1);
182172

183-
int r = check_expr_send(ast, in_final);
173+
int r = check_expr_send(opt, ast, in_final);
184174

185175
if(r == FINAL_NO_SEND)
186176
{
@@ -206,7 +196,7 @@ static bool entity_finaliser(pass_opt_t* opt, ast_t* entity, const char* final)
206196
return true;
207197

208198
AST_GET_CHILDREN(ast, cap, id, typeparams, params, result, can_error, body);
209-
int r = check_body_send(body, true);
199+
int r = check_body_send(opt, body, true);
210200

211201
if((r & FINAL_CAN_SEND) != 0 || (r & FINAL_MAY_SEND) != 0)
212202
{

test/libponyc/finalisers.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,53 @@ TEST_F(FinalisersTest, FinalCannotCallChainedBehaviour)
111111
TEST_ERRORS_1(src, "_final cannot create actors or send messages");
112112
}
113113

114+
TEST_F(FinalisersTest, FinalCanCallMethodOnGenericClassWithConcreteTypeArgs)
115+
{
116+
const char* src =
117+
"trait Gettable\n"
118+
" fun get(): U32\n"
119+
"primitive Prim is Gettable\n"
120+
" fun get(): U32 => 0\n"
121+
"class Generic[A: Gettable val]\n"
122+
" let _value: A\n"
123+
" new val create(value: A) =>\n"
124+
" _value = value\n"
125+
" fun get(): U32 =>\n"
126+
" _value.get()\n"
127+
"class Foo\n"
128+
" fun _final() =>\n"
129+
" Generic[Prim](Prim).get()";
130+
131+
TEST_COMPILE(src);
132+
}
133+
134+
TEST_F(FinalisersTest, FinalCannotCallMethodOnGenericClassThatSends)
135+
{
136+
// The send_msg method sends a message via a behavior call on an actor field.
137+
// Even though Generic is instantiated with concrete type args (U32),
138+
// the finaliser pass should still detect the send inside send_msg.
139+
const char* src =
140+
"actor Ping\n"
141+
" new create() => None\n"
142+
" be ping() => None\n"
143+
"class Holder\n"
144+
" let _p: Ping\n"
145+
" new create(p: Ping) => _p = p\n"
146+
" fun get(): Ping => _p\n"
147+
"class Generic[A]\n"
148+
" let _h: Holder\n"
149+
" new create(h: Holder) => _h = h\n"
150+
" fun send_msg() =>\n"
151+
" _h.get().ping()\n"
152+
"class Foo\n"
153+
" let _g: Generic[U32]\n"
154+
" new create(h: Holder) => _g = Generic[U32](h)\n"
155+
" fun _final() =>\n"
156+
" _g.send_msg()";
157+
158+
TEST_ERRORS_1(src, "_final cannot create actors or send messages");
159+
}
160+
114161
TEST_F(FinalisersTest, CannotLookupFinal)
115162
{
116163
const char* src =

0 commit comments

Comments
 (0)