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

llnl-asr / dftracer-utils / 36530534461

29 Sep 2026 06:20AM UTC coverage: 57.921% (-0.08%) from 57.996%
36530534461

push

github

rayandrew
Point repository URLs at the llnl-asr GitHub org

73912 of 162805 branches covered (45.4%)

Branch coverage included in aggregate %.

65885 of 78552 relevant lines covered (83.87%)

179172.3 hits per line

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

68.16
/src/dftracer/utils/dataframe/expr.cpp
1
#include <dftracer/utils/dataframe/batch_ops.h>          // concat_columns
2
#include <dftracer/utils/dataframe/expr.h>
3
#include <dftracer/utils/dataframe/internal/cell_ops.h>  // cell_to_string
4
#include <dftracer/utils/dataframe/internal/expr_handle.h>
5
#include <dftracer/utils/dataframe/internal/fingerprint.h>
6
#include <dftracer/utils/dataframe/internal/scalar.h>    // scalar_as
7
#include <dftracer/utils/dataframe/parallel.h>
8

9
#include <algorithm>
10
#include <bit>
11
#include <cstdint>
12
#include <map>
13
#include <stdexcept>
14
#include <string>
15
#include <string_view>
16
#include <tuple>
17
#include <vector>
18

19
namespace dataframe = dftracer::utils::dataframe;
20

21
namespace dftracer::utils::dataframe {
22

23
enum class ExprKind {
24
    LitI64,
25
    LitF64,
26
    Col,
27
    Binary,
28
    Prim,
29
    Unary,
30
    Clip,
31
    Fillna,
32
    Cmp,
33
    Logical,
34
    Not,
35
    Cast,
36
    StrPred,
37
    StrMap,
38
    StrLen,
39
    StrFind,
40
    StrReplace,
41
    StrSlice,
42
    IsIn,
43
    Select,
44
    IsNull
45
};
46

47
struct ExprNode {
8,041 ✔
48
    ExprKind kind;
49
    std::int32_t i = 0;     // col index / prim / unary / cmp op / logical op /
8,041 ✔
50
                            // cast type / binary op / str op / chars / all
51
    dftu_scalar scalar{};   // literal value / cmp rhs / clip lo / slice start
8,041 ✔
52
    dftu_scalar scalar2{};  // clip hi / slice len
8,041 ✔
53
    // Owns the text of a STR-tagged `scalar`, which borrows it, or a string
54
    // op's pattern / needle / from. The node is built once and never copied
55
    // (it lives behind shared_ptr<const>), and eval_many holds the roots for
56
    // the whole evaluation, so the borrow is valid for as long as the
57
    // compiled program can read it.
58
    std::string text;
59
    std::string text2;                  // str_replace `to`
60
    Series values;                      // is_in set
61
    std::shared_ptr<const ExprNode> a;  // first child (select: cond)
62
    std::shared_ptr<const ExprNode> b;  // second child (select: then)
63
    std::shared_ptr<const ExprNode> c;  // select: otherwise
64
};
65

66
namespace {
67

68
Expr make(ExprKind k, std::int32_t i, dftu_scalar s,
14,572 ✔
69
          std::shared_ptr<const ExprNode> a,
70
          std::shared_ptr<const ExprNode> b) {
71
    auto n = std::make_shared<ExprNode>();
14,572 !
72
    n->kind = k;
14,572 ✔
73
    n->i = i;
14,572 ✔
74
    n->scalar = s;
14,572 ✔
75
    n->a = std::move(a);
14,572 ✔
76
    n->b = std::move(b);
14,572 ✔
77
    return Expr{std::move(n)};
21,858 !
78
}
14,572 ✔
79

80
}  // namespace
81

82
Expr expr_col(std::int32_t index) {
8,218 ✔
83
    return make(ExprKind::Col, index, {}, nullptr, nullptr);
8,218 !
84
}
85
std::int32_t expr_col_index(const Expr& e) {
4,132 ✔
86
    const auto& n = e.node();
4,132 ✔
87
    return n && n->kind == ExprKind::Col ? n->i : -1;
4,132 !
88
}
89

90
namespace {
91

92
void fingerprint_node(detail::Fingerprint& fp, const ExprNode* n) {
2,472 ✔
93
    if (!n) {
2,472 ✔
94
        fp.pod(std::int32_t{-1});
1,750 ✔
95
        return;
1,750 ✔
96
    }
97
    fp.pod(static_cast<std::int32_t>(n->kind));
722 ✔
98
    fp.pod(n->i);
722 ✔
99
    fp.scalar(n->scalar);
722 ✔
100
    fp.scalar(n->scalar2);
722 ✔
101
    fp.str(n->text);
722 ✔
102
    fp.str(n->text2);
722 ✔
103
    const dftu_series* vals = n->values.handle();
722 ✔
104
    fp.pod(vals ? dftu_series_data(vals) : nullptr);
722 ✔
105
    fp.pod(vals ? n->values.length() : std::int64_t{0});
722 ✔
106
    fingerprint_node(fp, n->a.get());
722 ✔
107
    fingerprint_node(fp, n->b.get());
722 ✔
108
    fingerprint_node(fp, n->c.get());
722 ✔
109
}
1,236 ✔
110

111
}  // namespace
112

113
std::uint64_t expr_fingerprint(const Expr& e) {
306 ✔
114
    detail::Fingerprint fp;
306 ✔
115
    fingerprint_node(fp, e.node().get());
306 !
116
    return fp.value();
459 ✔
117
}
118

119
bool expr_as_col_cmp(const Expr& e, std::int32_t* col, CmpOp* op, Scalar* rhs) {
132 ✔
120
    const auto& n = e.node();
132 ✔
121
    if (!n || n->kind != ExprKind::Cmp || !n->a || n->a->kind != ExprKind::Col)
132 !
122
        return false;
50 ✔
123
    *col = n->a->i;
82 ✔
124
    *op = static_cast<CmpOp>(n->i);
82 ✔
125
    *rhs = n->scalar;
82 ✔
126
    return true;
82 ✔
127
}
66 ✔
128

129
bool expr_as_col_binary(const Expr& e, BinaryOp* op, std::int32_t* a,
16 ✔
130
                        std::int32_t* b) {
131
    const auto& n = e.node();
16 ✔
132
    if (!n || n->kind != ExprKind::Binary || !n->a || !n->b ||
24 !
133
        n->a->kind != ExprKind::Col || n->b->kind != ExprKind::Col)
24 !
134
        return false;
8 ✔
135
    *op = static_cast<BinaryOp>(n->i);
8 ✔
136
    *a = n->a->i;
8 ✔
137
    *b = n->b->i;
8 ✔
138
    return true;
8 ✔
139
}
8 ✔
140

141
bool expr_as_logical(const Expr& e, LogicalOp* op, Expr* a, Expr* b) {
186 ✔
142
    const auto& n = e.node();
186 ✔
143
    if (!n || n->kind != ExprKind::Logical || !n->a || !n->b) return false;
186 !
144
    *op = static_cast<LogicalOp>(n->i);
30 ✔
145
    *a = Expr{n->a};
30 !
146
    *b = Expr{n->b};
30 !
147
    return true;
30 ✔
148
}
93 ✔
149

150
bool expr_as_not(const Expr& e, Expr* a) {
156 ✔
151
    const auto& n = e.node();
156 ✔
152
    if (!n || n->kind != ExprKind::Not || !n->a) return false;
156 !
153
    *a = Expr{n->a};
14 !
154
    return true;
14 ✔
155
}
78 ✔
156

157
namespace {
158
bool node_references(const std::shared_ptr<const ExprNode>& n,
7,870 ✔
159
                     std::int32_t index) {
160
    if (!n) return false;
7,870 ✔
161
    if (n->kind == ExprKind::Col) return n->i == index;
5,042 ✔
162
    return node_references(n->a, index) || node_references(n->b, index) ||
3,889 ✔
163
           node_references(n->c, index);
3,128 !
164
}
3,935 ✔
165
}  // namespace
166

167
bool expr_references(const Expr& e, std::int32_t index) {
1,278 ✔
168
    return node_references(e.node(), index);
1,278 ✔
169
}
170

171
namespace {
172
std::int32_t node_max_col(const std::shared_ptr<const ExprNode>& n) {
168 ✔
173
    if (!n) return -1;
168 ✔
174
    if (n->kind == ExprKind::Col) return n->i;
84 ✔
175
    return std::max(
42 ✔
176
        {node_max_col(n->a), node_max_col(n->b), node_max_col(n->c)});
42 !
177
}
84 ✔
178
}  // namespace
179

180
std::int32_t expr_max_col(const Expr& e) { return node_max_col(e.node()); }
42 ✔
181

182
namespace {
183
// Series is move-only, so a node copy shares the values set explicitly.
184
std::shared_ptr<ExprNode> clone(const ExprNode& n) {
1,494 ✔
185
    auto c = std::make_shared<ExprNode>();
1,494 ✔
186
    c->kind = n.kind;
1,494 ✔
187
    c->i = n.i;
1,494 ✔
188
    c->scalar = n.scalar;
1,494 ✔
189
    c->scalar2 = n.scalar2;
1,494 ✔
190
    c->text = n.text;
1,494 !
191
    c->text2 = n.text2;
1,494 !
192
    if (n.scalar.kind == DFTU_SCALAR_TAG_STR) {
1,494 ✔
193
        c->scalar.value.s = c->text.data();
×
194
        c->scalar.len = static_cast<std::uint32_t>(c->text.size());
×
195
    }
196
    if (n.values.valid()) c->values = n.values.share();
1,494 ✔
197
    c->a = n.a;
1,494 ✔
198
    c->b = n.b;
1,494 ✔
199
    c->c = n.c;
1,494 ✔
200
    return c;
1,494 ✔
201
}
747 !
202

203
std::shared_ptr<const ExprNode> node_remap(
908 ✔
204
    const std::shared_ptr<const ExprNode>& n,
205
    const std::vector<std::int32_t>& old_to_new) {
206
    if (!n) return nullptr;
908 ✔
207
    if (n->kind == ExprKind::Col) {
450 ✔
208
        const bool in_range =
110 ✔
209
            n->i >= 0 && static_cast<std::size_t>(n->i) < old_to_new.size();
220 !
210
        auto c = clone(*n);
220 !
211
        c->i = in_range ? old_to_new[static_cast<std::size_t>(n->i)] : n->i;
220 !
212
        return c;
220 ✔
213
    }
220 ✔
214
    auto a = node_remap(n->a, old_to_new);
230 !
215
    auto b = node_remap(n->b, old_to_new);
230 !
216
    auto c3 = node_remap(n->c, old_to_new);
230 !
217
    if (a == n->a && b == n->b && c3 == n->c) return n;  // unchanged: share
230 !
218
    auto c = clone(*n);
202 !
219
    c->a = std::move(a);
202 ✔
220
    c->b = std::move(b);
202 ✔
221
    c->c = std::move(c3);
202 ✔
222
    return c;
202 ✔
223
}
569 ✔
224
std::shared_ptr<const ExprNode> node_rebind(
4,878 ✔
225
    const std::shared_ptr<const ExprNode>& n, const std::vector<Expr>& by) {
226
    if (!n) return nullptr;
4,878 ✔
227
    if (n->kind == ExprKind::Col) {
2,046 ✔
228
        const bool in_range =
302 ✔
229
            n->i >= 0 && static_cast<std::size_t>(n->i) < by.size();
604 !
230
        return in_range && by[static_cast<std::size_t>(n->i)].valid()
604 !
231
                   ? by[static_cast<std::size_t>(n->i)].node()
604 ✔
232
                   : n;
604 !
233
    }
234
    auto a = node_rebind(n->a, by);
1,442 !
235
    auto b = node_rebind(n->b, by);
1,442 !
236
    auto c3 = node_rebind(n->c, by);
1,442 !
237
    if (a == n->a && b == n->b && c3 == n->c) return n;
1,442 !
238
    auto c = clone(*n);
1,072 !
239
    c->a = std::move(a);
1,072 ✔
240
    c->b = std::move(b);
1,072 ✔
241
    c->c = std::move(c3);
1,072 ✔
242
    return c;
1,072 ✔
243
}
3,160 ✔
244

245
void canonical_scalar(std::string& out, const dftu_scalar& v) {
96 ✔
246
    out += std::to_string(v.kind);
96 !
247
    out += ':';
96 ✔
248
    if (v.kind == DFTU_SCALAR_TAG_STR) {
96 !
249
        const std::string_view s =
250
            v.value.s ? std::string_view(v.value.s, v.len) : std::string_view();
×
251
        out += std::to_string(s.size());
×
252
        out += ':';
×
253
        out += s;
×
254
    } else {
255
        out += std::to_string(v.value.u);
96 !
256
    }
257
}
96 ✔
258

259
void canonical_text(std::string& out, std::string_view s) {
96 ✔
260
    out += std::to_string(s.size());
96 !
261
    out += ':';
96 ✔
262
    out += s;
96 ✔
263
}
96 ✔
264

265
void canonical_node(std::string& out, const ExprNode* n) {
168 ✔
266
    if (!n) {
168 ✔
267
        out += '_';
120 ✔
268
        return;
120 ✔
269
    }
270
    out += '(';
48 ✔
271
    out += std::to_string(static_cast<int>(n->kind));
48 !
272
    out += ',';
48 ✔
273
    out += std::to_string(n->i);
48 !
274
    out += ',';
48 ✔
275
    canonical_scalar(out, n->scalar);
48 ✔
276
    out += ',';
48 ✔
277
    canonical_scalar(out, n->scalar2);
48 ✔
278
    out += ',';
48 ✔
279
    canonical_text(out, n->text);
48 ✔
280
    out += ',';
48 ✔
281
    canonical_text(out, n->text2);
48 ✔
282
    out += ',';
48 ✔
283
    if (n->values.valid()) {
48 ✔
284
        out += std::to_string(static_cast<int>(n->values.type()));
×
285
        for (std::int64_t r = 0; r < n->values.length(); ++r) {
×
286
            out += ',';
×
287
            canonical_text(out, cell_to_string(n->values, r));
×
288
        }
289
    }
290
    out += ',';
48 ✔
291
    canonical_node(out, n->a.get());
48 ✔
292
    out += ',';
48 ✔
293
    canonical_node(out, n->b.get());
48 ✔
294
    out += ',';
48 ✔
295
    canonical_node(out, n->c.get());
48 ✔
296
    out += ')';
48 ✔
297
}
84 ✔
298

299
}  // namespace
300

301
Expr expr_remap_cols(const Expr& e,
218 ✔
302
                     const std::vector<std::int32_t>& old_to_new) {
303
    return Expr{node_remap(e.node(), old_to_new)};
218 !
304
}
305

306
Expr expr_rebind_cols(const Expr& e, const std::vector<Expr>& by_index) {
552 ✔
307
    return Expr{node_rebind(e.node(), by_index)};
552 !
308
}
309

310
std::string expr_canonical(const Expr& e) {
24 ✔
311
    std::string out;
24 ✔
312
    canonical_node(out, e.node().get());
24 !
313
    return out;
24 ✔
314
}
12 !
315
Expr expr_lit(std::int64_t value) {
896 ✔
316
    dftu_scalar s{};
896 ✔
317
    s.kind = DFTU_SCALAR_TAG_I64;
896 ✔
318
    s.value.i = value;
896 ✔
319
    return make(ExprKind::LitI64, 0, s, nullptr, nullptr);
896 !
320
}
321
Expr expr_lit(double value) {
304 ✔
322
    dftu_scalar s{};
304 ✔
323
    s.kind = DFTU_SCALAR_TAG_F64;
304 ✔
324
    s.value.d = value;
304 ✔
325
    return make(ExprKind::LitF64, 0, s, nullptr, nullptr);
304 !
326
}
327
Expr expr_binary(BinaryOp op, const Expr& a, const Expr& b) {
2,288 ✔
328
    return make(ExprKind::Binary, static_cast<std::int32_t>(op), {}, a.node(),
2,288 !
329
                b.node());
2,288 !
330
}
331
Expr expr_prim(PrimOp prim, const Expr& a) {
24 ✔
332
    return make(ExprKind::Prim, static_cast<std::int32_t>(prim), {}, a.node(),
24 !
333
                nullptr);
24 !
334
}
335
Expr expr_unary(UnaryOp op, const Expr& a) {
342 ✔
336
    return make(ExprKind::Unary, static_cast<std::int32_t>(op), {}, a.node(),
342 !
337
                nullptr);
342 !
338
}
339
Expr expr_clip(const Expr& a, Scalar lo, Scalar hi) {
8 ✔
340
    auto n = std::make_shared<ExprNode>();
8 !
341
    n->kind = ExprKind::Clip;
8 ✔
342
    n->scalar = lo;
8 ✔
343
    n->scalar2 = hi;
8 ✔
344
    n->a = a.node();
8 ✔
345
    return Expr{std::move(n)};
12 !
346
}
8 ✔
347
Expr expr_fillna(const Expr& a, Scalar fill) {
12 ✔
348
    return make(ExprKind::Fillna, 0, fill, a.node(), nullptr);
12 !
349
}
350
Expr expr_cmp(CmpOp cmp, const Expr& a, Scalar rhs) {
812 ✔
351
    Expr e = make(ExprKind::Cmp, static_cast<std::int32_t>(cmp), rhs, a.node(),
812 !
352
                  nullptr);
1,218 !
353
    // A STR rhs only borrows its text, and the caller's buffer may die before
354
    // the Expr is evaluated. Take a copy the node owns and repoint at it.
355
    const dftu_scalar raw = rhs;
812 ✔
356
    if (raw.kind == DFTU_SCALAR_TAG_STR) {
812 ✔
357
        auto* n = const_cast<ExprNode*>(e.node().get());
30 ✔
358
        n->text.assign(raw.value.s != nullptr ? raw.value.s : "", raw.len);
30 !
359
        n->scalar.value.s = n->text.data();
30 ✔
360
        n->scalar.len = static_cast<std::uint32_t>(n->text.size());
30 ✔
361
    }
15 ✔
362
    return e;
1,218 ✔
363
}
406 !
364
Expr expr_logical(LogicalOp op, const Expr& a, const Expr& b) {
160 ✔
365
    return make(ExprKind::Logical, static_cast<std::int32_t>(op), {}, a.node(),
160 !
366
                b.node());
160 !
367
}
368
Expr expr_not(const Expr& a) {
18 ✔
369
    return make(ExprKind::Not, 0, {}, a.node(), nullptr);
18 !
370
}
371
Expr expr_cast(TypeId type, const Expr& a) {
298 ✔
372
    return make(ExprKind::Cast, static_cast<std::int32_t>(type), {}, a.node(),
298 !
373
                nullptr);
298 !
374
}
375
Expr expr_str_pred(StrPredOp op, const Expr& a, std::string_view pattern) {
90 ✔
376
    Expr e = make(ExprKind::StrPred, static_cast<std::int32_t>(op), {},
90 !
377
                  a.node(), nullptr);
135 !
378
    const_cast<ExprNode*>(e.node().get())->text.assign(pattern);
90 !
379
    return e;
90 ✔
380
}
45 !
381
Expr expr_str_map(StrMapOp op, const Expr& a) {
358 ✔
382
    return make(ExprKind::StrMap, static_cast<std::int32_t>(op), {}, a.node(),
358 !
383
                nullptr);
358 !
384
}
385
Expr expr_lower(const Expr& a) { return expr_str_map(StrMapOp::Lower, a); }
318 ✔
386
Expr expr_str_len(const Expr& a, bool chars) {
20 ✔
387
    return make(ExprKind::StrLen, chars ? 1 : 0, {}, a.node(), nullptr);
20 !
388
}
389
Expr expr_str_find(const Expr& a, std::string_view needle) {
8 ✔
390
    Expr e = make(ExprKind::StrFind, 0, {}, a.node(), nullptr);
12 !
391
    const_cast<ExprNode*>(e.node().get())->text.assign(needle);
8 !
392
    return e;
8 ✔
393
}
4 !
394
Expr expr_str_replace(const Expr& a, std::string_view from, std::string_view to,
14 ✔
395
                      bool all) {
396
    Expr e = make(ExprKind::StrReplace, all ? 1 : 0, {}, a.node(), nullptr);
21 !
397
    auto* n = const_cast<ExprNode*>(e.node().get());
14 ✔
398
    n->text.assign(from);
14 !
399
    n->text2.assign(to);
14 !
400
    return e;
14 ✔
401
}
7 !
402
Expr expr_str_slice(const Expr& a, std::int64_t start, std::int64_t len) {
8 ✔
403
    auto n = std::make_shared<ExprNode>();
8 !
404
    n->kind = ExprKind::StrSlice;
8 ✔
405
    n->scalar.kind = DFTU_SCALAR_TAG_I64;
8 ✔
406
    n->scalar.value.i = start;
8 ✔
407
    n->scalar2.kind = DFTU_SCALAR_TAG_I64;
8 ✔
408
    n->scalar2.value.i = len;
8 ✔
409
    n->a = a.node();
8 ✔
410
    return Expr{std::move(n)};
12 !
411
}
8 ✔
412
Expr expr_is_in(const Expr& a, Series values) {
116 ✔
413
    Expr e = make(ExprKind::IsIn, 0, {}, a.node(), nullptr);
174 !
414
    const_cast<ExprNode*>(e.node().get())->values = std::move(values);
116 ✔
415
    return e;
116 ✔
416
}
58 !
417
Expr expr_is_null(const Expr& a, bool null) {
514 ✔
418
    return make(ExprKind::IsNull, null ? 1 : 0, {}, a.node(), nullptr);
514 !
419
}
420
Expr expr_select(const Expr& cond, const Expr& a, const Expr& b) {
80 ✔
421
    Expr e = make(ExprKind::Select, 0, {}, cond.node(), a.node());
120 !
422
    const_cast<ExprNode*>(e.node().get())->c = b.node();
80 ✔
423
    return e;
80 ✔
424
}
40 !
425

426
bool expr_as_col_str_pred(const Expr& e, std::int32_t* col, StrPredOp* op,
148 ✔
427
                          std::string_view* pattern) {
428
    const auto& n = e.node();
148 ✔
429
    if (!n || n->kind != ExprKind::StrPred || !n->a ||
167 !
430
        n->a->kind != ExprKind::Col)
38 !
431
        return false;
110 ✔
432
    *col = n->a->i;
38 ✔
433
    *op = static_cast<StrPredOp>(n->i);
38 ✔
434
    *pattern = n->text;
38 ✔
435
    return true;
38 ✔
436
}
74 ✔
437

438
bool expr_as_col_is_in(const Expr& e, std::int32_t* col, Series* values) {
118 ✔
439
    const auto& n = e.node();
118 ✔
440
    if (!n || n->kind != ExprKind::IsIn || !n->a || n->a->kind != ExprKind::Col)
118 !
441
        return false;
106 ✔
442
    *col = n->a->i;
12 ✔
443
    *values = n->values.share();
12 ✔
444
    return true;
12 ✔
445
}
59 ✔
446

447
// ---- compiler: type inference + CSE + lowering to a slot program ----------
448

449
namespace {
450

451
// Slot-IR opcodes.
452
enum {
453
    OP_LOAD,
454
    OP_ADD,
455
    OP_SUB,
456
    OP_MUL,
457
    OP_DIV,
458
    OP_ADDS,
459
    OP_SUBS,
460
    OP_MULS,
461
    OP_DIVS,
462
    OP_PRIM,
463
    OP_UNARY,
464
    OP_CLIP,
465
    OP_FILLNA,
466
    OP_CMP,
467
    OP_LOGICAL,
468
    OP_NOT,
469
    OP_CAST,
470
    OP_STR_PRED,
471
    OP_STR_MAP,
472
    OP_STR_LEN,
473
    OP_STR_FIND,
474
    OP_STR_REPLACE,
475
    OP_STR_SLICE,
476
    OP_IS_IN,
477
    OP_CONST,   // a column of `param` type filled with `scalar`
478
    OP_SELECT,  // c ? a : b, with the mask in slot `c`
479
    OP_IS_NULL  // the null mask of slot `a` (param 1) or the valid mask (0)
480
};
481

482
struct SlotOp {
18,598 ✔
483
    int opcode = 0;
9,299 ✔
484
    int a = -1;
9,299 ✔
485
    int b = -1;
9,299 ✔
486
    int c = -1;
9,299 ✔
487
    std::int32_t param = 0;
9,299 ✔
488
    dftu_scalar scalar{};
9,299 ✔
489
    dftu_scalar scalar2{};  // clip hi / slice len
9,299 ✔
490
    // Borrowed from the ExprNode that emitted the op, which outlives the
491
    // program (eval_many holds the roots).
492
    std::string_view text;
493
    std::string_view text2;
494
    const dftu_series* values = nullptr;
9,299 ✔
495
};
496

497
const int COL_OP[4] = {OP_ADD, OP_SUB, OP_MUL, OP_DIV};
498
const int SCALAR_OP[4] = {OP_ADDS, OP_SUBS, OP_MULS, OP_DIVS};
499

500
// A compiled subexpression: either a compile-time scalar or a column in slot
501
// `slot` of type `type`. `full` is the complete DataType: a bare column
502
// reference passes its input DataType through, every other node reports a
503
// scalar DataType (no kernel here can target a parameterized type).
504
struct Val {
505
    bool is_scalar;
506
    int slot;
507
    TypeId type;
508
    dftu_scalar scalar;
509
    DataType full;
510
};
511

512
bool is_float(const Val& v) {
8,370 ✔
513
    if (v.is_scalar) return v.scalar.kind == DFTU_SCALAR_TAG_F64;
8,370 ✔
514
    return v.type == TypeId::Float32 || v.type == TypeId::Float64;
6,930 ✔
515
}
4,185 ✔
516

517
bool is_unknown(const Val& v) { return v.full.id == TypeId::Unknown; }
19,084 ✔
518
Val unknown_val() {
4,698 ✔
519
    return {false, -1, TypeId::Unknown, {}, scalar(TypeId::Unknown)};
4,698 ✔
520
}
521

522
double scalar_to_double(dftu_scalar s) {
970 ✔
523
    if (s.kind == DFTU_SCALAR_TAG_I64) return static_cast<double>(s.value.i);
970 ✔
524
    if (s.kind == DFTU_SCALAR_TAG_U64) return static_cast<double>(s.value.u);
304 !
525
    return s.value.d;
304 ✔
526
}
485 ✔
527

528
dftu_scalar to_f64_scalar(dftu_scalar s) {
970 ✔
529
    dftu_scalar r{};
970 ✔
530
    r.kind = DFTU_SCALAR_TAG_F64;
970 ✔
531
    r.value.d = scalar_to_double(s);
970 ✔
532
    return r;
970 ✔
533
}
534

535
// Depends only on each input column's DataType, never its data, so the same
536
// compile() drives eval() (Series::data_type()) and infer_type() (a schema
537
// with no data) and the two can never disagree.
538
class Compiler {
539
   public:
540
    explicit Compiler(const std::vector<DataType>& input_types)
15,632 ✔
541
        : input_types_(input_types) {}
15,632 ✔
542

543
    Val compile(const ExprNode* n) {
31,398 ✔
544
        switch (n->kind) {
31,398 !
545
            case ExprKind::LitI64:
2,379 ✔
546
            case ExprKind::LitF64:
547
                return {true, -1, TypeId::Int64, n->scalar,
4,758 ✔
548
                        scalar(TypeId::Int64)};
4,758 ✔
549
            case ExprKind::Col: {
6,361 ✔
550
                if (n->i < 0 ||
19,083 ✔
551
                    static_cast<std::size_t>(n->i) >= input_types_.size())
12,721 ✔
552
                    throw std::invalid_argument(
7 !
553
                        "expr: column index out of range");
2 ✔
554
                const DataType& dt =
6,360 ✔
555
                    input_types_[static_cast<std::size_t>(n->i)];
12,720 ✔
556
                if (dt.id == TypeId::Unknown)
12,720 ✔
557
                    return {false, -1, TypeId::Unknown, {}, dt};
4,552 ✔
558
                int slot = emit(OP_LOAD, -1, -1, n->i, {});
8,168 ✔
559
                return {false, slot, dt.id, {}, dt};
8,168 ✔
560
            }
561
            case ExprKind::Binary:
4,465 ✔
562
                return compile_binary(n);
8,930 ✔
563
            case ExprKind::Prim: {
18 ✔
564
                Val a0 = compile(n->a.get());
36 !
565
                if (is_unknown(a0)) return unknown_val();
36 !
566
                Val a = as_col(a0, "prim");
36 !
567
                if (a.type != TypeId::Int64 && a.type != TypeId::Uint64)
36 !
568
                    a = cast(a, TypeId::Int64);
×
569
                return {false,
18 ✔
570
                        emit(OP_PRIM, a.slot, -1, n->i, {}),
54 !
571
                        TypeId::Int64,
572
                        {},
18 ✔
573
                        scalar(TypeId::Int64)};
36 !
574
            }
36 ✔
575
            case ExprKind::Unary: {
674 ✔
576
                Val a0 = compile(n->a.get());
1,348 !
577
                if (is_unknown(a0)) return unknown_val();
1,348 !
578
                Val a = as_col(a0, "unary");
364 !
579
                // is_nan/is_finite/is_infinite yield a Bool mask; log/sqrt/exp
580
                // widen to Float64; the rest keep the input type (integer
581
                // floor/ceil/round/trunc are identities).
582
                const auto op = static_cast<UnaryOp>(n->i);
364 ✔
583
                TypeId t;
584
                switch (op) {
182 ✔
585
                    case UnaryOp::IsNan:
4 ✔
586
                    case UnaryOp::IsFinite:
587
                    case UnaryOp::IsInfinite:
588
                        t = TypeId::Bool;
8 ✔
589
                        break;
8 ✔
590
                    case UnaryOp::Log:
10 ✔
591
                    case UnaryOp::Sqrt:
592
                    case UnaryOp::Exp:
593
                        t = TypeId::Float64;
20 ✔
594
                        break;
20 ✔
595
                    default:
168 ✔
596
                        t = a.type;
336 ✔
597
                        break;
336 ✔
598
                }
599
                return {false,
182 ✔
600
                        emit(OP_UNARY, a.slot, -1, n->i, {}),
546 !
601
                        t,
182 ✔
602
                        {},
182 ✔
603
                        scalar(t)};
364 !
604
            }
1,348 ✔
605
            case ExprKind::Clip: {
4 ✔
606
                Val a0 = compile(n->a.get());
8 !
607
                if (is_unknown(a0)) return unknown_val();
8 !
608
                Val a = as_col(a0, "clip");
8 !
609
                return {false,
4 ✔
610
                        emit(OP_CLIP, a.slot, -1, 0, n->scalar, n->scalar2),
12 !
611
                        a.type,
8 ✔
612
                        {},
4 ✔
613
                        scalar(a.type)};
8 !
614
            }
8 ✔
615
            case ExprKind::Fillna: {
6 ✔
616
                Val a0 = compile(n->a.get());
12 !
617
                if (is_unknown(a0)) return unknown_val();
12 !
618
                Val a = as_col(a0, "fillna");
12 !
619
                return {false,
6 ✔
620
                        emit(OP_FILLNA, a.slot, -1, 0, n->scalar),
18 !
621
                        a.type,
12 ✔
622
                        {},
6 ✔
623
                        scalar(a.type)};
12 !
624
            }
12 ✔
625
            case ExprKind::Cmp: {
548 ✔
626
                Val a0 = compile(n->a.get());
1,096 !
627
                if (is_unknown(a0)) return unknown_val();
1,096 !
628
                Val a = as_col(a0, "compare");
1,094 !
629
                return {false,
547 ✔
630
                        emit(OP_CMP, a.slot, -1, n->i, n->scalar),
1,641 !
631
                        TypeId::Bool,
632
                        {},
547 ✔
633
                        scalar(TypeId::Bool)};
1,094 !
634
            }
1,096 ✔
635
            case ExprKind::Logical: {
120 ✔
636
                Val a0 = compile(n->a.get());
240 !
637
                Val b0 = compile(n->b.get());
240 !
638
                if (is_unknown(a0) || is_unknown(b0)) return unknown_val();
240 !
639
                Val a = as_col(a0, "logical");
240 !
640
                Val b = as_col(b0, "logical");
240 !
641
                return {false,
120 ✔
642
                        emit(OP_LOGICAL, a.slot, b.slot, n->i, {}),
360 !
643
                        TypeId::Bool,
644
                        {},
120 ✔
645
                        scalar(TypeId::Bool)};
240 !
646
            }
240 ✔
647
            case ExprKind::Not: {
7 ✔
648
                Val a0 = compile(n->a.get());
14 !
649
                if (is_unknown(a0)) return unknown_val();
14 !
650
                Val a = as_col(a0, "not");
14 !
651
                return {false,
7 ✔
652
                        emit(OP_NOT, a.slot, -1, 0, {}),
21 !
653
                        TypeId::Bool,
654
                        {},
7 ✔
655
                        scalar(TypeId::Bool)};
14 !
656
            }
14 ✔
657
            case ExprKind::Cast: {
394 ✔
658
                Val a = as_col(compile(n->a.get()), "cast");
788 !
659
                return cast(a, static_cast<TypeId>(n->i));
788 !
660
            }
788 ✔
661
            case ExprKind::StrPred: {
46 ✔
662
                Val a0 = compile(n->a.get());
92 !
663
                if (is_unknown(a0)) return unknown_val();
92 !
664
                Val a = as_str(a0, "string predicate", true);
93 !
665
                return {false,
45 ✔
666
                        emit_text(OP_STR_PRED, a.slot, n->i, n->text),
90 !
667
                        TypeId::Bool,
668
                        {},
45 ✔
669
                        scalar(TypeId::Bool)};
135 ✔
670
            }
92 ✔
671
            case ExprKind::StrMap: {
203 ✔
672
                Val a0 = compile(n->a.get());
406 !
673
                if (is_unknown(a0)) return unknown_val();
406 !
674
                Val a = as_str(a0, "string map", false);
406 !
675
                return {false,
200 ✔
676
                        emit(OP_STR_MAP, a.slot, -1, n->i, {}),
600 !
677
                        TypeId::String,
678
                        {},
200 ✔
679
                        scalar(TypeId::String)};
400 !
680
            }
406 ✔
681
            case ExprKind::StrLen: {
12 ✔
682
                Val a0 = compile(n->a.get());
24 !
683
                if (is_unknown(a0)) return unknown_val();
24 !
684
                Val a = as_str(a0, "string length", true);
22 !
685
                return {false,
11 ✔
686
                        emit(OP_STR_LEN, a.slot, -1, n->i, {}),
33 !
687
                        TypeId::Int64,
688
                        {},
11 ✔
689
                        scalar(TypeId::Int64)};
22 !
690
            }
24 ✔
691
            case ExprKind::StrFind: {
5 ✔
692
                Val a0 = compile(n->a.get());
10 !
693
                if (is_unknown(a0)) return unknown_val();
10 !
694
                Val a = as_str(a0, "string find", true);
10 !
695
                return {false,
5 ✔
696
                        emit_text(OP_STR_FIND, a.slot, 0, n->text),
10 !
697
                        TypeId::Int64,
698
                        {},
5 ✔
699
                        scalar(TypeId::Int64)};
15 ✔
700
            }
10 ✔
701
            case ExprKind::StrReplace: {
8 ✔
702
                Val a0 = compile(n->a.get());
16 !
703
                if (is_unknown(a0)) return unknown_val();
16 !
704
                Val a = as_str(a0, "string replace", false);
16 !
705
                return {
8 ✔
706
                    false,
707
                    emit_text(OP_STR_REPLACE, a.slot, n->i, n->text, n->text2),
16 !
708
                    TypeId::String,
709
                    {},
8 ✔
710
                    scalar(TypeId::String)};
24 ✔
711
            }
16 ✔
712
            case ExprKind::StrSlice: {
5 ✔
713
                Val a0 = compile(n->a.get());
10 !
714
                if (is_unknown(a0)) return unknown_val();
10 !
715
                Val a = as_str(a0, "string slice", false);
10 !
716
                return {
5 ✔
717
                    false,
718
                    emit(OP_STR_SLICE, a.slot, -1, 0, n->scalar, n->scalar2),
15 !
719
                    TypeId::String,
720
                    {},
5 ✔
721
                    scalar(TypeId::String)};
10 !
722
            }
10 ✔
723
            case ExprKind::IsIn: {
18 ✔
724
                Val a0 = compile(n->a.get());
36 !
725
                if (is_unknown(a0)) return unknown_val();
36 !
726
                Val a = as_col(a0, "is_in");
36 !
727
                if (!n->values.valid())
36 ✔
728
                    throw std::invalid_argument(
×
729
                        "expr: is_in needs a values column");
730
                if (value_domain(a.type) != value_domain(n->values.type()))
36 ✔
731
                    throw std::invalid_argument(
6 !
732
                        std::string("expr: is_in over a ") + type_name(a.type) +
6 !
733
                        " column needs " + type_name(a.type) + " values, got " +
8 !
734
                        type_name(n->values.type()));
6 ✔
735
                SlotOp op;
32 ✔
736
                op.opcode = OP_IS_IN;
32 ✔
737
                op.a = a.slot;
32 ✔
738
                op.values = n->values.handle();
32 ✔
739
                return {
16 ✔
740
                    false, emit_op(op), TypeId::Bool, {}, scalar(TypeId::Bool)};
32 !
741
            }
38 ✔
742
            case ExprKind::Select:
121 ✔
743
                return compile_select(n);
242 ✔
744
            case ExprKind::IsNull: {
305 ✔
745
                Val a0 = compile(n->a.get());
610 !
746
                if (is_unknown(a0)) return unknown_val();
610 !
747
                Val a = as_col(a0, "is_null");
610 !
748
                return {false,
305 ✔
749
                        emit(OP_IS_NULL, a.slot, -1, n->i, {}),
915 !
750
                        TypeId::Bool,
751
                        {},
305 ✔
752
                        scalar(TypeId::Bool)};
610 !
753
            }
610 ✔
754
        }
755
        throw std::invalid_argument("expr: unknown node");
×
756
    }
15,698 ✔
757

758
    std::vector<SlotOp> program;
759

760
   private:
761
    Val as_col(Val v, const char* who) {
4,238 ✔
762
        if (v.is_scalar)
4,238 ✔
763
            throw std::invalid_argument(std::string("expr: ") + who +
×
764
                                        " needs a column operand");
765
        return v;
4,238 ✔
766
    }
767

768
    // A string operand: String always; Binary too when `binary_ok` (the
769
    // predicate / length / find kernels read bytes, the maps produce text).
770
    Val as_str(Val v, const char* who, bool binary_ok) {
554 ✔
771
        Val a = as_col(v, who);
557 !
772
        const bool ok =
554 ✔
773
            a.type == TypeId::String || (binary_ok && a.type == TypeId::Binary);
554 !
774
        if (!ok)
554 ✔
775
            throw std::invalid_argument(std::string("expr: ") + who +
15 !
776
                                        " needs a String" +
12 !
777
                                        (binary_ok ? " or Binary" : "") +
9 !
778
                                        " column, got " + type_name(a.type));
15 !
779
        return a;
548 ✔
780
    }
283 !
781

782
    Val cast(Val v, TypeId t) {
1,822 ✔
783
        return {false,
4,555 ✔
784
                emit(OP_CAST, v.slot, -1, static_cast<std::int32_t>(t), {}),
2,733 ✔
785
                t,
911 ✔
786
                {},
911 ✔
787
                scalar(t)};
1,822 ✔
788
    }
789

790
    Val compile_binary(const ExprNode* n) {
8,930 ✔
791
        const int op = n->i;  // BinaryOp
8,930 ✔
792
        Val a = compile(n->a.get());
8,930 !
793
        Val b = compile(n->b.get());
8,930 !
794
        if (is_unknown(a) || is_unknown(b)) return unknown_val();
8,930 !
795
        if (a.is_scalar && b.is_scalar) return fold(op, a.scalar, b.scalar);
5,222 !
796

797
        const bool rf =
2,611 ✔
798
            op == static_cast<int>(BinaryOp::Div) || is_float(a) || is_float(b);
5,222 !
799
        TypeId out_type;
800
        if (rf) {
5,222 ✔
801
            promote_float(a);
2,824 !
802
            promote_float(b);
2,824 !
803
            out_type = TypeId::Float64;
2,824 ✔
804
        } else if (!a.is_scalar && !b.is_scalar && a.type != b.type) {
3,810 !
805
            a = cast(a, TypeId::Int64);
×
806
            b = cast(b, TypeId::Int64);
×
807
            out_type = TypeId::Int64;
×
808
        } else {
809
            out_type = a.is_scalar ? b.type : a.type;
2,398 ✔
810
        }
811

812
        if (!a.is_scalar && !b.is_scalar)
5,222 ✔
813
            return {false,
1,563 ✔
814
                    emit(COL_OP[op], a.slot, b.slot, 0, {}),
4,689 !
815
                    out_type,
1,563 ✔
816
                    {},
1,563 ✔
817
                    scalar(out_type)};
3,126 !
818
        if (!a.is_scalar)  // col op scalar
2,096 ✔
819
            return {false,
1,046 ✔
820
                    emit(SCALAR_OP[op], a.slot, -1, 0, b.scalar),
3,138 !
821
                    out_type,
1,046 ✔
822
                    {},
1,046 ✔
823
                    scalar(out_type)};
2,092 !
824
        if (op == static_cast<int>(BinaryOp::Add) ||
4 !
825
            op == static_cast<int>(BinaryOp::Mul))  // scalar op col commutes
1 ✔
826
            return {false,
2 ✔
827
                    emit(SCALAR_OP[op], b.slot, -1, 0, a.scalar),
6 !
828
                    out_type,
2 ✔
829
                    {},
2 ✔
830
                    scalar(out_type)};
4 !
831
        throw std::invalid_argument("expr: scalar - / column has no kernel");
×
832
    }
8,930 ✔
833

834
    void promote_float(Val& v) {
5,720 ✔
835
        if (v.is_scalar) {
5,720 ✔
836
            v.scalar = to_f64_scalar(v.scalar);
962 ✔
837
        } else if (v.type != TypeId::Float32 && v.type != TypeId::Float64) {
5,239 !
838
            v = cast(v, TypeId::Float64);
1,030 !
839
        }
515 ✔
840
    }
5,720 ✔
841

842
    // A column of `t` holding `s` in every row, for a scalar arm of a select.
843
    Val broadcast(dftu_scalar s, TypeId t) {
188 ✔
844
        return {false,
470 ✔
845
                emit(OP_CONST, -1, -1, static_cast<std::int32_t>(t), s),
282 ✔
846
                t,
94 ✔
847
                {},
94 ✔
848
                scalar(t)};
188 ✔
849
    }
850

851
    // cond ? a : b. The arms promote as arithmetic does: either float makes
852
    // both Float64, two different integer types meet at Int64; a scalar arm
853
    // broadcasts into a column of the promoted type. A string arm needs a
854
    // string on the other side too.
855
    Val compile_select(const ExprNode* n) {
242 ✔
856
        Val c0 = compile(n->a.get());
242 !
857
        Val a = compile(n->b.get());
242 !
858
        Val b = compile(n->c.get());
242 !
859
        if (is_unknown(c0) || is_unknown(a) || is_unknown(b))
242 !
860
            return unknown_val();
×
861
        Val c = as_col(c0, "select");
242 !
862
        if (c.type != TypeId::Bool)
242 ✔
863
            throw std::invalid_argument(
3 !
864
                std::string("expr: select needs a Bool condition, got ") +
3 !
865
                type_name(c.type));
3 ✔
866
        if (a.is_scalar && b.is_scalar) {
240 ✔
867
            const bool f = a.scalar.kind == DFTU_SCALAR_TAG_F64 ||
13 ✔
868
                           b.scalar.kind == DFTU_SCALAR_TAG_F64;
6 !
869
            const TypeId t = f ? TypeId::Float64 : TypeId::Int64;
10 ✔
870
            if (f) {
10 ✔
871
                a.scalar = to_f64_scalar(a.scalar);
4 ✔
872
                b.scalar = to_f64_scalar(b.scalar);
4 ✔
873
            }
2 ✔
874
            a = broadcast(a.scalar, t);
10 !
875
            b = broadcast(b.scalar, t);
10 !
876
        } else {
10 ✔
877
            const bool str_arm = (!a.is_scalar && a.type == TypeId::String) ||
456 ✔
878
                                 (!b.is_scalar && b.type == TypeId::String);
226 !
879
            if (str_arm) {
230 ✔
880
                if (a.is_scalar || b.is_scalar || a.type != b.type)
4 !
881
                    throw std::invalid_argument(
×
882
                        "expr: select needs two String columns for a "
883
                        "String arm");
884
            } else if (is_float(a) || is_float(b)) {
228 ✔
885
                promote_float(a);
36 !
886
                promote_float(b);
36 !
887
                if (a.is_scalar) a = broadcast(a.scalar, TypeId::Float64);
36 !
888
                if (b.is_scalar) b = broadcast(b.scalar, TypeId::Float64);
36 !
889
            } else {
18 ✔
890
                const TypeId t = a.is_scalar        ? b.type
268 ✔
891
                                 : b.is_scalar      ? a.type
128 ✔
892
                                 : a.type == b.type ? a.type
50 ✔
893
                                                    : TypeId::Int64;
894
                if (!a.is_scalar && a.type != t) a = cast(a, t);
190 !
895
                if (!b.is_scalar && b.type != t) b = cast(b, t);
190 !
896
                if (a.is_scalar) a = broadcast(a.scalar, t);
190 !
897
                if (b.is_scalar) b = broadcast(b.scalar, t);
190 !
898
            }
899
        }
900
        SlotOp op;
240 ✔
901
        op.opcode = OP_SELECT;
240 ✔
902
        op.a = a.slot;
240 ✔
903
        op.b = b.slot;
240 ✔
904
        op.c = c.slot;
240 ✔
905
        return {false, emit_op(op), a.type, {}, scalar(a.type)};
240 !
906
    }
246 ✔
907

908
    Val fold(int op, dftu_scalar a, dftu_scalar b) {
×
909
        const bool f = op == static_cast<int>(BinaryOp::Div) ||
×
910
                       a.kind == DFTU_SCALAR_TAG_F64 ||
×
911
                       b.kind == DFTU_SCALAR_TAG_F64;
×
912
        dftu_scalar r{};
×
913
        if (f) {
×
914
            double x = scalar_to_double(a), y = scalar_to_double(b);
×
915
            r.kind = DFTU_SCALAR_TAG_F64;
×
916
            r.value.d = op == 0   ? x + y
×
917
                        : op == 1 ? x - y
×
918
                        : op == 2 ? x * y
×
919
                                  : x / y;
920
        } else {
921
            std::int64_t x = a.value.i, y = b.value.i;
×
922
            r.kind = DFTU_SCALAR_TAG_I64;
×
923
            r.value.i = op == 0   ? x + y
×
924
                        : op == 1 ? x - y
×
925
                        : op == 2 ? x * y
×
926
                                  : (y != 0 ? x / y : 0);
×
927
        }
928
        return {true, -1, TypeId::Int64, r,
929
                scalar(f ? TypeId::Float64 : TypeId::Int64)};
×
930
    }
931

932
    // Emit an op, hash-consing structurally identical ops to the same slot
933
    // (common-subexpression elimination).
934
    int emit(int opcode, int a, int b, std::int32_t param, dftu_scalar s,
18,210 ✔
935
             dftu_scalar s2 = {}) {
936
        SlotOp op;
18,210 ✔
937
        op.opcode = opcode;
18,210 ✔
938
        op.a = a;
18,210 ✔
939
        op.b = b;
18,210 ✔
940
        op.param = param;
18,210 ✔
941
        op.scalar = s;
18,210 ✔
942
        op.scalar2 = s2;
18,210 ✔
943
        return emit_op(op);
27,315 !
944
    }
945

946
    int emit_text(int opcode, int a, std::int32_t param, std::string_view text,
116 ✔
947
                  std::string_view text2 = {}) {
948
        SlotOp op;
116 ✔
949
        op.opcode = opcode;
116 ✔
950
        op.a = a;
116 ✔
951
        op.param = param;
116 ✔
952
        op.text = text;
116 ✔
953
        op.text2 = text2;
116 ✔
954
        return emit_op(op);
174 !
955
    }
956

957
    int emit_op(const SlotOp& op) {
18,598 ✔
958
        auto bits = [](dftu_scalar x) {
46,495 ✔
959
            return x.kind == DFTU_SCALAR_TAG_F64
37,196 ✔
960
                       ? std::bit_cast<std::int64_t>(x.value.d)
19,087 ✔
961
                       : x.value.i;
36,707 ✔
962
        };
963
        // len is part of the key: a STR scalar's `bits` is its pointer, and
964
        // two scalars can share a pointer with different lengths (a prefix),
965
        // which would otherwise hash-cons to the same slot. A text operand is
966
        // keyed by value, a values set by identity.
967
        auto key = std::make_tuple(
9,299 !
968
            op.opcode, op.a, op.b, op.c, static_cast<int>(op.param),
9,299 ✔
969
            static_cast<int>(op.scalar.kind), bits(op.scalar), bits(op.scalar2),
27,897 ✔
970
            op.scalar.len, op.scalar2.len, std::string(op.text),
27,897 !
971
            std::string(op.text2), reinterpret_cast<std::uintptr_t>(op.values));
37,196 !
972
        auto it = memo_.find(key);
18,598 !
973
        if (it != memo_.end()) return it->second;
18,598 !
974
        int slot = static_cast<int>(program.size());
17,004 ✔
975
        program.push_back(op);
17,004 !
976
        memo_.emplace(key, slot);
17,004 !
977
        return slot;
17,004 ✔
978
    }
18,598 ✔
979

980
    const std::vector<DataType>& input_types_;
981
    std::map<std::tuple<int, int, int, int, int, int, std::int64_t,
982
                        std::int64_t, std::uint32_t, std::uint32_t, std::string,
983
                        std::string, std::uintptr_t>,
984
             int>
985
        memo_;
986
};
987

988
// dftu_series_slice is FLAT-fixed-width only, so it returns null for
989
// String/Binary/List; take()'s gather_column handles those instead.
990
Series load_slice(const Series& in, std::int64_t offset, std::int64_t len) {
3,703 ✔
991
    const TypeId t = in.type();
3,703 ✔
992
    if (byte_width(t, in.data_type().fixed_size)) return in.slice(offset, len);
3,703 ✔
993
    std::vector<std::int64_t> idx(static_cast<std::size_t>(len));
852 !
994
    for (std::int64_t i = 0; i < len; ++i)
46,922 ✔
995
        idx[static_cast<std::size_t>(i)] = offset + i;
46,070 ✔
996
    return in.take(idx);
852 !
997
}
2,278 ✔
998

999
template <class T>
1000
Series const_fixed(TypeId t, const dftu_scalar& s, std::int64_t len) {
58 ✔
1001
    std::vector<T> v(static_cast<std::size_t>(len), scalar_as<T>(s));
58 !
1002
    return Series::flat(t, v.data(), len);
87 !
1003
}
58 ✔
1004

1005
// A column of `len` rows of type `t`, every row holding `s` (a select arm
1006
// given as a literal). Refuses a type with no scalar form rather than build a
1007
// column whose contents would be a guess.
1008
Series const_column(TypeId t, const dftu_scalar& s, std::int64_t len) {
58 ✔
1009
    switch (t) {
58 !
1010
        case TypeId::Bool: {
1011
            const bool on = scalar_as<std::int64_t>(s) != 0;
×
1012
            std::vector<std::uint8_t> bits(buffer_bytes(TypeId::Bool, len),
1013
                                           on ? 0xFF : 0x00);
×
1014
            return Series::flat(t, bits.data(), len);
×
1015
        }
×
1016
        case TypeId::Int8:
1017
            return const_fixed<std::int8_t>(t, s, len);
×
1018
        case TypeId::Int16:
1019
            return const_fixed<std::int16_t>(t, s, len);
×
1020
        case TypeId::Int32:
1021
            return const_fixed<std::int32_t>(t, s, len);
×
1022
        case TypeId::Int64:
21 ✔
1023
            return const_fixed<std::int64_t>(t, s, len);
42 ✔
1024
        case TypeId::Uint8:
1025
            return const_fixed<std::uint8_t>(t, s, len);
×
1026
        case TypeId::Uint16:
1027
            return const_fixed<std::uint16_t>(t, s, len);
×
1028
        case TypeId::Uint32:
1029
            return const_fixed<std::uint32_t>(t, s, len);
×
1030
        case TypeId::Uint64:
1031
            return const_fixed<std::uint64_t>(t, s, len);
×
1032
        case TypeId::Float32:
1033
            return const_fixed<float>(t, s, len);
×
1034
        case TypeId::Float64:
8 ✔
1035
            return const_fixed<double>(t, s, len);
16 ✔
1036
        case TypeId::Unknown:
1037
        case TypeId::Float16:
1038
        case TypeId::String:
1039
        case TypeId::Binary:
1040
        case TypeId::List:
1041
        case TypeId::Struct:
1042
        case TypeId::Date32:
1043
        case TypeId::Date64:
1044
        case TypeId::Time32:
1045
        case TypeId::Time64:
1046
        case TypeId::Timestamp:
1047
        case TypeId::Duration:
1048
        case TypeId::Decimal128:
1049
        case TypeId::Decimal256:
1050
        case TypeId::FixedSizeBinary:
1051
        case TypeId::LargeString:
1052
        case TypeId::LargeBinary:
1053
        case TypeId::LargeList:
1054
        case TypeId::FixedSizeList:
1055
        case TypeId::Map:
1056
            break;
×
1057
    }
1058
    throw std::invalid_argument(
×
1059
        std::string("expr: a literal cannot broadcast to a ") + type_name(t) +
×
1060
        " column");
1061
}
29 ✔
1062

1063
// Evaluate the slot program over rows [offset, offset+len) and extract one
1064
// column per requested final slot (shared, so distinct outputs that resolved to
1065
// the same slot alias the one buffer).
1066
std::vector<Series> eval_chunk(const std::vector<SlotOp>& prog,
2,778 ✔
1067
                               const std::vector<Series>& inputs,
1068
                               const std::vector<int>& finals,
1069
                               std::int64_t offset, std::int64_t len) {
1070
    std::vector<Series> s(prog.size());
2,778 !
1071
    for (std::size_t k = 0; k < prog.size(); ++k) {
12,086 ✔
1072
        const SlotOp& op = prog[k];
9,313 ✔
1073
        auto A = [&]() { return s[static_cast<std::size_t>(op.a)].handle(); };
10,209 ✔
1074
        auto B = [&]() { return s[static_cast<std::size_t>(op.b)].handle(); };
6,045 ✔
1075
        switch (op.opcode) {
9,311 ✔
1076
            case OP_LOAD:
1,851 ✔
1077
                s[k] = load_slice(inputs[static_cast<std::size_t>(op.param)],
7,406 !
1078
                                  offset, len);
3,703 ✔
1079
                break;
3,700 ✔
1080
            case OP_ADD:
196 ✔
1081
                s[k] = Series{dftu_series_add(A(), B())};
392 !
1082
                break;
392 ✔
1083
            case OP_SUB:
309 ✔
1084
                s[k] = Series{dftu_series_sub(A(), B())};
618 !
1085
                break;
618 ✔
1086
            case OP_MUL:
55 ✔
1087
                s[k] = Series{dftu_series_mul(A(), B())};
110 !
1088
                break;
110 ✔
1089
            case OP_DIV:
14 ✔
1090
                s[k] = Series{dftu_series_div(A(), B())};
28 !
1091
                break;
28 ✔
1092
            case OP_ADDS:
70 ✔
1093
                s[k] = Series{dftu_series_add_scalar(A(), op.scalar)};
140 !
1094
                break;
140 ✔
1095
            case OP_SUBS:
204 ✔
1096
                s[k] = Series{dftu_series_sub_scalar(A(), op.scalar)};
408 !
1097
                break;
408 ✔
1098
            case OP_MULS:
249 ✔
1099
                s[k] = Series{dftu_series_mul_scalar(A(), op.scalar)};
498 !
1100
                break;
498 ✔
1101
            case OP_DIVS:
53 ✔
1102
                s[k] = Series{dftu_series_div_scalar(A(), op.scalar)};
106 !
1103
                break;
106 ✔
1104
            case OP_PRIM:
12 ✔
1105
                s[k] = Series{
36 !
1106
                    dftu_series_prim(A(), static_cast<dftu_prim_op>(op.param))};
36 !
1107
                break;
24 ✔
1108
            case OP_UNARY: {
171 ✔
1109
                dftu_series* r = nullptr;
342 ✔
1110
                switch (static_cast<UnaryOp>(op.param)) {
342 !
1111
                    case UnaryOp::Abs:
8 ✔
1112
                        r = dftu_series_abs(A());
16 !
1113
                        break;
16 ✔
1114
                    case UnaryOp::Round:
1 ✔
1115
                        r = dftu_series_round(A());
2 !
1116
                        break;
2 ✔
1117
                    case UnaryOp::Floor:
91 ✔
1118
                        r = dftu_series_floor(A());
182 !
1119
                        break;
182 ✔
1120
                    case UnaryOp::Ceil:
54 ✔
1121
                        r = dftu_series_ceil(A());
108 !
1122
                        break;
108 ✔
1123
                    case UnaryOp::Log:
1 ✔
1124
                        r = dftu_series_log(A());
2 !
1125
                        break;
2 ✔
1126
                    case UnaryOp::Sqrt:
6 ✔
1127
                        r = dftu_series_sqrt(A());
12 !
1128
                        break;
12 ✔
1129
                    case UnaryOp::Exp:
1 ✔
1130
                        r = dftu_series_exp(A());
2 !
1131
                        break;
2 ✔
1132
                    case UnaryOp::Sign:
2 ✔
1133
                        r = dftu_series_sign(A());
4 !
1134
                        break;
4 ✔
1135
                    case UnaryOp::Negate:
2 ✔
1136
                        r = dftu_series_negate(A());
4 !
1137
                        break;
4 ✔
1138
                    case UnaryOp::Trunc:
2 ✔
1139
                        r = dftu_series_trunc(A());
4 !
1140
                        break;
4 ✔
1141
                    case UnaryOp::IsNan:
2 ✔
1142
                        r = dftu_series_is_nan(A());
4 !
1143
                        break;
4 ✔
1144
                    case UnaryOp::IsFinite:
1 ✔
1145
                        r = dftu_series_is_finite(A());
2 !
1146
                        break;
2 ✔
1147
                    case UnaryOp::IsInfinite:
1148
                        r = dftu_series_is_infinite(A());
×
1149
                        break;
×
1150
                }
1151
                s[k] = Series{r};
342 ✔
1152
                break;
342 ✔
1153
            }
1154
            case OP_CLIP:
3 ✔
1155
                s[k] = Series{dftu_series_clip(A(), op.scalar, op.scalar2)};
6 !
1156
                break;
6 ✔
1157
            case OP_FILLNA:
5 ✔
1158
                s[k] = Series{dftu_series_fillna(A(), op.scalar)};
10 !
1159
                break;
10 ✔
1160
            case OP_CMP:
405 ✔
1161
                s[k] = Series{dftu_series_compare(
1,215 !
1162
                    A(), static_cast<dftu_cmp_op>(op.param), op.scalar)};
1,215 !
1163
                break;
810 ✔
1164
            case OP_LOGICAL:
81 ✔
1165
                s[k] = Series{dftu_series_logical(
243 !
1166
                    A(), B(), static_cast<dftu_logical_op>(op.param))};
243 !
1167
                break;
162 ✔
1168
            case OP_NOT:
6 ✔
1169
                s[k] = Series{dftu_series_logical_not(A())};
12 !
1170
                break;
12 ✔
1171
            case OP_CAST:
387 ✔
1172
                s[k] = Series{
1,161 !
1173
                    dftu_series_cast(A(), static_cast<dftu_dtype>(op.param))};
1,161 !
1174
                break;
774 ✔
1175
            case OP_STR_PRED: {
38 ✔
1176
                const char* p = op.text.data();
76 ✔
1177
                const auto n = static_cast<std::int32_t>(op.text.size());
76 ✔
1178
                dftu_series* r = nullptr;
76 ✔
1179
                switch (static_cast<StrPredOp>(op.param)) {
76 !
1180
                    case StrPredOp::Contains:
13 ✔
1181
                        r = dftu_series_str_contains(A(), p, n);
26 !
1182
                        break;
26 ✔
1183
                    case StrPredOp::StartsWith:
6 ✔
1184
                        r = dftu_series_str_starts_with(A(), p, n);
12 !
1185
                        break;
12 ✔
1186
                    case StrPredOp::EndsWith:
4 ✔
1187
                        r = dftu_series_str_ends_with(A(), p, n);
8 !
1188
                        break;
8 ✔
1189
                    case StrPredOp::Like:
5 ✔
1190
                        r = dftu_series_str_like(A(), p, n);
10 !
1191
                        break;
10 ✔
1192
                    case StrPredOp::Matches:
4 ✔
1193
                        r = dftu_series_str_matches(A(), p, n);
8 !
1194
                        break;
8 ✔
1195
                    case StrPredOp::Search:
6 ✔
1196
                        r = dftu_series_str_search(A(), p, n);
12 !
1197
                        break;
12 ✔
1198
                }
1199
                s[k] = Series{r};
76 ✔
1200
                break;
76 ✔
1201
            }
1202
            case OP_STR_MAP: {
196 ✔
1203
                dftu_series* r = nullptr;
392 ✔
1204
                switch (static_cast<StrMapOp>(op.param)) {
392 !
1205
                    case StrMapOp::Lower:
183 ✔
1206
                        r = dftu_series_to_lowercase(A());
366 !
1207
                        break;
366 ✔
1208
                    case StrMapOp::Upper:
6 ✔
1209
                        r = dftu_series_to_uppercase(A());
12 !
1210
                        break;
12 ✔
1211
                    case StrMapOp::Strip:
3 ✔
1212
                        r = dftu_series_str_strip(A());
6 !
1213
                        break;
6 ✔
1214
                    case StrMapOp::Lstrip:
2 ✔
1215
                        r = dftu_series_str_lstrip(A());
4 !
1216
                        break;
4 ✔
1217
                    case StrMapOp::Rstrip:
2 ✔
1218
                        r = dftu_series_str_rstrip(A());
4 !
1219
                        break;
4 ✔
1220
                }
1221
                s[k] = Series{r};
392 ✔
1222
                break;
392 ✔
1223
            }
1224
            case OP_STR_LEN:
9 ✔
1225
                s[k] = Series{op.param ? dftu_series_str_len_chars(A())
37 !
1226
                                       : dftu_series_str_len_bytes(A())};
19 !
1227
                break;
18 ✔
1228
            case OP_STR_FIND:
4 ✔
1229
                s[k] = Series{dftu_series_str_find(
16 !
1230
                    A(), op.text.data(),
8 !
1231
                    static_cast<std::int32_t>(op.text.size()))};
12 ✔
1232
                break;
8 ✔
1233
            case OP_STR_REPLACE: {
7 ✔
1234
                const char* f = op.text.data();
14 ✔
1235
                const auto fn = static_cast<std::int32_t>(op.text.size());
14 ✔
1236
                const char* t = op.text2.data();
14 ✔
1237
                const auto tn = static_cast<std::int32_t>(op.text2.size());
14 ✔
1238
                s[k] = Series{
21 ✔
1239
                    op.param ? dftu_series_str_replace_all(A(), f, fn, t, tn)
17 !
1240
                             : dftu_series_str_replace(A(), f, fn, t, tn)};
13 !
1241
                break;
14 ✔
1242
            }
1243
            case OP_STR_SLICE:
4 ✔
1244
                s[k] = Series{dftu_series_str_slice(A(), op.scalar.value.i,
12 !
1245
                                                    op.scalar2.value.i)};
12 ✔
1246
                break;
8 ✔
1247
            case OP_IS_IN:
15 ✔
1248
                s[k] = Series{dftu_series_is_in(A(), op.values)};
30 !
1249
                break;
30 ✔
1250
            case OP_IS_NULL:
244 ✔
1251
                s[k] = Series{op.param ? dftu_series_null_mask(A())
1,188 !
1252
                                       : dftu_series_valid_mask(A())};
700 !
1253
                break;
488 ✔
1254
            case OP_CONST:
29 ✔
1255
                s[k] =
58 ✔
1256
                    const_column(static_cast<TypeId>(op.param), op.scalar, len);
87 !
1257
                break;
58 ✔
1258
            case OP_SELECT:
39 ✔
1259
                s[k] = Series{dftu_series_where(
117 !
1260
                    s[static_cast<std::size_t>(op.c)].handle(), A(), B())};
117 !
1261
                break;
78 ✔
1262
            default:
1263
                return {};
×
1264
        }
1265
        if (!s[k].handle()) return {};
9,310 ✔
1266
    }
4,656 ✔
1267
    std::vector<Series> outs;
4,162 ✔
1268
    outs.reserve(finals.size());
2,776 !
1269
    for (int f : finals) outs.push_back(s[static_cast<std::size_t>(f)].share());
5,562 ✔
1270
    return outs;
2,776 ✔
1271
}
4,166 !
1272

1273
// eval_chunk returns no columns when a kernel does not take its input type;
1274
// raised here, on the calling thread, since the parallel workers cannot throw.
1275
void require_evaluated(const std::vector<Series>& outs) {
2,778 ✔
1276
    if (outs.empty())
2,778 ✔
1277
        throw std::invalid_argument(
2 !
1278
            "expr: an operation does not take its input column type");
2 ✔
1279
}
2,776 ✔
1280

1281
Series ensure_flat(const Series& c) {
3,674 ✔
1282
    if (c.encoding() == Encoding::Flat && c.null_count() == 0) return c.share();
3,674 ✔
1283
    return Series{dftu_series_materialize(c.handle())};
312 ✔
1284
}
1,837 ✔
1285

1286
constexpr std::int64_t GRAIN = 1 << 16;
1287

1288
}  // namespace
1289

1290
std::vector<Series> eval_many(const std::vector<Expr>& roots,
3,032 ✔
1291
                              const std::vector<const Series*>& inputs) {
1292
    if (roots.empty())
3,032 ✔
1293
        throw std::invalid_argument("expr: needs at least one expression");
6 !
1294
    if (inputs.empty())
3,032 ✔
1295
        throw std::invalid_argument("expr: needs at least one input column");
×
1296

1297
    // Bare column references share the input; the evaluator would slice and
1298
    // concat a copy.
1299
    bool all_cols = true;
3,032 ✔
1300
    for (const Expr& root : roots) {
6,074 ✔
1301
        const std::int32_t i = root.valid() ? expr_col_index(root) : -1;
3,042 !
1302
        all_cols &= i >= 0 && static_cast<std::size_t>(i) < inputs.size();
3,042 !
1303
    }
1304
    if (all_cols) {
3,032 ✔
1305
        std::vector<Series> outs;
262 ✔
1306
        outs.reserve(roots.size());
262 !
1307
        for (const Expr& root : roots) {
524 ✔
1308
            const Series& in =
131 ✔
1309
                *inputs[static_cast<std::size_t>(expr_col_index(root))];
262 !
1310
            outs.push_back(in.encoding() == Encoding::Flat ? in.share()
262 !
1311
                                                           : in.materialize());
×
1312
        }
1313
        return outs;
262 ✔
1314
    }
262 !
1315

1316
    // One compiler for all roots: hash-consing (CSE) spans the whole program.
1317
    std::vector<DataType> input_types;
2,770 ✔
1318
    input_types.reserve(inputs.size());
2,770 !
1319
    for (const Series* s : inputs) input_types.push_back(s->data_type());
11,386 !
1320
    Compiler c(input_types);
2,770 !
1321
    std::vector<int> finals;
2,770 ✔
1322
    finals.reserve(roots.size());
2,770 !
1323
    for (const Expr& root : roots) {
5,540 ✔
1324
        if (!root.valid()) throw std::invalid_argument("expr: null expression");
2,780 !
1325
        Val out = c.compile(root.node().get());
2,780 ✔
1326
        if (out.is_scalar)
2,770 ✔
1327
            throw std::invalid_argument(
×
1328
                "expr: a constant expression has no column");
1329
        finals.push_back(out.slot);
2,770 !
1330
    }
2,770 ✔
1331

1332
    // Pruner: materialize only the inputs the program actually loads.
1333
    std::vector<bool> used(inputs.size(), false);
2,760 !
1334
    for (const SlotOp& op : c.program)
12,002 ✔
1335
        if (op.opcode == OP_LOAD)
9,242 ✔
1336
            used[static_cast<std::size_t>(op.param)] = true;
3,674 !
1337
    std::vector<Series> flat(inputs.size());
2,760 !
1338
    for (std::size_t i = 0; i < inputs.size(); ++i)
11,366 ✔
1339
        if (used[i]) flat[i] = ensure_flat(*inputs[i]);
8,606 !
1340

1341
    const std::int64_t n = inputs.front()->length();
2,760 ✔
1342
    if (n <= GRAIN) {
2,760 ✔
1343
        std::vector<Series> outs = eval_chunk(c.program, flat, finals, 0, n);
2,754 !
1344
        require_evaluated(outs);
2,754 ✔
1345
        return outs;
2,752 ✔
1346
    }
2,754 !
1347

1348
    const std::int64_t chunks = (n + GRAIN - 1) / GRAIN;
6 ✔
1349
    std::vector<std::vector<Series>> parts(static_cast<std::size_t>(chunks));
6 !
1350
    parallel_for(n, GRAIN, [&](std::int64_t b, std::int64_t e) {
18 !
1351
        parts[static_cast<std::size_t>(b / GRAIN)] =
24 ✔
1352
            eval_chunk(c.program, flat, finals, b, e - b);
33 ✔
1353
    });
23 ✔
1354
    for (const std::vector<Series>& part : parts) require_evaluated(part);
30 ✔
1355
    std::vector<Series> outs;
6 ✔
1356
    outs.reserve(finals.size());
6 !
1357
    for (std::size_t j = 0; j < finals.size(); ++j) {
12 ✔
1358
        std::vector<const Series*> ptrs;
6 ✔
1359
        ptrs.reserve(parts.size());
6 !
1360
        for (const std::vector<Series>& part : parts) ptrs.push_back(&part[j]);
30 ✔
1361
        outs.push_back(concat_columns(ptrs));
6 !
1362
    }
6 ✔
1363
    return outs;
6 ✔
1364
}
2,915 !
1365

1366
Series eval(const Expr& root, const std::vector<const Series*>& inputs) {
3,014 ✔
1367
    if (!root.valid()) throw std::invalid_argument("expr: null expression");
3,020 !
1368
    std::vector<Series> outs = eval_many({root}, inputs);
6,046 !
1369
    return std::move(outs.front());
4,503 ✔
1370
}
3,008 ✔
1371

1372
DataType infer_type(const Expr& root,
5,046 ✔
1373
                    const std::vector<DataType>& input_types) {
1374
    if (!root.valid()) throw std::invalid_argument("expr: null expression");
5,049 !
1375
    Compiler c(input_types);
5,046 ✔
1376
    Val out = c.compile(root.node().get());
5,046 ✔
1377
    if (out.is_scalar)
5,042 ✔
1378
        throw std::invalid_argument(
2 !
1379
            "expr: a constant expression has no column");
2 ✔
1380
    return out.full;
7,560 !
1381
}
5,047 ✔
1382

1383
}  // namespace dftracer::utils::dataframe
1384

1385
// ---- C ABI ----------------------------------------------------------------
1386

1387
struct dftu_expr {
1388
    dftracer::utils::dataframe::Expr e;
1389
};
1390

1391
namespace dftracer::utils::dataframe {
1392
const Expr& expr_handle_unwrap(const dftu_expr* h) { return h->e; }
26 ✔
1393
dftu_expr* expr_handle_wrap(Expr e) { return new dftu_expr{std::move(e)}; }
64 ✔
1394
}  // namespace dftracer::utils::dataframe
1395

1396
namespace {
1397
dftu_expr* wrap(dataframe::Expr e) { return new dftu_expr{std::move(e)}; }
64 ✔
1398
const dataframe::Expr& unwrap(const dftu_expr* e) { return e->e; }
76 ✔
1399
}  // namespace
1400

1401
extern "C" {
1402

1403
dftu_expr* dftu_expr_col(int32_t index) {
24 ✔
1404
    return wrap(dataframe::expr_col(index));
24 !
1405
}
1406
dftu_expr* dftu_expr_lit_i64(int64_t value) {
4 ✔
1407
    return wrap(dataframe::expr_lit(static_cast<std::int64_t>(value)));
4 !
1408
}
1409
dftu_expr* dftu_expr_lit_f64(double value) {
×
1410
    return wrap(dataframe::expr_lit(value));
×
1411
}
1412
dftu_expr* dftu_expr_binary(int32_t op, const dftu_expr* a,
4 ✔
1413
                            const dftu_expr* b) {
1414
    return wrap(dataframe::expr_binary(static_cast<dataframe::BinaryOp>(op),
8 !
1415
                                       unwrap(a), unwrap(b)));
6 ✔
1416
}
1417
dftu_expr* dftu_expr_prim(int32_t prim, const dftu_expr* a) {
×
1418
    return wrap(
×
1419
        dataframe::expr_prim(static_cast<dataframe::PrimOp>(prim), unwrap(a)));
×
1420
}
1421
dftu_expr* dftu_expr_unary(int32_t op, const dftu_expr* a) {
×
1422
    return wrap(
×
1423
        dataframe::expr_unary(static_cast<dataframe::UnaryOp>(op), unwrap(a)));
×
1424
}
1425
dftu_expr* dftu_expr_clip(const dftu_expr* a, dftu_scalar lo, dftu_scalar hi) {
×
1426
    return wrap(dataframe::expr_clip(unwrap(a), lo, hi));
×
1427
}
1428
int32_t dftu_expr_col_index(const dftu_expr* e) {
×
1429
    return e ? dataframe::expr_col_index(unwrap(e)) : -1;
×
1430
}
1431
int32_t dftu_expr_as_col_cmp(const dftu_expr* e, int32_t* col, int32_t* cmp,
10 ✔
1432
                             dftu_scalar* rhs) {
1433
    if (!e || !col || !cmp || !rhs) return 0;
10 !
1434
    std::int32_t c = -1;
10 ✔
1435
    dataframe::CmpOp op{};
10 ✔
1436
    dataframe::Scalar r;
10 ✔
1437
    if (!dataframe::expr_as_col_cmp(unwrap(e), &c, &op, &r)) return 0;
10 !
1438
    *col = c;
10 ✔
1439
    *cmp = static_cast<int32_t>(op);
10 ✔
1440
    *rhs = r;
10 ✔
1441
    return 1;
10 ✔
1442
}
5 ✔
1443
int32_t dftu_expr_as_logical(const dftu_expr* e, int32_t* op, dftu_expr** a,
×
1444
                             dftu_expr** b) {
1445
    if (!e || !op || !a || !b) return 0;
×
1446
    dataframe::LogicalOp lop{};
×
1447
    dataframe::Expr lhs, rhs;
×
1448
    if (!dataframe::expr_as_logical(unwrap(e), &lop, &lhs, &rhs)) return 0;
×
1449
    *op = static_cast<int32_t>(lop);
×
1450
    *a = wrap(std::move(lhs));
×
1451
    *b = wrap(std::move(rhs));
×
1452
    return 1;
×
1453
}
×
1454
int32_t dftu_expr_as_not(const dftu_expr* e, dftu_expr** a) {
×
1455
    if (!e || !a) return 0;
×
1456
    dataframe::Expr inner;
×
1457
    if (!dataframe::expr_as_not(unwrap(e), &inner)) return 0;
×
1458
    *a = wrap(std::move(inner));
×
1459
    return 1;
×
1460
}
×
1461
int32_t dftu_expr_as_col_str_pred(const dftu_expr* e, int32_t* col, int32_t* op,
×
1462
                                  const char** pattern, int32_t* pattern_len) {
1463
    if (!e || !col || !op || !pattern || !pattern_len) return 0;
×
1464
    std::int32_t c = -1;
×
1465
    dataframe::StrPredOp sop{};
×
1466
    std::string_view pat;
×
1467
    if (!dataframe::expr_as_col_str_pred(unwrap(e), &c, &sop, &pat)) return 0;
×
1468
    *col = c;
×
1469
    *op = static_cast<int32_t>(sop);
×
1470
    *pattern = pat.data();
×
1471
    *pattern_len = static_cast<int32_t>(pat.size());
×
1472
    return 1;
×
1473
}
1474
int32_t dftu_expr_as_col_is_in(const dftu_expr* e, int32_t* col,
×
1475
                               dftu_series** values) {
1476
    if (!e || !col || !values) return 0;
×
1477
    std::int32_t c = -1;
×
1478
    dataframe::Series v;
×
1479
    if (!dataframe::expr_as_col_is_in(unwrap(e), &c, &v)) return 0;
×
1480
    *col = c;
×
1481
    *values = v.release();
×
1482
    return 1;
×
1483
}
×
1484
dftu_expr* dftu_expr_cmp(int32_t cmp, const dftu_expr* a, dftu_scalar rhs) {
12 ✔
1485
    return wrap(dataframe::expr_cmp(static_cast<dataframe::CmpOp>(cmp),
24 !
1486
                                    unwrap(a), rhs));
18 ✔
1487
}
1488
dftu_expr* dftu_expr_logical(int32_t op, const dftu_expr* a,
×
1489
                             const dftu_expr* b) {
1490
    return wrap(dataframe::expr_logical(static_cast<dataframe::LogicalOp>(op),
×
1491
                                        unwrap(a), unwrap(b)));
×
1492
}
1493
dftu_expr* dftu_expr_not(const dftu_expr* a) {
×
1494
    return wrap(dataframe::expr_not(unwrap(a)));
×
1495
}
1496
dftu_expr* dftu_expr_cast(int32_t type, const dftu_expr* a) {
×
1497
    return wrap(
×
1498
        dataframe::expr_cast(static_cast<dataframe::TypeId>(type), unwrap(a)));
×
1499
}
1500
dftu_expr* dftu_expr_lower(const dftu_expr* a) {
×
1501
    return wrap(dataframe::expr_lower(unwrap(a)));
×
1502
}
1503
dftu_expr* dftu_expr_str_pred(int32_t op, const dftu_expr* a,
4 ✔
1504
                              const char* pattern, int32_t pattern_len) {
1505
    if (!a || (pattern_len > 0 && !pattern)) return nullptr;
4 !
1506
    return wrap(dataframe::expr_str_pred(
3 !
1507
        static_cast<dataframe::StrPredOp>(op), unwrap(a),
1 ✔
1508
        std::string_view(pattern ? pattern : "",
1 !
1509
                         static_cast<std::size_t>(pattern_len))));
2 ✔
1510
}
2 ✔
1511
dftu_expr* dftu_expr_str_map(int32_t op, const dftu_expr* a) {
2 ✔
1512
    if (!a) return nullptr;
2 ✔
1513
    return wrap(dataframe::expr_str_map(static_cast<dataframe::StrMapOp>(op),
4 !
1514
                                        unwrap(a)));
2 ✔
1515
}
1 ✔
1516
dftu_expr* dftu_expr_str_len(const dftu_expr* a, int32_t chars) {
2 ✔
1517
    if (!a) return nullptr;
2 ✔
1518
    return wrap(dataframe::expr_str_len(unwrap(a), chars != 0));
2 !
1519
}
1 ✔
1520
dftu_expr* dftu_expr_str_find(const dftu_expr* a, const char* needle,
2 ✔
1521
                              int32_t needle_len) {
1522
    if (!a || (needle_len > 0 && !needle)) return nullptr;
2 !
1523
    return wrap(dataframe::expr_str_find(
3 !
1524
        unwrap(a), std::string_view(needle ? needle : "",
1 !
1525
                                    static_cast<std::size_t>(needle_len))));
2 ✔
1526
}
1 ✔
1527
dftu_expr* dftu_expr_str_replace(const dftu_expr* a, const char* from,
2 ✔
1528
                                 int32_t from_len, const char* to,
1529
                                 int32_t to_len, int32_t all) {
1530
    if (!a || (from_len > 0 && !from) || (to_len > 0 && !to)) return nullptr;
2 !
1531
    return wrap(dataframe::expr_str_replace(
3 !
1532
        unwrap(a),
1 ✔
1533
        std::string_view(from ? from : "", static_cast<std::size_t>(from_len)),
1 !
1534
        std::string_view(to ? to : "", static_cast<std::size_t>(to_len)),
1 !
1535
        all != 0));
2 ✔
1536
}
1 ✔
1537
dftu_expr* dftu_expr_str_slice(const dftu_expr* a, int64_t start, int64_t len) {
2 ✔
1538
    if (!a) return nullptr;
2 ✔
1539
    return wrap(dataframe::expr_str_slice(unwrap(a), start, len));
2 !
1540
}
1 ✔
1541
dftu_expr* dftu_expr_is_in(const dftu_expr* a, const dftu_series* values) {
4 ✔
1542
    if (!a || !values) return nullptr;
4 !
1543
    return wrap(dataframe::expr_is_in(
3 !
1544
        unwrap(a), dataframe::Series{dftu_series_share(values)}));
4 ✔
1545
}
2 ✔
1546
dftu_expr* dftu_expr_select(const dftu_expr* cond, const dftu_expr* a,
4 ✔
1547
                            const dftu_expr* b) {
1548
    if (!cond || !a || !b) return nullptr;
4 !
1549
    return wrap(dataframe::expr_select(unwrap(cond), unwrap(a), unwrap(b)));
2 !
1550
}
2 ✔
1551
dftu_expr* dftu_expr_is_null(const dftu_expr* a, int32_t null) {
6 ✔
1552
    if (!a) return nullptr;
6 ✔
1553
    return wrap(dataframe::expr_is_null(unwrap(a), null != 0));
4 !
1554
}
3 ✔
1555
void dftu_expr_free(dftu_expr* e) { delete e; }
128 ✔
1556

1557
dftu_series* dftu_expr_eval(const dftu_expr* root,
22 ✔
1558
                            const dftu_series* const* inputs,
1559
                            int32_t n_inputs) {
1560
    if (!root) return nullptr;
22 ✔
1561
    std::vector<dataframe::Series> owned;
22 ✔
1562
    std::vector<const dataframe::Series*> cols;
22 ✔
1563
    owned.reserve(static_cast<std::size_t>(n_inputs));
22 !
1564
    cols.reserve(static_cast<std::size_t>(n_inputs));
22 !
1565
    for (int32_t i = 0; i < n_inputs; ++i) {
46 ✔
1566
        owned.emplace_back(const_cast<dftu_series*>(inputs[i]));
24 !
1567
        cols.push_back(&owned.back());
24 !
1568
    }
12 ✔
1569
    dftu_series* out = nullptr;
22 ✔
1570
    try {
1571
        out = dataframe::eval(unwrap(root), cols).release();
22 !
1572
    } catch (const std::exception&) {
11 !
1573
        out = nullptr;
×
1574
    }
×
1575
    for (dataframe::Series& c : owned)
46 ✔
1576
        c.release();  // borrowed inputs, do not free
24 ✔
1577
    return out;
22 ✔
1578
}
22 ✔
1579

1580
int32_t dftu_expr_eval_many(const dftu_expr* const* roots, int32_t n_roots,
×
1581
                            const dftu_series* const* inputs, int32_t n_inputs,
1582
                            dftu_series** out) {
1583
    if (!roots || n_roots <= 0) return -1;
×
1584
    std::vector<dataframe::Expr> exprs;
×
1585
    exprs.reserve(static_cast<std::size_t>(n_roots));
×
1586
    for (int32_t i = 0; i < n_roots; ++i) {
×
1587
        if (!roots[i]) return -1;
×
1588
        exprs.push_back(unwrap(roots[i]));
×
1589
    }
1590
    std::vector<dataframe::Series> owned;
×
1591
    std::vector<const dataframe::Series*> cols;
×
1592
    owned.reserve(static_cast<std::size_t>(n_inputs));
×
1593
    cols.reserve(static_cast<std::size_t>(n_inputs));
×
1594
    for (int32_t i = 0; i < n_inputs; ++i) {
×
1595
        owned.emplace_back(const_cast<dftu_series*>(inputs[i]));
×
1596
        cols.push_back(&owned.back());
×
1597
    }
1598
    int32_t written = -1;
×
1599
    try {
1600
        std::vector<dataframe::Series> res = dataframe::eval_many(exprs, cols);
×
1601
        for (std::size_t i = 0; i < res.size(); ++i) out[i] = res[i].release();
×
1602
        written = static_cast<int32_t>(res.size());
×
1603
    } catch (const std::exception&) {
×
1604
        written = -1;
×
1605
    }
×
1606
    for (dataframe::Series& c : owned)
×
1607
        c.release();  // borrowed inputs, do not free
×
1608
    return written;
×
1609
}
×
1610
}
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