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

llnl / dftracer-utils / 30074477855

24 Jul 2026 07:08AM UTC coverage: 52.727% (+0.07%) from 52.66%
30074477855

Pull #99

github

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

42091 of 103330 branches covered (40.73%)

Branch coverage included in aggregate %.

2472 of 3143 new or added lines in 65 files covered. (78.65%)

146 existing lines in 14 files now uncovered.

37235 of 47117 relevant lines covered (79.03%)

68905.24 hits per line

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

69.98
/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) {
2,397✔
29
    return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
2,397!
30
}
31

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

36
bool iequals(std::string_view a, std::string_view b) {
15,107✔
37
    if (a.size() != b.size()) return false;
15,107✔
38
    return std::equal(a.begin(), a.end(), b.begin(),
4,056✔
39
                      [](unsigned char ca, unsigned char cb) {
5,177✔
40
                          return std::tolower(ca) == std::tolower(cb);
5,177✔
41
                      });
2,026✔
42
}
7,556✔
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,190✔
59
    std::string_view input) {
60
    std::vector<Token> tokens;
1,190✔
61
    std::size_t pos = 0;
1,192✔
62

63
    auto skip_ws = [&]() {
8,374✔
64
        while (pos < input.size() &&
22,165✔
65
               std::isspace(static_cast<unsigned char>(input[pos]))) {
11,389✔
66
            ++pos;
4,802✔
67
        }
68
    };
4,487✔
69

70
    while (true) {
3,891✔
71
        skip_ws();
7,780!
72
        if (pos >= input.size()) break;
7,781✔
73

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

77
        // Operators
78
        if (c == '=' && pos + 1 < input.size() && input[pos + 1] == '=') {
6,593!
79
            tokens.push_back({TokenKind::OP_EQ, input.substr(start, 2), start});
1,170!
80
            pos += 2;
1,170✔
81
            continue;
1,170✔
82
        }
83
        if (c == '!' && pos + 1 < input.size() && input[pos + 1] == '=') {
5,424!
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] == '~') {
5,416!
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] == '=') {
5,406!
101
            tokens.push_back({TokenKind::OP_GE, input.substr(start, 2), start});
164!
102
            pos += 2;
164✔
103
            continue;
164✔
104
        }
105
        if (c == '<' && pos + 1 < input.size() && input[pos + 1] == '=') {
5,240✔
106
            tokens.push_back({TokenKind::OP_LE, input.substr(start, 2), start});
116!
107
            pos += 2;
116✔
108
            continue;
116✔
109
        }
110
        if (c == '>') {
5,125✔
111
            tokens.push_back({TokenKind::OP_GT, input.substr(start, 1), start});
46!
112
            ++pos;
46✔
113
            continue;
46✔
114
        }
115
        if (c == '<') {
5,079✔
116
            tokens.push_back({TokenKind::OP_LT, input.substr(start, 1), start});
4!
117
            ++pos;
4✔
118
            continue;
4✔
119
        }
120
        if (c == '~') {
5,075✔
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 == '(') {
5,049✔
135
            tokens.push_back(
10!
136
                {TokenKind::LPAREN, input.substr(start, 1), start});
10!
137
            ++pos;
10✔
138
            continue;
10✔
139
        }
140
        if (c == ')') {
5,039✔
141
            tokens.push_back(
10!
142
                {TokenKind::RPAREN, input.substr(start, 1), start});
10!
143
            ++pos;
10✔
144
            continue;
10✔
145
        }
146
        if (c == '[') {
5,029✔
147
            tokens.push_back(
104!
148
                {TokenKind::LBRACKET, input.substr(start, 1), start});
104!
149
            ++pos;
104✔
150
            continue;
104✔
151
        }
152
        if (c == ']') {
4,925✔
153
            tokens.push_back(
104!
154
                {TokenKind::RBRACKET, input.substr(start, 1), start});
104!
155
            ++pos;
104✔
156
            continue;
104✔
157
        }
158
        if (c == ',') {
4,821✔
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 == '\'') {
4,447✔
166
            char quote = c;
1,628✔
167
            ++pos;
1,628✔
168
            while (pos < input.size() && input[pos] != quote) {
10,651✔
169
                if (input[pos] == '\\' && pos + 1 < input.size()) {
9,022✔
170
                    pos += 2;
4✔
171
                } else {
2✔
172
                    ++pos;
9,019✔
173
                }
174
            }
175
            if (pos >= input.size()) {
1,625✔
176
                return dftracer::utils::unexpected(make_error(
3!
177
                    input, start, pos - start, "Unterminated string literal"));
2!
178
            }
179
            ++pos;  // consume closing quote
1,624✔
180
            tokens.push_back({TokenKind::STRING,
1,623!
181
                              input.substr(start + 1, pos - start - 2), start});
1,624!
182
            continue;
1,624✔
183
        }
812✔
184

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

208
        // Identifiers and keywords
209
        if (is_ident_start(c)) {
2,395!
210
            ++pos;
2,396✔
211
            while (pos < input.size() && is_ident_char(input[pos])) {
8,406✔
212
                ++pos;
4,812✔
213
            }
214
            auto text = input.substr(start, pos - start);
2,396!
215
            TokenKind kind = TokenKind::IDENT;
2,396✔
216
            if (iequals(text, "and"))
2,396!
217
                kind = TokenKind::KW_AND;
272✔
218
            else if (iequals(text, "or"))
2,123!
219
                kind = TokenKind::KW_OR;
231✔
220
            else if (iequals(text, "not"))
1,892!
221
                kind = TokenKind::KW_NOT;
36✔
222
            else if (iequals(text, "in"))
1,856!
223
                kind = TokenKind::KW_IN;
128✔
224
            else if (iequals(text, "like"))
1,728!
225
                kind = TokenKind::KW_LIKE;
14✔
226
            else if (iequals(text, "ilike"))
1,714!
227
                kind = TokenKind::KW_ILIKE;
10✔
228
            else if (iequals(text, "true"))
1,704!
229
                kind = TokenKind::KW_TRUE;
4✔
230
            else if (iequals(text, "false"))
1,699!
231
                kind = TokenKind::KW_FALSE;
4✔
232
            tokens.push_back({kind, text, start});
2,395!
233
            continue;
2,394✔
234
        }
1,196✔
235

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

240
    tokens.push_back(
1,188!
241
        {TokenKind::END, input.substr(input.size(), 0), input.size()});
1,188!
242
    return tokens;
1,188!
243
}
1,192✔
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)
1,733✔
334
        : tokens_(tokens), source_(source) {}
2,311✔
335

336
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_query() {
1,155✔
337
        auto result = parse_or_expr();
1,155!
338
        if (!result) return result;
1,155✔
339
        if (current().kind != TokenKind::END) {
1,143!
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,143✔
345
    }
1,155✔
346

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

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

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

356
    bool match(TokenKind kind) {
2,330✔
357
        if (current().kind == kind) {
2,330✔
358
            ++pos_;
596✔
359
            return true;
596✔
360
        }
361
        return false;
1,734✔
362
    }
1,165✔
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,166✔
371
        auto left = parse_and_expr();
1,166!
372
        if (!left) return left;
1,166✔
373
        while (current().kind == TokenKind::KW_OR) {
1,381✔
374
            advance();
226✔
375
            auto right = parse_and_expr();
225✔
376
            if (!right) return right;
226✔
377
            left = make_node(OrNode{std::move(*left), std::move(*right)});
226!
378
        }
226✔
379
        return left;
1,152✔
380
    }
1,170✔
381

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

394
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_not_expr() {
1,653✔
395
        if (current().kind == TokenKind::KW_NOT) {
1,653✔
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();
1,643✔
402
    }
826✔
403

404
    dftracer::utils::expected<QueryNodePtr, QueryError> parse_primary() {
1,644✔
405
        if (match(TokenKind::LPAREN)) {
1,644✔
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) {
1,634✔
418
            return parse_contains();
24!
419
        }
420

421
        if (current().kind != TokenKind::IDENT) {
1,610!
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();
1,610!
428

429
        // Check for "in" or "not in"
430
        if (current().kind == TokenKind::KW_IN) {
1,610✔
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,522✔
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,504!
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:
NEW
478
                advance();
×
NEW
479
                return parse_match(std::move(field), MatchOp::IREGEX, true);
×
480
            default:
729✔
481
                break;
1,458✔
482
        }
483

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

492
    FieldNode parse_field() {
1,632✔
493
        auto& tok = advance();
1,632✔
494
        return FieldNode{std::string(tok.text)};
1,632!
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,457✔
569
        auto& tok = current();
1,457✔
570
        switch (tok.kind) {
1,458✔
571
            case TokenKind::OP_EQ:
567✔
572
                advance();
1,135✔
573
                return CompareOp::EQ;
1,136!
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:
81✔
584
                advance();
162✔
585
                return CompareOp::GE;
162!
586
            case TokenKind::OP_LE:
57✔
587
                advance();
114✔
588
                return CompareOp::LE;
114!
589
            default:
1✔
590
                return dftracer::utils::unexpected(
×
591
                    error("Expected comparison operator (==, !=, >, "
×
592
                          "<, >=, <=), got '" +
×
593
                          std::string(tok.text) + "'"));
1!
594
        }
595
    }
729✔
596

597
    dftracer::utils::expected<LiteralNode, QueryError> parse_value() {
1,929✔
598
        auto& tok = current();
1,929✔
599
        switch (tok.kind) {
1,929✔
600
            case TokenKind::STRING: {
764✔
601
                auto text = std::string(tok.text);
1,529!
602
                advance();
1,530!
603
                return LiteralNode{std::move(text)};
2,293!
604
            }
1,528✔
605
            case TokenKind::INT: {
191✔
606
                int64_t val = 0;
382✔
607
                auto [ptr, ec] = std::from_chars(
382!
608
                    tok.text.data(), tok.text.data() + tok.text.size(), val);
382✔
609
                if (ec != std::errc{}) {
382!
610
                    return dftracer::utils::unexpected(error(
×
611
                        "Invalid integer: '" + std::string(tok.text) + "'"));
×
612
                }
613
                advance();
382✔
614
                if (val >= 0) {
382✔
615
                    return LiteralNode{static_cast<uint64_t>(val)};
570!
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
    }
965✔
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,154✔
672
    const std::vector<Token>& tokens, std::string_view source) {
673
    Parser parser(tokens, source);
1,154✔
674
    return parser.parse_query();
1,734!
675
}
676

677
dftracer::utils::expected<QueryNodePtr, QueryError> parse(
1,158✔
678
    std::string_view input) {
679
    auto tokens = tokenize(input);
1,158!
680
    if (!tokens) return dftracer::utils::unexpected(tokens.error());
1,156!
681
    return parse_tokens(*tokens, input);
1,156!
682
}
1,156✔
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