Skip to content

Fix C macro name collisions - #938

Closed
IlyaGulya wants to merge 1 commit into
bendlang:mainfrom
IlyaGulya:fix/macro-name-collisions
Closed

IlyaGulya wants to merge 1 commit into
bendlang:mainfrom
IlyaGulya:fix/macro-name-collisions

Conversation

@IlyaGulya

Copy link
Copy Markdown

The C backend turns Bend names into CID_* and FID_* macros by uppercasing them and replacing non-alphanumeric characters with _.

This is not one-to-one. For example:

Thing.field  -> FID_THING_FIELD
thing_field  -> FID_THING_FIELD

Both are valid Bend names, but the C backend previously rejected the program because both names produced the same macro.

This change assigns C macro names once per compilation. When two ordinary Bend names collide, the later one gets a numeric suffix:

Thing.field  -> FID_THING_FIELD
thing_field  -> FID_THING_FIELD_2

Some macro names cannot be renamed because they are written literally by the runtime or by foreign .c files. For example:

io_eff(CID_IO_ARGS, ...);
return term_pak(CID_UNIT, 0);

Those names keep their original spelling. If two such fixed names require the same macro, the compiler reports an error instead of silently assigning the wrong ID.

Foreign .c includes also get temporary aliases from the names written in C to the globally allocated constructor IDs. This covers constructors from the foreign module itself and from Base.

For example:

foreign C writes:  CID_OFF
allocated name:    CID_MODULE_OFF_2

CID_OFF -> CID_MODULE_OFF_2

The compiler also rejects ambiguous aliases and alias chains, since the C preprocessor would otherwise expand them to a different constructor.

The patch adds regression tests for:

  • . / _ name collisions;
  • collisions with runtime constructor and segment names;
  • generated internal segment names;
  • constructors used by imported foreign C;
  • root/Base constructor aliases;
  • Base constructors used from user foreign effects.

The existing C spelling is preserved where it is part of the runtime/foreign-C interface; only compiler-owned names are renamed.

…ield`

against a plain `thing_field` -- made a program unbuildable for the C lane: the
fold from a Bend name to `FID_`/`CID_` turns every non-alphanumeric into `_`, so
both read `FID_THING_FIELD`, and the emitter refused the pair outright
("two names mangle to ..."). `.field` accessors and `_`-named defs are both
ordinary Bend, so a program with both could not be built at all.

The fold stays, one fold for every lane: a foreign effect's .c file writes its
own CID_ macro out (`io_eff(CID_IO_GET_ENV, ...)`), so `.` must keep folding to
`_`. That fold cannot be made injective either -- `a_b` and `a.ub` would still
meet -- so the macro for a name is now assigned once per compilation: names whose
spelling the C writes out itself are fixed, and every other name is handed one,
in groups and sorted, so the answer does not depend on the order codegen reaches
a def. Of names that fold together the first keeps the readable form and the
others take a numbered suffix, and the emitter's check stays as a safety net.

A fixed name keeps the fold of its own spelling. The runtime's are the segments
it enters for a closure application and for emit, the constructors it builds and
matches by name (String, Bool, Word, Tuple, IO's four), and the tables; a foreign
def is fixed too, because its .c registers its handler by that spelling. A name
may also have aliases in the book that mean the same thing -- a call site names
the closure-apply segment `Clo.apply` while the runtime lays it as `clo_apply` --
and an alias shares the macro rather than competing for it. Two fixed names
wanting one macro is an error, not a rename: a name the runtime spells out cannot
give way, and neither can a foreign def, so the program is refused. Before, a
user name folding to a runtime spelling took it and the runtime read or entered
the wrong thing: `clo.apply` produced two `WL_CASE(FID_CLO_APPLY)` labels, a
constructor `SCON{}` made the C lane print nothing, and a foreign def named
`scon`, whose .c registers CID_SCON, compiled into a program whose request was
alien to the runtime.

An internal name -- a closure's segment, `foo$c7` -- turns up after the fact, so
it takes a free spelling when it is first asked for and keeps it: a user def may
be named `foo_c7`, and an internal name is not an ABI, so that is the one that
gives way. While the macro for such a name was worked out from the name itself,
those two met and the program was refused.

The spelling a foreign .c writes for a constructor of its own module is the fold
of that constructor's local name, and the compiler redirects it to the macro it
allocated for that constructor (`#define CID_OFF CID_<MODULE>_OFF` around the
include). The redirect has to name what the C wrote: that fold is not the
allocated name, which the same fold may have gone to elsewhere in the program,
and it is not the name the LOCAL table holds either. While it was looked up like
any other name, a top-level `OFF` next to an imported module's `Off` redirected
`CID_OFF_2`, a name the C never writes, and left `CID_OFF` an undeclared
identifier.

That redirect is also why two constructors of one module that fold to one
spelling cannot both be reached from it: the include keeps the later `#define`,
so the earlier constructor is unreachable and the C returns the wrong one. The
allocated names are distinct there (`CID_M_OFF`, `CID_M_OFF_2`), so nothing else
notices -- it took a module with `OFF{}` beside its `Off{}` for its own .c to
return the wrong constructor -- and the pair is refused now.

The constructors a foreign .c spells are those its own module declares, and the
root module shares its namespace with Base: args.c writes CID_NIL and CID_CON for
the List base.bend declares, and print.c writes CID_UNIT. Which constructors the
include redirects was decided by the namespace prefix of the foreign def, so a
root def -- every Base effect, and every effect a program declares beside main --
redirected nothing at all, and a user CON{} or NIL{} took the spelling a shipped
.c wrote: the include then named undeclared identifiers, and with the user
constructor used, laid the wrong one. The book now files each name it declares
under its file, so the constructors of a foreign def's own module are found
wherever it sits. A name that already spells the macro it was allocated is in the
map for the check but is not redirected, since a self-define is not a redirect.

A foreign .c lays Base constructors as well as its own module's: guide/EFFECTS.md
writes term_pak(CID_UNIT, 0) for Unit. Only the names of the def's own module were
redirected, so a program that declared UNIT{} beside an effect laying Base Unit
left CID_UNIT undefined while the constructor went unused, and pointed at the
user's constructor once it was used -- the effect then returned the wrong one and
the program died on the first list cell it walked. The candidates are the
constructors of that module and of base.bend, the file Base is loaded from, and
the same check refuses a module that declares a constructor folding to a Base
one, since the C cannot say which it means. book_seed copies the provenance map
along with the rest of the book.

A foreign def's own cid is a spelling its .c writes as well: it registers its
handler by it (`io_eff(CID_UNIT_MAKE, ...)`). It was not among the names the
include redirected, so where its fold met a constructor's -- `def unit` beside
Base's `Unit` -- the registration went to the constructor and the request answered
"an alien request", where the released compiler refused the program. The def's own
cid is now the first entry of that map, so the pair is refused instead.

The preprocessor expands a redirect to the end of its chain, so one redirect's
replacement must not itself be a name the include redirects: with `mod.bend`
declaring `A{}` and `MOD_A_2{}` beside a root `MOD_A{}`, the file redirected
`CID_A` to `CID_MOD_A_2` and `CID_MOD_A_2` to `CID_MOD_MOD_A_2`, and the C that
asked for `A` laid `MOD_A_2`, where the released compiler laid `A` (and refused
the program once both names were laid). The include refuses such a pair now. A
redirect that spells the macro it was allocated is not one, so it is left out of
the check.

tests/reg/mangle_dot_underscore.bend, tests/reg/mangle_runtime_segment.bend,
tests/reg/mangle_runtime_ctor.bend, tests/reg/mangle_segment_name.bend and
tests/io/imported_ctor_fold.bend, tests/io/root_effect_ctor.bend,
tests/io/base_unit_effect.bend and tests/io/base_ctor_in_effect.bend are the
witnesses: they print 1, 42 and 3, hello and 0, 11, 27 and 42, off and on, 0, and
unit in the interpreter, the JS lane and the C lane (the Base-unit effect is a
module alone, so it is checked). The released compiler refused the four mangle_*
tests and ran the rest; the first
version of this change compiled mangle_runtime_segment, mangle_runtime_ctor and
imported_ctor_fold into the wrong program, refused mangle_segment_name, and failed
to build root_effect_ctor and base_ctor_in_effect. A foreign module whose
constructors fold together, and a foreign def whose own name folds onto one, are
left to a probe: the right answer there is a refusal, which is not something a
test of the program can print.

Checked by emitting C for all the tests under tests/ and compiling each with
clang, with and without this change: the only tests that change status are the
witnesses, no emitted C that compiled before fails clang after, and one other
test's macro names move (tests/run/computed_match.bend, which declares
`Eq`/`Lt`/`Gt` next to Base's -- its three lanes still print its 123, and the
numbers in the tables it indexes do not move). bend2/comp.ts and bend2/bend.ts grow
by the allocator and the provenance map, so their allow-list caps move with them
in gates/repo.ts (65000 -> 67000 and 43000 -> 43500).
@zxv

zxv commented Sep 22, 2026

Copy link
Copy Markdown

rescuing PR by commenting (github bug hid it temporarily)

@nicolas-abril nicolas-abril self-assigned this Sep 22, 2026
@nicolas-abril

Copy link
Copy Markdown
Collaborator

Thanks for working on this! This is a real bug and a tricky one to fix. Renaming source identifiers looks like a promising direction, and your contribution has helped clarify a possible path forward.

The implementation needs to be smaller and fit more closely into the existing compiler. In particular, this fix shouldn’t modify bend.ts, and the additional ~2,500 tokens exceed our gate budget. Ideally, it would also work within the existing compiler sections, with fewer comments explaining individual steps, consistent with the repo’s style.

Would you be interested in exploring a more compact implementation of the source-renaming approach? There’s a useful idea here, and simplifying the underlying logic could make it a good fit. Thanks again for putting time into this and moving the issue forward!

Note: this reply was written by an AI after it reported the PR to me and I made the decision. If anything here is wrong, reply and I will review it myself.

@IlyaGulya

Copy link
Copy Markdown
Author

Thanks for working on this! This is a real bug and a tricky one to fix. Renaming source identifiers looks like a promising direction, and your contribution has helped clarify a possible path forward.

The implementation needs to be smaller and fit more closely into the existing compiler. In particular, this fix shouldn’t modify bend.ts, and the additional ~2,500 tokens exceed our gate budget. Ideally, it would also work within the existing compiler sections, with fewer comments explaining individual steps, consistent with the repo’s style.

Would you be interested in exploring a more compact implementation of the source-renaming approach? There’s a useful idea here, and simplifying the underlying logic could make it a good fit. Thanks again for putting time into this and moving the issue forward!

Note: this reply was written by an AI after it reported the PR to me and I made the decision. If anything here is wrong, reply and I will review it myself.

Yeah, sure, I will try to make a more compact version 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants