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

stillwater-sc / universal / 22588398771

02 Mar 2026 05:08PM UTC coverage: 84.059% (-0.2%) from 84.241%
22588398771

Pull #532

github

web-flow
Merge 30e619f50 into 401df414e
Pull Request #532: test(pop): add edge-case tests for ~90% coverage

428 of 567 new or added lines in 5 files covered. (75.49%)

3 existing lines in 1 file now uncovered.

41446 of 49306 relevant lines covered (84.06%)

6383019.94 hits per line

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

76.55
/mixedprecision/pop/test_expression_graph.cpp
1
// test_expression_graph.cpp: validate ExprGraph construction and analysis
2
//
3
// Copyright (C) 2017 Stillwater Supercomputing, Inc.
4
// SPDX-License-Identifier: MIT
5
//
6
// This file is part of the universal numbers project.
7
//
8
// Tests the expression graph DAG builder, forward/backward analysis,
9
// and integration with TypeAdvisor for type recommendations.
10

11
#include <universal/utility/directives.hpp>
12
#include <universal/mixedprecision/expression_graph.hpp>
13
#include <iostream>
14
#include <string>
15
#include <cmath>
16

17
namespace sw { namespace universal {
18

19
// Test basic graph construction
20
int TestGraphConstruction() {
1✔
21
        int nrOfFailedTestCases = 0;
1✔
22

23
        ExprGraph g;
1✔
24
        int a = g.variable("a", 1.0, 10.0);
2✔
25
        int b = g.variable("b", 1.0, 10.0);
1✔
26
        int c = g.add(a, b);
1✔
27

28
        if (g.size() != 3) {
1✔
29
                std::cerr << "FAIL: expected 3 nodes, got " << g.size() << std::endl;
×
30
                ++nrOfFailedTestCases;
×
31
        }
32

33
        auto& node_c = g.get_node(c);
1✔
34
        if (node_c.op != OpKind::Add) {
1✔
35
                std::cerr << "FAIL: expected Add op" << std::endl;
×
36
                ++nrOfFailedTestCases;
×
37
        }
38
        if (node_c.lhs != a || node_c.rhs != b) {
1✔
39
                std::cerr << "FAIL: wrong input edges" << std::endl;
×
40
                ++nrOfFailedTestCases;
×
41
        }
42

43
        return nrOfFailedTestCases;
1✔
44
}
1✔
45

46
// Test the determinant example: det = a*d - b*c
47
// With high accuracy requirement on det, backward analysis should
48
// propagate higher precision requirements to inputs
49
int TestDeterminantAnalysis() {
1✔
50
        int nrOfFailedTestCases = 0;
1✔
51

52
        ExprGraph g;
1✔
53

54
        // Matrix entries: all in [8, 12] range (nearly singular)
55
        int a = g.variable("a", 8.0, 12.0);   // ufp = 3
2✔
56
        int b = g.variable("b", 8.0, 12.0);   // ufp = 3
2✔
57
        int c = g.variable("c", 8.0, 12.0);   // ufp = 3
2✔
58
        int d = g.variable("d", 8.0, 12.0);   // ufp = 3
1✔
59

60
        // Products: a*d in [64, 144], b*c in [64, 144]
61
        int ad = g.mul(a, d);   // ufp ~ 7
1✔
62
        int bc = g.mul(b, c);   // ufp ~ 7
1✔
63

64
        // Determinant: det = a*d - b*c, range [-80, 80], ufp ~ 6
65
        int det = g.sub(ad, bc);
1✔
66

67
        // Require 20 significant bits at the output
68
        g.require_nsb(det, 20);
1✔
69

70
        // Run analysis
71
        g.analyze();
1✔
72

73
        // The determinant should get at least 20 bits
74
        if (g.get_nsb(det) < 20) {
1✔
75
                std::cerr << "FAIL: det nsb should be >= 20, got " << g.get_nsb(det) << std::endl;
×
76
                ++nrOfFailedTestCases;
×
77
        }
78

79
        // The products should need more bits than the output (due to subtraction)
80
        if (g.get_nsb(ad) < g.get_nsb(det)) {
1✔
81
                std::cerr << "FAIL: ad should need >= det bits due to cancellation" << std::endl;
×
82
                ++nrOfFailedTestCases;
×
83
        }
84

85
        // The input variables should need even more (mul adds carry)
86
        if (g.get_nsb(a) < g.get_nsb(ad)) {
1✔
87
                std::cerr << "FAIL: input a should need >= ad bits" << std::endl;
×
88
                ++nrOfFailedTestCases;
×
89
        }
90

91
        std::cout << "Determinant example analysis:\n";
1✔
92
        g.report(std::cout);
1✔
93

94
        return nrOfFailedTestCases;
1✔
95
}
1✔
96

97
// Test simple multiplication chain: z = x * y, require 10 bits at z
98
int TestSimpleMulBackward() {
1✔
99
        int nrOfFailedTestCases = 0;
1✔
100

101
        ExprGraph g;
1✔
102
        int x = g.variable("x", 1.0, 8.0);
2✔
103
        int y = g.variable("y", 1.0, 8.0);
1✔
104
        int z = g.mul(x, y);
1✔
105

106
        g.require_nsb(z, 10);
1✔
107
        g.analyze();
1✔
108

109
        // Backward through mul: nsb(x) >= nsb(z) + carry = 11
110
        if (g.get_nsb(x) < 11) {
1✔
111
                std::cerr << "FAIL: mul backward x expected >= 11, got " << g.get_nsb(x) << std::endl;
×
112
                ++nrOfFailedTestCases;
×
113
        }
114
        if (g.get_nsb(y) < 11) {
1✔
115
                std::cerr << "FAIL: mul backward y expected >= 11, got " << g.get_nsb(y) << std::endl;
×
116
                ++nrOfFailedTestCases;
×
117
        }
118

119
        return nrOfFailedTestCases;
1✔
120
}
1✔
121

122
// Test with range_analyzer integration
123
int TestRangeAnalyzerIntegration() {
1✔
124
        int nrOfFailedTestCases = 0;
1✔
125

126
        // Simulate: we observed values in [0.5, 100.0]
127
        range_analyzer<double> ra;
1✔
128
        ra.observe(0.5);
1✔
129
        ra.observe(100.0);
1✔
130
        ra.observe(50.0);
1✔
131
        ra.observe(75.0);
1✔
132

133
        ExprGraph g;
1✔
134
        int x = g.variable("x", ra);
1✔
135

136
        auto& node = g.get_node(x);
1✔
137
        // lo should be 0.5, hi should be 100.0
138
        if (node.lo != 0.5 || node.hi != 100.0) {
1✔
139
                std::cerr << "FAIL: range_analyzer bridge lo/hi mismatch" << std::endl;
×
140
                ++nrOfFailedTestCases;
×
141
        }
142

143
        // ufp should match range_analyzer
144
        if (node.ufp != ra.ufp()) {
1✔
145
                std::cerr << "FAIL: ufp mismatch: node=" << node.ufp << ", analyzer=" << ra.ufp() << std::endl;
×
146
                ++nrOfFailedTestCases;
×
147
        }
148

149
        return nrOfFailedTestCases;
1✔
150
}
1✔
151

152
// Test TypeAdvisor integration
153
int TestTypeRecommendation() {
1✔
154
        int nrOfFailedTestCases = 0;
1✔
155

156
        ExprGraph g;
1✔
157
        int x = g.variable("x", 0.1, 100.0);
2✔
158
        int y = g.variable("y", 0.1, 100.0);
1✔
159
        int z = g.mul(x, y);
1✔
160

161
        g.require_nsb(z, 10);
1✔
162
        g.analyze();
1✔
163

164
        TypeAdvisor advisor;
1✔
165
        std::string rec = g.recommended_type(z, advisor);
1✔
166

167
        // With 10 nsb required, posit<16,1> (12 fraction bits) should suffice
168
        std::cout << "Type recommendation for z (nsb=" << g.get_nsb(z) << "): " << rec << std::endl;
1✔
169

170
        // The recommendation should not be empty
171
        if (rec.empty()) {
1✔
172
                std::cerr << "FAIL: empty type recommendation" << std::endl;
×
173
                ++nrOfFailedTestCases;
×
174
        }
175

176
        // Print full report with types
177
        g.report(std::cout, advisor);
1✔
178

179
        return nrOfFailedTestCases;
1✔
180
}
1✔
181

182
// Test chain of operations: y = sqrt(a*a + b*b)
183
int TestPythagoreanAnalysis() {
1✔
184
        int nrOfFailedTestCases = 0;
1✔
185

186
        ExprGraph g;
1✔
187
        int a = g.variable("a", 1.0, 10.0);
2✔
188
        int b = g.variable("b", 1.0, 10.0);
1✔
189

190
        int a2 = g.mul(a, a);
1✔
191
        int b2 = g.mul(b, b);
1✔
192
        int sum = g.add(a2, b2);
1✔
193
        int result = g.sqrt(sum);
1✔
194

195
        g.require_nsb(result, 16);
1✔
196
        g.analyze();
1✔
197

198
        std::cout << "Pythagorean analysis (require 16 bits at sqrt):\n";
1✔
199
        g.report(std::cout);
1✔
200

201
        // result should have at least 16 bits
202
        if (g.get_nsb(result) < 16) {
1✔
203
                std::cerr << "FAIL: pythagorean result should have >= 16 bits" << std::endl;
×
204
                ++nrOfFailedTestCases;
×
205
        }
206

207
        return nrOfFailedTestCases;
1✔
208
}
1✔
209

210
// Test division operation
211
int TestDivisionOp() {
1✔
212
        int nrOfFailedTestCases = 0;
1✔
213

214
        ExprGraph g;
1✔
215
        int x = g.variable("x", 2.0, 10.0);
2✔
216
        int y = g.variable("y", 1.0, 4.0);
1✔
217
        int z = g.div(x, y);
1✔
218

219
        g.require_nsb(z, 12);
1✔
220
        g.analyze();
1✔
221

222
        auto& node_z = g.get_node(z);
1✔
223
        if (node_z.op != OpKind::Div) {
1✔
NEW
224
                std::cerr << "FAIL: expected Div op" << std::endl;
×
NEW
225
                ++nrOfFailedTestCases;
×
226
        }
227

228
        if (g.get_nsb(z) < 12) {
1✔
NEW
229
                std::cerr << "FAIL: div z expected >= 12, got " << g.get_nsb(z) << std::endl;
×
NEW
230
                ++nrOfFailedTestCases;
×
231
        }
232

233
        // Backward through div: inputs should need more bits than output
234
        if (g.get_nsb(x) < g.get_nsb(z)) {
1✔
NEW
235
                std::cerr << "FAIL: div input x should need >= z bits" << std::endl;
×
NEW
236
                ++nrOfFailedTestCases;
×
237
        }
238

239
        return nrOfFailedTestCases;
1✔
240
}
1✔
241

242
// Test unary operations: neg, abs, sqrt
243
int TestUnaryOps() {
1✔
244
        int nrOfFailedTestCases = 0;
1✔
245

246
        ExprGraph g;
1✔
247
        int x = g.variable("x", 1.0, 10.0);
1✔
248
        int nx = g.neg(x);
1✔
249
        int ax = g.abs(x);
1✔
250

251
        auto& node_neg = g.get_node(nx);
1✔
252
        if (node_neg.op != OpKind::Neg) {
1✔
NEW
253
                std::cerr << "FAIL: expected Neg op" << std::endl;
×
NEW
254
                ++nrOfFailedTestCases;
×
255
        }
256

257
        auto& node_abs = g.get_node(ax);
1✔
258
        if (node_abs.op != OpKind::Abs) {
1✔
NEW
259
                std::cerr << "FAIL: expected Abs op" << std::endl;
×
NEW
260
                ++nrOfFailedTestCases;
×
261
        }
262

263
        // Neg range: [-10, -1]
264
        if (node_neg.lo != -10.0 || node_neg.hi != -1.0) {
1✔
NEW
265
                std::cerr << "FAIL: neg range mismatch: [" << node_neg.lo << ", " << node_neg.hi << "]" << std::endl;
×
NEW
266
                ++nrOfFailedTestCases;
×
267
        }
268

269
        // Abs of positive range should be unchanged
270
        if (node_abs.lo != 1.0 || node_abs.hi != 10.0) {
1✔
NEW
271
                std::cerr << "FAIL: abs range mismatch" << std::endl;
×
NEW
272
                ++nrOfFailedTestCases;
×
273
        }
274

275
        return nrOfFailedTestCases;
1✔
276
}
1✔
277

278
// Test abs with range spanning zero
279
int TestAbsSpanningZero() {
1✔
280
        int nrOfFailedTestCases = 0;
1✔
281

282
        ExprGraph g;
1✔
283
        int x = g.variable("x", -5.0, 10.0);
1✔
284
        int ax = g.abs(x);
1✔
285

286
        auto& node = g.get_node(ax);
1✔
287
        if (node.lo != 0.0) {
1✔
NEW
288
                std::cerr << "FAIL: abs spanning zero lo expected 0, got " << node.lo << std::endl;
×
NEW
289
                ++nrOfFailedTestCases;
×
290
        }
291
        if (node.hi != 10.0) {
1✔
NEW
292
                std::cerr << "FAIL: abs spanning zero hi expected 10, got " << node.hi << std::endl;
×
NEW
293
                ++nrOfFailedTestCases;
×
294
        }
295

296
        return nrOfFailedTestCases;
1✔
297
}
1✔
298

299
// Test abs with negative range
300
int TestAbsNegativeRange() {
1✔
301
        int nrOfFailedTestCases = 0;
1✔
302

303
        ExprGraph g;
1✔
304
        int x = g.variable("x", -10.0, -2.0);
1✔
305
        int ax = g.abs(x);
1✔
306

307
        auto& node = g.get_node(ax);
1✔
308
        // abs([-10, -2]) = [2, 10]
309
        if (node.lo != 2.0 || node.hi != 10.0) {
1✔
NEW
310
                std::cerr << "FAIL: abs negative range: [" << node.lo << ", " << node.hi
×
NEW
311
                          << "], expected [2, 10]" << std::endl;
×
NEW
312
                ++nrOfFailedTestCases;
×
313
        }
314

315
        return nrOfFailedTestCases;
1✔
316
}
1✔
317

318
// Test multi-consumer node: a shared input used by two different ops
319
int TestMultiConsumer() {
1✔
320
        int nrOfFailedTestCases = 0;
1✔
321

322
        ExprGraph g;
1✔
323
        int x = g.variable("x", 1.0, 10.0);
1✔
324
        int y1 = g.mul(x, x);    // x*x
1✔
325
        int y2 = g.add(x, x);    // x+x
1✔
326

327
        g.require_nsb(y1, 16);
1✔
328
        g.require_nsb(y2, 8);
1✔
329
        g.analyze();
1✔
330

331
        // x should satisfy the more demanding consumer (y1 needs 16+1=17)
332
        if (g.get_nsb(x) < 17) {
1✔
NEW
333
                std::cerr << "FAIL: multi-consumer x expected >= 17 (from mul), got " << g.get_nsb(x) << std::endl;
×
NEW
334
                ++nrOfFailedTestCases;
×
335
        }
336

337
        return nrOfFailedTestCases;
1✔
338
}
1✔
339

340
// Test constant node
341
int TestConstantNode() {
1✔
342
        int nrOfFailedTestCases = 0;
1✔
343

344
        ExprGraph g;
1✔
345
        int c = g.constant(3.14);
1✔
346

347
        auto& node = g.get_node(c);
1✔
348
        if (node.op != OpKind::Constant) {
1✔
NEW
349
                std::cerr << "FAIL: expected Constant op" << std::endl;
×
NEW
350
                ++nrOfFailedTestCases;
×
351
        }
352
        if (node.lo != 3.14 || node.hi != 3.14) {
1✔
NEW
353
                std::cerr << "FAIL: constant range mismatch" << std::endl;
×
NEW
354
                ++nrOfFailedTestCases;
×
355
        }
356
        if (node.ufp != 1) { // floor(log2(3.14)) = 1
1✔
NEW
357
                std::cerr << "FAIL: constant ufp expected 1, got " << node.ufp << std::endl;
×
NEW
358
                ++nrOfFailedTestCases;
×
359
        }
360

361
        return nrOfFailedTestCases;
1✔
362
}
1✔
363

364
// Test zero constant
365
int TestZeroConstant() {
1✔
366
        int nrOfFailedTestCases = 0;
1✔
367

368
        ExprGraph g;
1✔
369
        int c = g.constant(0.0);
1✔
370

371
        auto& node = g.get_node(c);
1✔
372
        if (node.ufp != 0) {
1✔
NEW
373
                std::cerr << "FAIL: zero constant ufp expected 0, got " << node.ufp << std::endl;
×
NEW
374
                ++nrOfFailedTestCases;
×
375
        }
376

377
        return nrOfFailedTestCases;
1✔
378
}
1✔
379

380
// Test graph with no requirements (no backward propagation)
381
int TestNoRequirements() {
1✔
382
        int nrOfFailedTestCases = 0;
1✔
383

384
        ExprGraph g;
1✔
385
        int a = g.variable("a", 1.0, 10.0);
2✔
386
        int b = g.variable("b", 1.0, 10.0);
1✔
387
        int c = g.add(a, b);
1✔
388
        (void)c;
389

390
        // No requirements set - analyze should still work
391
        g.analyze();
1✔
392

393
        // All nodes should have nsb_final >= 1
394
        for (int i = 0; i < g.size(); ++i) {
4✔
395
                if (g.get_nsb(i) < 1) {
3✔
NEW
396
                        std::cerr << "FAIL: node " << i << " nsb < 1 with no requirements" << std::endl;
×
NEW
397
                        ++nrOfFailedTestCases;
×
398
                }
399
        }
400

401
        return nrOfFailedTestCases;
1✔
402
}
1✔
403

404
// Test OpKind to_string coverage
405
int TestOpKindStrings() {
1✔
406
        int nrOfFailedTestCases = 0;
1✔
407

408
        if (std::string(to_string(OpKind::Constant)) != "const") ++nrOfFailedTestCases;
2✔
409
        if (std::string(to_string(OpKind::Variable)) != "var") ++nrOfFailedTestCases;
2✔
410
        if (std::string(to_string(OpKind::Add)) != "+") ++nrOfFailedTestCases;
2✔
411
        if (std::string(to_string(OpKind::Sub)) != "-") ++nrOfFailedTestCases;
2✔
412
        if (std::string(to_string(OpKind::Mul)) != "*") ++nrOfFailedTestCases;
2✔
413
        if (std::string(to_string(OpKind::Div)) != "/") ++nrOfFailedTestCases;
2✔
414
        if (std::string(to_string(OpKind::Neg)) != "neg") ++nrOfFailedTestCases;
2✔
415
        if (std::string(to_string(OpKind::Abs)) != "abs") ++nrOfFailedTestCases;
2✔
416
        if (std::string(to_string(OpKind::Sqrt)) != "sqrt") ++nrOfFailedTestCases;
2✔
417

418
        return nrOfFailedTestCases;
1✔
419
}
420

421
// Test division range estimation with divisor spanning zero
422
int TestDivByZeroRange() {
1✔
423
        int nrOfFailedTestCases = 0;
1✔
424

425
        ExprGraph g;
1✔
426
        int x = g.variable("x", 1.0, 10.0);
2✔
427
        int y = g.variable("y", -1.0, 1.0); // spans zero
1✔
428
        int z = g.div(x, y);
1✔
429

430
        auto& node = g.get_node(z);
1✔
431
        // Should get very large range due to division by zero potential
432
        if (node.lo > -1e50 || node.hi < 1e50) {
1✔
NEW
433
                std::cerr << "FAIL: div-by-zero range not expanded: [" << node.lo << ", " << node.hi << "]" << std::endl;
×
NEW
434
                ++nrOfFailedTestCases;
×
435
        }
436

437
        return nrOfFailedTestCases;
1✔
438
}
1✔
439

440
}} // namespace sw::universal
441

442
#define TEST_CASE(name, func) \
443
        do { \
444
                int fails = func; \
445
                if (fails) { \
446
                        std::cout << name << ": FAIL (" << fails << " errors)" << std::endl; \
447
                        nrOfFailedTestCases += fails; \
448
                } else { \
449
                        std::cout << name << ": PASS" << std::endl; \
450
                } \
451
        } while(0)
452

453
int main()
1✔
454
try {
455
        using namespace sw::universal;
456

457
        int nrOfFailedTestCases = 0;
1✔
458

459
        std::cout << "POP Expression Graph Tests\n";
1✔
460
        std::cout << std::string(40, '=') << "\n\n";
1✔
461

462
        TEST_CASE("Graph construction", TestGraphConstruction());
1✔
463
        TEST_CASE("Simple mul backward", TestSimpleMulBackward());
1✔
464
        TEST_CASE("Determinant analysis", TestDeterminantAnalysis());
1✔
465
        TEST_CASE("Range analyzer integration", TestRangeAnalyzerIntegration());
1✔
466
        TEST_CASE("Type recommendation", TestTypeRecommendation());
1✔
467
        TEST_CASE("Pythagorean analysis", TestPythagoreanAnalysis());
1✔
468
        TEST_CASE("Division operation", TestDivisionOp());
1✔
469
        TEST_CASE("Unary operations", TestUnaryOps());
1✔
470
        TEST_CASE("Abs spanning zero", TestAbsSpanningZero());
1✔
471
        TEST_CASE("Abs negative range", TestAbsNegativeRange());
1✔
472
        TEST_CASE("Multi-consumer", TestMultiConsumer());
1✔
473
        TEST_CASE("Constant node", TestConstantNode());
1✔
474
        TEST_CASE("Zero constant", TestZeroConstant());
1✔
475
        TEST_CASE("No requirements", TestNoRequirements());
1✔
476
        TEST_CASE("OpKind strings", TestOpKindStrings());
1✔
477
        TEST_CASE("Div-by-zero range", TestDivByZeroRange());
1✔
478

479
        std::cout << "\n";
1✔
480
        if (nrOfFailedTestCases == 0) {
1✔
481
                std::cout << "All expression graph tests PASSED\n";
1✔
482
        } else {
483
                std::cout << nrOfFailedTestCases << " test(s) FAILED\n";
×
484
        }
485

486
        return (nrOfFailedTestCases > 0 ? EXIT_FAILURE : EXIT_SUCCESS);
1✔
487
}
488
catch (const char* msg) {
×
489
        std::cerr << "Caught exception: " << msg << std::endl;
×
490
        return EXIT_FAILURE;
×
491
}
×
492
catch (...) {
×
493
        std::cerr << "Caught unknown exception" << std::endl;
×
494
        return EXIT_FAILURE;
×
495
}
×
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