• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

llnl / dftracer-utils / 30002033718

23 Jul 2026 11:08AM UTC coverage: 52.742% (+0.08%) from 52.66%
30002033718

Pull #99

github

web-flow
Merge df0b9588d into 06bc84ec9
Pull Request #99: Support CM time_metric (NS/MS/SEC/US) across reader and viz

41941 of 102829 branches covered (40.79%)

Branch coverage included in aggregate %.

2221 of 2839 new or added lines in 58 files covered. (78.23%)

137 existing lines in 13 files now uncovered.

37042 of 46924 relevant lines covered (78.94%)

59523.07 hits per line

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

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

4
#include <algorithm>
5
#include <cctype>
6
#include <charconv>
7
#include <memory>
8
#include <regex>
9
#include <sstream>
10
#include <string>
11

12
namespace dftracer::utils::utilities::common::query {
13

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

23
QueryParseError::QueryParseError(QueryError err)
3✔
24
    : DFTUtilsException(ErrorCode::QUERY, err.format()), err_(std::move(err)) {}
3!
25

26
namespace {
27

28
bool is_ident_start(char c) {
3,116✔
29
    return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
3,116!
30
}
31

32
bool is_ident_char(char c) {
9,088✔
33
    return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '.';
9,088✔
34
}
35

36
bool iequals(std::string_view a, std::string_view b) {
19,388✔
37
    if (a.size() != b.size()) return false;
19,388✔
38
    return std::equal(a.begin(), a.end(), b.begin(),
5,251✔
39
                      [](unsigned char ca, unsigned char cb) {
6,607✔
40
                          return std::tolower(ca) == std::tolower(cb);
6,607✔
41
                      });
2,624✔
42
}
9,705✔
43

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

56
}  // namespace
57

58
dftracer::utils::expected<std::vector<Token>, QueryError> tokenize(
1,451✔
59
    std::string_view input) {
60
    std::vector<Token> tokens;
1,451✔
61
    std::size_t pos = 0;
1,451✔
62

63
    auto skip_ws = [&]() {
10,424✔
64
        while (pos < input.size() &&
28,016✔
65
               std::isspace(static_cast<unsigned char>(input[pos]))) {
14,492✔
66
            ++pos;
6,235✔
67
        }
68
    };
5,565✔
69

70
    while (true) {
4,868✔
71
        skip_ws();
9,713✔
72
        if (pos >= input.size()) break;
9,694✔
73

74
        std::size_t start = pos;
8,265✔
75
        char c = input[pos];
8,265✔
76

77
        // Operators
78
        if (c == '=' && pos + 1 < input.size() && input[pos + 1] == '=') {
8,263!
79
            tokens.push_back({TokenKind::OP_EQ, input.substr(start, 2), start});
1,648✔
80
            pos += 2;
1,650✔
81
            continue;
1,650✔
82
        }
83
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '=') {
6,615✔
84
            tokens.push_back({TokenKind::OP_NE, input.substr(start, 2), start});
8!
85
            pos += 2;
8✔
86
            continue;
8✔
87
        }
88
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '~') {
6,608!
89
            if (pos + 2 < input.size() && input[pos + 2] == '*') {
10!
NEW
90
                tokens.push_back(
×
NEW
91
                    {TokenKind::OP_NIREGEX, input.substr(start, 3), start});
×
NEW
92
                pos += 3;
×
93
            } else {
94
                tokens.push_back(
10!
95
                    {TokenKind::OP_NREGEX, input.substr(start, 2), start});
10!
96
                pos += 2;
10✔
97
            }
98
            continue;
10✔
99
        }
100
        if (c == '>' && pos + 1 < input.size() && input[pos + 1] == '=') {
6,599!
101
            tokens.push_back({TokenKind::OP_GE, input.substr(start, 2), start});
163!
102
            pos += 2;
164✔
103
            continue;
164✔
104
        }
105
        if (c == '<' && pos + 1 < input.size() && input[pos + 1] == '=') {
6,436✔
106
            tokens.push_back({TokenKind::OP_LE, input.substr(start, 2), start});
115!
107
            pos += 2;
115✔
108
            continue;
115✔
109
        }
110
        if (c == '>') {
6,323✔
111
            tokens.push_back({TokenKind::OP_GT, input.substr(start, 1), start});
46!
112
            ++pos;
46✔
113
            continue;
46✔
114
        }
115
        if (c == '<') {
6,277✔
116
            tokens.push_back({TokenKind::OP_LT, input.substr(start, 1), start});
4!
117
            ++pos;
4✔
118
            continue;
4✔
119
        }
120
        if (c == '~') {
6,273✔
121
            if (pos + 1 < input.size() && input[pos + 1] == '*') {
26!
122
                tokens.push_back(
8!
123
                    {TokenKind::OP_IREGEX, input.substr(start, 2), start});
8!
124
                pos += 2;
8✔
125
            } else {
4✔
126
                tokens.push_back(
18!
127
                    {TokenKind::OP_REGEX, input.substr(start, 1), start});
18!
128
                ++pos;
18✔
129
            }
130
            continue;
26✔
131
        }
132

133
        // Punctuation
134
        if (c == '(') {
6,247✔
135
            tokens.push_back(
10!
136
                {TokenKind::LPAREN, input.substr(start, 1), start});
10!
137
            ++pos;
10✔
138
            continue;
10✔
139
        }
140
        if (c == ')') {
6,237✔
141
            tokens.push_back(
10!
142
                {TokenKind::RPAREN, input.substr(start, 1), start});
10!
143
            ++pos;
10✔
144
            continue;
10✔
145
        }
146
        if (c == '[') {
6,227✔
147
            tokens.push_back(
104!
148
                {TokenKind::LBRACKET, input.substr(start, 1), start});
104!
149
            ++pos;
104✔
150
            continue;
104✔
151
        }
152
        if (c == ']') {
6,123✔
153
            tokens.push_back(
104!
154
                {TokenKind::RBRACKET, input.substr(start, 1), start});
104!
155
            ++pos;
104✔
156
            continue;
104✔
157
        }
158
        if (c == ',') {
6,019✔
159
            tokens.push_back({TokenKind::COMMA, input.substr(start, 1), start});
374!
160
            ++pos;
374✔
161
            continue;
374✔
162
        }
163

164
        // Strings
165
        if (c == '"' || c == '\'') {
5,645✔
166
            char quote = c;
2,106✔
167
            ++pos;
2,106✔
168
            while (pos < input.size() && input[pos] != quote) {
13,517✔
169
                if (input[pos] == '\\' && pos + 1 < input.size()) {
11,410✔
170
                    pos += 2;
4✔
171
                } else {
2✔
172
                    ++pos;
11,407✔
173
                }
174
            }
175
            if (pos >= input.size()) {
2,104✔
176
                return dftracer::utils::unexpected(make_error(
3!
177
                    input, start, pos - start, "Unterminated string literal"));
2!
178
            }
179
            ++pos;  // consume closing quote
2,101✔
180
            tokens.push_back({TokenKind::STRING,
2,103✔
181
                              input.substr(start + 1, pos - start - 2), start});
2,101!
182
            continue;
2,101✔
183
        }
1,051✔
184

185
        // Numbers (including negative)
186
        if (std::isdigit(static_cast<unsigned char>(c)) ||
3,543✔
187
            (c == '-' && pos + 1 < input.size() &&
1,563!
188
             std::isdigit(static_cast<unsigned char>(input[pos + 1])))) {
4!
189
            bool is_float = false;
423✔
190
            ++pos;
423✔
191
            while (pos < input.size() &&
5,137✔
192
                   (std::isdigit(static_cast<unsigned char>(input[pos])) ||
2,451✔
193
                    input[pos] == '.' || input[pos] == 'e' ||
210✔
194
                    input[pos] == 'E' || input[pos] == '+' ||
202!
195
                    input[pos] == '-')) {
203!
196
                if (input[pos] == '.' || input[pos] == 'e' ||
3,373✔
197
                    input[pos] == 'E') {
2,244!
198
                    is_float = true;
10✔
199
                }
6✔
200
                ++pos;
2,260✔
201
            }
202
            auto text = input.substr(start, pos - start);
423!
203
            tokens.push_back(
423!
204
                {is_float ? TokenKind::FLOAT : TokenKind::INT, text, start});
423✔
205
            continue;
424✔
206
        }
212✔
207

208
        // Identifiers and keywords
209
        if (is_ident_start(c)) {
3,117✔
210
            ++pos;
3,112✔
211
            while (pos < input.size() && is_ident_char(input[pos])) {
10,677✔
212
                ++pos;
6,009✔
213
            }
214
            auto text = input.substr(start, pos - start);
3,111✔
215
            TokenKind kind = TokenKind::IDENT;
3,110✔
216
            if (iequals(text, "and"))
3,110✔
217
                kind = TokenKind::KW_AND;
272✔
218
            else if (iequals(text, "or"))
2,842✔
219
                kind = TokenKind::KW_OR;
471✔
220
            else if (iequals(text, "not"))
2,370!
221
                kind = TokenKind::KW_NOT;
36✔
222
            else if (iequals(text, "in"))
2,331!
223
                kind = TokenKind::KW_IN;
128✔
224
            else if (iequals(text, "like"))
2,205!
225
                kind = TokenKind::KW_LIKE;
14✔
226
            else if (iequals(text, "ilike"))
2,190!
227
                kind = TokenKind::KW_ILIKE;
10✔
228
            else if (iequals(text, "true"))
2,182✔
229
                kind = TokenKind::KW_TRUE;
4✔
230
            else if (iequals(text, "false"))
2,177!
231
                kind = TokenKind::KW_FALSE;
4✔
232
            tokens.push_back({kind, text, start});
3,114✔
233
            continue;
3,112✔
234
        }
1,554✔
235

236
        return dftracer::utils::unexpected(make_error(
3!
237
            input, start, 1, std::string("Unexpected character '") + c + "'"));
4!
238
    }
4,129✔
239

240
    tokens.push_back(
1,426!
241
        {TokenKind::END, input.substr(input.size(), 0), input.size()});
1,427!
242
    return tokens;
1,425!
243
}
1,466✔
244

245
// ============================================================
246
// Recursive Descent Parser
247
// ============================================================
248

249
namespace {
250

251
void append_escaped(std::string& out, char c) {
174✔
252
    switch (c) {
174!
253
        case '.':
254
        case '\\':
255
        case '+':
256
        case '*':
257
        case '?':
258
        case '(':
259
        case ')':
260
        case '[':
261
        case ']':
262
        case '{':
263
        case '}':
264
        case '^':
265
        case '$':
266
        case '|':
NEW
267
            out += '\\';
×
268
            [[fallthrough]];
269
        default:
87✔
270
            out += c;
174✔
271
    }
87✔
272
}
174✔
273

274
// Translate a SQL LIKE pattern to an anchored ECMAScript regex: '%' matches any
275
// run, '_' matches one char, everything else is literal.
276
std::string like_to_regex(const std::string& like) {
20✔
277
    std::string out = "^";
20!
278
    for (char c : like) {
124✔
279
        if (c == '%') {
104✔
280
            out += ".*";
32!
281
        } else if (c == '_') {
88✔
282
            out += '.';
4!
283
        } else {
2✔
284
            append_escaped(out, c);
68!
285
        }
286
    }
287
    out += '$';
20!
288
    return out;
20✔
289
}
10!
290

291
// Treat the whole literal as a substring to search for (unanchored).
292
std::string contains_to_regex(const std::string& sub) {
24✔
293
    std::string out;
24✔
294
    for (char c : sub) append_escaped(out, c);
130✔
295
    return out;
24✔
296
}
12!
297

298
dftracer::utils::expected<std::shared_ptr<CompiledPattern>, std::string>
299
compile_pattern(MatchOp op, const std::string& pattern) {
74✔
300
    std::string regex_src;
74✔
301
    auto flags = std::regex::ECMAScript;
74✔
302
    switch (op) {
74✔
303
        case MatchOp::LIKE:
6✔
304
            regex_src = like_to_regex(pattern);
12!
305
            break;
12✔
306
        case MatchOp::ILIKE:
4✔
307
            regex_src = like_to_regex(pattern);
8!
308
            flags |= std::regex::icase;
8!
309
            break;
8✔
310
        case MatchOp::REGEX:
12✔
311
            regex_src = pattern;
24!
312
            break;
24✔
313
        case MatchOp::IREGEX:
3✔
314
            regex_src = pattern;
6!
315
            flags |= std::regex::icase;
6!
316
            break;
6✔
317
        case MatchOp::ICONTAINS:
12✔
318
            regex_src = contains_to_regex(pattern);
24!
319
            flags |= std::regex::icase;
24!
320
            break;
24✔
321
    }
322
    try {
323
        auto cp = std::make_shared<CompiledPattern>();
74!
324
        cp->re = std::regex(regex_src, flags);
74✔
325
        return cp;
72!
326
    } catch (const std::regex_error& e) {
75!
327
        return dftracer::utils::unexpected(std::string(e.what()));
2!
328
    }
2!
329
}
75✔
330

331
class Parser {
332
   public:
333
    Parser(const std::vector<Token>& tokens, std::string_view source)
2,092✔
334
        : tokens_(tokens), source_(source) {}
2,790✔
335

336
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_query() {
1,395✔
337
        auto result = parse_or_expr();
1,395!
338
        if (!result) return result;
1,395✔
339
        if (current().kind != TokenKind::END) {
1,383!
340
            return dftracer::utils::unexpected(
×
341
                error("Expected 'and', 'or', or end of query, got '" +
×
342
                      std::string(current().text) + "'"));
×
343
        }
344
        return result;
1,382✔
345
    }
1,395✔
346

347
   private:
348
    const std::vector<Token>& tokens_;
349
    std::string_view source_;
350
    std::size_t pos_ = 0;
698✔
351

352
    const Token& current() const { return tokens_[pos_]; }
25,087✔
353

354
    const Token& advance() { return tokens_[pos_++]; }
7,435✔
355

356
    bool match(TokenKind kind) {
2,806✔
357
        if (current().kind == kind) {
2,806✔
358
            ++pos_;
596✔
359
            return true;
596✔
360
        }
361
        return false;
2,210✔
362
    }
1,404✔
363

364
    QueryError error(std::string msg) const {
10✔
365
        auto& tok = current();
10✔
366
        std::size_t len = tok.text.empty() ? 1 : tok.text.size();
10!
367
        return make_error(source_, tok.column, len, std::move(msg));
10!
368
    }
369

370
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_or_expr() {
1,406✔
371
        auto left = parse_and_expr();
1,406!
372
        if (!left) return left;
1,404✔
373
        while (current().kind == TokenKind::KW_OR) {
1,857✔
374
            advance();
466!
375
            auto right = parse_and_expr();
466!
376
            if (!right) return right;
465✔
377
            left = make_node(OrNode{std::move(*left), std::move(*right)});
465!
378
        }
466✔
379
        return left;
1,392✔
380
    }
1,406✔
381

382
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_and_expr() {
1,871✔
383
        auto left = parse_not_expr();
1,871!
384
        if (!left) return left;
1,869✔
385
        while (current().kind == TokenKind::KW_AND) {
2,108✔
386
            advance();
250✔
387
            auto right = parse_not_expr();
252✔
388
            if (!right) return right;
250✔
389
            left = make_node(AndNode{std::move(*left), std::move(*right)});
250!
390
        }
248✔
391
        return left;
1,857✔
392
    }
1,877✔
393

394
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_not_expr() {
2,130✔
395
        if (current().kind == TokenKind::KW_NOT) {
2,130✔
396
            advance();
10✔
397
            auto operand = parse_not_expr();
10!
398
            if (!operand) return operand;
10✔
399
            return make_node(NotNode{std::move(*operand)});
15!
400
        }
10✔
401
        return parse_primary();
2,119✔
402
    }
1,066✔
403

404
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_primary() {
2,120✔
405
        if (match(TokenKind::LPAREN)) {
2,120✔
406
            auto expr = parse_or_expr();
10!
407
            if (!expr) return expr;
10✔
408
            if (!match(TokenKind::RPAREN)) {
10!
409
                return dftracer::utils::unexpected(error("Expected ')'"));
×
410
            }
411
            return expr;
10✔
412
        }
10✔
413

414
        // Python-style substring: 'sub' in field / 'sub' not in field
415
        // (case-insensitive). Distinguished from "field in [array]" by the
416
        // string literal on the left.
417
        if (current().kind == TokenKind::STRING) {
2,108✔
418
            return parse_contains();
24!
419
        }
420

421
        if (current().kind != TokenKind::IDENT) {
2,084!
422
            return dftracer::utils::unexpected(
×
NEW
423
                error("Expected field name, string, or '(', got '" +
×
424
                      std::string(current().text) + "'"));
×
425
        }
426

427
        auto field = parse_field();
2,089!
428

429
        // Check for "in" or "not in"
430
        if (current().kind == TokenKind::KW_IN) {
2,089✔
431
            advance();
88!
432
            auto arr = parse_array();
88!
433
            if (!arr) return dftracer::utils::unexpected(arr.error());
88!
434
            return make_node(InNode{std::move(field), std::move(*arr)});
132!
435
        }
88✔
436
        if (current().kind == TokenKind::KW_NOT) {
1,998✔
437
            // Look ahead for "in", "like", or "ilike".
438
            if (pos_ + 1 < tokens_.size()) {
18✔
439
                auto next = tokens_[pos_ + 1].kind;
18✔
440
                if (next == TokenKind::KW_IN) {
18✔
441
                    advance();  // consume "not"
14!
442
                    advance();  // consume "in"
14!
443
                    auto arr = parse_array();
14!
444
                    if (!arr) return dftracer::utils::unexpected(arr.error());
14!
445
                    return make_node(
21!
446
                        NotInNode{std::move(field), std::move(*arr)});
28!
447
                }
14✔
448
                if (next == TokenKind::KW_LIKE || next == TokenKind::KW_ILIKE) {
4!
449
                    advance();  // consume "not"
4!
450
                    advance();  // consume "like"/"ilike"
4!
451
                    return parse_match(std::move(field),
6!
452
                                       next == TokenKind::KW_LIKE
2✔
453
                                           ? MatchOp::LIKE
2✔
454
                                           : MatchOp::ILIKE,
455
                                       /*negated=*/true);
2!
456
                }
457
            }
458
        }
459

460
        // Pattern match: like / ilike / ~ / ~* / !~ / !~*
461
        switch (current().kind) {
1,978!
462
            case TokenKind::KW_LIKE:
4✔
463
                advance();
8!
464
                return parse_match(std::move(field), MatchOp::LIKE, false);
8!
465
            case TokenKind::KW_ILIKE:
4✔
466
                advance();
8!
467
                return parse_match(std::move(field), MatchOp::ILIKE, false);
8!
468
            case TokenKind::OP_REGEX:
8✔
469
                advance();
16!
470
                return parse_match(std::move(field), MatchOp::REGEX, false);
16!
471
            case TokenKind::OP_IREGEX:
3✔
472
                advance();
6!
473
                return parse_match(std::move(field), MatchOp::IREGEX, false);
6!
474
            case TokenKind::OP_NREGEX:
4✔
475
                advance();
8!
476
                return parse_match(std::move(field), MatchOp::REGEX, true);
8!
477
            case TokenKind::OP_NIREGEX:
1✔
478
                advance();
1!
NEW
479
                return parse_match(std::move(field), MatchOp::IREGEX, true);
×
480
            default:
965✔
481
                break;
1,934✔
482
        }
483

484
        // Comparison
485
        auto op = parse_comp_op();
1,934!
486
        if (!op) return dftracer::utils::unexpected(op.error());
1,934!
487
        auto val = parse_value();
1,933!
488
        if (!val) return dftracer::utils::unexpected(val.error());
1,936!
489
        return make_node(CompareNode{std::move(field), *op, std::move(*val)});
2,881!
490
    }
2,107✔
491

492
    FieldNode parse_field() {
2,109✔
493
        auto& tok = advance();
2,109✔
494
        return FieldNode{std::string(tok.text)};
2,105!
495
    }
496

497
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_match(
50✔
498
        FieldNode field, MatchOp op, bool negated) {
499
        if (current().kind != TokenKind::STRING) {
50✔
NEW
500
            return dftracer::utils::unexpected(
×
NEW
501
                error("Expected string pattern, got '" +
×
NEW
502
                      std::string(current().text) + "'"));
×
503
        }
504
        auto& tok = advance();
50✔
505
        std::string pattern(tok.text);
50!
506
        auto compiled = compile_pattern(op, pattern);
50!
507
        if (!compiled) {
50✔
508
            return dftracer::utils::unexpected(make_error(
4!
509
                source_, tok.column, tok.text.empty() ? 1 : tok.text.size(),
3✔
510
                "Invalid pattern: " + compiled.error()));
4!
511
        }
512
        MatchNode node;
48✔
513
        node.field = std::move(field);
48✔
514
        node.op = op;
48✔
515
        node.pattern = std::move(pattern);
48✔
516
        node.negated = negated;
48✔
517
        node.compiled = std::move(*compiled);
48!
518
        return make_node(std::move(node));
72!
519
    }
50✔
520

521
    // 'literal' in field / 'literal' not in field (case-insensitive substring).
522
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_contains() {
24✔
523
        auto& str_tok = advance();  // the string literal
24✔
524
        std::string literal(str_tok.text);
24!
525

526
        bool negated = false;
24✔
527
        if (current().kind == TokenKind::KW_NOT) {
24✔
528
            if (pos_ + 1 < tokens_.size() &&
9!
529
                tokens_[pos_ + 1].kind == TokenKind::KW_IN) {
6!
530
                advance();  // "not"
6!
531
                advance();  // "in"
6!
532
                negated = true;
6✔
533
            } else {
3✔
NEW
534
                return dftracer::utils::unexpected(
×
NEW
535
                    error("Expected 'in' after 'not'"));
×
536
            }
537
        } else if (current().kind == TokenKind::KW_IN) {
21!
538
            advance();  // "in"
18!
539
        } else {
9✔
NEW
540
            return dftracer::utils::unexpected(
×
NEW
541
                error("Expected 'in' after string literal, got '" +
×
NEW
542
                      std::string(current().text) + "'"));
×
543
        }
544

545
        if (current().kind != TokenKind::IDENT) {
24!
NEW
546
            return dftracer::utils::unexpected(
×
NEW
547
                error("Expected field name after 'in', got '" +
×
NEW
548
                      std::string(current().text) + "'"));
×
549
        }
550
        auto field = parse_field();
24!
551

552
        auto compiled = compile_pattern(MatchOp::ICONTAINS, literal);
24!
553
        if (!compiled) {
24✔
NEW
554
            return dftracer::utils::unexpected(
×
NEW
555
                make_error(source_, str_tok.column,
×
NEW
556
                           str_tok.text.empty() ? 1 : str_tok.text.size(),
×
NEW
557
                           "Invalid pattern: " + compiled.error()));
×
558
        }
559
        MatchNode node;
24✔
560
        node.field = std::move(field);
24✔
561
        node.op = MatchOp::ICONTAINS;
24✔
562
        node.pattern = std::move(literal);
24✔
563
        node.negated = negated;
24✔
564
        node.compiled = std::move(*compiled);
24!
565
        return make_node(std::move(node));
36!
566
    }
24✔
567

568
    dftracer::utils::expected<CompareOp, QueryError> parse_comp_op() {
1,933✔
569
        auto& tok = current();
1,933✔
570
        switch (tok.kind) {
1,935✔
571
            case TokenKind::OP_EQ:
808✔
572
                advance();
1,616✔
573
                return CompareOp::EQ;
1,615!
574
            case TokenKind::OP_NE:
3✔
575
                advance();
6✔
576
                return CompareOp::NE;
6!
577
            case TokenKind::OP_GT:
19✔
578
                advance();
38✔
579
                return CompareOp::GT;
38!
580
            case TokenKind::OP_LT:
1✔
581
                advance();
2✔
582
                return CompareOp::LT;
2!
583
            case TokenKind::OP_GE:
79✔
584
                advance();
160✔
585
                return CompareOp::GE;
160!
586
            case TokenKind::OP_LE:
56✔
587
                advance();
113✔
588
                return CompareOp::LE;
113!
589
            default:
590
                return dftracer::utils::unexpected(
×
591
                    error("Expected comparison operator (==, !=, >, "
×
592
                          "<, >=, <=), got '" +
×
593
                          std::string(tok.text) + "'"));
×
594
        }
595
    }
969✔
596

597
    dftracer::utils::expected<LiteralNode, QueryError> parse_value() {
2,406✔
598
        auto& tok = current();
2,406✔
599
        switch (tok.kind) {
2,406✔
600
            case TokenKind::STRING: {
1,003✔
601
                auto text = std::string(tok.text);
2,008!
602
                advance();
2,008!
603
                return LiteralNode{std::move(text)};
3,010!
604
            }
2,006✔
605
            case TokenKind::INT: {
189✔
606
                int64_t val = 0;
380✔
607
                auto [ptr, ec] = std::from_chars(
381!
608
                    tok.text.data(), tok.text.data() + tok.text.size(), val);
380✔
609
                if (ec != std::errc{}) {
380!
610
                    return dftracer::utils::unexpected(error(
×
611
                        "Invalid integer: '" + std::string(tok.text) + "'"));
×
612
                }
613
                advance();
380✔
614
                if (val >= 0) {
380✔
615
                    return LiteralNode{static_cast<uint64_t>(val)};
567!
616
                }
617
                return LiteralNode{val};
3!
618
            }
619
            case TokenKind::FLOAT: {
2✔
620
                double val = 0;
4✔
621
                auto sv = tok.text;
4✔
622
                // from_chars for double not available on all
623
                // compilers; use stod
624
                try {
625
                    val = std::stod(std::string(sv));
4!
626
                } catch (...) {
2✔
627
                    return dftracer::utils::unexpected(
×
628
                        error("Invalid float: '" + std::string(sv) + "'"));
×
629
                }
×
630
                advance();
4✔
631
                return LiteralNode{val};
6!
632
            }
633
            case TokenKind::KW_TRUE:
1✔
634
                advance();
2✔
635
                return LiteralNode{true};
3!
636
            case TokenKind::KW_FALSE:
1✔
637
                advance();
2✔
638
                return LiteralNode{false};
3!
639
            default:
5✔
640
                return dftracer::utils::unexpected(
15!
641
                    error("Expected value (string, number, or bool), "
15!
642
                          "got '" +
10!
643
                          std::string(tok.text) + "'"));
25!
644
        }
645
    }
1,205✔
646

647
    dftracer::utils::expected<ArrayNode, QueryError> parse_array() {
102✔
648
        if (!match(TokenKind::LBRACKET)) {
102!
649
            return dftracer::utils::unexpected(error("Expected '['"));
×
650
        }
651
        ArrayNode arr;
102✔
652
        if (current().kind != TokenKind::RBRACKET) {
102✔
653
            auto val = parse_value();
100!
654
            if (!val) return dftracer::utils::unexpected(val.error());
100!
655
            arr.elements.push_back(std::move(*val));
100!
656
            while (match(TokenKind::COMMA)) {
472✔
657
                val = parse_value();
372!
658
                if (!val) return dftracer::utils::unexpected(val.error());
372!
659
                arr.elements.push_back(std::move(*val));
372!
660
            }
661
        }
100✔
662
        if (!match(TokenKind::RBRACKET)) {
102!
663
            return dftracer::utils::unexpected(error("Expected ']' or ','"));
×
664
        }
665
        return arr;
102!
666
    }
102✔
667
};
668

669
}  // namespace
670

671
dftracer::utils::expected<QueryNodePtr, QueryError> parse_tokens(
1,393✔
672
    const std::vector<Token>& tokens, std::string_view source) {
673
    Parser parser(tokens, source);
1,393✔
674
    return parser.parse_query();
2,093!
675
}
676

677
dftracer::utils::expected<QueryNodePtr, QueryError> parse(
1,398✔
678
    std::string_view input) {
679
    auto tokens = tokenize(input);
1,398!
680
    if (!tokens) return dftracer::utils::unexpected(tokens.error());
1,397!
681
    return parse_tokens(*tokens, input);
1,390!
682
}
1,399✔
683

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

© 2026 Coveralls, Inc