• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

llnl / dftracer-utils / 23529483807

25 Mar 2026 07:17AM UTC coverage: 48.515% (-1.6%) from 50.098%
23529483807

Pull #57

github

web-flow
Merge 5b1e117ad into 38f9f3616
Pull Request #57: feat(comparator): add pairwise traces comparator

18829 of 49412 branches covered (38.11%)

Branch coverage included in aggregate %.

1584 of 1933 new or added lines in 14 files covered. (81.95%)

3552 existing lines in 135 files now uncovered.

18474 of 27477 relevant lines covered (67.23%)

241072.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.22
/src/dftracer/utils/utilities/common/query/parser.cpp
1
#include <dftracer/utils/utilities/common/query/parser.h>
2

3
#include <algorithm>
4
#include <cctype>
5
#include <charconv>
6
#include <sstream>
7
#include <string>
8

9
namespace dftracer::utils::utilities::common::query {
10

11
std::string QueryError::format() const {
2✔
12
    std::ostringstream os;
2✔
13
    os << "Query parse error at column " << column << ":\n";
2!
14
    os << "  " << source << '\n';
2!
15
    os << "  " << indicator << '\n';
2!
16
    os << "  " << message << '\n';
2!
17
    return os.str();
2!
18
}
2✔
19

20
QueryParseError::QueryParseError(QueryError err)
2✔
21
    : std::runtime_error(err.format()), err_(std::move(err)) {}
2!
22

23
namespace {
24

25
bool is_ident_start(char c) {
828✔
26
    return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
828!
27
}
28

29
bool is_ident_char(char c) {
2,348✔
30
    return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '.';
2,348!
31
}
32

33
bool iequals(std::string_view a, std::string_view b) {
4,034✔
34
    if (a.size() != b.size()) return false;
4,034✔
35
    return std::equal(a.begin(), a.end(), b.begin(),
1,330✔
36
                      [](unsigned char ca, unsigned char cb) {
1,656✔
37
                          return std::tolower(ca) == std::tolower(cb);
1,656✔
38
                      });
39
}
4,034✔
40

41
QueryError make_error(std::string_view source, std::size_t col, std::size_t len,
5✔
42
                      std::string msg) {
43
    std::string ind(col, ' ');
5✔
44
    if (len > 0) {
5!
45
        ind += '^';
5!
46
        for (std::size_t i = 1; i < len; ++i) ind += '~';
10!
47
    } else {
5✔
48
        ind += '^';
×
49
    }
50
    return QueryError{std::move(msg), col, std::string(source), std::move(ind)};
5!
51
}
5✔
52

53
}  // namespace
54

55
dftracer::utils::expected<std::vector<Token>, QueryError> tokenize(
392✔
56
    std::string_view input) {
57
    std::vector<Token> tokens;
392✔
58
    std::size_t pos = 0;
392✔
59

60
    auto skip_ws = [&]() {
3,065✔
61
        while (pos < input.size() &&
7,036✔
62
               std::isspace(static_cast<unsigned char>(input[pos]))) {
3,972✔
63
            ++pos;
1,690✔
64
        }
65
    };
2,673✔
66

67
    while (true) {
2,673✔
68
        skip_ws();
2,673!
69
        if (pos >= input.size()) break;
2,673✔
70

71
        std::size_t start = pos;
2,282✔
72
        char c = input[pos];
2,282✔
73

74
        // Operators
75
        if (c == '=' && pos + 1 < input.size() && input[pos + 1] == '=') {
2,282!
76
            tokens.push_back({TokenKind::OP_EQ, input.substr(start, 2), start});
502!
77
            pos += 2;
502✔
78
            continue;
502✔
79
        }
80
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '=') {
1,780!
81
            tokens.push_back({TokenKind::OP_NE, input.substr(start, 2), start});
3!
82
            pos += 2;
3✔
83
            continue;
3✔
84
        }
85
        if (c == '>' && pos + 1 < input.size() && input[pos + 1] == '=') {
1,777!
86
            tokens.push_back({TokenKind::OP_GE, input.substr(start, 2), start});
14!
87
            pos += 2;
14✔
88
            continue;
14✔
89
        }
90
        if (c == '<' && pos + 1 < input.size() && input[pos + 1] == '=') {
1,763!
91
            tokens.push_back({TokenKind::OP_LE, input.substr(start, 2), start});
11!
92
            pos += 2;
11✔
93
            continue;
11✔
94
        }
95
        if (c == '>') {
1,752✔
96
            tokens.push_back({TokenKind::OP_GT, input.substr(start, 1), start});
20!
97
            ++pos;
20✔
98
            continue;
20✔
99
        }
100
        if (c == '<') {
1,732✔
101
            tokens.push_back({TokenKind::OP_LT, input.substr(start, 1), start});
2!
102
            ++pos;
2✔
103
            continue;
2✔
104
        }
105

106
        // Punctuation
107
        if (c == '(') {
1,730✔
108
            tokens.push_back(
4!
109
                {TokenKind::LPAREN, input.substr(start, 1), start});
4!
110
            ++pos;
4✔
111
            continue;
4✔
112
        }
113
        if (c == ')') {
1,726✔
114
            tokens.push_back(
4!
115
                {TokenKind::RPAREN, input.substr(start, 1), start});
4!
116
            ++pos;
4✔
117
            continue;
4✔
118
        }
119
        if (c == '[') {
1,722✔
120
            tokens.push_back(
34!
121
                {TokenKind::LBRACKET, input.substr(start, 1), start});
34!
122
            ++pos;
34✔
123
            continue;
34✔
124
        }
125
        if (c == ']') {
1,688✔
126
            tokens.push_back(
34!
127
                {TokenKind::RBRACKET, input.substr(start, 1), start});
34!
128
            ++pos;
34✔
129
            continue;
34✔
130
        }
131
        if (c == ',') {
1,654✔
132
            tokens.push_back({TokenKind::COMMA, input.substr(start, 1), start});
124!
133
            ++pos;
124✔
134
            continue;
124✔
135
        }
136

137
        // Strings
138
        if (c == '"' || c == '\'') {
1,530✔
139
            char quote = c;
630✔
140
            ++pos;
630✔
141
            while (pos < input.size() && input[pos] != quote) {
3,762✔
142
                if (input[pos] == '\\' && pos + 1 < input.size()) {
3,132!
143
                    pos += 2;
×
UNCOV
144
                } else {
×
145
                    ++pos;
3,132✔
146
                }
147
            }
148
            if (pos >= input.size()) {
630✔
149
                return dftracer::utils::unexpected(make_error(
1!
150
                    input, start, pos - start, "Unterminated string literal"));
1!
151
            }
152
            ++pos;  // consume closing quote
629✔
153
            tokens.push_back({TokenKind::STRING,
629!
154
                              input.substr(start + 1, pos - start - 2), start});
629!
155
            continue;
629✔
156
        }
157

158
        // Numbers (including negative)
159
        if (std::isdigit(static_cast<unsigned char>(c)) ||
902!
160
            (c == '-' && pos + 1 < input.size() &&
830!
161
             std::isdigit(static_cast<unsigned char>(input[pos + 1])))) {
2!
162
            bool is_float = false;
72✔
163
            ++pos;
72✔
164
            while (pos < input.size() &&
331✔
165
                   (std::isdigit(static_cast<unsigned char>(input[pos])) ||
145!
166
                    input[pos] == '.' || input[pos] == 'e' ||
35✔
167
                    input[pos] == 'E' || input[pos] == '+' ||
31!
168
                    input[pos] == '-')) {
31✔
169
                if (input[pos] == '.' || input[pos] == 'e' ||
114!
170
                    input[pos] == 'E') {
110✔
171
                    is_float = true;
4✔
172
                }
4✔
173
                ++pos;
114✔
174
            }
175
            auto text = input.substr(start, pos - start);
72!
176
            tokens.push_back(
72!
177
                {is_float ? TokenKind::FLOAT : TokenKind::INT, text, start});
72✔
178
            continue;
72✔
179
        }
180

181
        // Identifiers and keywords
182
        if (is_ident_start(c)) {
828!
183
            ++pos;
828✔
184
            while (pos < input.size() && is_ident_char(input[pos])) {
3,179!
185
                ++pos;
1,523✔
186
            }
187
            auto text = input.substr(start, pos - start);
828!
188
            TokenKind kind = TokenKind::IDENT;
828✔
189
            if (iequals(text, "and"))
828!
190
                kind = TokenKind::KW_AND;
58✔
191
            else if (iequals(text, "or"))
770!
192
                kind = TokenKind::KW_OR;
136✔
193
            else if (iequals(text, "not"))
634!
194
                kind = TokenKind::KW_NOT;
10✔
195
            else if (iequals(text, "in"))
624!
196
                kind = TokenKind::KW_IN;
34✔
197
            else if (iequals(text, "true"))
590!
198
                kind = TokenKind::KW_TRUE;
2✔
199
            else if (iequals(text, "false"))
588!
200
                kind = TokenKind::KW_FALSE;
2✔
201
            tokens.push_back({kind, text, start});
828!
202
            continue;
828✔
203
        }
204

205
        return dftracer::utils::unexpected(make_error(
×
206
            input, start, 1, std::string("Unexpected character '") + c + "'"));
×
207
    }
208

209
    tokens.push_back(
391!
210
        {TokenKind::END, input.substr(input.size(), 0), input.size()});
391!
211
    return tokens;
391!
212
}
392✔
213

214
// ============================================================
215
// Recursive Descent Parser
216
// ============================================================
217

218
namespace {
219

220
class Parser {
221
   public:
222
    Parser(const std::vector<Token>& tokens, std::string_view source)
754✔
223
        : tokens_(tokens), source_(source) {}
1,131✔
224

225
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_query() {
377✔
226
        auto result = parse_or_expr();
377✔
227
        if (!result) return result;
377✔
228
        if (current().kind != TokenKind::END) {
373!
229
            return dftracer::utils::unexpected(
×
230
                error("Expected 'and', 'or', or end of query, got '" +
×
231
                      std::string(current().text) + "'"));
×
232
        }
233
        return result;
373✔
234
    }
377✔
235

236
   private:
237
    const std::vector<Token>& tokens_;
238
    std::string_view source_;
239
    std::size_t pos_ = 0;
377✔
240

241
    const Token& current() const { return tokens_[pos_]; }
5,710✔
242

243
    const Token& advance() { return tokens_[pos_++]; }
1,995✔
244

245
    bool match(TokenKind kind) {
791✔
246
        if (current().kind == kind) {
791✔
247
            ++pos_;
197✔
248
            return true;
197✔
249
        }
250
        return false;
594✔
251
    }
791✔
252

253
    QueryError error(std::string msg) const {
4✔
254
        auto& tok = current();
4✔
255
        std::size_t len = tok.text.empty() ? 1 : tok.text.size();
4!
256
        return make_error(source_, tok.column, len, std::move(msg));
4!
UNCOV
257
    }
×
258

259
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_or_expr() {
381✔
260
        auto left = parse_and_expr();
381✔
261
        if (!left) return left;
381✔
262
        while (current().kind == TokenKind::KW_OR) {
512!
263
            advance();
135!
264
            auto right = parse_and_expr();
135!
265
            if (!right) return right;
135!
266
            left = make_node(OrNode{std::move(*left), std::move(*right)});
135!
267
        }
135!
268
        return left;
377✔
269
    }
381✔
270

271
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_and_expr() {
516✔
272
        auto left = parse_not_expr();
516✔
273
        if (!left) return left;
516✔
274
        while (current().kind == TokenKind::KW_AND) {
561!
275
            advance();
49!
276
            auto right = parse_not_expr();
49!
277
            if (!right) return right;
49!
278
            left = make_node(AndNode{std::move(*left), std::move(*right)});
49!
279
        }
49!
280
        return left;
512✔
281
    }
516✔
282

283
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_not_expr() {
569✔
284
        if (current().kind == TokenKind::KW_NOT) {
569✔
285
            advance();
4✔
286
            auto operand = parse_not_expr();
4✔
287
            if (!operand) return operand;
4!
288
            return make_node(NotNode{std::move(*operand)});
4!
289
        }
4✔
290
        return parse_primary();
565✔
291
    }
569✔
292

293
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_primary() {
565✔
294
        if (match(TokenKind::LPAREN)) {
565✔
295
            auto expr = parse_or_expr();
4✔
296
            if (!expr) return expr;
4!
297
            if (!match(TokenKind::RPAREN)) {
4!
298
                return dftracer::utils::unexpected(error("Expected ')'"));
×
299
            }
300
            return expr;
4✔
301
        }
4✔
302

303
        if (current().kind != TokenKind::IDENT) {
561!
304
            return dftracer::utils::unexpected(
×
305
                error("Expected field name or '(', got '" +
×
306
                      std::string(current().text) + "'"));
×
307
        }
308

309
        auto field = parse_field();
561✔
310

311
        // Check for "in" or "not in"
312
        if (current().kind == TokenKind::KW_IN) {
561!
313
            advance();
28!
314
            auto arr = parse_array();
28!
315
            if (!arr) return dftracer::utils::unexpected(arr.error());
28!
316
            return make_node(InNode{std::move(field), std::move(*arr)});
28!
317
        }
28✔
318
        if (current().kind == TokenKind::KW_NOT) {
533!
319
            // Look ahead for "in"
320
            if (pos_ + 1 < tokens_.size() &&
5!
321
                tokens_[pos_ + 1].kind == TokenKind::KW_IN) {
5✔
322
                advance();  // consume "not"
5!
323
                advance();  // consume "in"
5!
324
                auto arr = parse_array();
5!
325
                if (!arr) return dftracer::utils::unexpected(arr.error());
5!
326
                return make_node(NotInNode{std::move(field), std::move(*arr)});
5!
327
            }
5✔
UNCOV
328
        }
×
329

330
        // Comparison
331
        auto op = parse_comp_op();
528!
332
        if (!op) return dftracer::utils::unexpected(op.error());
528!
333
        auto val = parse_value();
528!
334
        if (!val) return dftracer::utils::unexpected(val.error());
528!
335
        return make_node(CompareNode{std::move(field), *op, std::move(*val)});
524!
336
    }
565✔
337

338
    FieldNode parse_field() {
561✔
339
        auto& tok = advance();
561✔
340
        return FieldNode{std::string(tok.text)};
561✔
341
    }
342

343
    dftracer::utils::expected<CompareOp, QueryError> parse_comp_op() {
528✔
344
        auto& tok = current();
528✔
345
        switch (tok.kind) {
528!
346
            case TokenKind::OP_EQ:
347
                advance();
486✔
348
                return CompareOp::EQ;
486✔
349
            case TokenKind::OP_NE:
350
                advance();
2✔
351
                return CompareOp::NE;
2✔
352
            case TokenKind::OP_GT:
353
                advance();
16✔
354
                return CompareOp::GT;
16✔
355
            case TokenKind::OP_LT:
356
                advance();
1✔
357
                return CompareOp::LT;
1✔
358
            case TokenKind::OP_GE:
359
                advance();
13✔
360
                return CompareOp::GE;
13✔
361
            case TokenKind::OP_LE:
362
                advance();
10✔
363
                return CompareOp::LE;
10✔
364
            default:
365
                return dftracer::utils::unexpected(
×
366
                    error("Expected comparison operator (==, !=, >, "
×
367
                          "<, >=, <=), got '" +
×
368
                          std::string(tok.text) + "'"));
×
369
        }
370
    }
528✔
371

372
    dftracer::utils::expected<LiteralNode, QueryError> parse_value() {
684✔
373
        auto& tok = current();
684✔
374
        switch (tok.kind) {
684✔
375
            case TokenKind::STRING: {
376
                auto text = std::string(tok.text);
625✔
377
                advance();
625!
378
                return LiteralNode{std::move(text)};
625!
379
            }
625✔
380
            case TokenKind::INT: {
381
                int64_t val = 0;
51✔
382
                auto [ptr, ec] = std::from_chars(
51✔
383
                    tok.text.data(), tok.text.data() + tok.text.size(), val);
51✔
384
                if (ec != std::errc{}) {
51!
385
                    return dftracer::utils::unexpected(error(
×
386
                        "Invalid integer: '" + std::string(tok.text) + "'"));
×
387
                }
388
                advance();
51✔
389
                if (val >= 0) {
51✔
390
                    return LiteralNode{static_cast<uint64_t>(val)};
50!
391
                }
392
                return LiteralNode{val};
1!
393
            }
394
            case TokenKind::FLOAT: {
395
                double val = 0;
2✔
396
                auto sv = tok.text;
2✔
397
                // from_chars for double not available on all
398
                // compilers; use stod
399
                try {
400
                    val = std::stod(std::string(sv));
2!
401
                } catch (...) {
2✔
402
                    return dftracer::utils::unexpected(
×
403
                        error("Invalid float: '" + std::string(sv) + "'"));
×
404
                }
×
405
                advance();
2✔
406
                return LiteralNode{val};
2!
407
            }
408
            case TokenKind::KW_TRUE:
409
                advance();
1✔
410
                return LiteralNode{true};
1!
411
            case TokenKind::KW_FALSE:
412
                advance();
1✔
413
                return LiteralNode{false};
1!
414
            default:
415
                return dftracer::utils::unexpected(
4!
416
                    error("Expected value (string, number, or bool), "
4!
417
                          "got '" +
4!
418
                          std::string(tok.text) + "'"));
4!
419
        }
420
    }
684✔
421

422
    dftracer::utils::expected<ArrayNode, QueryError> parse_array() {
33✔
423
        if (!match(TokenKind::LBRACKET)) {
33!
424
            return dftracer::utils::unexpected(error("Expected '['"));
×
425
        }
426
        ArrayNode arr;
33✔
427
        if (current().kind != TokenKind::RBRACKET) {
33!
428
            auto val = parse_value();
33!
429
            if (!val) return dftracer::utils::unexpected(val.error());
33!
430
            arr.elements.push_back(std::move(*val));
33!
431
            while (match(TokenKind::COMMA)) {
156!
432
                val = parse_value();
123!
433
                if (!val) return dftracer::utils::unexpected(val.error());
123!
434
                arr.elements.push_back(std::move(*val));
123!
435
            }
436
        }
33!
437
        if (!match(TokenKind::RBRACKET)) {
33!
438
            return dftracer::utils::unexpected(error("Expected ']' or ','"));
×
439
        }
440
        return arr;
33!
441
    }
33✔
442
};
443

444
}  // namespace
445

446
dftracer::utils::expected<QueryNodePtr, QueryError> parse_tokens(
377✔
447
    const std::vector<Token>& tokens, std::string_view source) {
448
    Parser parser(tokens, source);
377✔
449
    return parser.parse_query();
377✔
450
}
451

452
dftracer::utils::expected<QueryNodePtr, QueryError> parse(
377✔
453
    std::string_view input) {
454
    auto tokens = tokenize(input);
377✔
455
    if (!tokens) return dftracer::utils::unexpected(tokens.error());
377!
456
    return parse_tokens(*tokens, input);
377!
457
}
377✔
458

459
}  // namespace dftracer::utils::utilities::common::query
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc