28491, "DanilaFe", "[Bug]: Compiler-generated functions are missed during function-method ambiguity checks", "2026-03-03T22:15:43Z"
opened 10:15PM - 03 Mar 26 UTC
type: Bug
area: Compiler
This is a weird one. Arkouda is a motivator. In particular, Arkouda has a class,… `ServerDaemon`, which invokes a proc called `deserialize` in its body:
https://github.com/Bears-R-Us/arkouda/blob/dee36a704db8d5cf02c45ee8b3352ef72d3fde73/src/ServerDaemon.chpl#L131
This `deserialize` method is actually a free-standing (non-method) procedure:
https://github.com/Bears-R-Us/arkouda/blob/dee36a704db8d5cf02c45ee8b3352ef72d3fde73/src/Message.chpl#L590
At first, this looks innocent. However, Dyno balked at this, and when I took a closer look, I realized why: this is normally an ambiguous call!
In Dyno and in production, the following program is ambiguous:
```Chapel
proc foo() {}
record R {
proc foo() {}
proc bar() { foo(); }
}
```
Specifically, we have an ambiguity between a method and a non-method call. This was implemented as part of https://github.com/chapel-lang/chapel/pull/22917, though I haven't nailed down the specific GitHub issue in which the method-non-method ambiguity was discussed.
So... why does Arkouda compile here? It turns out that the ambiguity error is emitted during scope resolution, but default functions aren't inserted until AFTER scope resolution. So, as a result, **all default functions automatically have lower precedence when called with an implicit receiver than standalone procedures**.
In other words, this is ambiguous:
```Chapel
proc bar(x, y) {}
record R {
proc bar(x, y) {}
proc foo() {
bar(1, 2);
}
}
(new R()).foo();
```
But this is not:
```Chapel
proc deserialize(x, y) {}
record R {
/* generated proc deserialize (x, y) {} */
proc foo() {
bar(1, 2);
}
}
(new R()).foo();
```
This is a weird one. Arkouda is a motivator. In particular, Arkouda has a class, ServerDaemon, which invokes a proc called deserialize in its body:
*/
proc requestShutdown(user: string) throws {
this.shutdownDaemon = true;
}
/*
* Converts the incoming request JSON string into RequestMsg object.
*/
proc extractRequest(request : string) : RequestMsg throws {
var rm = new RequestMsg();
deserialize(rm, request);
return rm;
}
/**
* Encapsulates logic that is to be invoked once a ArkoduaSeverDaemon
* has exited the daemon loop.
*/
proc shutdown() throws {
sdLogger.error(getModuleName(),getRoutineName(),getLineNumber(),
"shutdown sequence complete");
This deserialize method is actually a free-standing (non-method) procedure:
return new owned MessageArgs(param_list, payload);
}
/*
* Deserializes a JSON-formatted string to a RequestMsg object, where the
* JSON format is as follows (size is only set for json args. Otherwise, -1):
*
* {"user": "user", "token": "token", "cmd": "cmd", "format": "STRING", "args": "arg1 arg2", "size": "-1"}
*
*/
proc deserialize(ref msg: RequestMsg, request: string) throws {
var newmem = openMemFile();
newmem.writer(locking=false).write(request);
try {
var nreader = newmem.reader(deserializer=new jsonDeserializer(), locking=false);
nreader.readf("%?", msg);
} catch bfe : BadFormatError {
throw new owned ErrorWithContext("Incorrect JSON format %s".format(request),
getLineNumber(),
getRoutineName(),
getModuleName(),
At first, this looks innocent. However, Dyno balked at this, and when I took a closer look, I realized why: this is normally an ambiguous call!
In Dyno and in production, the following program is ambiguous:
proc foo() {}
record R {
proc foo() {}
proc bar() { foo(); }
}
Specifically, we have an ambiguity between a method and a non-method call. This was implemented as part of Shadowing improvements by mppf · Pull Request #22917 · chapel-lang/chapel · GitHub , though I haven't nailed down the specific GitHub issue in which the method-non-method ambiguity was discussed.
So... why does Arkouda compile here? It turns out that the ambiguity error is emitted during scope resolution, but default functions aren't inserted until AFTER scope resolution. So, as a result, all default functions automatically have lower precedence when called with an implicit receiver than standalone procedures .
In other words, this is ambiguous:
proc bar(x, y) {}
record R {
proc bar(x, y) {}
proc foo() {
bar(1, 2);
}
}
(new R()).foo();
But this is not:
proc deserialize(x, y) {}
record R {
/* generated proc deserialize (x, y) {} */
proc foo() {
bar(1, 2);
}
}
(new R()).foo();