[Lldb-commits] [lldb] [lldb] Fix compile Python f-strings to improved `@fmt` (PR #232997)
Dave Lee via lldb-commits
lldb-commits at lists.llvm.org
Tue Oct 20 10:51:53 PDT 2026
https://github.com/kastiglione updated https://github.com/llvm/llvm-project/pull/232997
>From 9c75761aa0009d376645a8eddc5956c88556f0d6 Mon Sep 17 00:00:00 2001
From: Dave Lee <davelee.com at gmail.com>
Date: Fri, 16 Oct 2026 15:45:23 -0700
Subject: [PATCH 1/2] [lldb] Fix compile Python f-strings to improved `@fmt`
Updates the `@fmt` selector, by adding support for Python f-strings to the Python to
bytecode compiler, and updating the evaluation of `@fmt` by improved interfacing with
`llvm::formatv`.
Changes to `@fmt`:
* Consume the arguments, leaving only the resulting string on the stack. Previously, the
arguments were left on the stack, unlike what the docs describe and what the Python
interpreter does.
* In version 2 formatters, replacements are automatically indexed. Explicit indexes,such
as {0}, are an error. This makes the stack effect of `@fmt` clear from the format
string: the number of arguments is the number of replacements, and each argument is used
once, in stack order. Arguments can be reordered or reused with the stack operations.
* In version 2 formatters, layouts and options ({,8}, {:x}) are accepted, but are
currently ignored. They are treated as best effort hints, so that support for them can
be added later without changing the ABI.
* In version 2 formatters, the format string is checked by LLDB before it reaches
`formatv`, which is given only literal text, escaped braces, and {}. Format strings come
from the binary being debugged, and `formatv` asserts on malformed input. Layouts with a
very large width, such as {,4000000000}, would also build a very large string, even in
release builds.
Version 1 formatters are unchanged, except for consuming their arguments.
Changes to formatter_bytecode.py:
* f-strings are compiled to `@fmt`. Conversions ({x!r}) and format specs ({x:>8}) are not
supported, and are a compile error.
* String literals, including f-strings, escape quotes and backslashes when emitting assembly.
* The interpreter's `@fmt` matches the C++ interpreter. It now passes arguments in stack
order, supports {}, and follows the `formatv` rules for braces ({{ is a literal {, and a
lone } is a literal).
Assisted-by: claude
---
lldb/docs/resources/formatterbytecode.md | 24 ++-
lldb/examples/python/formatter_bytecode.py | 194 ++++++++++++++++--
.../DataFormatters/FormatterBytecode.cpp | 63 +++++-
.../DataFormatter/FormatterBytecodeTest.cpp | 91 +++++++-
4 files changed, 355 insertions(+), 17 deletions(-)
diff --git a/lldb/docs/resources/formatterbytecode.md b/lldb/docs/resources/formatterbytecode.md
index 29e2addb03b88..32c49893dfb02 100644
--- a/lldb/docs/resources/formatterbytecode.md
+++ b/lldb/docs/resources/formatterbytecode.md
@@ -176,12 +176,34 @@ Sel. Mnemonic Stack Effect
0x44 ``read_memory_int64`` ``(UInt @read_memory_int64 -> Int)`` ``Target::ReadMemory``
0x45 ``read_memory_address`` ``(UInt @read_memory_uint64 -> UInt)`` ``Target::ReadMemory``
0x46 ``read_memory`` ``(UInt Type @read_memory -> Object)`` ``Target::ReadMemory``
-0x50 ``fmt`` ``(String arg0 ... @fmt -> String)`` ``llvm::format``
+0x50 ``fmt`` ``(arg0 ... argN String @fmt -> String)`` ``llvm::formatv``
0x51 ``sprintf`` ``(String arg0 ... sprintf -> String)`` ``sprintf``
0x52 ``strlen`` ``(String strlen -> Integer)`` ``strlen in bytes``
==== =============================== ==================================================================== ======================================
```
+### Formatting strings
+
+`@fmt` formats its arguments using an `llvm::formatv` format string, which is on top of the stack, above the arguments. The arguments are consumed, and only the resulting string is left on the stack.
+
+The format string may contain literal text, escaped braces (`{{`, a literal `{`), and replacements. A replacement has no index, and an optional layout and options:
+
+```
+"{" [ "," layout ] [ ":" options ] "}"
+```
+
+The layout may not contain `{`, `}`, or `:`, and the options may not contain `{` or `}`. For example, `{}`, `{,8}`, `{:x}`, and `{,8:x}` are all replacements.
+
+Each replacement is replaced with the next argument, in stack order, so the first argument is the deepest one on the stack, and the number of arguments is the number of replacements. Explicit indexes, such as `{0}`, are an error. To reorder or reuse arguments, use the stack operations before calling `@fmt`. For example, this formats `"(1, 2)"`:
+
+```
+1 2 "({}, {})" @fmt call
+```
+
+Layouts and options are hints, which are applied on a best effort basis. Support for them may vary between versions of LLDB, and a layout or option that is not supported is ignored, so that the replacement is formatted like `{}`. Layouts and options never change which arguments are used. Currently, no layouts or options are supported.
+
+In version 1 formatters, any `llvm::formatv` format string is accepted.
+
### Dictionary objects
`Dictionary` objects are key-value containers, with `String` value keys, and values of any data type. `Dictionary` is a reference type, mutating it through one reference is visible through any other reference to the same dictionary (e.g. one obtained earlier with `dup`). Empty `Dictionary` objects are created with `dict`. `Dictionary` objects are populated with `dict_set`. Values are retrieved with `dict_get`. When `dict_get` is called with a key that is not present in the dictionary, an error is emitted. Use `dict_has` first to check for a key's existence. Dictionary operations consumes the `Dictionary` argument, so `dup` it first if the `Dictionary` is needed afterward. For example, to set multiple keys in a row:
diff --git a/lldb/examples/python/formatter_bytecode.py b/lldb/examples/python/formatter_bytecode.py
index 5fca4c7cdee3f..c094a5963c6e0 100644
--- a/lldb/examples/python/formatter_bytecode.py
+++ b/lldb/examples/python/formatter_bytecode.py
@@ -522,16 +522,71 @@ def next_byte():
################################################################################
+def _parse_fmt(fmt: str) -> list[Union[str, int]]:
+ """Parse an llvm::formatv format string into literals (str) and
+ replacement indexes (int). Adjacent literals are merged, so two items in a
+ row are never both literals.
+
+ As in version 2 of the bytecode, replacements are automatically indexed:
+
+ "{" [ "," layout ] [ ":" options ] "}"
+
+ Explicit indexes are not supported. Layouts and options are accepted, but
+ are not yet supported, and are ignored."""
+
+ items: list[Union[str, int]] = []
+ literal = ""
+ next_index = 0
+ i = 0
+ while i < len(fmt):
+ if fmt.startswith("{{", i):
+ literal += "{"
+ i += 2
+ elif fmt[i] == "{":
+ start = i
+ i += 1
+ # Skip the layout, which ends at the options or the closing brace.
+ if i < len(fmt) and fmt[i] == ",":
+ i += 1
+ while i < len(fmt) and fmt[i] not in "{}:":
+ i += 1
+ # Skip the options, which end at the closing brace.
+ if i < len(fmt) and fmt[i] == ":":
+ i += 1
+ while i < len(fmt) and fmt[i] not in "{}":
+ i += 1
+ if i == len(fmt) or fmt[i] != "}":
+ raise ValueError(
+ f"invalid replacement in format string at offset {start}"
+ )
+ i += 1
+ if literal:
+ items.append(literal)
+ literal = ""
+ items.append(next_index)
+ next_index += 1
+ else:
+ # Consume the text up to the next brace.
+ end = fmt.find("{", i)
+ if end == -1:
+ end = len(fmt)
+ literal += fmt[i:end]
+ i = end
+ if literal:
+ items.append(literal)
+ return items
+
+
def count_fmt_params(fmt: str) -> int:
"""Count the number of parameters in a format string"""
- from string import Formatter
+ return sum(isinstance(item, int) for item in _parse_fmt(fmt))
- f = Formatter()
- n = 0
- for _, name, _, _ in f.parse(fmt):
- if name > n:
- n = name
- return n
+
+def formatv(fmt: str, args: list) -> str:
+ """Format args with an llvm::formatv format string"""
+ return "".join(
+ item if isinstance(item, str) else str(args[item]) for item in _parse_fmt(fmt)
+ )
def interpret(bytecode: bytes, control: list, data: list, tracing: bool = False):
@@ -761,10 +816,10 @@ def next_byte():
elif sel == sel_fmt:
fmt = data.pop()
n = count_fmt_params(fmt)
- args = []
- for i in range(n):
- args.append(data.pop())
- data.append(fmt.format(*args))
+ # The first argument is the deepest on the stack.
+ args = data[len(data) - n :]
+ del data[len(data) - n :]
+ data.append(formatv(fmt, args))
else:
print("not implemented: " + selector[sel])
assert False
@@ -835,6 +890,12 @@ def next_byte():
}
+def _string_literal(s: str) -> str:
+ """Quote a string as an assembly string literal."""
+ escaped = s.replace("\\", "\\\\").replace('"', '\\"')
+ return f'"{escaped}"'
+
+
class CompilerError(Exception):
lineno: int
@@ -1031,7 +1092,7 @@ def visit_Return(self, node: ast.Return) -> None:
def visit_Constant(self, node: ast.Constant) -> None:
if isinstance(node.value, str):
- self._output(f'"{node.value}"')
+ self._output(_string_literal(node.value))
elif isinstance(node.value, bool):
self._output(int(node.value))
elif node.value is None:
@@ -1039,6 +1100,29 @@ def visit_Constant(self, node: ast.Constant) -> None:
else:
self._output(node.value)
+ def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
+ # f-strings are compiled to @fmt, which formats with llvm::formatv. The
+ # formatted values are pushed in order, followed by the format string.
+ fmt = ""
+ for child in node.values:
+ if isinstance(child, ast.Constant):
+ string = str(child.value)
+ # formatv escapes a literal "{" as "{{". A "}" is a literal as is.
+ fmt += string.replace("{", "{{")
+ else:
+ self.visit(child)
+ fmt += "{}"
+
+ self._output(_string_literal(fmt))
+ self._output("@fmt call")
+
+ def visit_FormattedValue(self, node: ast.FormattedValue) -> None:
+ if node.conversion != -1:
+ raise CompilerError("unsupported format conversion", node)
+ if node.format_spec:
+ raise CompilerError("unsupported format spec", node)
+ self.visit(node.value)
+
def visit_Call(self, node: ast.Call) -> None:
func = node.func
if isinstance(func, ast.Attribute):
@@ -1319,6 +1403,44 @@ def roundtrip(asm):
interpret(assemble('2 1 > { "yes" } { "no" } ifelse'), [], []), "yes"
)
+ def test_fmt(self):
+ def fmt(asm):
+ return interpret(assemble(f"{asm} @fmt call"), [], [])
+
+ # Arguments are in stack order, the first is the deepest.
+ self.assertEqual(fmt('1 2 "{}-{}"'), "1-2")
+ self.assertEqual(fmt('"no args"'), "no args")
+ # Like llvm::formatv, "{{" is a literal "{", and "}" is a literal.
+ self.assertEqual(fmt('1 "{{{}}"'), "{1}")
+ self.assertEqual(count_fmt_params("{} {}"), 2)
+ self.assertEqual(count_fmt_params("{{}"), 0)
+ self.assertEqual(formatv("{}{}", ["a", "b"]), "ab")
+ self.assertEqual(formatv("{{{}}", [1]), "{1}")
+
+ # Adjacent literals, including escaped braces, are merged.
+ self.assertEqual(_parse_fmt(""), [])
+ self.assertEqual(_parse_fmt("abc"), ["abc"])
+ self.assertEqual(_parse_fmt("a{{b}c"), ["a{b}c"])
+ self.assertEqual(_parse_fmt("{}"), [0])
+ self.assertEqual(
+ _parse_fmt("x={}, {{y}}={}!"), ["x=", 0, ", {y}}=", 1, "!"]
+ )
+ self.assertEqual(_parse_fmt("{}{}"), [0, 1])
+
+ # Layouts and options are accepted, but ignored.
+ self.assertEqual(formatv("[{,8}]", ["ab"]), "[ab]")
+ self.assertEqual(formatv("[{,*=8:x}]", ["ab"]), "[ab]")
+ self.assertEqual(formatv("[{:a:b,c}]", ["ab"]), "[ab]")
+ self.assertEqual(_parse_fmt("{}{,8}{:x}"), [0, 1, 2])
+
+ # Explicit indexes and malformed replacements are errors.
+ bad = ["{0}", "ab{1,8}", "{x}", "{", "{{{", "{,8", "{:x", "{,a{}", "{:a{}"]
+ for fmt_string in bad:
+ with self.assertRaisesRegex(ValueError, "invalid replacement"):
+ _parse_fmt(fmt_string)
+ with self.assertRaisesRegex(ValueError, "at offset 2"):
+ _parse_fmt("ab{1,8}")
+
def test_assemble_file(self):
def run_assemble(type_name, asm):
out = io.BytesIO()
@@ -1503,6 +1625,54 @@ def num_children(self):
self.assertEqual(interpret(methods["update"], [], [self_dict]), 0)
self.assertEqual(interpret(methods["get_num_children"], [], [self_dict]), 3)
+ def test_fstring(self):
+ def compile_fstring(expr):
+ methods = self.compile_methods(
+ f"""
+ class Synthetic:
+ def get_child_index(self, name):
+ return {expr}
+ """
+ )
+ return methods["get_child_index"]
+
+ def run(expr, name="x", valobj=None):
+ self_dict = {"valobj": valobj or self.FakeValue(42)}
+ return interpret(compile_fstring(expr), [], [self_dict, name])
+
+ # Only the formatted values are pushed, followed by the format string.
+ self.assertEqual(
+ compile_fstring('f"a{name}b"'),
+ assemble('1 pick "a{}b" @fmt call return'),
+ )
+ self.assertEqual(run('f"a{name}b"'), "axb")
+
+ # Multiple values, including attributes and selector calls.
+ self.assertEqual(
+ run('f"{name}: {self.valobj.GetValueAsUnsigned()}"'), "x: 42"
+ )
+ self.assertEqual(run('f"{name}{name}"'), "xx")
+
+ # An f-string without replacements.
+ self.assertEqual(run('f"plain"'), "plain")
+
+ # Literal braces are escaped for formatv.
+ self.assertEqual(
+ compile_fstring('f"{{{name}}}"'),
+ assemble('1 pick "{{{}}" @fmt call return'),
+ )
+ self.assertEqual(run('f"{{{name}}}"'), "{x}")
+
+ # Quotes and backslashes are escaped in the assembly.
+ self.assertEqual(run(r"""f'"{name}"\\'"""), '"x"\\')
+ self.assertEqual(run(r"""'say "hi"'"""), 'say "hi"')
+
+ # Conversions and format specs are not supported.
+ with self.assertRaisesRegex(CompilerError, "unsupported format conversion"):
+ compile_fstring('f"{name!r}"')
+ with self.assertRaisesRegex(CompilerError, "unsupported format spec"):
+ compile_fstring('f"{name:>8}"')
+
def test_errors(self):
with self.assertRaisesRegex(CompilerError, "unknown attribute: missing"):
self.compile_methods(
diff --git a/lldb/source/DataFormatters/FormatterBytecode.cpp b/lldb/source/DataFormatters/FormatterBytecode.cpp
index 6801dcf07ea40..45043dfd539fc 100644
--- a/lldb/source/DataFormatters/FormatterBytecode.cpp
+++ b/lldb/source/DataFormatters/FormatterBytecode.cpp
@@ -97,9 +97,66 @@ std::string toString(const FormatterBytecode::DataStack &data) {
namespace FormatterBytecode {
+/// Check a version 2 format string, and remove the layout and options of each
+/// replacement. The format string may contain literal text, escaped braces
+/// ("{{"), and automatically indexed replacements:
+///
+/// "{" [ "," layout ] [ ":" options ] "}"
+///
+/// Explicit indexes are not supported. Layouts and options are accepted, so
+/// that support for them can be added without changing the ABI, but they are
+/// currently ignored. The result contains only literal text, escaped braces,
+/// and "{}", so malformed format strings never reach formatv.
+static llvm::Expected<std::string> SanitizeFormatString(llvm::StringRef fmt) {
+ std::string result;
+ size_t i = 0;
+ while (i < fmt.size()) {
+ if (fmt[i] != '{') {
+ result += fmt[i++];
+ continue;
+ }
+ if (fmt.drop_front(i).starts_with("{{")) {
+ result += "{{";
+ i += 2;
+ continue;
+ }
+ size_t start = i++;
+ // Skip the layout, which ends at the options or the closing brace.
+ if (i < fmt.size() && fmt[i] == ',') {
+ ++i;
+ while (i < fmt.size() && !llvm::StringRef("{}:").contains(fmt[i]))
+ ++i;
+ }
+ // Skip the options, which end at the closing brace.
+ if (i < fmt.size() && fmt[i] == ':') {
+ ++i;
+ while (i < fmt.size() && !llvm::StringRef("{}").contains(fmt[i]))
+ ++i;
+ }
+ if (i == fmt.size() || fmt[i] != '}')
+ return llvm::createStringErrorV(
+ "invalid replacement in format string at offset {0}", start);
+ ++i;
+ llvm::StringRef replacement = fmt.slice(start, i);
+ if (replacement != "{}")
+ LLDB_LOG(GetLog(LLDBLog::DataFormatters),
+ "ignoring unsupported layout or options in format replacement "
+ "'{0}'",
+ replacement);
+ result += "{}";
+ }
+ return result;
+}
+
/// Implement the @format function.
-static llvm::Error FormatImpl(DataStack &data) {
+static llvm::Error FormatImpl(DataStack &data, uint32_t version) {
auto fmt = data.Pop<std::string>();
+ if (version >= 2) {
+ auto sanitized = SanitizeFormatString(fmt);
+ if (!sanitized)
+ return sanitized.takeError();
+ fmt = std::move(*sanitized);
+ }
auto replacements =
llvm::formatv_object_base::parseFormatString(fmt, 0, false);
std::string s;
@@ -144,6 +201,8 @@ static llvm::Error FormatImpl(DataStack &data) {
else if (std::holds_alternative<DictionarySP>(arg))
format(FormatFunctor("dict"));
}
+ // Consume the arguments, leaving only the result.
+ data.erase(data.end() - num_args, data.end());
data.Push(s);
return llvm::Error::success();
}
@@ -826,7 +885,7 @@ llvm::Error Interpret(ControlStack &control, DataStack &data, Signatures sig,
}
case sel_fmt: {
TYPE_CHECK(String);
- if (auto error = FormatImpl(data))
+ if (auto error = FormatImpl(data, version))
return error;
break;
}
diff --git a/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp b/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp
index 5f6c7541f17ef..8de9babd57faa 100644
--- a/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp
+++ b/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp
@@ -10,6 +10,7 @@
using namespace lldb_private;
using namespace lldb;
using namespace FormatterBytecode;
+using llvm::Failed;
using llvm::FailedWithMessage;
using llvm::StringRef;
@@ -35,12 +36,13 @@ static bool Interpret(std::vector<uint8_t> code, DataStack &data,
/// Like Interpret() above, but returns (instead of discarding) the Error,
/// allowing tests to assert on the error message.
-static llvm::Error InterpretFail(std::vector<uint8_t> code) {
+static llvm::Error InterpretFail(std::vector<uint8_t> code,
+ uint32_t version = 1) {
auto buf =
StringRef(reinterpret_cast<const char *>(code.data()), code.size());
ControlStack control({buf});
DataStack data;
- return Interpret(control, data, sig_summary, /*version=*/1);
+ return Interpret(control, data, sig_summary, version);
}
TEST_F(FormatterBytecodeTest, StackOps) {
@@ -502,6 +504,28 @@ TEST_F(FormatterBytecodeTest, CallOps) {
data.Push(std::string{"{1}{0}"});
ASSERT_TRUE(Interpret({op_lit_selector, sel_fmt, op_call}, data));
ASSERT_EQ(data.Pop<std::string>(), "BA");
+ // The arguments are consumed.
+ ASSERT_TRUE(data.empty());
+ }
+ {
+ // Only the arguments are consumed, not values below them.
+ DataStack data;
+ data.Push(std::string{"below"});
+ data.Push(std::string{"x"});
+ data.Push(std::string{"<{}>"});
+ ASSERT_TRUE(Interpret({op_lit_selector, sel_fmt, op_call}, data));
+ ASSERT_EQ(data.Pop<std::string>(), "<x>");
+ ASSERT_EQ(data.size(), 1u);
+ ASSERT_EQ(data.Pop<std::string>(), "below");
+ }
+ {
+ // A format string without replacements consumes nothing else.
+ DataStack data;
+ data.Push(std::string{"below"});
+ data.Push(std::string{"plain"});
+ ASSERT_TRUE(Interpret({op_lit_selector, sel_fmt, op_call}, data));
+ ASSERT_EQ(data.Pop<std::string>(), "plain");
+ ASSERT_EQ(data.Pop<std::string>(), "below");
}
{
DataStack data;
@@ -510,6 +534,69 @@ TEST_F(FormatterBytecodeTest, CallOps) {
}
}
+TEST_F(FormatterBytecodeTest, FormatVersion2) {
+ auto fmt = [](std::vector<std::string> args, std::string fmt) {
+ DataStack data;
+ for (auto &arg : args)
+ data.Push(arg);
+ data.Push(fmt);
+ EXPECT_TRUE(Interpret({op_lit_selector, sel_fmt, op_call}, data,
+ /*version=*/2));
+ EXPECT_EQ(data.size(), 1u);
+ return data.Pop<std::string>();
+ };
+ // Replacements are automatically indexed, in stack order.
+ EXPECT_EQ(fmt({"A", "B"}, "{}{}"), "AB");
+ EXPECT_EQ(fmt({"A"}, "<{}>"), "<A>");
+ EXPECT_EQ(fmt({}, "plain"), "plain");
+ // "{{" is a literal "{", and "}" is a literal.
+ EXPECT_EQ(fmt({"A"}, "{{{}}"), "{A}");
+ EXPECT_EQ(fmt({}, "a}b"), "a}b");
+
+ // Layouts and options are accepted, but not yet supported, so they are
+ // ignored and format the same as "{}".
+ EXPECT_EQ(fmt({"ab"}, "[{,8}]"), "[ab]");
+ EXPECT_EQ(fmt({"ab"}, "[{,*=8}]"), "[ab]");
+ EXPECT_EQ(fmt({"ab"}, "[{:x}]"), "[ab]");
+ EXPECT_EQ(fmt({"ab"}, "[{,8:x}]"), "[ab]");
+ EXPECT_EQ(fmt({"ab"}, "[{:a:b,c}]"), "[ab]");
+ EXPECT_EQ(fmt({"ab", "cd"}, "[{,}{:}]"), "[abcd]");
+ // Layouts and options don't change the automatic indexing.
+ EXPECT_EQ(fmt({"A", "B", "C"}, "{}{,8}{:x}"), "ABC");
+
+ // Error: explicit indexes and malformed replacements.
+ auto fmt_error = [](std::string fmt) {
+ std::vector<uint8_t> code = {op_lit_string, uint8_t(fmt.size())};
+ code.insert(code.end(), fmt.begin(), fmt.end());
+ code.insert(code.end(), {op_lit_selector, sel_fmt, op_call});
+ return InterpretFail(code, /*version=*/2);
+ };
+ EXPECT_THAT_ERROR(
+ fmt_error("{0}"),
+ FailedWithMessage("invalid replacement in format string at offset 0"));
+ EXPECT_THAT_ERROR(
+ fmt_error("ab{1,8}"),
+ FailedWithMessage("invalid replacement in format string at offset 2"));
+ EXPECT_THAT_ERROR(fmt_error("{x}"), Failed());
+ EXPECT_THAT_ERROR(fmt_error("{"), Failed());
+ EXPECT_THAT_ERROR(fmt_error("{{{"), Failed());
+ EXPECT_THAT_ERROR(fmt_error("{,8"), Failed());
+ EXPECT_THAT_ERROR(fmt_error("{:x"), Failed());
+ EXPECT_THAT_ERROR(fmt_error("{,a{}"), Failed());
+ EXPECT_THAT_ERROR(fmt_error("{:a{}"), Failed());
+
+ // Version 1 still supports explicit indexes.
+ {
+ DataStack data;
+ data.Push(std::string{"A"});
+ data.Push(std::string{"B"});
+ data.Push(std::string{"{1}{0}"});
+ ASSERT_TRUE(Interpret({op_lit_selector, sel_fmt, op_call}, data,
+ /*version=*/1));
+ ASSERT_EQ(data.Pop<std::string>(), "BA");
+ }
+}
+
TEST_F(FormatterBytecodeTest, DictionaryOps) {
{
// Set key a, then read it back.
>From 2990f0a7ffe7f161cdffbf91583ca384672c293d Mon Sep 17 00:00:00 2001
From: Dave Lee <davelee.com at gmail.com>
Date: Tue, 20 Oct 2026 10:51:23 -0700
Subject: [PATCH 2/2] Improvements to: docs, parsing, tests
---
lldb/docs/resources/formatterbytecode.md | 2 -
lldb/examples/python/formatter_bytecode.py | 23 +++++++++-
.../DataFormatters/FormatterBytecode.cpp | 44 +++++++++----------
.../DataFormatter/FormatterBytecodeTest.cpp | 5 +++
4 files changed, 46 insertions(+), 28 deletions(-)
diff --git a/lldb/docs/resources/formatterbytecode.md b/lldb/docs/resources/formatterbytecode.md
index 32c49893dfb02..67b3e604013d8 100644
--- a/lldb/docs/resources/formatterbytecode.md
+++ b/lldb/docs/resources/formatterbytecode.md
@@ -202,8 +202,6 @@ Each replacement is replaced with the next argument, in stack order, so the firs
Layouts and options are hints, which are applied on a best effort basis. Support for them may vary between versions of LLDB, and a layout or option that is not supported is ignored, so that the replacement is formatted like `{}`. Layouts and options never change which arguments are used. Currently, no layouts or options are supported.
-In version 1 formatters, any `llvm::formatv` format string is accepted.
-
### Dictionary objects
`Dictionary` objects are key-value containers, with `String` value keys, and values of any data type. `Dictionary` is a reference type, mutating it through one reference is visible through any other reference to the same dictionary (e.g. one obtained earlier with `dup`). Empty `Dictionary` objects are created with `dict`. `Dictionary` objects are populated with `dict_set`. Values are retrieved with `dict_get`. When `dict_get` is called with a key that is not present in the dictionary, an error is emitted. Use `dict_has` first to check for a key's existence. Dictionary operations consumes the `Dictionary` argument, so `dup` it first if the `Dictionary` is needed afterward. For example, to set multiple keys in a row:
diff --git a/lldb/examples/python/formatter_bytecode.py b/lldb/examples/python/formatter_bytecode.py
index c094a5963c6e0..401018bc3f516 100644
--- a/lldb/examples/python/formatter_bytecode.py
+++ b/lldb/examples/python/formatter_bytecode.py
@@ -1101,8 +1101,11 @@ def visit_Constant(self, node: ast.Constant) -> None:
self._output(node.value)
def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
- # f-strings are compiled to @fmt, which formats with llvm::formatv. The
- # formatted values are pushed in order, followed by the format string.
+ """
+ f-strings are compiled to @fmt, which formats using llvm::formatv. The
+ formatted values are pushed in order, followed by the format string.
+ """
+
fmt = ""
for child in node.values:
if isinstance(child, ast.Constant):
@@ -1416,6 +1419,12 @@ def fmt(asm):
self.assertEqual(count_fmt_params("{{}"), 0)
self.assertEqual(formatv("{}{}", ["a", "b"]), "ab")
self.assertEqual(formatv("{{{}}", [1]), "{1}")
+ # A "}" outside of a replacement is always a literal, even if it
+ # follows a replacement, or another "}". Unlike "{{", "}}" is not an
+ # escape. This differs from Python's str.format.
+ self.assertEqual(formatv("}", []), "}")
+ self.assertEqual(formatv("{}}", ["A"]), "A}")
+ self.assertEqual(formatv("}}", []), "}}")
# Adjacent literals, including escaped braces, are merged.
self.assertEqual(_parse_fmt(""), [])
@@ -1426,6 +1435,8 @@ def fmt(asm):
_parse_fmt("x={}, {{y}}={}!"), ["x=", 0, ", {y}}=", 1, "!"]
)
self.assertEqual(_parse_fmt("{}{}"), [0, 1])
+ self.assertEqual(_parse_fmt("}"), ["}"])
+ self.assertEqual(_parse_fmt("{}}"), [0, "}"])
# Layouts and options are accepted, but ignored.
self.assertEqual(formatv("[{,8}]", ["ab"]), "[ab]")
@@ -1662,6 +1673,14 @@ def run(expr, name="x", valobj=None):
assemble('1 pick "{{{}}" @fmt call return'),
)
self.assertEqual(run('f"{{{name}}}"'), "{x}")
+ # The f-string escape "}}" is a literal "}", which formatv
+ # doesn't escape.
+ self.assertEqual(
+ compile_fstring('f"{name}}}"'),
+ assemble('1 pick "{}}" @fmt call return'),
+ )
+ self.assertEqual(run('f"{name}}}"'), "x}")
+ self.assertEqual(run('f"}}"'), "}")
# Quotes and backslashes are escaped in the assembly.
self.assertEqual(run(r"""f'"{name}"\\'"""), '"x"\\')
diff --git a/lldb/source/DataFormatters/FormatterBytecode.cpp b/lldb/source/DataFormatters/FormatterBytecode.cpp
index 45043dfd539fc..cb21bea129d18 100644
--- a/lldb/source/DataFormatters/FormatterBytecode.cpp
+++ b/lldb/source/DataFormatters/FormatterBytecode.cpp
@@ -97,7 +97,7 @@ std::string toString(const FormatterBytecode::DataStack &data) {
namespace FormatterBytecode {
-/// Check a version 2 format string, and remove the layout and options of each
+/// Check a format string, and remove the layout and options of each
/// replacement. The format string may contain literal text, escaped braces
/// ("{{"), and automatically indexed replacements:
///
@@ -108,36 +108,32 @@ namespace FormatterBytecode {
/// currently ignored. The result contains only literal text, escaped braces,
/// and "{}", so malformed format strings never reach formatv.
static llvm::Expected<std::string> SanitizeFormatString(llvm::StringRef fmt) {
+ const llvm::StringRef original = fmt;
std::string result;
- size_t i = 0;
- while (i < fmt.size()) {
- if (fmt[i] != '{') {
- result += fmt[i++];
- continue;
- }
- if (fmt.drop_front(i).starts_with("{{")) {
+ while (!fmt.empty()) {
+ // Copy the literal text up to the next brace.
+ llvm::StringRef literal = fmt.take_front(fmt.find('{'));
+ result += literal;
+ fmt = fmt.drop_front(literal.size());
+ if (fmt.empty())
+ break;
+ if (fmt.consume_front("{{")) {
result += "{{";
- i += 2;
continue;
}
- size_t start = i++;
+ llvm::StringRef replacement = fmt;
+ fmt = fmt.drop_front();
// Skip the layout, which ends at the options or the closing brace.
- if (i < fmt.size() && fmt[i] == ',') {
- ++i;
- while (i < fmt.size() && !llvm::StringRef("{}:").contains(fmt[i]))
- ++i;
- }
+ if (fmt.consume_front(","))
+ fmt = fmt.substr(fmt.find_first_of("{}:"));
// Skip the options, which end at the closing brace.
- if (i < fmt.size() && fmt[i] == ':') {
- ++i;
- while (i < fmt.size() && !llvm::StringRef("{}").contains(fmt[i]))
- ++i;
- }
- if (i == fmt.size() || fmt[i] != '}')
+ if (fmt.consume_front(":"))
+ fmt = fmt.substr(fmt.find_first_of("{}"));
+ if (!fmt.consume_front("}"))
return llvm::createStringErrorV(
- "invalid replacement in format string at offset {0}", start);
- ++i;
- llvm::StringRef replacement = fmt.slice(start, i);
+ "invalid replacement in format string at offset {0}",
+ original.size() - replacement.size());
+ replacement = replacement.drop_back(fmt.size());
if (replacement != "{}")
LLDB_LOG(GetLog(LLDBLog::DataFormatters),
"ignoring unsupported layout or options in format replacement "
diff --git a/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp b/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp
index 8de9babd57faa..54b07afd09866 100644
--- a/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp
+++ b/lldb/unittests/DataFormatter/FormatterBytecodeTest.cpp
@@ -552,6 +552,11 @@ TEST_F(FormatterBytecodeTest, FormatVersion2) {
// "{{" is a literal "{", and "}" is a literal.
EXPECT_EQ(fmt({"A"}, "{{{}}"), "{A}");
EXPECT_EQ(fmt({}, "a}b"), "a}b");
+ // A "}" outside of a replacement is always a literal, even if it follows a
+ // replacement, or another "}". Unlike "{{", "}}" is not an escape.
+ EXPECT_EQ(fmt({}, "}"), "}");
+ EXPECT_EQ(fmt({"A"}, "{}}"), "A}");
+ EXPECT_EQ(fmt({}, "}}"), "}}");
// Layouts and options are accepted, but not yet supported, so they are
// ignored and format the same as "{}".
More information about the lldb-commits
mailing list