https://github.com/bcardosolopes updated https://github.com/llvm/llvm-project/pull/229586
>From 15c610516266f7a91a6d88efaef5077796001f82 Mon Sep 17 00:00:00 2001 From: Bruno Cardoso Lopes <[email protected]> Date: Mon, 5 Oct 2026 16:04:43 -0700 Subject: [PATCH 1/2] [CIR] Add native bytecode encodings for attributes Without a BytecodeDialectInterface, MLIR encodes a dialect's attributes through their assembly format, embedding the printed text in the bytecode. This adds native encodings for 27 CIR attributes (constants, constant initializers, global views, method/data-member pointers, C++ special-member attributes, and small metadata attributes), modelled on the LLVM dialect's bytecode support. On attribute-dense modules this shrinks the bytecode ~30-40% (8.6KB to 5.4KB across the new tests); op-heavy modules without CIR attributes are unaffected. The room ahead is huge: emitting bytecode for CIRGenModule.cpp (441MB of CIR text) still exceeds 22GB RSS and 30 minutes with these encodings (64GB and 45 minutes without), since types keep using the assembly fallback. This is paving towards selfhosting with bytecode. Coverage is partial by design: anything not listed keeps using the assembly-format fallback, exactly as before. Types keep the fallback too until the type encodings land separately. Tests in the new clang/test/CIR/Bytecode directory round-trip each encoding through -verify-roundtrip and check the emitted bytecode (via llvm-strings) contains none of the attributes' printed forms. --- .../CIR/Dialect/IR/CIRDialectBytecode.td | 349 ++++++++++++++++++ .../clang/CIR/Dialect/IR/CMakeLists.txt | 5 + clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 3 + .../lib/CIR/Dialect/IR/CIRDialectBytecode.cpp | 228 ++++++++++++ clang/lib/CIR/Dialect/IR/CIRDialectBytecode.h | 26 ++ clang/lib/CIR/Dialect/IR/CMakeLists.txt | 2 + clang/test/CIR/Bytecode/attributes.cir | 58 +++ clang/test/CIR/Bytecode/constants.cir | 43 +++ clang/test/CIR/Bytecode/global-view.cir | 69 ++++ clang/test/CIR/Bytecode/special-member.cir | 34 ++ clang/test/CIR/Bytecode/vtable-blockaddr.cir | 32 ++ 11 files changed, 849 insertions(+) create mode 100644 clang/include/clang/CIR/Dialect/IR/CIRDialectBytecode.td create mode 100644 clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp create mode 100644 clang/lib/CIR/Dialect/IR/CIRDialectBytecode.h create mode 100644 clang/test/CIR/Bytecode/attributes.cir create mode 100644 clang/test/CIR/Bytecode/constants.cir create mode 100644 clang/test/CIR/Bytecode/global-view.cir create mode 100644 clang/test/CIR/Bytecode/special-member.cir create mode 100644 clang/test/CIR/Bytecode/vtable-blockaddr.cir diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialectBytecode.td b/clang/include/clang/CIR/Dialect/IR/CIRDialectBytecode.td new file mode 100644 index 00000000000000..caf2203b321f58 --- /dev/null +++ b/clang/include/clang/CIR/Dialect/IR/CIRDialectBytecode.td @@ -0,0 +1,349 @@ +//===-- CIRDialectBytecode.td - CIR bytecode defs ----------*- tablegen -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This is the CIR bytecode reader/writer definition file. +// +// Without a BytecodeDialectInterface, MLIR falls back to encoding an unknown +// dialect's attributes and types through their *assembly format*: it embeds +// the printed text inside the bytecode. That fallback is correct but it is the +// reason a bytecode file can be larger than the equivalent text. +// +// Modelled on mlir/include/mlir/Dialect/LLVMIR/LLVMDialectBytecode.td. Every +// entry's parameters must appear in the same order as the attribute's get(), +// because the generated reader feeds them positionally, and each name must +// derive the C++ getter (`get` + the name in UpperCamel). +// +//===----------------------------------------------------------------------===// + +#ifndef CIR_DIALECT_BYTECODE +#define CIR_DIALECT_BYTECODE + +include "mlir/IR/BytecodeBase.td" + +//===----------------------------------------------------------------------===// +// Bytecode classes for attributes and types. +//===----------------------------------------------------------------------===// + +class Attr<string type> : WithType<type, Attribute>; + +// Read a generic type into a narrowed C++ handle. The reader's templated +// readType does the dyn_cast and fails cleanly on a mismatch, so this stays +// safe against a malformed or cross-version file. +class Ty<string type> : WithType<type, Type>; + +class OptionalAttribute<string type> : + WithParser <"succeeded($_reader.readOptionalAttribute($_var))", + WithPrinter<"$_writer.writeOptionalAttribute($_getter)", + WithType<type, Attribute>>>; + +// For a parameter declared OptionalParameter<"std::optional<...>">, where the +// C++ type is an optional rather than a nullable attribute, so "absent" needs +// a flag of its own. The helpers live in CIRDialectBytecode.cpp. +class OptionalInt<string type> : + WithParser <"succeeded(readOptionalInt($_reader, $_var))", + WithBuilder<"$_args", + WithPrinter<"writeOptionalInt($_writer, $_getter)", + WithType <"std::optional<" # type # ">">>>>; + +// Named for the C++ spelling, not the concept, because the difference from +// OptionalAttribute above is exactly the C++ spelling and nothing else: +// OptionalAttribute is for a parameter that is a nullable attribute, this is +// for one declared std::optional<...>. The two share the same wire format, so +// at a glance nothing but the name says which a given parameter needs. +class StdOptionalAttribute<string type> : + WithParser <"succeeded(readStdOptionalAttribute($_reader, $_var))", + WithBuilder<"$_args", + WithPrinter<"writeStdOptionalAttribute($_writer, $_getter)", + WithType <"std::optional<" # type # ">">>>>; + +// A scoped enum read from an untrusted varint. The value is validated with +// the enum's symbolizer; an out-of-range value fails the read with a +// diagnostic instead of casting into undefined behavior. +def CtorKindParam : + WithParser<"succeeded(readEnum($_reader, $_var, cir::symbolizeCtorKind, \"CtorKind\"))", + WithPrinter<"$_writer.writeVarInt(static_cast<uint64_t>($_getter))", + WithType<"cir::CtorKind">>>; + +def AssignKindParam : + WithParser<"succeeded(readEnum($_reader, $_var, cir::symbolizeAssignKind, \"AssignKind\"))", + WithPrinter<"$_writer.writeVarInt(static_cast<uint64_t>($_getter))", + WithType<"cir::AssignKind">>>; + +// A varint on the wire narrowed to a fixed-width integer. The bound is checked +// on read; a file carrying a value the parameter cannot hold fails instead of +// truncating into a plausible count or index. +def CheckedInt : + WithParser<"succeeded(readCheckedInt($_reader, $_var))", + WithPrinter<"$_writer.writeSignedVarInt($_getter)", + WithType<"int">>>; + +def CheckedInt32 : + WithParser<"succeeded(readCheckedInt($_reader, $_var))", + WithPrinter<"$_writer.writeSignedVarInt($_getter)", + WithType<"int32_t">>>; + +def CheckedUnsigned : + WithParser<"succeeded(readCheckedInt($_reader, $_var))", + WithPrinter<"$_writer.writeVarInt($_getter)", + WithType<"unsigned">>>; + +//===----------------------------------------------------------------------===// +// OptInfoAttr +//===----------------------------------------------------------------------===// + +def OptInfoAttr : DialectAttribute<(attr + CheckedUnsigned:$level, + CheckedUnsigned:$size +)>; + +//===----------------------------------------------------------------------===// +// AddressPointAttr +// +// index and offset are int32_t and an address point legitimately carries a +// negative offset, so these are signed. +//===----------------------------------------------------------------------===// + +def AddressPointAttr : DialectAttribute<(attr + CheckedInt32:$index, + CheckedInt32:$offset +)>; + +//===----------------------------------------------------------------------===// +// OpenCLVersionAttr +//===----------------------------------------------------------------------===// + +def OpenCLVersionAttr : DialectAttribute<(attr + CheckedInt32:$major, + CheckedInt32:$minor +)>; + +//===----------------------------------------------------------------------===// +// BlockAddrInfoAttr +//===----------------------------------------------------------------------===// + +def BlockAddrInfoAttr : DialectAttribute<(attr + Ty<"cir::PointerType">:$type, + Attr<"mlir::FlatSymbolRefAttr">:$func, + Attr<"mlir::StringAttr">:$label +)>; + +//===----------------------------------------------------------------------===// +// BitfieldInfoAttr +//===----------------------------------------------------------------------===// + +def BitfieldInfoAttr : DialectAttribute<(attr + Attr<"mlir::StringAttr">:$name, + Type:$storage_type, + VarInt:$size, + VarInt:$offset, + Bool:$is_signed +)>; + +//===----------------------------------------------------------------------===// +// Constants +//===----------------------------------------------------------------------===// +// +// These are the entries that pay for themselves. Every literal in a function +// body is an IntAttr, an FPAttr or a BoolAttr, so under the assembly fallback +// each one costs its full printed form ("#cir.int<42> : !s32i") while the +// native encoding costs a type reference plus the payload's own width. + +// The value's width is implied by the type, so it is not stored twice; the +// helpers below recover it. This mirrors builtin's IntegerAttr / FloatAttr. +def IntAttr : DialectAttribute<(attr + Type:$type, + KnownWidthAPInt<"type">:$value +)> { + let cBuilder = "get<$_resultType>(context, mlir::cast<cir::IntTypeInterface>(type), *value)"; +} + +def FPAttr : DialectAttribute<(attr + Type:$type, + KnownSemanticsAPFloat<"type">:$value +)> { + let cBuilder = "get<$_resultType>(context, mlir::cast<cir::FPTypeInterface>(type), *value)"; +} + +// cir::BoolType is a singleton, so the self type carries no information and is +// rebuilt on read rather than stored. +def BoolAttr : DialectAttribute<(attr + Bool:$value +)> { + let cBuilder = "get<$_resultType>(context, cir::BoolType::get(context), value)"; +} + +def ConstPtrAttr : DialectAttribute<(attr + Ty<"cir::PointerType">:$type, + Attr<"mlir::IntegerAttr">:$value +)>; + +//===----------------------------------------------------------------------===// +// Parameterless marker attributes +//===----------------------------------------------------------------------===// +// +// No payload at all: the entry exists so the attribute costs a kind index +// instead of its printed mnemonic. + +def CatchAllAttr : DialectAttribute<(attr)>; +def UnwindAttr : DialectAttribute<(attr)>; + +//===----------------------------------------------------------------------===// +// Constant initializers +//===----------------------------------------------------------------------===// +// +// These carry a self type, and under the fallback each *occurrence* embeds +// that type's whole printed form, so a zero-initialized aggregate costs the +// full type text at every use. Encoding them natively writes a reference +// into the bytecode type table instead. + +def ZeroAttr : DialectAttribute<(attr + Type:$type +)>; + +def UndefAttr : DialectAttribute<(attr + Type:$type +)>; + +def PoisonAttr : DialectAttribute<(attr + Type:$type +)>; + +def ConstArrayAttr : DialectAttribute<(attr + Type:$type, + Attribute:$elts, + CheckedInt:$trailingZerosNum +)> { + let cBuilder = "getChecked<$_resultType>([&]() { return reader.emitError(); }, context, type, elts, trailingZerosNum)"; +} + +def ConstRecordAttr : DialectAttribute<(attr + Type:$type, + Attr<"mlir::ArrayAttr">:$members +)>; + +// The C++ special-member attributes on cir.func. Each is small, a type and a +// flag, but its type is the aggregate the member belongs to, and while these +// fell back the printed form dragged that type in fully expanded. +def CXXCtorAttr : DialectAttribute<(attr + Type:$type, + CtorKindParam:$ctorKind, + Bool:$isTrivial +)>; + +def CXXDtorAttr : DialectAttribute<(attr + Type:$type, + Bool:$isTrivial +)>; + +def CXXAssignAttr : DialectAttribute<(attr + Type:$type, + AssignKindParam:$assignKind, + Bool:$isTrivial +)>; + +def VTableAttr : DialectAttribute<(attr + Type:$type, + Attr<"mlir::ArrayAttr">:$data +)>; + +def BlockAddrDiffAttr : DialectAttribute<(attr + Ty<"cir::IntType">:$type, + Attr<"mlir::FlatSymbolRefAttr">:$func, + Attr<"mlir::StringAttr">:$lhs_label, + Attr<"mlir::StringAttr">:$rhs_label +)>; + +// The rest of the type-carrying attributes. A container that nests other +// attributes only pays off once those are encoded too: the container would +// otherwise write its operands as nested attributes, each of which drops +// straight back to the fallback and prints its own self type in full. +def GlobalViewAttr : DialectAttribute<(attr + Type:$type, + Attr<"mlir::FlatSymbolRefAttr">:$symbol, + OptionalAttribute<"mlir::ArrayAttr">:$indices +)>; + +def TypeInfoAttr : DialectAttribute<(attr + Type:$type, + Attr<"mlir::ArrayAttr">:$data +)>; + +def DynamicCastInfoAttr : DialectAttribute<(attr + Attr<"cir::GlobalViewAttr">:$src_rtti, + Attr<"cir::GlobalViewAttr">:$dest_rtti, + Attr<"mlir::FlatSymbolRefAttr">:$runtime_func, + Attr<"mlir::FlatSymbolRefAttr">:$bad_cast_func, + Attr<"cir::IntAttr">:$offset_hint +)>; + +def ConstVectorAttr : DialectAttribute<(attr + Type:$type, + Attr<"mlir::ArrayAttr">:$elts +)>; + +def ConstComplexAttr : DialectAttribute<(attr + Ty<"cir::ComplexType">:$type, + Attr<"mlir::TypedAttr">:$real, + Attr<"mlir::TypedAttr">:$imag +)>; + +def DataMemberAttr : DialectAttribute<(attr + Ty<"cir::DataMemberType">:$type, + OptionalAttribute<"mlir::DenseI32ArrayAttr">:$member_path +)> { + // Not the default builder: that one resolves to the ArrayRef<int32_t> + // overload through DenseI32ArrayAttr's implicit conversion, which + // dereferences a null attribute for a null pointer-to-member. The context + // form takes the attribute as-is. + let cBuilder = "cir::DataMemberAttr::getChecked([&]() { return reader.emitError(); }, context, type, member_path)"; +} + +def MethodAttr : DialectAttribute<(attr + Ty<"cir::MethodType">:$type, + StdOptionalAttribute<"mlir::FlatSymbolRefAttr">:$symbol, + OptionalInt<"uint64_t">:$vtable_offset +)>; + +def CIRDialectAttributes : DialectAttributes<"CIR"> { + let elems = [ + OptInfoAttr, + AddressPointAttr, + OpenCLVersionAttr, + BlockAddrInfoAttr, + BitfieldInfoAttr, + IntAttr, + FPAttr, + BoolAttr, + ConstPtrAttr, + CatchAllAttr, + UnwindAttr, + ZeroAttr, + UndefAttr, + PoisonAttr, + ConstArrayAttr, + ConstRecordAttr, + VTableAttr, + BlockAddrDiffAttr, + CXXCtorAttr, + CXXDtorAttr, + CXXAssignAttr, + GlobalViewAttr, + TypeInfoAttr, + DynamicCastInfoAttr, + ConstVectorAttr, + ConstComplexAttr, + DataMemberAttr, + MethodAttr, + // APPEND ONLY. An entry's position in this list *is* its kind + // discriminator on disk, so inserting one renumbers every entry after + // it. No dialect version is recorded yet, so this is a same-revision + // scheme, not a compatibility promise. + ]; +} + +#endif // CIR_DIALECT_BYTECODE diff --git a/clang/include/clang/CIR/Dialect/IR/CMakeLists.txt b/clang/include/clang/CIR/Dialect/IR/CMakeLists.txt index 8add2f04afaf76..66fa7a40d28f7a 100644 --- a/clang/include/clang/CIR/Dialect/IR/CMakeLists.txt +++ b/clang/include/clang/CIR/Dialect/IR/CMakeLists.txt @@ -23,6 +23,11 @@ mlir_tablegen(CIROpsAttributes.h.inc -gen-attrdef-decls) mlir_tablegen(CIROpsAttributes.cpp.inc -gen-attrdef-defs) add_public_tablegen_target(MLIRCIREnumsGen) +# CIR dialect bytecode reader/writer tables. +set(LLVM_TARGET_DEFINITIONS CIRDialectBytecode.td) +mlir_tablegen(CIRDialectBytecode.cpp.inc -gen-bytecode -bytecode-dialect="CIR") +add_public_tablegen_target(MLIRCIRDialectBytecodeIncGen) + clang_tablegen(CIRLowering.inc -gen-cir-lowering SOURCE CIROps.td TARGET CIRLowering) diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp index 15cebd358040e3..43d030c179faa5 100644 --- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp @@ -35,6 +35,8 @@ #include "llvm/Support/Casting.h" #include "llvm/Support/LogicalResult.h" +#include "CIRDialectBytecode.h" + using namespace mlir; using namespace cir; @@ -102,6 +104,7 @@ void cir::CIRDialect::initialize() { #include "clang/CIR/Dialect/IR/CIROps.cpp.inc" >(); addInterfaces<CIROpAsmDialectInterface>(); + detail::addBytecodeInterface(this); } Operation *cir::CIRDialect::materializeConstant(mlir::OpBuilder &builder, diff --git a/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp b/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp new file mode 100644 index 00000000000000..4025b7100bd995 --- /dev/null +++ b/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp @@ -0,0 +1,228 @@ +//===- CIRDialectBytecode.cpp - CIR Bytecode Implementation ---------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "CIRDialectBytecode.h" +#include "mlir/Bytecode/BytecodeImplementation.h" +#include "mlir/IR/Diagnostics.h" +#include "clang/CIR/Dialect/IR/CIRAttrs.h" +#include "clang/CIR/Dialect/IR/CIRDialect.h" +#include "clang/CIR/Dialect/IR/CIRTypes.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/TypeSwitch.h" + +#include <limits> +#include <type_traits> + +using namespace mlir; +using namespace cir; + +namespace { + +// Both namespaces opened above declare a BoolAttr, and the generated readers +// and writers name attributes unqualified. A using-declaration at global scope +// would not help: it merges with the names the using-directives inject there +// and stays ambiguous, so it has to live in this inner namespace, where it +// hides them. +using cir::BoolAttr; + +//===--------------------------------------------------------------------===// +// Helpers referenced by the generated readers +//===--------------------------------------------------------------------===// + +// IntAttr and FPAttr do not store their payload's width: it is already implied +// by the attribute's type, so storing it again would be redundant and would +// give a malformed file two disagreeing sources of truth. These recover the +// width from the type, mirroring the same helpers in BuiltinDialectBytecode. +// +// Both fail rather than assert on a type that is not the expected interface: a +// bytecode file is untrusted input, and the type here was read from that same +// file, so a mismatch is a corrupt-input case and not a bug. + +static LogicalResult readAPIntWithKnownWidth(DialectBytecodeReader &reader, + Type type, FailureOr<APInt> &val) { + auto intType = mlir::dyn_cast<cir::IntTypeInterface>(type); + if (!intType) + return reader.emitError() + << "expected a CIR integer type for cir.int, but got: " << type; + val = reader.readAPIntWithKnownWidth(intType.getWidth()); + return success(succeeded(val)); +} + +static LogicalResult +readAPFloatWithKnownSemantics(DialectBytecodeReader &reader, Type type, + FailureOr<APFloat> &val) { + auto fpType = mlir::dyn_cast<cir::FPTypeInterface>(type); + if (!fpType) + return reader.emitError() + << "expected a CIR floating-point type for cir.fp, but got: " + << type; + val = reader.readAPFloatWithKnownSemantics(fpType.getFloatSemantics()); + return success(succeeded(val)); +} + +// An optional parameter spelled std::optional<T> rather than as a null-valued +// attribute needs its own presence flag. A null Attribute already means +// "absent", which is why OptionalAttribute in the .td needs none of this, but +// there is no null unsigned. +// +// writeVarIntWithFlag packs the flag into the varint's low bit, so an absent +// value costs one byte. These mirror the LLVM dialect's helpers of the same +// names (LLVMDialectBytecode.cpp). + +// Unsigned only, and deliberately narrower than the LLVM dialect's version. +// writeVarIntWithFlag packs the presence bit into the low bit, i.e. it shifts +// the value left by one, so the channel is 63 bits and a negative EntryTy, +// which becomes a uint64_t with the top bit set, cannot round-trip at all. +// Rejecting signed types here beats discovering that at the first caller that +// has one. +template <typename EntryTy> +static LogicalResult readOptionalInt(DialectBytecodeReader &reader, + std::optional<EntryTy> &storage) { + static_assert(std::is_unsigned_v<EntryTy>, + "EntryTy must be unsigned: writeVarIntWithFlag spends the low " + "bit on the presence flag, so a negative value cannot be " + "represented"); + uint64_t value = 0; + bool present = false; + if (failed(reader.readVarIntWithFlag(value, present))) + return failure(); + if (!present) { + storage = std::nullopt; + return success(); + } + // A file written by another version, or a corrupt one, can carry a value + // this parameter cannot hold. Truncating it would turn that into a plausible + // member index or byte offset; failing the read is what the width-sensitive + // readers above (readAPIntWithKnownWidth, readAPFloatWithKnownSemantics) do, + // and it is what the caller can actually act on. + if (value > static_cast<uint64_t>(std::numeric_limits<EntryTy>::max())) + return reader.emitError() << "optional integer value " << value + << " does not fit in destination type"; + storage = static_cast<EntryTy>(value); + return success(); +} + +template <typename EntryTy> +static void writeOptionalInt(DialectBytecodeWriter &writer, + std::optional<EntryTy> storage) { + static_assert(std::is_unsigned_v<EntryTy>, + "EntryTy must be unsigned: writeVarIntWithFlag spends the low " + "bit on the presence flag, so a negative value cannot be " + "represented"); + // The channel is only 63 bits: values at or above 2^63 would lose the top + // bit on the wire. No caller has one, and this is where that stops being + // true loudly instead of silently. + assert(!storage || *storage < (uint64_t(1) << 63)); + writer.writeVarIntWithFlag(storage.value_or(0), storage.has_value()); +} + +// The same, for a parameter that is an attribute but is still spelled +// std::optional<> (cir.method's symbol). An engaged optional holding a null +// attribute is not a valid state: it normalizes to std::nullopt on the wire, +// and the read side only ever produces an engaged optional holding a +// non-null attribute, or std::nullopt. + +template <typename AttrTy> +static LogicalResult readStdOptionalAttribute(DialectBytecodeReader &reader, + std::optional<AttrTy> &storage) { + AttrTy attr; + if (failed(reader.readOptionalAttribute(attr))) + return failure(); + if (attr) + storage = attr; + else + storage = std::nullopt; + return success(); +} + +template <typename AttrTy> +static void writeStdOptionalAttribute(DialectBytecodeWriter &writer, + std::optional<AttrTy> storage) { + assert(!storage || *storage); + writer.writeOptionalAttribute(storage.value_or(AttrTy())); +} + +// A scoped enum read from an untrusted varint. The symbolizer rejects +// out-of-range values; without this a corrupt file would cast one straight +// into the enum, which is undefined behavior for a switch on the kind and an +// empty string from the printer's stringify. +template <typename EnumTy> +static LogicalResult readEnum(DialectBytecodeReader &reader, EnumTy &result, + std::optional<EnumTy> (*symbolize)(uint32_t), + llvm::StringRef enumName) { + uint64_t value = 0; + if (failed(reader.readVarInt(value))) + return failure(); + if (value > static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) + return reader.emitError() + << enumName << " value " << value << " out of range"; + std::optional<EnumTy> kind = symbolize(static_cast<uint32_t>(value)); + if (!kind) + return reader.emitError() << "invalid " << enumName << " value " << value; + result = *kind; + return success(); +} + +// A varint on the wire narrowed to a fixed-width integer. Mirror of +// readOptionalInt's bound check: a value the parameter cannot hold fails the +// read instead of truncating into a plausible count or index. +template <typename IntTy> +static LogicalResult readCheckedInt(DialectBytecodeReader &reader, + IntTy &result) { + static_assert(std::is_integral_v<IntTy> && !std::is_same_v<IntTy, bool>); + if constexpr (std::is_signed_v<IntTy>) { + int64_t value = 0; + if (failed(reader.readSignedVarInt(value))) + return failure(); + if (value > static_cast<int64_t>(std::numeric_limits<IntTy>::max()) || + value < static_cast<int64_t>(std::numeric_limits<IntTy>::min())) + return reader.emitError() << "integer value " << value + << " does not fit in destination type"; + result = static_cast<IntTy>(value); + } else { + uint64_t value = 0; + if (failed(reader.readVarInt(value))) + return failure(); + if (value > static_cast<uint64_t>(std::numeric_limits<IntTy>::max())) + return reader.emitError() << "integer value " << value + << " does not fit in destination type"; + result = static_cast<IntTy>(value); + } + return success(); +} + +//===--------------------------------------------------------------------===// +// Tablegen generated bytecode functions +//===--------------------------------------------------------------------===// + +#include "clang/CIR/Dialect/IR/CIRDialectBytecode.cpp.inc" + +//===--------------------------------------------------------------------===// +// CIRDialectBytecodeInterface +//===--------------------------------------------------------------------===// + +/// Bytecode interface for the CIR dialect. +struct CIRDialectBytecodeInterface : public BytecodeDialectInterface { + CIRDialectBytecodeInterface(Dialect *dialect) + : BytecodeDialectInterface(dialect) {} + + Attribute readAttribute(DialectBytecodeReader &reader) const override { + return ::readAttribute(getContext(), reader); + } + + LogicalResult writeAttribute(Attribute attr, + DialectBytecodeWriter &writer) const override { + return ::writeAttribute(attr, writer); + } +}; +} // namespace + +void cir::detail::addBytecodeInterface(CIRDialect *dialect) { + dialect->addInterfaces<CIRDialectBytecodeInterface>(); +} diff --git a/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.h b/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.h new file mode 100644 index 00000000000000..d16459200b9edf --- /dev/null +++ b/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.h @@ -0,0 +1,26 @@ +//===- CIRDialectBytecode.h - CIR Bytecode Implementation -------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This header defines hooks into the CIR dialect bytecode implementation. +// +//===----------------------------------------------------------------------===// + +#ifndef LIB_CLANG_CIR_DIALECT_IR_CIRDIALECTBYTECODE_H +#define LIB_CLANG_CIR_DIALECT_IR_CIRDIALECTBYTECODE_H + +namespace cir { +class CIRDialect; + +namespace detail { +/// Add the interfaces necessary for encoding the CIR dialect components in +/// bytecode. +void addBytecodeInterface(CIRDialect *dialect); +} // namespace detail +} // namespace cir + +#endif // LIB_CLANG_CIR_DIALECT_IR_CIRDIALECTBYTECODE_H diff --git a/clang/lib/CIR/Dialect/IR/CMakeLists.txt b/clang/lib/CIR/Dialect/IR/CMakeLists.txt index 0274005a288589..ac79ad52f95ec1 100644 --- a/clang/lib/CIR/Dialect/IR/CMakeLists.txt +++ b/clang/lib/CIR/Dialect/IR/CMakeLists.txt @@ -1,6 +1,7 @@ add_clang_library(MLIRCIR CIRAttrs.cpp CIRDialect.cpp + CIRDialectBytecode.cpp CIRMemorySlot.cpp CIRTypes.cpp CIRDataLayout.cpp @@ -8,6 +9,7 @@ add_clang_library(MLIRCIR DEPENDS MLIRCIROpsIncGen + MLIRCIRDialectBytecodeIncGen MLIRCIRTypeConstraintsIncGen MLIRCIREnumsGen MLIRCIROpInterfacesIncGen diff --git a/clang/test/CIR/Bytecode/attributes.cir b/clang/test/CIR/Bytecode/attributes.cir new file mode 100644 index 00000000000000..b8a840b8cfe693 --- /dev/null +++ b/clang/test/CIR/Bytecode/attributes.cir @@ -0,0 +1,58 @@ +// RUN: cir-opt %s -verify-roundtrip | FileCheck %s +// RUN: cir-opt %s --emit-bytecode | llvm-strings | FileCheck %s --check-prefix=NATIVE + +// Round-trip the natively encoded attributes. Values are asymmetric (size != +// offset, negative offset) because a transposed pair of same-typed parameters +// is exactly what a symmetric test cannot see. +// +// Check the attributes are not string-encoded in the blob. +// NATIVE-NOT: #cir. + +!u8i = !cir.int<u, 8> +!s32i = !cir.int<s, 32> +!u64i = !cir.int<u, 64> + +module attributes { + cir.opt_info = #cir.opt_info<level = 2, size = 1>, + test.bitfield = #cir.bitfield_info<name = "f", storage_type = !u8i, size = 3, + offset = 5, is_signed = true>, + test.blockaddr = #cir.block_addr_info<@some_func, "some_label"> : !cir.ptr<!cir.void>, + test.addrpoint = #cir.address_point<index = 1, offset = -2>, + test.oclversion = #cir.cl.version<3, 0>, + test.i8 = #cir.int<127> : !u8i, + test.s32 = #cir.int<-559038737> : !s32i, + test.u64 = #cir.int<1099511627776> : !u64i, + test.i128 = #cir.int<18446744073709551617> : !cir.int<s, 128>, + test.f32 = #cir.fp<1.500000e+00> : !cir.float, + test.f64 = #cir.fp<3.141592653589793e+00> : !cir.double, + test.f80 = #cir.fp<1.500000e+00> : !cir.long_double<!cir.f80>, + test.true = #cir.bool<true> : !cir.bool, + test.false = #cir.bool<false> : !cir.bool, + test.nullptr = #cir.ptr<null> : !cir.ptr<!u8i>, + test.ptr = #cir.ptr<32 : i64> : !cir.ptr<!u8i>, + test.catchall = #cir.all, + test.unwind = #cir.unwind +} { +} + +// CHECK-DAG: #bfi_f = #cir.bitfield_info<name = "f", storage_type = !u8i, size = 3, offset = 5, is_signed = true> +// CHECK-DAG: test.bitfield = #bfi_f +// CHECK-DAG: cir.opt_info = #cir.opt_info<level = 2, size = 1> +// CHECK-DAG: test.addrpoint = #cir.address_point<index = 1, offset = -2> +// CHECK-DAG: test.blockaddr = #cir.block_addr_info<@some_func, "some_label"> : !cir.ptr<!void> +// CHECK-DAG: test.oclversion = #cir.cl.version<3, 0> +// CHECK-DAG: test.i8 = #cir.int<127> : !u8i +// CHECK-DAG: test.s32 = #cir.int<-559038737> : !s32i +// CHECK-DAG: test.u64 = #cir.int<1099511627776> : !u64i +// CHECK-DAG: test.i128 = #cir.int<18446744073709551617> : !s128i +// CHECK-DAG: test.f80 = #cir.fp<1.500000e+00> : !cir.long_double<!cir.f80> +// CHECK-DAG: test.f32 = #cir.fp<1.500000e+00> : !cir.float +// CHECK-DAG: test.f64 = #cir.fp<3.1415926535897931> : !cir.double +// CHECK-DAG: #true = #cir.bool<true> : !cir.bool +// CHECK-DAG: #false = #cir.bool<false> : !cir.bool +// CHECK-DAG: test.true = #true +// CHECK-DAG: test.false = #false +// CHECK-DAG: test.nullptr = #cir.ptr<null> : !cir.ptr<!u8i> +// CHECK-DAG: test.ptr = #cir.ptr<32 : i64> : !cir.ptr<!u8i> +// CHECK-DAG: test.catchall = #cir.all +// CHECK-DAG: test.unwind = #cir.unwind diff --git a/clang/test/CIR/Bytecode/constants.cir b/clang/test/CIR/Bytecode/constants.cir new file mode 100644 index 00000000000000..22e35944781042 --- /dev/null +++ b/clang/test/CIR/Bytecode/constants.cir @@ -0,0 +1,43 @@ +// RUN: cir-opt %s -verify-roundtrip | FileCheck %s +// RUN: cir-opt %s --emit-bytecode | llvm-strings | FileCheck %s --check-prefix=NATIVE + +// Round-trip the constant-initializer attributes. +// +// Check the attributes are not string-encoded in the blob. +// NATIVE-NOT: #cir.const_array< +// NATIVE-NOT: #cir.const_record< +// NATIVE-NOT: #cir.zero +// NATIVE-NOT: #cir.undef +// NATIVE-NOT: #cir.poison + +!s32i = !cir.int<s, 32> +!s8i = !cir.int<s, 8> +!rec = !cir.struct<"R" {data !s32i, data !s32i}> + +module { + cir.global external @z = #cir.zero : !cir.array<!s32i x 2> + cir.global external @u = #cir.undef : !s32i + cir.global external @p = #cir.poison : !s32i + cir.global external @ca = #cir.const_array<[#cir.int<1> : !s32i, + #cir.int<2> : !s32i]> : !cir.array<!s32i x 2> + + // trailing_zeros is separate from the element list: 4-long array, one + // element stored. Dropping the count would produce a 1-element array. + cir.global external @caz = #cir.const_array<[#cir.int<7> : !s32i], + trailing_zeros> : !cir.array<!s32i x 4> + cir.global external @cr = #cir.const_record<{#cir.int<3> : !s32i, + #cir.int<4> : !s32i}> : !rec + + // The string branch of elts: a StringAttr payload rather than an array of + // per-element attributes. + cir.global external @cs = #cir.const_array<"ab" : !cir.array<!s8i x 2>, + trailing_zeros> : !cir.array<!s8i x 4> +} + +// CHECK-DAG: @z = #cir.zero : !cir.array<!s32i x 2> +// CHECK-DAG: @u = #cir.undef : !s32i +// CHECK-DAG: @p = #cir.poison : !s32i +// CHECK-DAG: @ca = #cir.const_array<[#cir.int<1> : !s32i, #cir.int<2> : !s32i]> : !cir.array<!s32i x 2> +// CHECK-DAG: @caz = #cir.const_array<[#cir.int<7> : !s32i], trailing_zeros> : !cir.array<!s32i x 4> +// CHECK-DAG: @cr = #cir.const_record<{#cir.int<3> : !s32i, #cir.int<4> : !s32i}> : !rec_R +// CHECK-DAG: @cs = #cir.const_array<"ab" : !cir.array<!s8i x 2>, trailing_zeros> : !cir.array<!s8i x 4> diff --git a/clang/test/CIR/Bytecode/global-view.cir b/clang/test/CIR/Bytecode/global-view.cir new file mode 100644 index 00000000000000..84ec1a618c2b84 --- /dev/null +++ b/clang/test/CIR/Bytecode/global-view.cir @@ -0,0 +1,69 @@ +// RUN: cir-opt %s -verify-roundtrip | FileCheck %s +// RUN: cir-opt %s --emit-bytecode | llvm-strings | FileCheck %s --check-prefix=NATIVE + +// The type-carrying attributes: a container nesting other attributes only pays +// off once those are encoded too. +// +// Check the attributes are not string-encoded in the blob. +// NATIVE-NOT: #cir.global_view< +// NATIVE-NOT: #cir.method< +// NATIVE-NOT: #cir.data_member< +// NATIVE-NOT: #cir.dyn_cast_info< +// NATIVE-NOT: #cir.typeinfo< +// NATIVE-NOT: #cir.const_vector< +// NATIVE-NOT: #cir.const_complex< + +!s32i = !cir.int<s, 32> +!u8i = !cir.int<u, 8> +!rec_Marker = !cir.struct<"Marker" {data !s32i}> +!rec_TI = !cir.struct<"TI" {data !cir.ptr<!u8i>, data !s32i}> +!rec_Other = !cir.struct<"Other" {data !s32i, data !s32i}> +!method_Other = !cir.method<!cir.func<(!s32i)> in !rec_Other> +!s64i = !cir.int<s, 64> +!rec_Base = !cir.struct<"Base" {data !cir.vptr}> +!rec_Derived = !cir.struct<"Derived" {data !rec_Base}> + +#info = #cir.dyn_cast_info<src_rtti = #cir.global_view<@_ZTI4Base> : !cir.ptr<!u8i>, dest_rtti = #cir.global_view<@_ZTI7Derived> : !cir.ptr<!u8i>, runtime_func = @__dynamic_cast, bad_cast_func = @__cxa_bad_cast, offset_hint = #cir.int<0> : !s64i> + +module { + cir.global external @plain = #cir.global_view<@some_sym> : !cir.ptr<!rec_Marker> + cir.global external @indexed = #cir.global_view<@some_sym, [3 : i32, 0 : i32]> : !cir.ptr<!rec_Marker> + cir.global external @g7 = #cir.global_view<@s7> : !cir.ptr<!rec_Marker> + cir.global external @info = #cir.typeinfo<{#cir.global_view<@_ZTS6Widget> : !cir.ptr<!u8i>, #cir.int<2> : !s32i}> : !rec_TI + cir.global external @vec = #cir.const_vector<[#cir.int<1> : !s32i, #cir.int<2> : !s32i]> : !cir.vector<2 x !s32i> + cir.global external @cpx = #cir.const_complex<#cir.fp<1.000000e+00> : !cir.double, #cir.fp<2.000000e+00> : !cir.double> : !cir.complex<!cir.double> + cir.global external @dm = #cir.data_member<[0]> : !cir.data_member<!s32i in !rec_Other> + cir.global external @dm_single = #cir.data_member<[1]> : !cir.data_member<!s32i in !rec_Other> + cir.global external @dm_null = #cir.data_member<null> : !cir.data_member<!s32i in !rec_Other> + + // A symbol, no target at all, and a vtable slot. Collapsing null into + // vtable_offset = 0 would miscompile a null pointer-to-member into a call + // of the first virtual function, hence the explicit present-zero case. + cir.global external @m_sym = #cir.method<@f> : !method_Other + cir.global external @m_null = #cir.method<null> : !method_Other + cir.global external @m_virtual = #cir.method<vtable_offset = 8> : !method_Other + cir.global external @m_virtual0 = #cir.method<vtable_offset = 0> : !method_Other + + cir.func private @__dynamic_cast(!cir.ptr<!u8i>, !cir.ptr<!u8i>, !cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i> + cir.func private @__cxa_bad_cast() + + cir.func @cast(%arg0: !cir.ptr<!rec_Base>) -> !cir.ptr<!rec_Derived> { + %0 = cir.dyn_cast ptr %arg0 : !cir.ptr<!rec_Base> -> !cir.ptr<!rec_Derived> #info + cir.return %0 : !cir.ptr<!rec_Derived> + } +} + +// CHECK: cir.global external @plain = #cir.global_view<@some_sym> : !cir.ptr<!rec_Marker> +// CHECK: cir.global external @indexed = #cir.global_view<@some_sym, [3 : i32, 0 : i32]> : !cir.ptr<!rec_Marker> +// CHECK: cir.global external @g7 = #cir.global_view<@s7> : !cir.ptr<!rec_Marker> +// CHECK: cir.global external @info = #cir.typeinfo<{#cir.global_view<@_ZTS6Widget> : !cir.ptr<!u8i>, #cir.int<2> : !s32i}> : !rec_TI +// CHECK: cir.global external @vec = #cir.const_vector<[#cir.int<1> : !s32i, #cir.int<2> : !s32i]> : !cir.vector<2 x !s32i> +// CHECK: cir.global external @cpx = #cir.const_complex<#cir.fp<1.000000e+00> : !cir.double, #cir.fp<2.000000e+00> : !cir.double> : !cir.complex<!cir.double> +// CHECK: cir.global external @dm = #cir.data_member<[0]> : !cir.data_member<!s32i in !rec_Other> +// CHECK: cir.global external @dm_single = #cir.data_member<[1]> : !cir.data_member<!s32i in !rec_Other> +// CHECK: cir.global external @dm_null = #cir.data_member<null> : !cir.data_member<!s32i in !rec_Other> +// CHECK: cir.global external @m_sym = #cir.method<@f> : !cir.method<!cir.func<(!s32i)> in !rec_Other> +// CHECK: cir.global external @m_null = #cir.method<null> : !cir.method<!cir.func<(!s32i)> in !rec_Other> +// CHECK: cir.global external @m_virtual = #cir.method<vtable_offset = 8> : !cir.method<!cir.func<(!s32i)> in !rec_Other> +// CHECK: cir.global external @m_virtual0 = #cir.method<vtable_offset = 0> : !cir.method<!cir.func<(!s32i)> in !rec_Other> +// CHECK: %0 = cir.dyn_cast ptr %arg0 : !cir.ptr<!rec_Base> -> !cir.ptr<!rec_Derived> #dyn_cast_info__ZTI4Base__ZTI7Derived diff --git a/clang/test/CIR/Bytecode/special-member.cir b/clang/test/CIR/Bytecode/special-member.cir new file mode 100644 index 00000000000000..c98322449683d3 --- /dev/null +++ b/clang/test/CIR/Bytecode/special-member.cir @@ -0,0 +1,34 @@ +// RUN: cir-opt %s -verify-roundtrip | FileCheck %s +// RUN: cir-opt %s --emit-bytecode | llvm-strings | FileCheck %s --check-prefix=NATIVE + +// The special-member attributes carry an aggregate type, so the fallback +// embeds its fully expanded printed form per attribute. Encoded, each +// attribute refers to the type table by index instead. +// +// Check the attributes are not string-encoded in the blob. +// NATIVE-NOT: #cir.cxx_ctor< +// NATIVE-NOT: #cir.cxx_dtor< +// NATIVE-NOT: #cir.cxx_assign< + +!s32i = !cir.int<s, 32> +!rec_Marker = !cir.struct<"Marker" {data !s32i}> + +module { + cir.func private @ctor_default(!cir.ptr<!rec_Marker>) func_info<#cir.cxx_ctor<!rec_Marker, default>> + cir.func private @ctor_copy(!cir.ptr<!rec_Marker>, !cir.ptr<!rec_Marker>) func_info<#cir.cxx_ctor<!rec_Marker, copy>> + cir.func private @ctor_move_trivial(!cir.ptr<!rec_Marker>, !cir.ptr<!rec_Marker>) func_info<#cir.cxx_ctor<!rec_Marker, move, trivial true>> + cir.func private @dtor(!cir.ptr<!rec_Marker>) func_info<#cir.cxx_dtor<!rec_Marker>> + cir.func private @dtor_trivial(!cir.ptr<!rec_Marker>) func_info<#cir.cxx_dtor<!rec_Marker, trivial true>> + cir.func private @assign_copy(!cir.ptr<!rec_Marker>, !cir.ptr<!rec_Marker>) func_info<#cir.cxx_assign<!rec_Marker, copy>> + cir.func private @assign_move_trivial(!cir.ptr<!rec_Marker>, !cir.ptr<!rec_Marker>) func_info<#cir.cxx_assign<!rec_Marker, move, trivial true>> +} + +// Each kind appears: the write side widens a scoped enum to a varint the +// reader casts back, and getting that wrong silently retargets the kind. +// CHECK: cir.func private @ctor_default({{.*}}) func_info<#cir.cxx_ctor<!rec_Marker, default>> +// CHECK: cir.func private @ctor_copy({{.*}}) func_info<#cir.cxx_ctor<!rec_Marker, copy>> +// CHECK: cir.func private @ctor_move_trivial({{.*}}) func_info<#cir.cxx_ctor<!rec_Marker, move, trivial true>> +// CHECK: cir.func private @dtor({{.*}}) func_info<#cir.cxx_dtor<!rec_Marker>> +// CHECK: cir.func private @dtor_trivial({{.*}}) func_info<#cir.cxx_dtor<!rec_Marker, trivial true>> +// CHECK: cir.func private @assign_copy({{.*}}) func_info<#cir.cxx_assign<!rec_Marker, copy>> +// CHECK: cir.func private @assign_move_trivial({{.*}}) func_info<#cir.cxx_assign<!rec_Marker, move, trivial true>> diff --git a/clang/test/CIR/Bytecode/vtable-blockaddr.cir b/clang/test/CIR/Bytecode/vtable-blockaddr.cir new file mode 100644 index 00000000000000..2ecc209e755a05 --- /dev/null +++ b/clang/test/CIR/Bytecode/vtable-blockaddr.cir @@ -0,0 +1,32 @@ +// RUN: cir-opt %s -verify-roundtrip | FileCheck %s +// RUN: cir-opt %s --emit-bytecode | llvm-strings | FileCheck %s --check-prefix=NATIVE + +// A vtable nests arrays of global_views, so under the fallback its printed +// form repeats the element types at every level. Encoded, each level is a +// reference instead. +// +// Check the attributes are not string-encoded in the blob. +// NATIVE-NOT: #cir.block_addr_diff< +// NATIVE-NOT: #cir.vtable< + +!s32i = !cir.int<s, 32> +!u8i = !cir.int<u, 8> + +module { + cir.global external @tgt = #cir.int<0> : !s32i + + cir.global external @vt = #cir.vtable<{ + #cir.const_array<[#cir.global_view<@tgt> : !cir.ptr<!u8i>]> : !cir.array<!cir.ptr<!u8i> x 1>, + #cir.const_array<[#cir.global_view<@vt, [0 : i32, 2 : i32]> : !cir.ptr<!u8i>]> : !cir.array<!cir.ptr<!u8i> x 1> + }> : !cir.struct<"VT" {data !cir.array<!cir.ptr<!u8i> x 1>, data !cir.array<!cir.ptr<!u8i> x 1>}> + + cir.global external @bad = #cir.block_addr_diff<@fn, "lbl", "lbl2"> : !s32i + + cir.func @fn() { + cir.return + } +} + +// CHECK-DAG: #cir.global_view<@tgt> : !cir.ptr<!u8i> +// CHECK-DAG: #cir.global_view<@vt, [0 : i32, 2 : i32]> : !cir.ptr<!u8i> +// CHECK-DAG: #cir.block_addr_diff<@fn, "lbl", "lbl2"> : !s32i >From cb75301f08c4230850881fd410ba3df8a995d705 Mon Sep 17 00:00:00 2001 From: Bruno Cardoso Lopes <[email protected]> Date: Tue, 6 Oct 2026 15:56:32 -0700 Subject: [PATCH 2/2] [CIR] Route types through generated bytecode reader/writer shells Fixes -Werror,-Wunused-function on readType/writeType: tablegen always emits both dispatchers even with no type entries, and the interface left them unreferenced. The empty shells decline every type, preserving the assembly fallback exactly; type entries will plug in with no interface churn. --- clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp b/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp index 4025b7100bd995..879d1c27008231 100644 --- a/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRDialectBytecode.cpp @@ -220,6 +220,15 @@ struct CIRDialectBytecodeInterface : public BytecodeDialectInterface { DialectBytecodeWriter &writer) const override { return ::writeAttribute(attr, writer); } + + Type readType(DialectBytecodeReader &reader) const override { + return ::readType(getContext(), reader); + } + + LogicalResult writeType(Type type, + DialectBytecodeWriter &writer) const override { + return ::writeType(type, writer); + } }; } // namespace _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
