[Mlir-commits] [mlir] [mlir][tblgen] Add checked formatting with owned substitution errors (PR #238148)
Mehdi Amini
llvmlistbot at llvm.org
Mon Nov 2 15:47:06 PST 2026
https://github.com/joker-eph created https://github.com/llvm/llvm-project/pull/238148
Add tgfmtChecked and tgfmtvChecked, validating all placeholders before rendering and returning owned FmtError payloads on failure. Retain failed occurrences, available substitutions, and provenance for diagnostic users.
Unify builtin and custom binding storage: addSubst("_self", ...) now aliases withSelf(), including consistent last-registration-wins behavior. Parse positional indices as decimal, so $010 means 10 instead of 8 and $08 means 8 instead of reaching an unreachable path. Unrepresentable indices become checked errors or legacy markers instead of assertions.
Assisted-by: Codex
>From c7b2e8f44db3e51b989896f44566df4b4e6c8b04 Mon Sep 17 00:00:00 2001
From: Mehdi Amini <joker.eph at gmail.com>
Date: Sun, 1 Nov 2026 04:10:34 -0800
Subject: [PATCH] [mlir][tblgen] Add checked formatting with owned substitution
errors
Add tgfmtChecked and tgfmtvChecked, validating all placeholders before
rendering and returning owned FmtError payloads on failure. Retain
failed occurrences, available substitutions, and provenance for
diagnostic users.
Unify builtin and custom binding storage: addSubst("_self", ...) now
aliases withSelf(), including consistent last-registration-wins
behavior. Parse positional indices as decimal, so $010 means 10 instead
of 8 and $08 means 8 instead of reaching an unreachable path.
Unrepresentable indices become checked errors or legacy markers instead
of assertions.
Keep the legacy API lazy, including observing later context mutations.
The compatibility PHKind entry points remain until caller migration.
Validation: 66 TableGen unit tests; unchecked Expected arguments
rejected by the compiler. Include snapshot logging and typed error
propagation tests.
Assisted-by: Codex
---
mlir/include/mlir/TableGen/Format.h | 154 ++++++++++++++---
mlir/lib/TableGen/Format.cpp | 193 ++++++++++++++++++---
mlir/unittests/TableGen/FormatTest.cpp | 228 ++++++++++++++++++++++++-
3 files changed, 520 insertions(+), 55 deletions(-)
diff --git a/mlir/include/mlir/TableGen/Format.h b/mlir/include/mlir/TableGen/Format.h
index bdecf491faf21d..eb259d802d8a33 100644
--- a/mlir/include/mlir/TableGen/Format.h
+++ b/mlir/include/mlir/TableGen/Format.h
@@ -16,27 +16,102 @@
#define MLIR_TABLEGEN_FORMAT_H_
#include "mlir/Support/LLVM.h"
-#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringMap.h"
+#include "llvm/Support/Error.h"
#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/SMLoc.h"
namespace mlir {
namespace tblgen {
+/// An owned snapshot of a binding. Names omit the leading '$'.
+struct FmtBinding {
+ std::string name;
+ std::string replacement;
+};
+
+/// Source context attached to a formatting failure. Location arrays retain
+/// Record::getLoc() ordering: the definition followed by multiclass locations.
+/// Source buffers must outlive
+/// diagnostic emission; the arrays and descriptions themselves are owned.
+struct FmtSite {
+ SmallVector<llvm::SMLoc, 1> locations;
+ std::string description;
+};
+
+/// An owned definition and its use sites, attached as an error propagates.
+class FmtOrigin {
+public:
+ static FmtOrigin at(ArrayRef<llvm::SMLoc> locations,
+ const Twine &description);
+ static FmtOrigin describe(const Twine &description) {
+ return at({}, description);
+ }
+ FmtOrigin &withUse(ArrayRef<llvm::SMLoc> locations, const Twine &description);
+ const FmtSite &getDefinition() const { return definition; }
+ ArrayRef<FmtSite> getUses() const { return uses; }
+
+private:
+ FmtSite definition;
+ SmallVector<FmtSite, 1> uses;
+};
+
+/// Why a placeholder could not be expanded in a formatting invocation.
+enum class FmtIssueKind {
+ MissingNamedBinding,
+ PositionalOutOfRange,
+ RangeStartOutOfRange,
+ InvalidPositionalIndex
+};
+
+/// One failed occurrence. Offsets count bytes in the original template.
+struct FmtIssue {
+ FmtIssueKind kind;
+ std::string placeholder;
+ size_t offset;
+};
+
+/// A failed invocation, including all failed occurrences in template order.
+/// All textual data is owned, independently of the template and context.
+class FmtError : public llvm::ErrorInfo<FmtError> {
+public:
+ static char ID;
+ FmtError(StringRef format, SmallVector<FmtIssue> issues,
+ SmallVector<FmtBinding> bindings, size_t numPositionalArgs);
+ StringRef getTemplate() const { return format; }
+ ArrayRef<FmtIssue> getIssues() const { return issues; }
+ ArrayRef<FmtBinding> getBindings() const { return bindings; }
+ size_t getNumPositionalArgs() const { return numPositionalArgs; }
+ ArrayRef<FmtOrigin> getOrigins() const { return origins; }
+ void appendOrigin(const FmtOrigin &origin) { origins.push_back(origin); }
+ void printBindings(raw_ostream &os) const;
+ void log(raw_ostream &os) const override;
+ std::error_code convertToErrorCode() const override {
+ return llvm::inconvertibleErrorCode();
+ }
+
+private:
+ std::string format;
+ SmallVector<FmtIssue> issues;
+ SmallVector<FmtBinding> bindings;
+ size_t numPositionalArgs;
+ SmallVector<FmtOrigin> origins;
+};
+
+/// Append owned context without changing the error's type. Other errors pass
+/// through unchanged, including non-formatting leaves in an ErrorList.
+llvm::Error addFmtOrigin(llvm::Error error, const FmtOrigin &origin);
/// Format context containing substitutions for special placeholders.
///
-/// This context divides special placeholders into two categories: builtin ones
-/// and custom ones.
+/// Builtin placeholders have convenience setters and share storage and lookup
+/// with custom placeholders. They can be used in all dialects. Their names
+/// follow the convention of `$_<name>`. The rationale of the leading underscore
+/// is to avoid confusion and name collision: op arguments/attributes/results
+/// are named as $<name>, and we can potentially support referencing those
+/// entities directly in the format template in the future.
///
-/// Builtin placeholders are baked into `FmtContext` and each one of them has a
-/// dedicated setter. They can be used in all dialects. Their names follow the
-/// convention of `$_<name>`. The rationale of the leading underscore is to
-/// avoid confusion and name collision: op arguments/attributes/results are
-/// named as $<name>, and we can potentially support referencing those entities
-/// directly in the format template in the future.
-//
-/// Custom ones are registered by dialect-specific TableGen backends and use the
-/// same unified setter.
+/// Custom placeholders are registered by dialect-specific TableGen backends.
+/// Registration and lookup names omit the leading '$'.
class FmtContext {
public:
// Placeholder kinds
@@ -64,21 +139,13 @@ class FmtContext {
static PHKind getPlaceHolderKind(StringRef str);
-private:
- struct PHKindInfo : DenseMapInfo<PHKind> {
- using CharInfo = DenseMapInfo<char>;
-
- static unsigned getHashValue(const PHKind &val) {
- return CharInfo::getHashValue(static_cast<char>(val));
- }
-
- static bool isEqual(const PHKind &lhs, const PHKind &rhs) {
- return lhs == rhs;
- }
- };
+ /// Sorted, owned snapshots, including builtin bindings and empty
+ /// replacements.
+ SmallVector<FmtBinding> getBindings() const;
+ void printBindings(raw_ostream &os) const;
- llvm::SmallDenseMap<PHKind, std::string, 4, PHKindInfo> builtinSubstMap;
- llvm::StringMap<std::string> customSubstMap;
+private:
+ llvm::StringMap<std::string> substMap;
};
/// Struct representing a replacement segment for the formatted string. It can
@@ -90,6 +157,7 @@ struct FmtReplacement {
Literal,
PositionalPH,
PositionalRangePH,
+ InvalidPH,
SpecialPH
};
@@ -151,6 +219,10 @@ class FmtObjectBase {
void format(llvm::raw_ostream &s) const;
+ /// Validate the complete invocation before rendering any positional adapter.
+ /// Unlike str(), this never encodes failure in the returned string.
+ llvm::Expected<std::string> strChecked() const;
+
std::string str() const {
std::string result;
llvm::raw_string_ostream s(result);
@@ -243,6 +315,7 @@ class FmtStrVecObject : public FmtObjectBase {
/// as-is with an additional marker '<no-subst-found>', to aid debugging.
///
/// To print a '$' literally, escape it with '$$'.
+/// Replacement text is emitted literally, without recursive expansion.
///
/// This utility function is inspired by LLVM formatv(), with modifications
/// specially tailored for TableGen C++ generation usage:
@@ -269,6 +342,35 @@ inline FmtStrVecObject tgfmtv(StringRef fmt, const FmtContext *ctx,
return FmtStrVecObject(fmt, ctx, params);
}
+/// Eager formatting that validates every placeholder before invoking positional
+/// formatting providers. Returns FmtError if:
+/// - a named placeholder has no binding in ctx;
+/// - a decimal positional index cannot be represented as size_t;
+/// - a positional placeholder's index is at least the argument count; or
+/// - a positional range starts at or beyond the argument count, including an
+/// empty range.
+///
+/// Empty replacements and unused bindings or arguments are allowed. Replacement
+/// text is emitted literally, without recursive expansion. Failures inside
+/// positional formatting providers are outside this validation.
+///
+/// On failure, FmtError owns the template, all failed occurrences and their byte
+/// offsets, a snapshot of named bindings, and the positional argument count. No
+/// partial output is returned on validation failure. Successful results own
+/// their text; neither result nor error borrows the input template, context, or
+/// arguments.
+template <typename... Ts>
+llvm::Expected<std::string> tgfmtChecked(StringRef fmt, const FmtContext &ctx,
+ Ts &&...vals) {
+ return tgfmt(fmt, &ctx, std::forward<Ts>(vals)...).strChecked();
+}
+
+/// Array form of tgfmtChecked, with the same validation and ownership.
+inline llvm::Expected<std::string> tgfmtvChecked(StringRef fmt,
+ const FmtContext &ctx,
+ ArrayRef<std::string> params) {
+ return tgfmtv(fmt, &ctx, params).strChecked();
+}
} // namespace tblgen
} // namespace mlir
diff --git a/mlir/lib/TableGen/Format.cpp b/mlir/lib/TableGen/Format.cpp
index 2153de2af4797e..10c31ff1cdeba0 100644
--- a/mlir/lib/TableGen/Format.cpp
+++ b/mlir/lib/TableGen/Format.cpp
@@ -13,9 +13,10 @@
//===----------------------------------------------------------------------===//
#include "mlir/TableGen/Format.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/ADT/Twine.h"
-#include <cctype>
using namespace mlir;
using namespace mlir::tblgen;
@@ -23,44 +24,138 @@ using namespace mlir::tblgen;
// Marker to indicate an error happened when replacing a placeholder.
const char *const kMarkerForNoSubst = "<no-subst-found>";
+char FmtError::ID;
+
+FmtOrigin FmtOrigin::at(ArrayRef<llvm::SMLoc> locations,
+ const Twine &description) {
+ FmtOrigin result;
+ result.definition = {{locations.begin(), locations.end()}, description.str()};
+ return result;
+}
+
+FmtOrigin &FmtOrigin::withUse(ArrayRef<llvm::SMLoc> locations,
+ const Twine &description) {
+ uses.push_back({{locations.begin(), locations.end()}, description.str()});
+ return *this;
+}
+
+FmtError::FmtError(StringRef format, SmallVector<FmtIssue> issues,
+ SmallVector<FmtBinding> bindings, size_t numPositionalArgs)
+ : format(format.str()), issues(std::move(issues)),
+ bindings(std::move(bindings)), numPositionalArgs(numPositionalArgs) {
+ assert(!this->issues.empty() &&
+ "formatting error without failed occurrences");
+}
+
+static void printBindings(raw_ostream &os, ArrayRef<FmtBinding> bindings) {
+ for (const FmtBinding &binding : bindings) {
+ os << '$' << binding.name << " = \"";
+ os.write_escaped(binding.replacement, /*UseHexEscapes=*/true) << "\"\n";
+ }
+}
+
+void FmtError::printBindings(raw_ostream &os) const {
+ ::printBindings(os, bindings);
+}
+
+void FmtError::log(raw_ostream &os) const {
+ for (const FmtIssue &issue : issues) {
+ os << "cannot substitute '" << issue.placeholder
+ << "' at template byte offset " << issue.offset;
+ switch (issue.kind) {
+ case FmtIssueKind::MissingNamedBinding:
+ os << ": missing named binding";
+ break;
+ case FmtIssueKind::PositionalOutOfRange:
+ os << ": positional argument out of range";
+ break;
+ case FmtIssueKind::RangeStartOutOfRange:
+ os << ": positional range starts out of range";
+ break;
+ case FmtIssueKind::InvalidPositionalIndex:
+ os << ": invalid positional index";
+ break;
+ }
+ os << '\n';
+ }
+ os << "template: ";
+ os.write_escaped(format, /*UseHexEscapes=*/true) << '\n';
+ os << "available named substitutions:";
+ if (bindings.empty())
+ os << " (none)";
+ for (const FmtBinding &binding : bindings)
+ os << " $" << binding.name;
+ os << "\npositional arguments supplied: " << numPositionalArgs;
+ for (const FmtOrigin &origin : origins) {
+ os << "\nwhile formatting " << origin.getDefinition().description;
+ for (const FmtSite &use : origin.getUses())
+ os << "\nwhile expanding " << use.description;
+ }
+}
+
+llvm::Error mlir::tblgen::addFmtOrigin(llvm::Error error,
+ const FmtOrigin &origin) {
+ return llvm::handleErrors(
+ std::move(error), [&](std::unique_ptr<FmtError> payload) -> llvm::Error {
+ payload->appendOrigin(origin);
+ return llvm::Error(std::move(payload));
+ });
+}
+
FmtContext::FmtContext(ArrayRef<std::pair<StringRef, StringRef>> subs) {
for (auto &sub : subs)
addSubst(sub.first, sub.second);
}
FmtContext &FmtContext::addSubst(StringRef placeholder, const Twine &subst) {
- customSubstMap[placeholder] = subst.str();
+ substMap[placeholder] = subst.str();
return *this;
}
FmtContext &FmtContext::withBuilder(Twine subst) {
- builtinSubstMap[PHKind::Builder] = subst.str();
- return *this;
+ return addSubst("_builder", subst);
}
FmtContext &FmtContext::withSelf(Twine subst) {
- builtinSubstMap[PHKind::Self] = subst.str();
- return *this;
+ return addSubst("_self", subst);
}
std::optional<StringRef>
FmtContext::getSubstFor(FmtContext::PHKind placeholder) const {
- if (placeholder == FmtContext::PHKind::None ||
- placeholder == FmtContext::PHKind::Custom)
+ switch (placeholder) {
+ case PHKind::Builder:
+ return getSubstFor("_builder");
+ case PHKind::Self:
+ return getSubstFor("_self");
+ case PHKind::None:
+ case PHKind::Custom:
return {};
- auto it = builtinSubstMap.find(placeholder);
- if (it == builtinSubstMap.end())
- return {};
- return StringRef(it->second);
+ }
+ llvm_unreachable("unknown placeholder kind");
}
std::optional<StringRef> FmtContext::getSubstFor(StringRef placeholder) const {
- auto it = customSubstMap.find(placeholder);
- if (it == customSubstMap.end())
+ auto it = substMap.find(placeholder);
+ if (it == substMap.end())
return {};
return StringRef(it->second);
}
+SmallVector<FmtBinding> FmtContext::getBindings() const {
+ SmallVector<FmtBinding> bindings;
+ bindings.reserve(substMap.size());
+ for (const auto &entry : substMap)
+ bindings.push_back({entry.getKey().str(), entry.getValue()});
+ llvm::sort(bindings, [](const FmtBinding &lhs, const FmtBinding &rhs) {
+ return lhs.name < rhs.name;
+ });
+ return bindings;
+}
+
+void FmtContext::printBindings(raw_ostream &os) const {
+ ::printBindings(os, getBindings());
+}
+
FmtContext::PHKind FmtContext::getPlaceHolderKind(StringRef str) {
return StringSwitch<FmtContext::PHKind>(str)
.Case("_builder", FmtContext::PHKind::Builder)
@@ -98,31 +193,37 @@ FmtObjectBase::splitFmtSegment(StringRef fmt) {
// First try to see if it's a positional placeholder, and then handle special
// placeholders.
- size_t end =
- fmt.find_if_not([](char c) { return std::isdigit(c); }, /*From=*/1);
+ size_t end = fmt.find_if_not(llvm::isDigit, /*From=*/1);
if (end != 1) {
// We have a positional placeholder. Parse the index.
size_t index = 0;
- if (fmt.substr(1, end - 1).consumeInteger(0, index)) {
- llvm_unreachable("invalid replacement sequence index");
+ if (end == StringRef::npos)
+ end = fmt.size();
+ bool invalid = fmt.slice(1, end).getAsInteger(10, index);
+ bool isRange = fmt.substr(end).starts_with("...");
+ size_t tokenEnd = end + (isRange ? 3 : 0);
+ if (invalid) {
+ FmtReplacement replacement(fmt.take_front(tokenEnd));
+ replacement.type = FmtReplacement::Type::InvalidPH;
+ return {replacement, fmt.drop_front(tokenEnd)};
}
// Check if this is the part of a range specification.
- if (fmt.substr(end, 3) == "...") {
+ if (isRange) {
// Currently only ranges without upper bound are supported.
return {
FmtReplacement{fmt.substr(0, end + 3), index, FmtReplacement::kUnset},
fmt.substr(end + 3)};
}
- if (end == StringRef::npos) {
+ if (end == fmt.size()) {
// All the remaining characters are part of the positional placeholder.
return {FmtReplacement{fmt, index}, StringRef()};
}
return {FmtReplacement{fmt.substr(0, end), index}, fmt.substr(end)};
}
- end = fmt.find_if_not([](char c) { return std::isalnum(c) || c == '_'; }, 1);
+ end = fmt.find_if_not([](char c) { return llvm::isAlnum(c) || c == '_'; }, 1);
auto placeholder = FmtContext::getPlaceHolderKind(fmt.substr(1, end - 1));
if (end == StringRef::npos) {
// All the remaining characters are part of the special placeholder.
@@ -152,6 +253,11 @@ void FmtObjectBase::format(raw_ostream &s) const {
continue;
}
+ if (repl.type == FmtReplacement::Type::InvalidPH) {
+ s << repl.spec << kMarkerForNoSubst;
+ continue;
+ }
+
if (repl.type == FmtReplacement::Type::SpecialPH) {
if (repl.placeholder == FmtContext::PHKind::None) {
s << repl.spec;
@@ -160,12 +266,8 @@ void FmtObjectBase::format(raw_ostream &s) const {
s << repl.spec << kMarkerForNoSubst;
} else {
std::optional<StringRef> subst;
- if (repl.placeholder == FmtContext::PHKind::Custom) {
- // Skip the leading '$' sign for the custom placeholder
- subst = context->getSubstFor(repl.spec.substr(1));
- } else {
- subst = context->getSubstFor(repl.placeholder);
- }
+ // Builtin and custom placeholders use the same lookup path.
+ subst = context->getSubstFor(repl.spec.drop_front());
if (subst)
s << *subst;
else
@@ -197,6 +299,43 @@ void FmtObjectBase::format(raw_ostream &s) const {
}
}
+llvm::Expected<std::string> FmtObjectBase::strChecked() const {
+ SmallVector<FmtIssue> issues;
+ for (const FmtReplacement &repl : replacements) {
+ std::optional<FmtIssueKind> kind;
+ switch (repl.type) {
+ case FmtReplacement::Type::Empty:
+ case FmtReplacement::Type::Literal:
+ break;
+ case FmtReplacement::Type::InvalidPH:
+ kind = FmtIssueKind::InvalidPositionalIndex;
+ break;
+ case FmtReplacement::Type::SpecialPH:
+ if (repl.placeholder != FmtContext::PHKind::None &&
+ (!context || !context->getSubstFor(repl.spec.drop_front())))
+ kind = FmtIssueKind::MissingNamedBinding;
+ break;
+ case FmtReplacement::Type::PositionalPH:
+ if (repl.index >= adapters.size())
+ kind = FmtIssueKind::PositionalOutOfRange;
+ break;
+ case FmtReplacement::Type::PositionalRangePH:
+ if (repl.index >= adapters.size())
+ kind = FmtIssueKind::RangeStartOutOfRange;
+ break;
+ }
+ if (kind)
+ issues.push_back({*kind, repl.spec.str(),
+ static_cast<size_t>(repl.spec.data() - fmt.data())});
+ }
+ if (!issues.empty())
+ return llvm::make_error<FmtError>(fmt, std::move(issues),
+ context ? context->getBindings()
+ : SmallVector<FmtBinding>{},
+ adapters.size());
+ return str();
+}
+
FmtStrVecObject::FmtStrVecObject(StringRef fmt, const FmtContext *ctx,
ArrayRef<std::string> params)
: FmtObjectBase(fmt, ctx, params.size()) {
diff --git a/mlir/unittests/TableGen/FormatTest.cpp b/mlir/unittests/TableGen/FormatTest.cpp
index ef00cb4a1c24b3..ae36ac1c2930c7 100644
--- a/mlir/unittests/TableGen/FormatTest.cpp
+++ b/mlir/unittests/TableGen/FormatTest.cpp
@@ -9,10 +9,25 @@
#include "mlir/TableGen/Format.h"
#include "gmock/gmock.h"
-using mlir::tblgen::FmtContext;
-using mlir::tblgen::tgfmt;
using ::testing::StrEq;
+using namespace mlir::tblgen;
+
+static std::unique_ptr<FmtError>
+takeFormatError(llvm::Expected<std::string> result) {
+ EXPECT_FALSE(static_cast<bool>(result));
+ if (result)
+ return nullptr;
+ std::unique_ptr<FmtError> error;
+ llvm::handleAllErrors(
+ result.takeError(),
+ [&](std::unique_ptr<FmtError> e) { error = std::move(e); },
+ [&](const llvm::ErrorInfoBase &e) {
+ ADD_FAILURE() << "expected FmtError, got " << e.message();
+ });
+ return error;
+}
+
TEST(FormatTest, EmptyFmtStr) {
FmtContext ctx;
std::string result = std::string(tgfmt("", &ctx));
@@ -150,3 +165,212 @@ TEST(FormatTest, MixedFmtStr) {
&ctx.withSelf("sss"), "a", "b"));
EXPECT_THAT(result, StrEq("bbb.build(sss, {a, b})"));
}
+
+TEST(FormatTest, CheckedBuiltinRegistrationAndSnapshots) {
+ FmtContext ctx({{"z", ""},
+ {"w", "w"},
+ {"_self", "self"},
+ {"a", "a"},
+ {"_builder", "builder"}});
+ auto result = tgfmtChecked("$_builder($_self)$z", ctx);
+ ASSERT_TRUE(static_cast<bool>(result));
+ EXPECT_EQ(*result, "builder(self)");
+ ctx.withSelf("first").addSubst("_self", "second");
+ EXPECT_EQ(ctx.getSubstFor(FmtContext::PHKind::Self), "second");
+ ctx.withSelf("third");
+ EXPECT_EQ(ctx.getSubstFor("_self"), "third");
+ auto bindings = ctx.getBindings();
+ ASSERT_EQ(bindings.size(), 5u);
+ EXPECT_EQ(bindings[0].name, "_builder");
+ EXPECT_EQ(bindings[1].name, "_self");
+ EXPECT_EQ(bindings[2].name, "a");
+ EXPECT_EQ(bindings[3].name, "w");
+ EXPECT_EQ(bindings[4].name, "z");
+ EXPECT_EQ(bindings[4].replacement, "");
+ ctx.withSelf("fourth");
+ EXPECT_EQ(bindings[1].replacement, "third");
+ EXPECT_EQ(*result, "builder(self)");
+}
+
+TEST(FormatTest, CheckedErrorsOwnInvocationData) {
+ auto result = [] {
+ FmtContext ctx({{"_self", "self"}, {"empty", ""}});
+ std::string fmt = "$missing + $missing + $3";
+ return tgfmtChecked(fmt, ctx, "only");
+ }();
+ auto error = takeFormatError(std::move(result));
+ ASSERT_TRUE(error);
+ EXPECT_EQ(error->getTemplate(), "$missing + $missing + $3");
+ auto issues = error->getIssues();
+ ASSERT_EQ(issues.size(), 3u);
+ EXPECT_EQ(issues[0].kind, FmtIssueKind::MissingNamedBinding);
+ EXPECT_EQ(issues[0].placeholder, "$missing");
+ EXPECT_EQ(issues[0].offset, 0u);
+ EXPECT_EQ(issues[1].offset, 11u);
+ EXPECT_EQ(issues[2].offset, 22u);
+ EXPECT_EQ(issues[2].kind, FmtIssueKind::PositionalOutOfRange);
+ EXPECT_EQ(error->getNumPositionalArgs(), 1u);
+ ASSERT_EQ(error->getBindings().size(), 2u);
+ EXPECT_EQ(error->getBindings()[0].replacement, "self");
+ EXPECT_EQ(error->getBindings()[1].replacement, "");
+}
+
+TEST(FormatTest, CheckedDecimalAndRangeBoundaries) {
+ std::vector<std::string> values;
+ for (unsigned i = 0; i != 11; ++i)
+ values.push_back(std::to_string(i));
+ auto result = tgfmtvChecked("$00 $08 $010 $9...", FmtContext{}, values);
+ ASSERT_TRUE(static_cast<bool>(result));
+ EXPECT_EQ(*result, "0 8 10 9, 10");
+ EXPECT_EQ(tgfmtv("$08 $010 $10...", nullptr, values).str(), "8 10 10");
+ auto single = tgfmtvChecked("$10...", FmtContext{}, values);
+ ASSERT_TRUE(static_cast<bool>(single));
+ EXPECT_EQ(*single, "10");
+ auto error = takeFormatError(tgfmtChecked("$1...", FmtContext{}, "one"));
+ ASSERT_TRUE(error);
+ EXPECT_EQ(error->getIssues()[0].kind, FmtIssueKind::RangeStartOutOfRange);
+ auto emptyRange = takeFormatError(tgfmtChecked("$0...", FmtContext{}));
+ ASSERT_TRUE(emptyRange);
+ EXPECT_EQ(emptyRange->getIssues()[0].kind,
+ FmtIssueKind::RangeStartOutOfRange);
+}
+
+TEST(FormatTest, CheckedInvalidIndexAndLiteralSyntax) {
+ llvm::StringRef huge =
+ "$999999999999999999999999999999999999999999999999999...";
+ auto error = takeFormatError(tgfmtChecked(huge, FmtContext{}));
+ ASSERT_TRUE(error);
+ EXPECT_EQ(error->getIssues()[0].kind, FmtIssueKind::InvalidPositionalIndex);
+ EXPECT_EQ(error->getIssues()[0].placeholder, huge);
+ EXPECT_EQ(std::string(tgfmt(huge, nullptr)),
+ (huge + "<no-subst-found>").str());
+ auto result = tgfmtChecked("$$ $- $? foo$0bar $0..2 $", FmtContext{}, "x");
+ ASSERT_TRUE(static_cast<bool>(result));
+ EXPECT_EQ(*result, "$ $- $? fooxbar x..2 $");
+}
+
+TEST(FormatTest, CheckedInvalidIndexWithoutRange) {
+ llvm::StringRef huge = "$99999999999999999999999999999999999999999999999999";
+ auto error = takeFormatError(tgfmtChecked(huge, FmtContext{}));
+ ASSERT_TRUE(error);
+ ASSERT_EQ(error->getIssues().size(), 1u);
+ EXPECT_EQ(error->getIssues()[0].kind, FmtIssueKind::InvalidPositionalIndex);
+ EXPECT_EQ(error->getIssues()[0].placeholder, huge);
+ EXPECT_EQ(error->getIssues()[0].offset, 0u);
+}
+
+TEST(FormatTest, CheckedMissingBuiltinAndNullLegacyContext) {
+ auto missing = takeFormatError(tgfmtChecked("$_self", FmtContext{}));
+ ASSERT_TRUE(missing);
+ EXPECT_EQ(missing->getIssues()[0].kind, FmtIssueKind::MissingNamedBinding);
+ EXPECT_EQ(missing->getIssues()[0].placeholder, "$_self");
+ auto noContext = takeFormatError(tgfmt("$_self", nullptr).strChecked());
+ ASSERT_TRUE(noContext);
+ EXPECT_EQ(noContext->getIssues()[0].kind, FmtIssueKind::MissingNamedBinding);
+ EXPECT_TRUE(noContext->getBindings().empty());
+}
+
+TEST(FormatTest, ErrorLogIncludesFailureAndAvailability) {
+ auto result = tgfmtChecked("$_self $2", FmtContext{}, "only");
+ ASSERT_FALSE(static_cast<bool>(result));
+ std::string log = llvm::toString(addFmtOrigin(
+ result.takeError(), FmtOrigin::describe("predicate").withUse({}, "op")));
+ EXPECT_THAT(log, ::testing::HasSubstr("'$_self' at template byte offset 0"));
+ EXPECT_THAT(log, ::testing::HasSubstr("missing named binding"));
+ EXPECT_THAT(log, ::testing::HasSubstr("'$2' at template byte offset 7"));
+ EXPECT_THAT(log, ::testing::HasSubstr("positional argument out of range"));
+ EXPECT_THAT(log, ::testing::HasSubstr("template: $_self $2"));
+ EXPECT_THAT(log,
+ ::testing::HasSubstr("available named substitutions: (none)"));
+ EXPECT_THAT(log, ::testing::HasSubstr("positional arguments supplied: 1"));
+ EXPECT_THAT(log, ::testing::HasSubstr("while formatting predicate"));
+ EXPECT_THAT(log, ::testing::HasSubstr("while expanding op"));
+}
+
+struct CountFormatCalls {
+ unsigned *calls;
+ void format(llvm::raw_ostream &os, llvm::StringRef) const {
+ ++*calls;
+ os << "value";
+ }
+};
+
+TEST(FormatTest, CheckedValidationPrecedesRendering) {
+ unsigned calls = 0;
+ auto error = takeFormatError(
+ tgfmtChecked("$0 $missing $2", FmtContext{}, CountFormatCalls{&calls}));
+ ASSERT_TRUE(error);
+ EXPECT_EQ(calls, 0u);
+ EXPECT_EQ(error->getIssues().size(), 2u);
+ auto result = tgfmtChecked("$0 $0", FmtContext{}, CountFormatCalls{&calls});
+ ASSERT_TRUE(static_cast<bool>(result));
+ EXPECT_EQ(*result, "value value");
+ EXPECT_EQ(calls, 2u);
+}
+
+TEST(FormatTest, CheckedReplacementTextIsLiteral) {
+ FmtContext ctx({{"text", "$_missing<no-subst-found>"}});
+ auto result = tgfmtChecked("$text $0", ctx, "$_also_missing");
+ ASSERT_TRUE(static_cast<bool>(result));
+ EXPECT_EQ(*result, "$_missing<no-subst-found> $_also_missing");
+}
+
+TEST(FormatTest, OriginsAreOwnedAndKeepTypedErrors) {
+ const char source[] = "definition and use";
+ llvm::SMLoc loc = llvm::SMLoc::getFromPointer(source);
+ llvm::SmallVector<llvm::SMLoc> locations{loc};
+ std::string description = "predicate";
+ FmtOrigin origin =
+ FmtOrigin::at(locations, description).withUse(locations, "operation");
+ locations.clear();
+ description.clear();
+ auto result = tgfmtChecked("$unknown", FmtContext{});
+ ASSERT_FALSE(static_cast<bool>(result));
+ auto error = addFmtOrigin(result.takeError(), origin);
+ error = addFmtOrigin(std::move(error), FmtOrigin::describe("outer"));
+ unsigned formattedErrors = 0, otherErrors = 0;
+ error = llvm::joinErrors(std::move(error),
+ llvm::createStringError("unrelated error"));
+ error = addFmtOrigin(std::move(error), FmtOrigin::describe("caller"));
+ llvm::handleAllErrors(
+ std::move(error),
+ [&](const FmtError &e) {
+ ++formattedErrors;
+ ASSERT_EQ(e.getOrigins().size(), 3u);
+ EXPECT_EQ(e.getOrigins()[0].getDefinition().description, "predicate");
+ EXPECT_EQ(e.getOrigins()[0].getDefinition().locations[0], loc);
+ EXPECT_EQ(e.getOrigins()[0].getUses()[0].description, "operation");
+ EXPECT_EQ(e.getOrigins()[1].getDefinition().description, "outer");
+ EXPECT_EQ(e.getOrigins()[2].getDefinition().description, "caller");
+ },
+ [&](const llvm::StringError &) { ++otherErrors; });
+ EXPECT_EQ(formattedErrors, 1u);
+ EXPECT_EQ(otherErrors, 1u);
+ llvm::cantFail(addFmtOrigin(llvm::Error::success(), origin));
+}
+
+TEST(FormatTest, BindingLogsEscapeAndSort) {
+ FmtContext ctx({{"z", "line\nbreak"}, {"_self", "a\"b\\c"}});
+ std::string log;
+ llvm::raw_string_ostream os(log);
+ ctx.printBindings(os);
+ EXPECT_EQ(log, "$_self = \"a\\\"b\\\\c\"\n$z = \"line\\nbreak\"\n");
+ auto error = takeFormatError(tgfmtChecked("$missing", ctx));
+ ASSERT_TRUE(error);
+ ctx.withSelf("changed");
+ std::string snapshotLog;
+ llvm::raw_string_ostream snapshotStream(snapshotLog);
+ error->printBindings(snapshotStream);
+ EXPECT_EQ(snapshotLog, log);
+}
+
+TEST(FormatTest, LegacyRemainsLazy) {
+ FmtContext ctx;
+ ctx.withSelf("before");
+ auto legacy = tgfmt("$_self", &ctx);
+ auto eager = tgfmtChecked("$_self", ctx);
+ ASSERT_TRUE(static_cast<bool>(eager));
+ ctx.withSelf("after");
+ EXPECT_EQ(legacy.str(), "after");
+ EXPECT_EQ(*eager, "before");
+}
More information about the Mlir-commits
mailing list