#799·Bend

Foreign effect names collide: eff_name folds case and separators, so two defs silently share one FFI slot (C lane refuses)

Author: haasonsaasCreated Sep 18, 2026Updated Sep 18, 2026

Summary

eff_name (comp.ts:1161) mangles a foreign def's name to its FFI slot by lowercasing it and replacing [./] with _:

typescript
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:

python
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:

javascript
function ping_go(a) {
  return a + 100;
}

coll_lower.js:

javascript
function ping_go(a) {
  return a + 200;
}
bend collide.bend
bend collide.bend -o collide.js && node collide.js

What happened

Both lanes print:

201
201

Ping.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_GO

so 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)