diff --git a/.github/workflows/build-reusable.yml b/.github/workflows/build-reusable.yml index cceabc11ff5c..9cfcdaa7f515 100644 --- a/.github/workflows/build-reusable.yml +++ b/.github/workflows/build-reusable.yml @@ -718,33 +718,6 @@ jobs: while read -r name; do mv "dl/$name/bun-webkit.tar.gz" "out/$name.tar.gz"; done < artifacts.txt ls -la out - # A bytecode cache payload written on one platform is read in place on the others (bun build --compile - # cross-compiles), so the records it is made of must be laid out identically everywhere. Every Docker-built - # archive carries its layouts (Tools/Scripts/dump-bytecode-cache-layout.ts); any difference from linux-amd64, or a - # missing dump, fails the release. The one native build (windows-arm64-debug) has no dump and is not compared. - - name: Bytecode cache layouts agree across platforms - if: false # each image still writes bytecode-cache-layout.txt; the cross-platform comparison moves to its own fast job in a follow-up - run: | - set -euo pipefail - mkdir -p layouts - status=0 - while read -r name; do - [ "$name" = bun-webkit-windows-arm64-debug ] && continue - if ! tar -xzf "out/$name.tar.gz" -O bun-webkit/bytecode-cache-layout.txt > "layouts/$name.txt"; then - echo "::error::$name has no bytecode-cache-layout.txt" - status=1 - fi - done < artifacts.txt - for f in layouts/*.txt; do - if ! diff -u layouts/bun-webkit-linux-amd64.txt "$f" > "$f.diff"; then - echo "::error::bytecode cache record layouts in $(basename "$f" .txt) differ from linux-amd64" - head -50 "$f.diff" - status=1 - fi - done - echo "$(ls layouts/*.txt | wc -l) platforms compared" - exit $status - - name: Set release info id: release_info run: | diff --git a/Dockerfile b/Dockerfile index 5ebd695e7df2..8dff2f78e788 100644 --- a/Dockerfile +++ b/Dockerfile @@ -308,7 +308,6 @@ RUN --mount=type=tmpfs,target=/webkitbuild \ cd /webkitbuild && \ cmake --build /webkitbuild --config $WEBKIT_RELEASE_TYPE --target "jsc" --target "testFFI" && \ python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \ - node --experimental-strip-types /webkit/Tools/Scripts/dump-bytecode-cache-layout.ts /webkitbuild > /output/bytecode-cache-layout.txt && \ cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \ cp $WEBKIT_OUT_DIR/*.h /output/include && \ cp -r $WEBKIT_OUT_DIR/bin /output/bin && \ diff --git a/Dockerfile.android b/Dockerfile.android index 8855d167cbdf..88b403c094e4 100644 --- a/Dockerfile.android +++ b/Dockerfile.android @@ -86,13 +86,6 @@ RUN mkdir -p /output/lib /output/include /output/include/JavaScriptCore /output/ # etc.) at build time, so a host build is required first; the target build # points at it via --with-cross-build. # ─────────────────────────────────────────────────────────────────────────── - -# Node — only used to run Tools/Scripts/dump-bytecode-cache-layout.ts after the build. -ARG NODE_VERSION=24.16.0 -RUN wget -qO- "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tar.xz" \ - | tar -xJ -C /usr/local --strip-components=1 && \ - node --version - FROM base AS build_icu ARG MARCH_FLAG @@ -214,7 +207,6 @@ RUN --mount=type=tmpfs,target=/webkitbuild \ /webkit && \ cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \ python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \ - node --experimental-strip-types /webkit/Tools/Scripts/dump-bytecode-cache-layout.ts /webkitbuild > /output/bytecode-cache-layout.txt && \ cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \ cp $WEBKIT_OUT_DIR/*.h /output/include && \ cp -r $WEBKIT_OUT_DIR/bin /output/bin && \ diff --git a/Dockerfile.freebsd b/Dockerfile.freebsd index 33b875f5ddb9..05bf38e59e8d 100644 --- a/Dockerfile.freebsd +++ b/Dockerfile.freebsd @@ -75,13 +75,6 @@ RUN mkdir -p /output/lib /output/include /output/include/JavaScriptCore /output/ # ICU — cross-compiled. Same two-stage host/target build as Android: host # tools first, then --with-cross-build. # ─────────────────────────────────────────────────────────────────────────── - -# Node — only used to run Tools/Scripts/dump-bytecode-cache-layout.ts after the build. -ARG NODE_VERSION=24.16.0 -RUN wget -qO- "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tar.xz" \ - | tar -xJ -C /usr/local --strip-components=1 && \ - node --version - FROM base AS build_icu ARG MARCH_FLAG @@ -206,7 +199,6 @@ RUN --mount=type=tmpfs,target=/webkitbuild \ /webkit && \ cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \ python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \ - node --experimental-strip-types /webkit/Tools/Scripts/dump-bytecode-cache-layout.ts /webkitbuild > /output/bytecode-cache-layout.txt && \ cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \ cp $WEBKIT_OUT_DIR/*.h /output/include && \ cp -r $WEBKIT_OUT_DIR/bin /output/bin && \ diff --git a/Dockerfile.macos b/Dockerfile.macos index 388371018fb6..a613843c98d6 100644 --- a/Dockerfile.macos +++ b/Dockerfile.macos @@ -231,7 +231,6 @@ RUN --mount=type=tmpfs,target=/webkitbuild \ /webkit && \ cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \ python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \ - bun /webkit/Tools/Scripts/dump-bytecode-cache-layout.ts /webkitbuild > /output/bytecode-cache-layout.txt && \ # -L everywhere: WebKit's cmake stages the WTF/bmalloc framework headers # as *symlinks* into the source tree when the host filesystem supports # them. Copied verbatim they'd dangle outside this container (pointing at diff --git a/Dockerfile.musl b/Dockerfile.musl index a857975a447b..8dd04397fa80 100644 --- a/Dockerfile.musl +++ b/Dockerfile.musl @@ -101,7 +101,7 @@ ARG DEFAULT_CFLAGS ARG USE_MIMALLOC ARG USE_EXTERNAL_MIMALLOC -RUN apk add --no-cache cpio curl file gnupg ninja nodejs ruby ruby-getoptlong unzip rsync perl python3 openssl-dev openssl linux-headers +RUN apk add --no-cache cpio curl file gnupg ninja ruby ruby-getoptlong unzip rsync perl python3 openssl-dev openssl linux-headers ENV WEBKIT_OUT_DIR=/webkitbuild # These are unnecessary on musl @@ -143,7 +143,6 @@ RUN --mount=type=tmpfs,target=/webkitbuild \ cd /webkitbuild && \ cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target "jsc" --target "testFFI" && \ python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \ - node --experimental-strip-types /webkit/Tools/Scripts/dump-bytecode-cache-layout.ts /webkitbuild > /output/bytecode-cache-layout.txt && \ cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \ cp $WEBKIT_OUT_DIR/*.h /output/include && \ cp -r $WEBKIT_OUT_DIR/bin /output/bin && \ diff --git a/Dockerfile.windows b/Dockerfile.windows index 8a99b66576b4..c344daedbefd 100644 --- a/Dockerfile.windows +++ b/Dockerfile.windows @@ -325,7 +325,6 @@ ARG USE_MIMALLOC ARG USE_EXTERNAL_MIMALLOC COPY --from=build_icu /icu-out /icu-out -COPY --from=build_icu /usr/local/bin/node /usr/local/bin/node COPY . /webkit WORKDIR /webkit @@ -412,11 +411,9 @@ RUN --mount=type=tmpfs,target=/webkitbuild \ "-DCMAKE_CXX_FLAGS_RELEASE=/O2 /Ob2 /DNDEBUG ${LTO_FLAG}" \ "-DCMAKE_C_FLAGS_DEBUG=/FS /O0 /Ob0" \ "-DCMAKE_CXX_FLAGS_DEBUG=/FS /O0 /Ob0" \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -G Ninja \ /webkit && \ cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \ - mkdir -p /output && node --experimental-strip-types /webkit/Tools/Scripts/dump-bytecode-cache-layout.ts /webkitbuild > /output/bytecode-cache-layout.txt && \ \ mkdir -p /output/lib /output/include/JavaScriptCore /output/include/wtf /output/include/bmalloc && \ cp -rL $WEBKIT_OUT_DIR/lib/* /output/lib/ && \ diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp index 6c163bf7f36e..c00f5630d9cf 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp @@ -1694,6 +1694,10 @@ RegisterID* BytecodeGenerator::addConstantValue(JSValue v, SourceCodeRepresentat if (sourceCodeRepresentation == SourceCodeRepresentation::Double && v.isInt32()) v = jsDoubleNumber(v.asNumber()); + // A NaN the parser folded (0 / 0) has whatever bits this CPU's arithmetic produces (the sign differs between x86 and + // ARM); the constant, and so the bytecode, should not depend on that. + if (v.isDouble() && std::isnan(v.asDouble())) + v = jsNaN(); EncodedJSValueWithRepresentation valueMapKey { JSValue::encode(v), sourceCodeRepresentation }; JSValueMap::AddResult result = m_jsValueMap.add(valueMapKey, m_nextConstantOffset); if (result.isNewEntry) { diff --git a/Source/JavaScriptCore/runtime/CachedTypes.cpp b/Source/JavaScriptCore/runtime/CachedTypes.cpp index 085688f8ba32..5cd9210534ed 100644 --- a/Source/JavaScriptCore/runtime/CachedTypes.cpp +++ b/Source/JavaScriptCore/runtime/CachedTypes.cpp @@ -60,6 +60,22 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN +// A payload written on one platform is read in place on another, so the records below must be laid out identically under +// the Itanium and MSVC C++ ABIs. Their bit-fields are the likeliest thing to break that: make the compiler refuse any +// bit-field in this file that MSVC would pack differently. +#if defined(__has_warning) +#if __has_warning("-Wms-bitfield-padding") +#pragma clang diagnostic error "-Wms-bitfield-padding" +#endif +#endif + +// Everything placed in a payload must have no padding bytes and no unused bit-field bits on the ABI compiling this. The +// C++ ABIs a payload moves between all place bases and fields in declaration order and differ only in where they pad, so a +// type that is padding-free under each of them has the same field offsets under all of them. +// (Hence the m_unused members below: padding, spelled out.) double only fails the trait because +0/-0 and NaNs have +// several representations; its layout is IEEE-754 binary64 everywhere. +template concept PayloadType = std::has_unique_object_representations_v || std::is_same_v; + namespace JSC { bool Decoder::canBorrowPayload() const @@ -545,7 +561,7 @@ class Encoder { return malloc(size, alignment); } - template + template T* malloc(Args&&... args) { return new (malloc(sizeof(T), alignof(T)).buffer()) T(std::forward(args)...); @@ -559,6 +575,7 @@ class Encoder { tail = T::tailSize(*this, source); else if constexpr (requires { T::tailSize(source); }) tail = T::tailSize(source); + static_assert(PayloadType); return new (malloc(sizeof(T) + tail, alignof(T)).buffer()) T(); } @@ -1115,6 +1132,7 @@ class VariableLengthObject : public CachedObject, VariableLengthObjectBa #endif T* allocate(Encoder& encoder, unsigned size = 1) { + static_assert(PayloadType); uint8_t* result = allocate(encoder, sizeof(T) * size, alignof(T)); ASSERT(!(std::bit_cast(result) % alignof(T))); return new (result) T[size]; @@ -1147,6 +1165,7 @@ class VariableLengthObject : public CachedObject, VariableLengthObjectBa tail = T::tailSize(encoder, source); else if constexpr (requires { T::tailSize(source); }) tail = T::tailSize(source); + static_assert(PayloadType); uint8_t* result = allocate(encoder, sizeof(T) + tail, alignof(T)); return new (result) T(); } @@ -1947,7 +1966,7 @@ class CachedBitVector : public VariableLengthObject { public: void encode(Encoder& encoder, const BitVector& bitVector) { - m_numBits = bitVector.size(); + m_numBits = safeCast(bitVector.size()); if (!m_numBits) return; size_t sizeInBytes = BitVector::byteCount(m_numBits); @@ -1965,7 +1984,7 @@ class CachedBitVector : public VariableLengthObject { } private: - size_t m_numBits; + uint32_t m_numBits; }; template> @@ -2060,6 +2079,7 @@ class CachedCodeBlockRareData : public CachedObject CachedVector, IdentifierRepHash>> m_constantIdentifierSets; unsigned m_needsClassFieldInitializer : 1; unsigned m_privateBrandRequirement : 1; + unsigned m_unused : 30 { 0 }; }; // [u32 numberOfEncodedInfo][u8 flags][varint chapters][varint extensions][pad to 4][payload words][u32 checksum if flagged] @@ -2136,7 +2156,19 @@ class CachedExpressionInfo : public CachedObject { }; static_assert(sizeof(CachedExpressionInfo) == sizeof(uint32_t) && alignof(CachedExpressionInfo) == 4); -typedef CachedHashMap>, PrivateNameEntry, IdentifierRepHash, HashTraits>, PrivateNameEntryHashTraits> CachedPrivateNameEnvironment; +// VariableEnvironmentEntry and PrivateNameEntry are 16 bits; held in 32 so the pairs that hold them have no padding. +template +class CachedEntryBits : public CachedObject { +public: + void encode(Encoder&, const Entry& entry) { m_bits = std::bit_cast(entry); } + void decode(Decoder&, Entry& entry) const { entry = std::bit_cast(static_cast(m_bits)); } + Entry decode(Decoder&) const { return std::bit_cast(static_cast(m_bits)); } + +private: + uint32_t m_bits; +}; + +typedef CachedHashMap>, CachedEntryBits, IdentifierRepHash, HashTraits>, PrivateNameEntryHashTraits> CachedPrivateNameEnvironment; class CachedVariableEnvironmentRareData : public CachedObject { public: @@ -2182,7 +2214,8 @@ class CachedVariableEnvironment : public CachedObject { private: bool m_isEverythingCaptured; bool m_hasAwaitUsingDeclaration; - CachedInlineMap>, VariableEnvironmentEntry, VariableEnvironment::inlineMapCapacity, IdentifierRepHash, HashTraits>, VariableEnvironmentEntryHashTraits> m_map; + uint8_t m_unused[2] { }; + CachedInlineMap>, CachedEntryBits, VariableEnvironment::inlineMapCapacity, IdentifierRepHash, HashTraits>, VariableEnvironmentEntryHashTraits> m_map; CachedPtr m_rareData; }; @@ -2374,6 +2407,7 @@ class CachedSymbolTable : public CachedObject { unsigned m_usesSloppyEval : 1; unsigned m_nestedLexicalScope : 1; unsigned m_scopeType : 3; + unsigned m_unused : 27 { 0 }; CachedPtr m_arguments; CachedPtr m_rareData; }; @@ -2415,6 +2449,7 @@ class CachedImmutableButterfly : public CachedObject { private: IndexingType m_indexingType; + uint8_t m_unused[3] { }; unsigned m_length; union { CachedArray m_cachedDoubles; @@ -2439,6 +2474,7 @@ class CachedRegExp : public CachedObject { private: CachedString m_patternString; OptionSet m_flags; + uint8_t m_unused[2] { }; }; class CachedTemplateObjectDescriptor : public CachedObject { @@ -2496,6 +2532,7 @@ class CachedBigInt : public VariableLengthObject { private: unsigned m_length; bool m_sign; + uint8_t m_unused[3] { }; }; // A constant is a kind byte and a 4-byte slot; the owner keeps the kinds in a parallel array (CachedJSValuePool). Small @@ -2744,6 +2781,7 @@ static ptrdiff_t encodeArrayForTail(Encoder& encoder, const Container& container encoder.registerArray(hash, result.offset(), bytes.size()); return result.offset(); } else { + static_assert(PayloadType); auto result = encoder.malloc(sizeof(T) * size, alignof(T)); T* buffer = new (result.buffer()) T[size]; for (unsigned i = 0; i < size; ++i) @@ -2826,6 +2864,7 @@ class CachedSourceProviderShape : public CachedObject { CachedString m_sourceMappingURLDirective; CachedTextPosition m_startPosition; SourceTaintedOrigin m_sourceTaintedOrigin; + uint8_t m_unused[3] { }; }; class CachedStringSourceProvider : public CachedSourceProviderShape { @@ -2977,6 +3016,7 @@ class CachedSourceProvider : public VariableLengthObject { private: SourceProviderSourceType m_sourceType; + uint8_t m_unused[3] { }; }; template @@ -3091,6 +3131,7 @@ class CachedClassElementDefinition : public CachedObject m_initializerPosition; uint8_t m_kind; + uint8_t m_unused[3] { }; }; // A header word of presence bits, then only the members that are set: the three vectors (4-byte aligned), then the @@ -4404,6 +4445,7 @@ auto CachedCodeBlock::create(Encoder& encoder, const CodeBlockTyp // The children's slots are part of this block's bytes; the records they point at are written after the region. auto allocateSlots = [&](unsigned count) { auto result = encoder.malloc(sizeof(CachedWriteBarrier) * count, alignof(CachedWriteBarrier)); + static_assert(PayloadType>); new (result.buffer()) CachedWriteBarrier[count]; return result.offset(); }; @@ -4413,6 +4455,7 @@ auto CachedCodeBlock::create(Encoder& encoder, const CodeBlockTyp layout.flags |= LayoutHasExtras; auto result = encoder.malloc(sizeof(CachedCodeBlockExtras), alignof(CachedCodeBlockExtras)); layout.extrasAt = safeCast(result.offset() - regionStart); + static_assert(PayloadType); (new (result.buffer()) CachedCodeBlockExtras())->encode(encoder, codeBlock); } @@ -4428,6 +4471,7 @@ auto CachedCodeBlock::create(Encoder& encoder, const CodeBlockTyp packScalars(codeBlock, writer); return sizeof(Record) + writer.size() + trailerBytes; }); + static_assert(PayloadType); Record* record = new (result.buffer()) Record(); writer.copyTo(record->tailBytes()); ptrdiff_t trailerOffset = result.offset() + sizeof(Record) + writer.size(); @@ -4793,6 +4837,66 @@ bool isCachedBytecodeStillValid(VM& vm, Ref cachedBytecode, cons return cachedEntry->isStillValid(decoder.get(), key, tagFromSourceCodeType(type)); } + +// The size of every record under every ABI we build (see PayloadType). Changing a record means changing its number here, +// and with it the serialized form. +static_assert(sizeof(GenericCacheEntry) == 24); +static_assert(sizeof(CacheEntry) == 56); +static_assert(sizeof(CacheEntry) == 56); +static_assert(sizeof(BuiltinFunctionCacheEntry) == 36); +static_assert(sizeof(VariableLengthObjectBase) == 4); +static_assert(sizeof(CachedPtr) == 4); +static_assert(sizeof(CachedRefPtr) == 4); +static_assert(sizeof(CachedWriteBarrier) == 4); +static_assert(sizeof(CachedVector) == 8); +static_assert(sizeof(CachedArray) == 4); +static_assert(sizeof(CachedOptional) == 4); +static_assert(sizeof(CachedPair, CachedEntryBits>) == 8); +static_assert(sizeof(CachedHashSet, IdentifierRepHash>) == 8); +static_assert(sizeof(CachedPrivateNameEnvironment) == 8); +static_assert(sizeof(CachedBigInt) == 12); +static_assert(sizeof(CachedBitVector) == 8); +static_assert(sizeof(CachedClassElementDefinition) == 24); +static_assert(sizeof(CachedCodeBlockExtras) == 12); +static_assert(sizeof(CachedCodeBlockRareData) == 60); +static_assert(sizeof(CachedCompactTDZEnvironment) == 12); +static_assert(sizeof(CachedCompactTDZEnvironmentMapHandle) == 4); +static_assert(sizeof(CachedEvalCodeBlock) == 40); +static_assert(sizeof(CachedExpressionInfo) == 4); +static_assert(sizeof(CachedFunctionCodeBlock) == 4); +static_assert(sizeof(CachedFunctionExecutable) == 4); +static_assert(sizeof(CachedFunctionExecutableRareData) == 4); +static_assert(sizeof(CachedHandlerInfo) == 16); +static_assert(sizeof(CachedIdentifier) == 4); +static_assert(sizeof(CachedImmutableButterfly) == 12); +static_assert(sizeof(CachedJSTextPosition) == 12); +static_assert(sizeof(CachedJSValue) == 4); +static_assert(sizeof(CachedJSValuePoolRef) == 4); +static_assert(sizeof(CachedModuleCodeBlock) == 44); +static_assert(sizeof(CachedProgramCodeBlock) == 56); +static_assert(sizeof(CachedRegExp) == 8); +static_assert(sizeof(CachedScopedArgumentsTable) == 8); +static_assert(sizeof(CachedSimpleJumpTable) == 20); +static_assert(sizeof(CachedSourceCodeKey) == 28); +static_assert(sizeof(CachedSourceOrigin) == 4); +static_assert(sizeof(CachedSourceProvider) == 8); +static_assert(sizeof(CachedString) == 4); +static_assert(sizeof(CachedStringImpl) == 12); +static_assert(sizeof(CachedStringJumpTable) == 20); +static_assert(sizeof(CachedStringSourceProvider) == 36); +static_assert(sizeof(CachedSymbolTable) == 24); +static_assert(sizeof(CachedSymbolTableEntry) == 4); +static_assert(sizeof(CachedSymbolTableRareData) == 8); +static_assert(sizeof(CachedTDZEnvironmentLink) == 8); +static_assert(sizeof(CachedTemplateObjectDescriptor) == 20); +static_assert(sizeof(CachedTextPosition) == 8); +static_assert(sizeof(CachedUniquedStringImpl) == 12); +static_assert(sizeof(CachedUnlinkedSourceCode) == 12); +static_assert(sizeof(CachedVariableEnvironment) == 16); +static_assert(sizeof(CachedVariableEnvironmentRareData) == 8); +#if ENABLE(WEBASSEMBLY) +static_assert(sizeof(CachedWebAssemblySourceProvider) == 40); +#endif void decodeFunctionCodeBlock(Decoder& decoder, int32_t cachedFunctionCodeBlockOffset, WriteBarrier& codeBlock, const JSCell* owner) { ASSERT(decoder.vm().heap.isDeferred()); diff --git a/Tools/Scripts/dump-bytecode-cache-layout.ts b/Tools/Scripts/dump-bytecode-cache-layout.ts deleted file mode 100644 index f366b190964d..000000000000 --- a/Tools/Scripts/dump-bytecode-cache-layout.ts +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node -// Prints the memory layout of every record the bytecode cache (runtime/CachedTypes.cpp) writes into a payload, in a -// form that is textually identical across targets exactly when the layouts are. The release workflow diffs this file -// between platforms: a payload encoded on one must decode on the others, so their Cached* records must agree. -// -// Usage: (bun | node) dump-bytecode-cache-layout.ts recompiles the CachedTypes.cpp translation unit with -fdump-record-layouts -// (bun | node) dump-bytecode-cache-layout.ts --dump parses an existing dump (for testing) - -import { execFileSync } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; -import { basename, isAbsolute, join } from "node:path"; - -function fail(message: string): never { - process.stderr.write(message + "\n"); - process.exit(1); -} - -function splitCommand(command: string): string[] { - const args: string[] = []; - for (const m of command.matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'|(\S+)/g)) args.push(m[1]?.replace(/\\(["\\])/g, "$1") ?? m[2] ?? m[3]); - return args; -} - -function compileCommand(buildDir: string): { directory: string; args: string[] } { - const commands: { directory: string; file: string; command?: string; arguments?: string[] }[] = JSON.parse(readFileSync(join(buildDir, "compile_commands.json"), "utf8")); - for (const entry of commands) { - if (!entry.file.includes("JavaScriptCore") || !entry.file.includes("UnifiedSource")) continue; - const path = isAbsolute(entry.file) ? entry.file : join(entry.directory, entry.file); - if (!existsSync(path) || !readFileSync(path, "utf8").includes('"runtime/CachedTypes.cpp"')) continue; - return { directory: entry.directory, args: entry.arguments ?? splitCommand(entry.command!) }; - } - fail("no translation unit in compile_commands.json includes runtime/CachedTypes.cpp"); -} - -function recordLayouts(buildDir: string): string { - let { directory, args } = compileCommand(buildDir); - if (["ccache", "sccache"].includes(basename(args[0]))) args = args.slice(1); - // The same command minus its outputs: only the front end runs, and it prints every record layout it computes. - const filtered: string[] = []; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (["-o", "-MF", "-MT", "-MQ"].includes(arg)) i++; - else if (["-c", "/c", "-MD", "-MMD", "/showIncludes"].includes(arg) || /^(-o|\/Fo|\/Fd|-MF|-MT)/.test(arg)) continue; - else filtered.push(arg); - } - filtered.splice(1, 0, "-fsyntax-only", "-Xclang", "-fdump-record-layouts"); // ahead of any `--` (clang-cl commands end `-- `) - try { - return execFileSync(filtered[0], filtered.slice(1), { cwd: directory, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], maxBuffer: 1 << 30 }); - } catch { - fail("dumping record layouts failed: " + filtered.join(" ")); - } -} - -function normalize(name: string): string { - return name - .replace(/\b(class|struct|union|enum) /g, "") - .replace(/\(unnamed (?:\w+ )?at [^)]*?([^/\\:)]+:\d+:\d+)\)/g, "(unnamed at $1)") - .replace(/\bunsigned long long\b|\bunsigned long\b/g, "ulong") // uint64_t's spelling differs (LP64 / LLP64); sizes are compared, not spellings - .replace(/(? { - const records = new Map(); - for (const block of dump.split("*** Dumping AST Record Layout\n").slice(1)) { - const lines = block.split("\n"); - const head = lines[0].match(/^\s*0 \| (.*)$/); - const sizes = block.match(/\[sizeof=(\d+),(?:\s*dsize=\d+,)?\s*align=(\d+)/); - if (!head || !sizes) continue; - const record: Record = { name: normalize(head[1]), size: +sizes[1], align: +sizes[2], rows: [], members: [], bases: [] }; - for (const line of lines.slice(1)) { - const row = line.match(/^\s*([\d:.-]+) \| ( +)(.*)$/); - if (!row) continue; - const depth = row[2].length / 2, text = normalize(row[3]); - record.rows.push({ offset: row[1], depth, text }); - if (depth !== 1) continue; - const base = / \((?:primary |virtual )?base\)(?: \(empty\))?$/; - if (base.test(text)) { - const type = text.replace(base, ""); - record.bases.push(type); - record.members.push({ type }); - } else record.members.push({ type: text.match(/^(.+) [A-Za-z_]\w*(\[\d*\])*$/)?.[1] ?? null }); - } - records.set(record.name, record); - } - return records; -} - -function templateArguments(name: string): string[] { - const out: string[] = []; - let depth = 0, start = 0; - for (let i = 0; i < name.length; i++) { - const c = name[i]; - if (c === "<" && ++depth === 1) start = i + 1; - else if (c === ">" && --depth === 0) out.push(name.slice(start, i)); - else if (c === "," && depth === 1) { out.push(name.slice(start, i)); start = i + 1; } - } - return out.map(a => a.trim()); -} - -function cacheRecords(records: Map): Record[] { - // Roots: everything laid out in a payload derives from CachedObject<> / VariableLengthObjectBase, plus the entry headers. - const isRoot = (record: Record, seen: string[] = []): boolean => - /^JSC::(\w*CacheEntry\b|CachedObject<|VariableLengthObjectBase$)/.test(record.name) || - record.bases.some(base => records.has(base) && !seen.includes(base) && isRoot(records.get(base)!, [...seen, record.name])); - // T of every CachedObject / VariableLengthObject: the in-memory types the cache converts from, never laid out in a payload. - const sourceTypes = new Set([...records.values()].filter(r => /^JSC::(CachedObject|VariableLengthObject) templateArguments(r.name)[0])); - const selected = new Map(); - const queue = [...records.values()].filter(r => isRoot(r)); - while (queue.length) { - const record = queue.pop()!; - if (selected.has(record.name)) continue; - selected.set(record.name, record); - // Members held by value are part of the record. So are the elements a Cached container writes into its tail: - // the leading template arguments of these (a JSC::Cached* argument anywhere is a record in its own right). Other - // template arguments name in-memory source types or traits, and RefPtr, Bag and the like point at T rather - // than contain it, so no other template argument is followed. - const reached = record.members.map(m => m.type).filter((t): t is string => !!t); - const elementArguments: { [container: string]: number } = { CachedVector: 1, CachedArray: 1, CachedOptional: 1, CachedHashSet: 1, CachedPair: 2, CachedHashMap: 2, CachedInlineMap: 2 }; - const container = record.name.match(/^JSC::(Cached\w+) i < (elementArguments[container] ?? 0) || /^JSC::Cached/.test(t)).filter(t => !sourceTypes.has(t))); - } - for (const name of reached) { - if (/[*&]$/.test(name)) continue; - const r = records.get(normalize(name)); - if (r && !selected.has(r.name)) queue.push(r); - } - } - return [...selected.keys()].sort().map(name => selected.get(name)!); -} - -const argv = process.argv.slice(2); -const dump = argv[0] === "--dump" ? readFileSync(argv[1], "utf8") : recordLayouts(argv[0] ?? fail("usage: dump-bytecode-cache-layout.ts ")); -const selected = cacheRecords(parse(dump)); -if (selected.length < 50) fail(`found only ${selected.length} bytecode cache records; the dump or the selection is broken`); -const out: string[] = []; -for (const record of selected) { - out.push(`${record.name} size=${record.size} align=${record.align}`); - for (const { offset, depth, text } of record.rows) out.push(` ${" ".repeat(depth - 1)}${offset} | ${text}`); -} -process.stdout.write(out.join("\n") + "\n");