Skip to content

Prefix generated C locals: ts_32 is a host macro on macOS and the native build fails - #926

Closed
nood-co1 wants to merge 1 commit into
bendlang:mainfrom
nood-co1:fix/c-generated-names
Closed

nood-co1 wants to merge 1 commit into
bendlang:mainfrom
nood-co1:fix/c-generated-names

Conversation

@nood-co1

Copy link
Copy Markdown
Contributor

bend prog.bend -o prog fails on macOS arm64 for a program that checks and runs on the JS
lane. name_local (bend2/comp.ts:648) builds every generated C local from the Bend-level
name plus a counter, with no prefix:

function name_local(fl: File, k: Bend.Name): string {
  const base = name_clean(k);          // <- "ts"
  const n = fl.fresh.get(base) ?? 0;
  fl.fresh.set(base, n + 1);
  return base + "_" + n;               // <- "ts_32"
}

so a generated local can land on a macro the platform headers already define. On macOS one
does: ts_32 is #define ts_32 uts.ts_32 in <mach/arm/thread_status.h>, and it is reachable
from Base itself — bend2/base.bend:1459 declares U32.divmod.go.shl(..., ts: Bool & Word(32n)), and compiling it mints ts_0 … ts_32.

Repro (12 lines)

import Base

def snd(p: Word(64n) & U32) -> U32:
  (q, r) = p
  r

def half(w: Word(64n), b: U32) -> U32:
  snd(U32.divmod.go(64n, w, b))

def main() -> IO(Unit):
  do IO<Unit>:
    IO.print(U32.show(half(Word.zero(64n), 10)))
$ bend repro.bend --check-only   # All terms check.
$ bend repro.bend                # the JS lane: 0
$ bend repro.bend -o repro       # native build
…/repro.c:1580:7: error: expected ';' at end of declaration
 1580 |   u32 ts_32 = r34;
/…/MacOSX.sdk/usr/include/mach/arm/thread_status.h:199:18: note: expanded from macro 'ts_32'
  199 | #define ts_32 uts.ts_32
…/repro.c:1582:655: error: member reference base type 'u32' is not a structure or union

The change

 function name_local(fl: File, k: Bend.Name): string {
-  const base = name_clean(k);
+  const base = name_clean(k).replace(/^bend_/, "");
   const n = fl.fresh.get(base) ?? 0;
   fl.fresh.set(base, n + 1);
-  return base + "_" + n;
+  return "bend_" + base + "_" + n;
 }

The strip is required for the case measured, not cosmetic: seg_open (comp.ts:1640) feeds
names it has already minted back through name_local, so a plain prefix accumulates — this
repro, without the strip, emits bend_b_0, bend_bend_b_0, bend_bend_bend_b_0 (12 doubled
occurrences, 5 distinct, longest identifier 18 characters; with the strip, 0 and 11). It closes
the observed path; it is not a general idempotence property.

name_local is shared with the JS emitter, so generated JS identifiers move as well as the C
ones. The JS output is unchanged here: 0 on this repro before and after, and no per-test
difference in the sampled differential below.

Scope: the names name_local mints — locals, parameters, continuations, and the fixed names
(sp, fb, fv, o, x, k, v). Top-level/def naming is untouched, and CID_/FID_ are
already prefixed.

Cost

bend2/comp.ts 64840 → 64852 ttok — a whole-file delta of 12 — against the repo gate's
65000 cap (headroom 160 → 148). I left out a comment line to keep it there; this message carries
the explanation instead. bun gates/repo.ts: PASS 46/46.

The emitted C is larger, because every generated local reference gains 5 characters: 140864 →
153104 bytes (+8.7%) on this 12-line repro. I have not measured that ratio on a large program.

What I checked

check result
native build, before / after rc=1, expected ';' at end of declaration / rc=0, binary prints 0
check (--check-only) and the JS lane, before / after All terms check. and 0, both times
generated C, bare ts_N locals 33 distinct before, 0 after (33 bend_ts_N after)
interpreter differential, whole tests/ corpus identical on all 1395 tests
cross-lane sample, 106 tests with a main 0 of 106 per-test records differ; 51 built on both compiled lanes; the same 2 pre-existing divergences on both sides (comptime/unsafe.bend, the interpreter-only unsafe banner; gfx/window.bend, which needs a display)
bun gates/repo.ts PASS 46/46

gates/test.ts and gates/perf.ts need the SSH cluster and gates/ping.ts cannot run outside the
site project. None ran here, and the local differential above is not a substitute for them.

Not a duplicate

Searching the 920-item issue and PR snapshot, I found no direct duplicate about the C emitter's
generated locals meeting a host macro. The closest items use different mechanisms or layers:
#478 / PR #487 reserves __ for generated function names; #790, #799, #800 and #875 concern
different JS, FFI, or compiler-sentinel naming mechanisms; #904/#908 concern JS import-path
identifiers; and #905/#922 concern checker/parser name collisions.

What this does not claim

  • Not that collisions are impossible after it: bend_ is an ordinary C identifier prefix and a
    foreign header could still define bend_x_0. A reserved prefix would be stronger; this is a
    small change that removes the reproduced failure.
  • Not that the compiled lanes are regression-free: the interpreter differential is complete over
    tests/, the compiled lanes are sampled.
  • Not that ts_32 is the only such name on any SDK: it is the one reachable from Base on this
    host, and the change addresses the class while claiming only the observed instance.

Found by an agent working for @blockbrain_labs

…ld fails

name_local derives the C local from the Bend-level name plus a counter and
no prefix, so a generated binder named `ts` emits `u32 ts_32 = r34;` -- and
on macOS `ts_32` is a macro in <mach/arm/thread_status.h>:

    mach/arm/thread_status.h:199:18: note: expanded from macro 'ts_32'
      199 | #define ts_32 uts.ts_32

The native build then fails with "expected ';' at end of declaration" on a
program that checks and runs on the JS lane. The name is reachable from Base
itself: base.bend:1459 declares U32.divmod.go.shl(..., ts: Bool & Word(32n)),
and compiling it mints ts_0 through ts_32.

The strip first is required for the case measured: seg_open re-opens names it
already minted, so without it this repro emits `bend_b_0`, `bend_bend_b_0`,
`bend_bend_bend_b_0` -- 12 doubled occurrences, 5 distinct, longest 18
characters; with it, 0 and 11. That closes the observed path, not a general
idempotence property.

comp.ts 64840 -> 64852 ttok, a whole-file delta of 12 against the 65000 cap.
VictorTaelin pushed a commit that referenced this pull request Sep 21, 2026
…ot capture it

name_local mints _<name>_<n>: macOS's <mach/arm/thread_status.h> defines ts_32 and ts_64, which Base's U32.divmod reaches with its 33 ts locals, and the native build failed. A name is stripped of a leading _ first, since seg_open feeds minted names back through name_local. Emitted C grows 1-5%. (PR #926, with a one-character prefix)
@VictorTaelin

Copy link
Copy Markdown
Contributor

Merged into 2.0.25 under your authorship, thank you, with one trim: the prefix is a single _ (_ts_32), which closes the same class (every generated local is a block-scope identifier, where _x is not reserved) at +0.9 to +4.6 % of emitted C instead of +8.7 to +23 %. Your 12-line reproduction builds and prints 0. We also found the include chain: <mach-o/dyld.h> for _NSGetExecutablePath pulls thread_status.h, and ts_32/ts_64 are the only lowercase macros it leaks in that shape.

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

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.

2 participants