diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 40f7287fb..accae34d8 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -393,6 +393,38 @@ export function dynamicTierSymbols(names) { * program without a call into Node.js may define any of these. */ export const NODE_ISLAND_PREFIXES = ["scr_node_island", "scr_loop_set_node_island"]; +/** The namespace ICU's own symbols live in after its version-suffixed + * renaming: `icu_78`, which Itanium mangling spells `6icu_78`. */ +export const ICU_CPP_NAMESPACE = "6icu_78"; + +/** The ICU archive: no program that answers its Intl forms without + * ICU — that is, every program whose IR has no `icu.*` libCall — may define + * a symbol from it. The test is exact because ICU builds with symbol + * renaming on: every C API symbol carries the `_78` version suffix + * (`ubrk_open_78`), its data symbol is `icudt78_dat`, and every C++ symbol + * is an Itanium mangling that names namespace `icu_78`, which the mangling + * spells `6icu_78` wherever the name occurs — as the function's own + * namespace (`_ZN6icu_78...`), in its parameter types, in vtables, typeinfo + * and guard variables, or in a libc++ instantiation over an ICU type. No + * symbol outside the archive carries either mark. + * + * scr_intl.c's own functions are deliberately not tested by name. The + * engine-free Intl paths share their prefix (`scr_intl_num_format_en_us` in + * scr_lib.c, `scr_intl_segmenter_new` in scr_grapheme.c), and they must stay + * linkable without ICU (I4). Any scr_intl.c function that does link brings an + * ICU symbol with it, which the test catches. The C++ standard library + * merged into the archive cannot leak on its own either: an archive + * contributes a member only when something references it, and only ICU's + * objects reference libc++. */ +export function icuSymbols(names) { + return [...new Set(names)] + .filter( + (name) => + name.endsWith("_78") || name.includes(ICU_CPP_NAMESPACE) || name.startsWith("icudt78"), + ) + .sort(); +} + /** The Node island symbols among `names`, sorted. */ export function nodeIslandSymbols(names) { return [...new Set(names)] diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index 5b5f51a21..aef7a4e60 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -48,6 +48,7 @@ import { linkedRuntimeFiles, llvmFunctionHashes, median, + icuSymbols, nodeIslandSymbols, nmOutput, parseNm, @@ -379,6 +380,10 @@ function dynamicTierCheck(program, baseline, candidate, artifacts, allowed) { const nodeIsland = nodeIslandSymbols(definedFunctions(candidate.exe)); if (nodeIsland.length > 0 && !allowed) failures.push(`links Node island symbols: ${nodeIsland.slice(0, 5).join(", ")}`); + // Intl over ICU is linked only into programs whose IR lowers to it. A + // program that legitimately calls it names the pattern in --allow. + const icu = icuSymbols(definedFunctions(candidate.exe)); + if (icu.length > 0 && !allowed) failures.push(`links ICU symbols: ${icu.slice(0, 5).join(", ")}`); if (typed && after.length > 0) failures.push( `fully typed program links dynamic-tier symbols: ${after.slice(0, 5).join(", ")}`, diff --git a/tests/harness/zero-cost-icu-symbols.test.ts b/tests/harness/zero-cost-icu-symbols.test.ts new file mode 100644 index 000000000..ee9fac2a1 --- /dev/null +++ b/tests/harness/zero-cost-icu-symbols.test.ts @@ -0,0 +1,56 @@ +// The symbol classifier the zero-cost gate uses to prove that a program which +// does not use Intl links no part of the ICU archive. The samples +// are the shapes `nm` printed for the archive-linked binaries: ICU's C API +// with its `_78` version suffix, its data symbol, and the several Itanium +// manglings of names in namespace icu_78 — own namespace, parameters only, +// internal linkage, guard variables, vtables and typeinfo. +import { describe, expect, it } from "vitest"; + +import { icuSymbols } from "../../scripts/zero-cost-compare.mjs"; + +const ICU_SYMBOLS = [ + "ubrk_open_78", + "unum_open_78", + "u_strToUTF8_78", + "icudt78_dat", + "_ZN6icu_7813UnicodeString6appendEPKDsi", + "_ZNK6icu_7824DateTimePatternGenerator17getDateTimeFormatE16UDateFormatStyleR10UErrorCode", + "_Z24ulocimp_toLanguageTag_78PKcRN6icu_788ByteSinkEbR10UErrorCode", + "_ZL10createPathPKciS0_iS0_RN6icu_7810CharStringEP10UErrorCode", + "_ZGVZN6icu_7823IslamicUmalquraCalendar19handleComputeFieldsEiR10UErrorCodeE14kUmalquraStart", + "_ZTWN6icu_7810CharStringE", + "_ZTVN6icu_7813UnicodeStringE", + "_ZTIN6icu_7813UnicodeStringE", + "_ZTSN6icu_7813UnicodeStringE", + // A libc++ instantiation over an ICU type exists only inside the archive. + "_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6appendERN6icu_7810CharStringE", +]; + +// The engine-free Intl paths share the `scr_intl_` prefix and must stay +// linkable without ICU (grapheme segmentation, the en-US number fast path), +// so the classifier does not key on it. +const NON_ICU_SYMBOLS = [ + "main", + "scr_intl_num_format_en_us", + "scr_intl_segmenter_new", + "scr_intl_segmenter_refusing", + "scr_dyn_call", + "scr_string_concat", +]; + +describe("icuSymbols", () => { + it("classifies every ICU archive symbol", () => { + expect(icuSymbols(ICU_SYMBOLS)).toEqual([...ICU_SYMBOLS].sort()); + }); + + it("leaves the engine-free runtime alone", () => { + expect(icuSymbols(NON_ICU_SYMBOLS)).toEqual([]); + }); + + it("deduplicates and sorts", () => { + expect(icuSymbols(["ubrk_close_78", "main", "ubrk_close_78", "icudt78_dat"])).toEqual([ + "icudt78_dat", + "ubrk_close_78", + ]); + }); +});