Foreign effect names collide: eff_name folds case and separators, so two defs silently share one FFI slot (C lane refuses)
Summary
eff_name (comp.ts:1161) mangles a foreign def's name to its FFI slot by
lowercasing it and replacing [./] with _:
function eff_name(k: Bend.Name): string {
return (LOCAL.get(k) ?? k).toLowerCase().replace(/[./]/g, "_");
}That map is not injective. Ping.go and ping.Go both become ping_go, as do
foo/bar.baz and foo.bar.baz. js_lib builds the $0eff table by emitting one
row per foreign def keyed on exactly that string, with no duplicate check, so two
distinct checked defs land on one object key and the later row wins. Both defs then
dispatch to one implementation, silently.
The C lane refuses the same program (compile_tables, comp.ts:2751). The
interpreter and the JS lane do not.
This is the collision class of #790, but at a different boundary: #790 is about
js_sat ([./~] -> $, case-preserving) and the def table. eff_name is a
separate mangler feeding a separate table, and it additionally folds case, so
making js_sat injective would not close this. Note also that eff_name resolves
through LOCAL (comp.ts:1392, via name_own comp.ts:612), which substitutes the
name as written in the def's own source, so the colliding pair need not share a
namespace in the canonical names.
What you did
collide.bend:
import Base
law Ping.go:
for a: U32
IO(U32)
def Ping.go(a):
import "./coll_upper.js"
law ping.Go:
for a: U32
IO(U32)
def ping.Go(a):
import "./coll_lower.js"
law main:
IO(Unit)
def main():
do IO<Unit>:
x : U32 <- Ping.go(1)
u : Unit <- IO.print(U32.show(x))
y : U32 <- ping.Go(1)
IO.print(U32.show(y))coll_upper.js:
function ping_go(a) {
return a + 100;
}coll_lower.js:
function ping_go(a) {
return a + 200;
}bend collide.bend
bend collide.bend -o collide.js && node collide.jsWhat happened
Both lanes print:
201
201Ping.go runs ping.Go's implementation. Expected 101 then 201.
Control: renaming the second def to ping.Zo and its function to ping_zo
removes the collision, and both lanes then print 101 / 201 correctly. The
divergence tracks the mangled name, not the program.
C lane, with .c imports added alongside the .js ones, refuses to build:
Error: two names mangle to CID_PING_GOso this is the C-refuses / JS-silently-accepts asymmetry #790 describes, one layer down.
Expected
Either reject the program the way the C lane does -- a duplicate check over the
emitted $0eff keys, mirroring compile_tables -- or make eff_name injective.
Dropping the .toLowerCase() would shrink the collision set but not close it,
since . and / still both map to _.
Version / system
- bend 2.0.5 (main @ 0b7e2b1)
- Darwin arm64
- Apple clang version 21.0.0 (clang-2100.3.34.2)
Source: HigherOrderCO/Bend