Repository navigation
Effect C names its own definitions with HID, and a module that uses HID must (#1533) - #2017
Lorenzobattistela wants to merge 3 commits into
Conversation
Behavior-preserving simplifications to make room under comp.ts's 66k cap: term_use takes a term, single-use helpers inlined (arr_leaf, val_brw), switch defaults return after the switch, set "seen" checks use size, empty checks use !length, Seg flags optional, type and table declarations packed. The C and JS emitted for every test are byte-identical to main's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ses it must (#1533) Two versions of one hub package paste the same C into one file, so `-o` failed with `redefinition` while the interpreter ran. HID(name) is now a C id of the importing module. An imported module whose C says HID (outside comments and strings) pastes it twice, the second copy dead (constructor and destructor attributes become __unused__), so a name defined without HID fails the author's own build instead of the first program that meets two versions: `redefinition`, or, under -Wtentative-definition-compat made an error there, a repeated global without an initializer. Each copy push/pops the macros it defines, so include guards and macros need no HID. C without HID builds as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every name Base's effect sources define at file scope (functions, the effects and their constructors, globals, types and tags, enum constants, the BendView class) is now HID(name), so bend2/effs is the reference a package's C copies, and a package's functions, globals and types cannot clash with Base's. Base's macros keep their global names, as before. The gfx tests that reach into window.c's internals say HID too; Base and the program's own file share a namespace, so HID(BendWin) there is Base's. EFFECTS.md's examples use HID. No runtime or emitter change; window_dev the CUDA kernel is the runtime's and keeps its name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42ba453 to
1b42dc1
Compare
nicolas-abril
left a comment
There was a problem hiding this comment.
All issues are in the dead-copy logic in compile_book/effect_src (cf0d171). I found nothing wrong in the refactor(comp) commit or the Base HID conversion. I reproduced the tentative-definition error with Homebrew clang 22 and the unknown-warning message with Apple clang 17. The rest come from reading the code.
| .join("") + `\n#pragma clang diagnostic ${d}\n`; | ||
| return at("push") + c_ids(src, m) + at("pop") + at("push") | ||
| + dead.replace(/\S+/g, "#define $& __unused__\n") | ||
| + `#pragma clang diagnostic error "-Wtentative-definition-compat"\n` |
There was a problem hiding this comment.
Making -Wtentative-definition-compat an error in the dead copy also rejects valid HID code: a static global declared first and defined later.
static const struct N HID(a);
static const struct N HID(b) = {&HID(a)};
static const struct N HID(a) = {&HID(b)};Copy 1 compiles. In copy 2, Homebrew clang 22 reports error: duplicate declaration of 'HID2_..._a' is invalid in C++, so a correct package can't be built natively.
Separately, this pragma isn't guarded. Clang before 21, including Apple clang 17 (the macOS default clang, which cc_find picks first), prints warning: unknown warning group '-Wtentative-definition-compat', ignored once per HID module on every -o build. The check then does nothing. Wrapping it in #if __has_warning("-Wtentative-definition-compat") fixes that.
| ...src.match(/^[ \t]*#[ \t]*(define|undef)[ \t]+\w+/gm) ?? []] | ||
| .map((x) => `\n#pragma ${d}_macro("${x.split(/\s/).pop()}")`) | ||
| .join("") + `\n#pragma clang diagnostic ${d}\n`; | ||
| return at("push") + c_ids(src, m) + at("pop") + at("push") |
There was a problem hiding this comment.
push_macro saves an include guard's earlier state but never #undefs it. If the guard is already defined, the HID copy skips its own guarded body.
Example: a program imports bend-kit-time v1 (no HID, #ifndef TIME_EFFS guard) and v2 (HID, same guard, since the guide says guards don't need HID). v1 is in the non-HID part, so it's pasted first and defines TIME_EFFS. v2's copy then skips its own helpers and fails with an undeclared HID_..._helper. If the guarded block only has macros, v2 silently uses v1's, which is #1533 again. Emitting #undef NAME after each push_macro fixes it.
| const src = FL.js ? raw : raw.replace(/\\[ \t]*(\r\n?|\n)/g, ""); | ||
| FL.effs.set(path, [m, src]); | ||
| if (!FL.js) c_ids(src, m, (p, q) => { | ||
| if (p === "HID" && m) FL.hids.add(m); |
There was a problem hiding this comment.
This skips HID enforcement when the root module imports the package's C (m === ""). So the author's own build never catches a bare name, contrary to the PR's claim. A package author who runs bend -o out lib.bend on the file that does import "./lib.c" (like effect_version_a.bend) gets no dead copy, and a bare static int count; passes. The clash only shows up when a user imports two versions.
| return t; | ||
| } | ||
| if (p === "HID") { | ||
| return use ? (use(p, k), t) : name_id(h, m + ":" + k); |
There was a problem hiding this comment.
HID(k) always resolves in the importing module's namespace, so a package's C can't name Base's effect internals, which are now HID-renamed, or another package's.
A hub package whose C extends windows or channels by naming Base's BendWin, WinQueue or a chan.c helper breaks after the Base conversion. The bare name is undeclared, and HID(BendWin) in the package gives HID_pkg_BendWin, not Base's HID__BendWin. Only the root file (namespace "") can reach Base, so there's no supported way to depend on another module's C.
| const reqs = effs.map(([m, s]) => FL.hids.has(m) ? "" : c_ids(s, m)).join("") | ||
| + [...FL.hids].map((m) => { | ||
| const src = effs.filter((e) => e[0] === m).map((e) => e[1]).join("\n"); | ||
| const dead = "constructor destructor __constructor__ __destructor__"; |
There was a problem hiding this comment.
The dead copy only neutralizes constructor and destructor attributes. Objective-C in it still runs or registers at image load: +load methods run and categories attach.
Example: a macOS package with an #ifdef __OBJC__ path defines @implementation NSWindow (HID(Ext)) or a class with +load. The dead copy's +load runs at startup a second time. Its category methods share copy 1's selectors and may win, and they read HID2_ globals whose constructor never ran.
| return at("push") + c_ids(src, m) + at("pop") + at("push") | ||
| + dead.replace(/\S+/g, "#define $& __unused__\n") | ||
| + `#pragma clang diagnostic error "-Wtentative-definition-compat"\n` | ||
| + c_ids(src, m, undefined, "HID2_") + at("pop"); |
There was a problem hiding this comment.
The dead copy keeps every external-linkage function and the data it reaches, so a package's initialized tables end up twice in every consuming binary. For example, a non-static Term HID(run)(...) that reads a 2 MB static const u8 HID(font)[] gets a second, unused 2 MB HID2_ copy, and -O3 can't drop it because the HID2_ entry point has external linkage. The 136-byte cost in the PR description only holds for tiny packages.
| // (no constructor or destructor), so a name defined without HID clashes | ||
| // in its own build; each copy keeps its macros. | ||
| const effs = [...FL.effs.values()]; | ||
| const reqs = effs.map(([m, s]) => FL.hids.has(m) ? "" : c_ids(s, m)).join("") |
There was a problem hiding this comment.
Non-HID effect sources are still joined with "" (the HID path uses "\n"), so a .c file without a trailing newline merges into the next one. #endif followed by Term foo(...) becomes #endifTerm foo(...), an invalid directive. If the first file ends in a // comment, the next file's first line becomes part of the comment.
| }).join("\n\n"); | ||
| const reqs = [...FL.effs.values()].map(([m, src]) => c_ids(src, m)).join(""); | ||
| // An imported module whose C says HID pastes it twice, the second dead | ||
| // (no constructor or destructor), so a name defined without HID clashes |
There was a problem hiding this comment.
Design: this enforces an author-side lint by compiling every HID package's C twice in every downstream build. That costs compile time and binary size, needs clang 21, and still misses Base and root. It also brings the issues above: ObjC load side effects, no constructor priorities, and false tentative-definition errors. All to catch a mistake only the publisher can fix.
The same check, run once when a package is tested or published on the hub, or as a TS-side scan of top-level definitions, would enforce the rule with no per-build cost.
| + [...FL.hids].map((m) => { | ||
| const src = effs.filter((e) => e[0] === m).map((e) => e[1]).join("\n"); | ||
| const dead = "constructor destructor __constructor__ __destructor__"; | ||
| const at = (d: string) => [...dead.split(" "), |
There was a problem hiding this comment.
at() reruns the #define/#undef regex over the whole module source and re-splits dead on each of its four calls per module. Computing const names = [...dead, ...macros] once per module and mapping it to push and pop avoids that. Making dead an array also removes the split and the replace.
Fixes #1533 for packages that adopt
HID; supersedes #1546.Problem
-opastes every effect.cinto one C file. A program that imports two versions of one hub package pastes the same C twice, so clang refuses the second definition (redefinition of 'wall_raw_run'), whilebend main.bendruns fine. In the realbend-kit-timecase it is also silently wrong: the helper sits behind#ifndef TIME_EFFS, so version 2 would quietly use version 1's helper.Fix
HID(name)in an effect's C is a C id of the module that imports it (c_ids, same pass asCID/FID). The key is the module, not the file:bend_tensorsshares a guard and a struct across five.cfiles of one module.HID(outside comments and strings, found ineffect_src's existing scan) is held to it.compile_bookpastes its C twice, and the second copy is dead:constructor/destructorbecome__unused__, and-Wtentative-definition-compatis an error inside it. A name defined withoutHIDtherefore fails the author's own build, instead of the first program that meets two versions.HID.HID, Base, and the root module are emitted exactly as before.Cost: in the two-version
bend-kit-timebuild, the dead copy adds 136 bytes to the binary and no measurable build time. Runtime is unchanged.Commits
refactor(comp): sheds 386 ttok fromcomp.ts. The C and JS emitted for every test are byte-identical to main's.fix(effects): the feature,tests/io/effect_versions(two versions with the same names, each keeping its own counter: 8 71 9 72 in the interpreter, JS and native), and a paragraph inguide/EFFECTS.md.refactor(effs): Base's own effect C (bend2/effs/*.c, 281 names) usesHID, so the reference packages copy shows the rule, and a package's names cannot clash with Base's. The threetests/gfxC files that reach intowindow.cinternals sayHIDtoo. Base and the program's own file share namespace"", soHID(BendWin)there is Base's. Base's macros keep their names. WithHIDundone, the diff is token-identical to main, including the Linux X11/ALSA and CUDA paths.comp.tsends at 65,987 / 66k.Verified
bend-kit-timeversions withHIDadopted build and run natively together.HID(helper, global, entry, constructor, tentative global) fails the build, even with a single version imported.push_;__constructor__/destructors in the dead copy;HID(in a comment; externalHID_*macros; a package's ownunusedmacro.-DBEND_METAL=1(the 44!programs), and on Debian clang 14 with X11/XI/ALSA headers. A throwaway compiler that forces the dead copy on Base reports no unwrapped Base definition on either OS, and covers all 281 names.Limits
HIDand republish.HIDmodule's constructors cannot take a priority.""is shared with the program's own file, which must stay free to skipHID), so a future bare name inbend2/effsis not caught by the build.🤖 Generated with Claude Code