Skip to content

Commit ffb9b92

Browse files
dplassgitcopybara-github
authored andcommitted
[DSLX Fuzz testing] Support backtick-quoted strings as arguments to
the `#[fuzz_test]` attribute, in the AST and parser. PiperOrigin-RevId: 889323770
1 parent f41a74a commit ffb9b92

14 files changed

Lines changed: 336 additions & 16 deletions

xls/dslx/frontend/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ cc_test(
598598
":ast_test_utils",
599599
":module",
600600
":pos",
601+
"//xls/common:attribute_data",
601602
"//xls/common:xls_gunit_main",
602603
"//xls/common/status:matchers",
603604
"@com_google_absl//absl/status",

xls/dslx/frontend/ast.cc

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -646,22 +646,33 @@ std::string Attribute::ToString() const {
646646
std::string args;
647647
if (!attribute_data_.args().empty()) {
648648
absl::StrAppend(&args, "(");
649+
std::vector<std::string> pieces;
649650
for (const AttributeData::Argument& next : attribute_data_.args()) {
650-
absl::StrAppend(
651-
&args,
652-
absl::visit(Visitor{
653-
[](auto arg) {
654-
return absl::Substitute("$0 = $1", arg.first,
655-
arg.second);
656-
},
657-
[](const AttributeData::StringLiteralArgument& arg) {
658-
return arg.text;
659-
},
660-
[](const std::string& arg) { return arg; },
661-
},
662-
next));
651+
pieces.push_back(absl::visit(
652+
Visitor{
653+
[&](const AttributeData::StringKeyValueArgument& arg) {
654+
if (attribute_data_.kind() == AttributeKind::kFuzzTest &&
655+
arg.first == "domains") {
656+
return absl::Substitute("$0 = `$1`", arg.first, arg.second);
657+
}
658+
return absl::Substitute("$0 = \"$1\"", arg.first, arg.second);
659+
},
660+
[](const AttributeData::IntKeyValueArgument& arg) {
661+
return absl::Substitute("$0 = $1", arg.first, arg.second);
662+
},
663+
[](const AttributeData::StringLiteralArgument& arg) {
664+
return absl::Substitute("\"$0\"", arg.text);
665+
},
666+
[this](const std::string& arg) {
667+
if (attribute_data_.kind() == AttributeKind::kFuzzTest) {
668+
return absl::StrCat("`", arg, "`");
669+
}
670+
return arg;
671+
},
672+
},
673+
next));
663674
}
664-
absl::StrAppend(&args, ")");
675+
absl::StrAppend(&args, absl::StrJoin(pieces, ", "), ")");
665676
}
666677
return absl::Substitute("#[$0$1]",
667678
AttributeKindToString(attribute_data_.kind()), args);

xls/dslx/frontend/ast_test.cc

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "gtest/gtest.h"
2525
#include "absl/status/status.h"
2626
#include "absl/status/status_matchers.h"
27+
#include "xls/common/attribute_data.h"
2728
#include "xls/common/status/matchers.h"
2829
#include "xls/dslx/frontend/ast_test_utils.h"
2930
#include "xls/dslx/frontend/module.h"
@@ -363,5 +364,92 @@ TEST(AstTest, IsConstantEmptyArray) {
363364
EXPECT_TRUE(IsConstant(array));
364365
}
365366

367+
TEST(AstTest, AttributeToStringFuzzTestNoArgs) {
368+
FileTable file_table;
369+
Module m("test", /*fs_path=*/std::nullopt, file_table);
370+
const Span fake_span;
371+
372+
Attribute* attr = m.Make<Attribute>(
373+
fake_span, std::nullopt, AttributeData(AttributeKind::kFuzzTest, {}));
374+
EXPECT_EQ(attr->ToString(), "#[fuzz_test]");
375+
}
376+
377+
TEST(AstTest, AttributeToStringFuzzTestSingleArg) {
378+
FileTable file_table;
379+
Module m("test", /*fs_path=*/std::nullopt, file_table);
380+
const Span fake_span;
381+
382+
AttributeData::StringKeyValueArgument arg("domains", "u32:0..1");
383+
Attribute* attr = m.Make<Attribute>(
384+
fake_span, fake_span, AttributeData(AttributeKind::kFuzzTest, {arg}));
385+
EXPECT_EQ(attr->ToString(), "#[fuzz_test(domains = `u32:0..1`)]");
386+
}
387+
388+
TEST(AstTest, AttributeToStringFuzzTestMultipleArgs) {
389+
FileTable file_table;
390+
Module m("test", /*fs_path=*/std::nullopt, file_table);
391+
const Span fake_span;
392+
393+
AttributeData::StringKeyValueArgument arg("domains", "u32:0..1, u32:10..20");
394+
Attribute* attr = m.Make<Attribute>(
395+
fake_span, fake_span, AttributeData(AttributeKind::kFuzzTest, {arg}));
396+
EXPECT_EQ(attr->ToString(), "#[fuzz_test(domains = `u32:0..1, u32:10..20`)]");
397+
}
398+
399+
TEST(AstTest, AttributeToStringGenericArg) {
400+
FileTable file_table;
401+
Module m("test", /*fs_path=*/std::nullopt, file_table);
402+
const Span fake_span;
403+
404+
Attribute* attr =
405+
m.Make<Attribute>(fake_span, fake_span,
406+
AttributeData(AttributeKind::kTest, {"some_string"}));
407+
EXPECT_EQ(attr->ToString(), "#[test(some_string)]");
408+
}
409+
410+
TEST(AstTest, AttributeToStringQuickcheckNoArgs) {
411+
FileTable file_table;
412+
Module m("test", /*fs_path=*/std::nullopt, file_table);
413+
const Span fake_span;
414+
415+
Attribute* attr = m.Make<Attribute>(
416+
fake_span, std::nullopt, AttributeData(AttributeKind::kQuickcheck, {}));
417+
EXPECT_EQ(attr->ToString(), "#[quickcheck]");
418+
}
419+
420+
TEST(AstTest, AttributeToStringQuickcheckExhaustive) {
421+
FileTable file_table;
422+
Module m("test", /*fs_path=*/std::nullopt, file_table);
423+
const Span fake_span;
424+
425+
Attribute* attr = m.Make<Attribute>(
426+
fake_span, fake_span,
427+
AttributeData(AttributeKind::kQuickcheck, {std::string("exhaustive")}));
428+
EXPECT_EQ(attr->ToString(), "#[quickcheck(exhaustive)]");
429+
}
430+
431+
TEST(AstTest, AttributeToStringQuickcheckTestCount) {
432+
FileTable file_table;
433+
Module m("test", /*fs_path=*/std::nullopt, file_table);
434+
const Span fake_span;
435+
436+
AttributeData::IntKeyValueArgument arg("test_count", 1000);
437+
Attribute* attr = m.Make<Attribute>(
438+
fake_span, fake_span, AttributeData(AttributeKind::kQuickcheck, {arg}));
439+
EXPECT_EQ(attr->ToString(), "#[quickcheck(test_count = 1000)]");
440+
}
441+
442+
TEST(AstTest, AttributeToStringExternVerilogStringLiteral) {
443+
FileTable file_table;
444+
Module m("test", /*fs_path=*/std::nullopt, file_table);
445+
const Span fake_span;
446+
447+
AttributeData::StringLiteralArgument arg{"my_module"};
448+
Attribute* attr =
449+
m.Make<Attribute>(fake_span, fake_span,
450+
AttributeData(AttributeKind::kExternVerilog, {arg}));
451+
EXPECT_EQ(attr->ToString(), "#[extern_verilog(\"my_module\")]");
452+
}
453+
366454
} // namespace
367455
} // namespace xls::dslx

xls/dslx/frontend/parser.cc

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,8 @@ Parser::ParseAttributeArguments() {
831831
if (delim->kind() == TokenKind::kEquals) {
832832
XLS_RETURN_IF_ERROR(DropToken());
833833
XLS_ASSIGN_OR_RETURN(Token rhs, PopToken());
834-
if (rhs.kind() == TokenKind::kString) {
834+
if (rhs.kind() == TokenKind::kString ||
835+
rhs.kind() == TokenKind::kBacktickString) {
835836
result.push_back(AttributeData::StringKeyValueArgument(
836837
lhs.GetStringValue(), rhs.GetStringValue()));
837838
} else if (rhs.kind() == TokenKind::kNumber) {
@@ -912,6 +913,26 @@ absl::StatusOr<QuickCheckTestCases> Parser::GetQuickCheckTestCases(
912913
"Expected 'exhaustive' or 'test_count' in quickcheck attribute");
913914
}
914915

916+
absl::Status Parser::ValidateFuzzTestAttribute(const Attribute& attribute) {
917+
for (const AttributeData::Argument& arg : attribute.args()) {
918+
if (auto* kv = std::get_if<AttributeData::StringKeyValueArgument>(&arg)) {
919+
if (kv->first != "domains") {
920+
return ParseErrorStatus(
921+
*attribute.GetSpan(),
922+
absl::StrFormat(
923+
"Unknown attribute argument: '%s'; expected 'domains'",
924+
kv->first));
925+
}
926+
} else {
927+
return ParseErrorStatus(
928+
*attribute.GetSpan(),
929+
"The 'fuzz_test' attribute requires a 'domains' argument that is a "
930+
"backtick-quoted DSLX tuple.");
931+
}
932+
}
933+
return absl::OkStatus();
934+
}
935+
915936
absl::Status Parser::UnsupportedAttributeError(const Attribute& attribute) {
916937
Span span = *attribute.GetSpan();
917938
switch (attribute.attribute_kind()) {
@@ -1018,6 +1039,7 @@ absl::StatusOr<ModuleMember> Parser::ApplyFunctionAttributes(
10181039
is_test = true;
10191040
break;
10201041
case AttributeKind::kFuzzTest:
1042+
XLS_RETURN_IF_ERROR(ValidateFuzzTestAttribute(*next));
10211043
test_attributes.push_back(next->ToString());
10221044
break;
10231045

xls/dslx/frontend/parser.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,8 @@ class Parser : public TokenParser {
665665
absl::StatusOr<QuickCheckTestCases> GetQuickCheckTestCases(
666666
const Attribute& attribute);
667667

668+
absl::Status ValidateFuzzTestAttribute(const Attribute& attribute);
669+
668670
// Parses a "spawn" statement, which creates & initializes a proc.
669671
absl::StatusOr<Spawn*> ParseSpawn(Bindings& bindings);
670672

xls/dslx/frontend/parser_test.cc

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4523,4 +4523,101 @@ struct S {
45234523
HasSubstr("#[fuzz_test] is only valid on a function.")));
45244524
}
45254525

4526+
TEST_F(ParserTest, FuzzTestAttributeSingleArg) {
4527+
const char* kProgram = R"(
4528+
#[fuzz_test(domains=`u32:0..1`)]
4529+
fn f(x: u32) {}
4530+
)";
4531+
XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr<Module> module, Parse(kProgram));
4532+
XLS_ASSERT_OK_AND_ASSIGN(Function * f,
4533+
module->GetMemberOrError<Function>("f"));
4534+
ASSERT_EQ(f->attributes().size(), 1);
4535+
EXPECT_EQ(f->attributes()[0]->attribute_kind(), AttributeKind::kFuzzTest);
4536+
ASSERT_EQ(f->attributes()[0]->args().size(), 1);
4537+
auto arg = std::get<AttributeData::StringKeyValueArgument>(
4538+
f->attributes()[0]->args()[0]);
4539+
EXPECT_EQ(arg.first, "domains");
4540+
EXPECT_EQ(arg.second, "u32:0..1");
4541+
EXPECT_EQ(f->attributes()[0]->ToString(),
4542+
"#[fuzz_test(domains = `u32:0..1`)]");
4543+
}
4544+
4545+
TEST_F(ParserTest, FuzzTestAttributeComplexArgs) {
4546+
const char* kProgram = R"(
4547+
enum Op : u32 { Add = 0, Sub = 1 }
4548+
#[fuzz_test(domains=`[u32:0, u32:10], [Op::Add, Op::Sub]`)]
4549+
fn f(x: u32[2], op: Op[2]) {}
4550+
)";
4551+
XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr<Module> module, Parse(kProgram));
4552+
XLS_ASSERT_OK_AND_ASSIGN(Function * f,
4553+
module->GetMemberOrError<Function>("f"));
4554+
ASSERT_EQ(f->attributes().size(), 1);
4555+
EXPECT_EQ(f->attributes()[0]->attribute_kind(), AttributeKind::kFuzzTest);
4556+
ASSERT_EQ(f->attributes()[0]->args().size(), 1);
4557+
auto arg = std::get<AttributeData::StringKeyValueArgument>(
4558+
f->attributes()[0]->args()[0]);
4559+
EXPECT_EQ(arg.first, "domains");
4560+
EXPECT_EQ(arg.second, "[u32:0, u32:10], [Op::Add, Op::Sub]");
4561+
}
4562+
4563+
TEST_F(ParserTest, FuzzTestAttributeInvalidNoDomains) {
4564+
const char* kProgram = R"(
4565+
#[fuzz_test(`u32:0..1`)]
4566+
fn f(x: u32) {}
4567+
)";
4568+
EXPECT_THAT(Parse(kProgram),
4569+
StatusIs(absl::StatusCode::kInvalidArgument,
4570+
HasSubstr("Expected attribute argument.")));
4571+
}
4572+
4573+
TEST_F(ParserTest, FuzzTestAttributeInvalidNoList) {
4574+
const char* kProgram = R"(
4575+
#[fuzz_test()]
4576+
fn f(x: u32) {}
4577+
)";
4578+
EXPECT_THAT(Parse(kProgram),
4579+
StatusIs(absl::StatusCode::kInvalidArgument,
4580+
HasSubstr("Expected attribute argument.")));
4581+
}
4582+
4583+
TEST_F(ParserTest, FuzzTestAttributeInvalidInteger) {
4584+
const char* kProgram = R"(
4585+
#[fuzz_test(domains=1)]
4586+
fn f(x: u32) {}
4587+
)";
4588+
EXPECT_THAT(Parse(kProgram),
4589+
StatusIs(absl::StatusCode::kInvalidArgument,
4590+
HasSubstr("that is a backtick-quoted DSLX tuple")));
4591+
}
4592+
4593+
TEST_F(ParserTest, FuzzTestInvalidArgInvalidInteger) {
4594+
const char* kProgram = R"(
4595+
#[fuzz_test(foo=1)]
4596+
fn f(x: u32) {}
4597+
)";
4598+
EXPECT_THAT(Parse(kProgram),
4599+
StatusIs(absl::StatusCode::kInvalidArgument,
4600+
HasSubstr("that is a backtick-quoted DSLX tuple")));
4601+
}
4602+
4603+
TEST_F(ParserTest, FuzzTestAttributeNoDomainInteger) {
4604+
const char* kProgram = R"(
4605+
#[fuzz_test(1)]
4606+
fn f(x: u32) {}
4607+
)";
4608+
EXPECT_THAT(Parse(kProgram),
4609+
StatusIs(absl::StatusCode::kInvalidArgument,
4610+
HasSubstr("Expected attribute argument")));
4611+
}
4612+
4613+
TEST_F(ParserTest, FuzzTestAttributeInvalidArgName) {
4614+
const char* kProgram = R"(
4615+
#[fuzz_test(foo=`u32:0..1`)]
4616+
fn f(x: u32) {}
4617+
)";
4618+
EXPECT_THAT(Parse(kProgram),
4619+
StatusIs(absl::StatusCode::kInvalidArgument,
4620+
HasSubstr("Unknown attribute argument: 'foo'")));
4621+
}
4622+
45264623
} // namespace xls::dslx

xls/dslx/frontend/scanned_token_to_string_fuzz_test.cc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ class TokenStreamMatcher {
161161
case xls::dslx::TokenKind::kString:
162162
return MatchTokenWithDelimiter<'\"'>(expected, token, file_table,
163163
listener);
164+
case xls::dslx::TokenKind::kBacktickString:
165+
return MatchTokenWithDelimiter<'`'>(expected, token, file_table,
166+
listener);
164167
default: {
165168
std::string token_str = token.ToString();
166169
std::string_view token_str_view = token_str;
@@ -282,4 +285,14 @@ TEST(ScanFuzzTest,
282285
ScanningGivesErrorOrConvertsToOriginal("\t\t\"\n\"");
283286
}
284287

288+
TEST(ScanFuzzTest,
289+
ScanningGivesErrorOrConvertsToOriginalRegressionBacktickStringLiteral) {
290+
ScanningGivesErrorOrConvertsToOriginal("`\232`");
291+
}
292+
293+
TEST(ScanFuzzTest,
294+
ScanningGivesErrorOrConvertsToOriginalRegressionBacktickString) {
295+
ScanningGivesErrorOrConvertsToOriginal("`hi`");
296+
}
297+
285298
} // namespace

xls/dslx/frontend/scanner.cc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,30 @@ absl::StatusOr<Token> Scanner::ScanChar(const Pos& start_pos) {
451451
std::string(1, c));
452452
}
453453

454+
absl::StatusOr<Token> Scanner::ScanBacktickString(const Pos& start_pos) {
455+
DropChar();
456+
std::string s;
457+
while (!AtCharEof() && PeekChar() != '`') {
458+
XLS_ASSIGN_OR_RETURN(std::string next, ProcessNextStringChar());
459+
absl::StrAppend(&s, next);
460+
}
461+
462+
if (AtEof()) {
463+
return ScanErrorStatus(
464+
Span(GetPos(), GetPos()),
465+
"Reached end of file without finding a closing backtick.");
466+
}
467+
468+
if (!TryDropChar('`')) {
469+
return ScanErrorStatus(Span(start_pos, GetPos()),
470+
"Expected close backtick character to terminate "
471+
"open backtick character.");
472+
}
473+
474+
return Token(TokenKind::kBacktickString, Span(start_pos, GetPos()),
475+
std::move(s));
476+
}
477+
454478
absl::StatusOr<Token> Scanner::Pop() {
455479
if (include_whitespace_and_comments_) {
456480
XLS_ASSIGN_OR_RETURN(std::optional<Token> tok, TryPopWhitespaceOrComment());
@@ -490,6 +514,10 @@ absl::StatusOr<Token> Scanner::Pop() {
490514
XLS_ASSIGN_OR_RETURN(result, ScanString(start_pos));
491515
break;
492516
}
517+
case '`': {
518+
XLS_ASSIGN_OR_RETURN(result, ScanBacktickString(start_pos));
519+
break;
520+
}
493521
case '\'': {
494522
if (IsCharLiteral()) {
495523
XLS_ASSIGN_OR_RETURN(result, ScanChar(start_pos));

xls/dslx/frontend/scanner.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ class Scanner {
176176
// be over the opening quote character.
177177
absl::StatusOr<Token> ScanString(const Pos& start_pos);
178178

179+
// Scans a backtick-quoted string out of the character stream -- character
180+
// cursor should be over the opening backtick character.
181+
absl::StatusOr<Token> ScanBacktickString(const Pos& start_pos);
182+
179183
// Scans a character literal from the character stream as a character token.
180184
//
181185
// Precondition: The character stream must be positioned at an open quote.

0 commit comments

Comments
 (0)