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

wurstscript / WurstScript / 228

29 Nov 2023 05:00PM UTC coverage: 62.48% (-0.09%) from 62.574%
228

push

circleci

web-flow
Show dialog for choosing game path, cleanup (#1083)

* show dialog for choosing game path

* cleanup code

* remove logs and refactor

* remove confusing mpq error, make some mpq loads readonly

17295 of 27681 relevant lines covered (62.48%)

0.62 hits per line

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

88.57
de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java
1
package de.peeeq.wurstscript.translation.imtranslation;
2

3
import com.google.common.base.Preconditions;
4
import de.peeeq.wurstscript.WurstOperator;
5
import de.peeeq.wurstscript.attributes.CompileError;
6
import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer;
7
import de.peeeq.wurstscript.jassIm.*;
8
import de.peeeq.wurstscript.translation.imoptimizer.Replacer;
9
import de.peeeq.wurstscript.translation.imtranslation.ImTranslator.VarsForTupleResult;
10
import de.peeeq.wurstscript.types.TypesHelper;
11

12
import java.util.*;
13
import java.util.stream.Collectors;
14

15
/**
16
 * a rewrite would return a combination of
17
 * - List of statements
18
 * - list of expressions
19
 * for expressions, returned expressions would never have a parent
20
 */
21
public class EliminateTuples {
1✔
22

23

24

25
    public static void eliminateTuplesProg(ImProg imProg, ImTranslator translator) {
26

27
        Runnable removeOldGlobals = transformVars(imProg.getGlobals(), translator);
1✔
28
        for (ImFunction f : imProg.getFunctions()) {
1✔
29
            transformFunctionReturnsAndParameters(f, translator);
1✔
30
        }
1✔
31
        for (ImFunction f : imProg.getFunctions()) {
1✔
32
            eliminateTuplesFunc(f, translator);
1✔
33
        }
1✔
34
        removeOldGlobals.run();
1✔
35
        translator.assertProperties(AssertProperty.NOTUPLES);
1✔
36
    }
1✔
37

38
    private static void transformFunctionReturnsAndParameters(ImFunction f, ImTranslator translator) {
39
        transformVars(f.getParameters(), translator).run();
1✔
40
        translator.setOriginalReturnValue(f, f.getReturnType());
1✔
41
        f.setReturnType(getFirstType(f.getReturnType()));
1✔
42
    }
1✔
43

44

45
    private static void eliminateTuplesFunc(ImFunction f, final ImTranslator translator) {
46
        transformVars(f.getLocals(), translator).run();
1✔
47

48
        tryStep(f, translator, EliminateTuples::toTupleExpressions);
1✔
49
        tryStep(f, translator, EliminateTuples::normalizeTuplesInStatementExprs);
1✔
50
        tryStep(f, translator, EliminateTuples::removeTupleSelections);
1✔
51
        tryStep(f, translator, EliminateTuples::normalizeTuplesInStatementExprs);
1✔
52
        tryStep(f, translator, (stmts, translator1, fn) -> removeTupleExprs(0, stmts, translator1, fn));
1✔
53

54
    }
1✔
55

56
    private static void removeTupleSelections(ImStmts stmts, ImTranslator tr, ImFunction f) {
57
        Replacer replacer = new Replacer();
1✔
58
        stmts.accept(new Element.DefaultVisitor() {
1✔
59
            @Override
60
            public void visit(ImTupleSelection ts) {
61
                super.visit(ts);
1✔
62

63
                if (!(ts.getTupleExpr() instanceof ImTupleExpr)) {
1✔
64
                    throw new CompileError(ts.attrTrace().attrSource(), "Wrong tuple selection: " + ts);
×
65
                }
66

67
                ImTupleExpr tupleExpr = (ImTupleExpr) ts.getTupleExpr();
1✔
68

69
                int ti = ts.getTupleIndex();
1✔
70

71
                ImStmts stmts = JassIm.ImStmts();
1✔
72
                ImExpr result = null;
1✔
73

74
                assert ti >= 0;
1✔
75
                if (ti >= tupleExpr.getExprs().size()) {
1✔
76
                    throw new RuntimeException("invalid selection: " + ts);
×
77
                }
78
                for (int i = 0; i < tupleExpr.getExprs().size(); i++) {
1✔
79
                    ImExpr te = tupleExpr.getExprs().get(i);
1✔
80
                    de.peeeq.wurstscript.ast.Element trace = te.attrTrace();
1✔
81
                    te.setParent(null);
1✔
82
                    if (i != ti) {
1✔
83
                        // if not the thing we want to return, just keep it in statements for side-effects
84
                        extractSideEffect(te, stmts);
1✔
85
                    } else { // if it is the part we want to return ...
86
                        result = extractSideEffect(te, stmts);
1✔
87
                    }
88
                }
89
                assert result != null;
1✔
90
                ImStatementExpr replacement1 = JassIm.ImStatementExpr(stmts, result);
1✔
91
                ImLExpr replacement2 = normalizeStatementExpr(replacement1, tr);
1✔
92
                if (replacement2 == null) {
1✔
93
                    replacer.replace(ts, replacement1);
1✔
94
                } else {
95
                    replacer.replace(ts, replacement2);
1✔
96
                }
97
            }
1✔
98
        });
99
    }
1✔
100

101
    interface Step {
102
        void apply(ImStmts e, ImTranslator t, ImFunction f);
103
    }
104

105
    private static void tryStep(ImFunction f, final ImTranslator translator, Step step) {
106
        String before = f.toString();
1✔
107
        try {
108
            step.apply(f.getBody(), translator, f);
1✔
109
//            translator.assertProperties(Collections.emptySet(), f.getBody());
110
        } catch (Throwable t) {
×
111
            throw new RuntimeException("\n//// Before -----------\n" + before
×
112
                    + "\n\n// After -------------------\n" + f, t);
113
        }
1✔
114

115
    }
1✔
116

117

118
    private static Runnable transformVars(ImVars vars, ImTranslator translator) {
119
        Set<ImVar> varsToRemove = new LinkedHashSet<>();
1✔
120
        ListIterator<ImVar> it = vars.listIterator();
1✔
121
        while (it.hasNext()) {
1✔
122
            ImVar v = it.next();
1✔
123
            Preconditions.checkNotNull(v.getParent(), "null parent: " + v);
1✔
124
            if (TypesHelper.typeContainsTuples(v.getType())) {
1✔
125
                VarsForTupleResult varsForTuple = translator.getVarsForTuple(v);
1✔
126
                varsToRemove.add(v);
1✔
127
                for (ImVar nv : varsForTuple.allValues()) {
1✔
128
                    it.add(nv);
1✔
129
                }
1✔
130
            }
131
        }
1✔
132
        return () -> vars.removeAll(varsToRemove);
1✔
133
    }
134

135

136
    private static ImType getFirstType(ImType t) {
137
        if (t instanceof ImTupleType) {
1✔
138
            ImTupleType tt = (ImTupleType) t;
1✔
139
            return getFirstType(tt.getTypes().get(0));
1✔
140
        }
141
        return t;
1✔
142
    }
143

144
    /**
145
     * 1. replace tuples with tuple-expression
146
     * <p>
147
     * - Variable access
148
     * a --> <a_1, a_2, a_3>
149
     * - Function calls
150
     * f() --> <f(), temp_return1, temp_return2>
151
     * - Tuple selections
152
     * <e_1, e_2, e_3>.2 --> {e_1; temp = e_2; e_3 >> temp}
153
     * <e_1, e_2, e_3>.3 --> {e_1; e_2 >> e_3}
154
     * - ...
155
     */
156
    private static void toTupleExpressions(ImStmts body, ImTranslator translator, ImFunction f) {
157
        Replacer replacer = new Replacer();
1✔
158
        body.accept(new Element.DefaultVisitor() {
1✔
159
            @Override
160
            public void visit(ImVarAccess va) {
161
                if (va.attrTyp() instanceof ImTupleType) {
1✔
162
                    ImVar v = va.getVar();
1✔
163
                    VarsForTupleResult vars = translator.getVarsForTuple(v);
1✔
164
                    ImExpr expr = vars.<ImExpr>map(
1✔
165
                            parts -> JassIm.ImTupleExpr(
1✔
166
                                    parts.collect(Collectors.toCollection(JassIm::ImExprs))),
1✔
167
                            JassIm::ImVarAccess
168
                    );
169
                    replacer.replace(va, expr);
1✔
170
                }
171
            }
1✔
172

173
            @Override
174
            public void visit(ImVarArrayAccess va) {
175
                super.visit(va);
1✔
176
                if (va.attrTyp() instanceof ImTupleType) {
1✔
177
                    ImExprs indexes = va.getIndexes();
1✔
178
                    ImExprs indexExprs = JassIm.ImExprs();
1✔
179
                    ImStmts stmts = JassIm.ImStmts();
1✔
180
                    boolean sideEffects = indexes.stream().anyMatch(SideEffectAnalyzer::quickcheckHasSideeffects);
1✔
181
                    for (ImExpr ie : indexes) {
1✔
182
                        if (sideEffects) {
1✔
183
                            // use temp variables if there are side effects
184
                            ImVar tempIndex = JassIm.ImVar(ie.attrTrace(), TypesHelper.imInt(), "tempIndex", false);
1✔
185
                            indexExprs.add(JassIm.ImVarAccess(tempIndex));
1✔
186
                            f.getLocals().add(tempIndex);
1✔
187
                            ie.setParent(null);
1✔
188
                            stmts.add(JassIm.ImSet(va.attrTrace(), JassIm.ImVarAccess(tempIndex), ie));
1✔
189
                        } else {
1✔
190
                            ie.setParent(null);
1✔
191
                            indexExprs.add(ie);
1✔
192
                        }
193
                    }
1✔
194

195
                    ImVar v = va.getVar();
1✔
196
                    VarsForTupleResult vars = translator.getVarsForTuple(v);
1✔
197
                    ImExpr expr = vars.<ImExpr>map(
1✔
198
                            parts -> JassIm.ImTupleExpr(
1✔
199
                                    parts.collect(Collectors.toCollection(JassIm::ImExprs))),
1✔
200
                            var -> JassIm.ImVarArrayAccess(va.getTrace(), var, indexExprs.copy())
1✔
201
                    );
202
                    if (stmts.isEmpty()) {
1✔
203
                        replacer.replace(va, expr);
1✔
204
                    } else {
205
                        replacer.replace(va,
1✔
206
                                JassIm.ImStatementExpr(stmts,
1✔
207
                                        expr));
208
                    }
209
                }
210
            }
1✔
211

212

213
            @Override
214
            public void visit(ImFunctionCall fc) {
215
                super.visit(fc);
1✔
216
                if (translator.getOriginalReturnValue(fc.getFunc()) instanceof ImTupleType) {
1✔
217
                    Element parent = fc.getParent();
1✔
218
                    fc.setParent(null);
1✔
219

220
                    VarsForTupleResult returnVars = translator.getTupleTempReturnVarsFor(fc.getFunc());
1✔
221

222
                    ImVar firstVar = returnVars.allValuesStream().findFirst().get();
1✔
223

224
                    ImExpr newFc = returnVars.map(
1✔
225
                            parts -> JassIm.ImTupleExpr(
1✔
226
                                    parts.collect(Collectors.toCollection(JassIm::ImExprs))),
1✔
227
                            var -> var == firstVar
1✔
228
                                    ? fc.copy()
1✔
229
                                    : JassIm.ImVarAccess(var)
1✔
230
                    );
231

232
                    replacer.replaceInParent(parent, fc, newFc);
1✔
233
                }
234
            }
1✔
235

236
        });
237
    }
1✔
238

239

240
    /**
241
     * Normalize Tuples in statement-expressions (move to first tuple param)
242
     * {stmts >> <e1,e2,e3>}
243
     * becomes <{stmts >> e1}, e2, e3}
244
     */
245
    private static void normalizeTuplesInStatementExprs(ImStmts body, ImTranslator translator, ImFunction f) {
246
        Replacer replacer = new Replacer();
1✔
247
        body.accept(new Element.DefaultVisitor() {
1✔
248

249
            @Override
250
            public void visit(ImStatementExpr se) {
251
                super.visit(se);
1✔
252
                ImTupleExpr newExpr = normalizeStatementExpr(se, translator);
1✔
253
                if (newExpr != null) {
1✔
254
                    replacer.replace(se, newExpr);
1✔
255
                    newExpr.getExprs().get(0).accept(this);
1✔
256
                }
257
            }
1✔
258
        });
259
    }
1✔
260

261
    private static ImTupleExpr normalizeStatementExpr(ImStatementExpr se, ImTranslator translator) {
262
        if (se.getExpr() instanceof ImTupleExpr) {
1✔
263
            ImTupleExpr te = (ImTupleExpr) se.getExpr();
1✔
264
            translator.assertProperties(Collections.emptySet(), te);
1✔
265
            ImStmts seStmts = se.getStatements();
1✔
266
            seStmts.setParent(null);
1✔
267
            ImExpr firstExpr = te.getExprs().remove(0);
1✔
268
            ImStatementExpr newStatementExpr = JassIm.ImStatementExpr(seStmts, firstExpr);
1✔
269
            te.getExprs().add(0, newStatementExpr);
1✔
270
            te.setParent(null);
1✔
271
            translator.assertProperties(Collections.emptySet(), te.getExprs());
1✔
272
            return te;
1✔
273
        }
274
        return null;
1✔
275
    }
276

277
    /**
278
     * Remove tuple expressions
279
     * - In parameters: Just flatten
280
     * - Assignments: Become several assignments
281
     * - In Return: Use temp returns
282
     */
283
    private static void removeTupleExprs(int posHint, Element elem, ImTranslator translator, ImFunction f) {
284
        if (elem.getParent() == null) {
1✔
285
            throw new RuntimeException("elem not used: " + elem);
×
286
        }
287
        for (int i = 0; i < elem.size(); i++) {
1✔
288
            Element child = elem.get(i);
1✔
289
            removeTupleExprs(i, child, translator, f);
1✔
290
        }
291
        Replacer replacer = new Replacer();
1✔
292
        for (int i = 0; i < elem.size(); i++) {
1✔
293
            Element child = elem.get(i);
1✔
294

295
            if (child instanceof ImTupleExpr) {
1✔
296
                ImTupleExpr tupleExpr = (ImTupleExpr) child;
1✔
297

298
                Element newElem;
299
                if (elem instanceof ImTupleSelection) {
1✔
300
                    newElem = inTupleSelection((ImTupleSelection) elem, tupleExpr, f);
×
301
                } else if (elem instanceof ImReturn) {
1✔
302
                    newElem = inReturn((ImReturn) elem, tupleExpr, translator, f);
1✔
303
                } else if (elem instanceof ImSet) {
1✔
304
                    ImSet imSet = (ImSet) elem;
1✔
305
                    newElem = inSet(imSet, f);
1✔
306
                } else if (elem instanceof ImExprs) {
1✔
307
                    ImExprs exprs = (ImExprs) elem;
1✔
308
                    if (exprs.getParent() instanceof ImOperatorCall) {
1✔
309
                        ImOperatorCall opCall = (ImOperatorCall) exprs.getParent();
1✔
310
                        handleTupleInOpCall(replacer, opCall);
1✔
311
                        return;
1✔
312
                    } else {
313
                        // in function arguments, other tuples
314
                        // just flatten tuples
315
                        exprs.remove(i);
1✔
316
                        List<ImExpr> tupleExprs = tupleExpr.getExprs().removeAll();
1✔
317
                        exprs.addAll(i, tupleExprs);
1✔
318
                        i--;
1✔
319
                    }
320
                    continue;
1✔
321
                } else if (elem instanceof ImStmts) {
1✔
322
                    ImStmts stmts = (ImStmts) elem;
1✔
323
                    stmts.remove(i);
1✔
324
                    List<ImExpr> tupleExprs = tupleExpr.getExprs().removeAll();
1✔
325
                    stmts.addAll(i, tupleExprs);
1✔
326
                    i--;
1✔
327
                    continue;
1✔
328
                } else {
329
                    throw new CompileError(tupleExpr.attrTrace().attrSource(), "Unhandled tuple position: " + elem.getClass().getSimpleName() + " // " + elem);
×
330
                }
331
                replacer.hintPosition(posHint);
1✔
332
                replacer.replace(elem, newElem);
1✔
333
                // since we replaced elem we are done
334
                // the new element should have no more tuple expressions
335

336
                return;
1✔
337
            }
338

339
        }
340

341
    }
1✔
342

343
    private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall) {
344
        if (opCall.getParent() == null) {
1✔
345
            throw new RuntimeException("opCall not used: " + opCall);
×
346
        }
347
        ImTupleExpr left = (ImTupleExpr) opCall.getArguments().get(0);
1✔
348
        ImTupleExpr right = (ImTupleExpr) opCall.getArguments().get(1);
1✔
349
        WurstOperator op = opCall.getOp();
1✔
350

351
        List<ImExpr> componentComparisons = new ArrayList<>();
1✔
352
        for (int i = 0; i < left.getExprs().size(); i++) {
1✔
353
            ImExpr l = left.getExprs().get(i);
1✔
354
            ImExpr r = right.getExprs().get(i);
1✔
355
            l.setParent(null);
1✔
356
            r.setParent(null);
1✔
357
            componentComparisons.add(JassIm.ImOperatorCall(op, JassIm.ImExprs(l, r)));
1✔
358
        }
359

360
        ImExpr newExpr;
361
        if (op == WurstOperator.EQ) {
1✔
362
            // (x1,y1,z1) == (x2,y2,z2)
363
            // ==> x1 == x2 && y1 == y2 && z1 == z2
364
            newExpr = componentComparisons.stream()
1✔
365
                    .reduce((l, r) -> JassIm.ImOperatorCall(WurstOperator.AND, JassIm.ImExprs(l, r)))
1✔
366
                    .get();
1✔
367
        } else {
368
            assert op == WurstOperator.NOTEQ;
1✔
369
            // (x1,y1,z1) == (x2,y2,z2)
370
            // ==> x1 != x2 || y1 != y2 && z1 != z2
371
            newExpr = componentComparisons.stream()
1✔
372
                    .reduce((l, r) -> JassIm.ImOperatorCall(WurstOperator.OR, JassIm.ImExprs(l, r)))
1✔
373
                    .get();
1✔
374
        }
375
        replacer.replace(opCall, newExpr);
1✔
376
    }
1✔
377

378
    private static ImStatementExpr inSet(ImSet imSet, ImFunction f) {
379
        if (!(imSet.getLeft() instanceof ImTupleExpr && imSet.getRight() instanceof ImTupleExpr)) {
1✔
380
            throw new RuntimeException("invalid set statement:\n" + imSet);
×
381
        }
382
        ImTupleExpr left = (ImTupleExpr) imSet.getLeft();
1✔
383
        ImTupleExpr right = (ImTupleExpr) imSet.getRight();
1✔
384

385
        ImStmts stmts = JassIm.ImStmts();
1✔
386

387
        // 1) extract side effects from left expressions
388
        List<ImExpr> leftExprs = new ArrayList<>();
1✔
389
        for (ImExpr expr : left.getExprs()) {
1✔
390
            leftExprs.add(extractSideEffect(expr, stmts));
1✔
391
        }
1✔
392

393

394
        List<ImVar> tempVars = new ArrayList<>();
1✔
395
        // 2) assign right hand side to temporary variables:
396
        for (ImExpr expr : right.getExprs()) {
1✔
397
            ImVar temp = JassIm.ImVar(expr.attrTrace(), expr.attrTyp(), "tuple_temp", false);
1✔
398
            expr.setParent(null);
1✔
399
            stmts.add(JassIm.ImSet(expr.attrTrace(), JassIm.ImVarAccess(temp), expr));
1✔
400
            tempVars.add(temp);
1✔
401
            f.getLocals().add(temp);
1✔
402
        }
1✔
403
        // then assign right vars
404
        for (int i = 0; i < leftExprs.size(); i++) {
1✔
405
            ImLExpr leftE = (ImLExpr) leftExprs.get(i);
1✔
406
            leftE.setParent(null);
1✔
407
            stmts.add(JassIm.ImSet(imSet.getTrace(), leftE, JassIm.ImVarAccess(tempVars.get(i))));
1✔
408
        }
409
        return ImHelper.statementExprVoid(stmts);
1✔
410
    }
411

412
    private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, ImTranslator translator, ImFunction f) {
413
        VarsForTupleResult returnVars1 = translator.getTupleTempReturnVarsFor(f);
1✔
414
        List<ImVar> returnVars = returnVars1.allValuesStream().collect(Collectors.toList());
1✔
415
        ImStmts stmts = JassIm.ImStmts();
1✔
416

417
        for (int i = 0; i < returnVars.size(); i++) {
1✔
418
            ImVar rv = returnVars.get(i);
1✔
419
            ImExpr te = tupleExpr.getExprs().get(i);
1✔
420
            te.setParent(null);
1✔
421
            stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(rv), te));
1✔
422
        }
423
        stmts.add(JassIm.ImReturn(parent.getTrace(), JassIm.ImVarAccess(returnVars.get(0))));
1✔
424

425
        return ImHelper.statementExprVoid(stmts);
1✔
426
    }
427

428

429
    private static Element inTupleSelection(ImTupleSelection ts, ImTupleExpr tupleExpr, ImFunction f) {
430
        assert ts.getTupleExpr() == tupleExpr;
×
431

432
        int ti = ts.getTupleIndex();
×
433

434
        ImStmts stmts = JassIm.ImStmts();
×
435
        ImExpr result = null;
×
436

437

438
        for (int i = 0; i < tupleExpr.getExprs().size(); i++) {
×
439
            ImExpr te = tupleExpr.getExprs().get(i);
×
440
            de.peeeq.wurstscript.ast.Element trace = te.attrTrace();
×
441
            te.setParent(null);
×
442
            if (i != ti) {
×
443
                // if not the thing we want to return, just keep it in statements for side-effects
444
                stmts.add(te);
×
445
            } else { // if it is the part we want to return ...
446
                if (i == tupleExpr.getExprs().size() - 1) {
×
447
                    // last expression of tuple
448
                    result = te;
×
449
                } else {
450
                    if (ts.isUsedAsLValue()) {
×
451
                        // if this is used as L-value we cannot use temporary variables, so just
452
                        // use the current expression as result.
453
                        // This assumes that the expression te cannot be influenced by subsequent expressions
454
                        // TODO maybe this assumption should be validated ...
455
                        result = extractSideEffect(te, stmts);
×
456
                    } else {
457
                        ImVar temp = JassIm.ImVar(trace, te.attrTyp(), "tupleSelection", false);
×
458
                        f.getLocals().add(temp);
×
459
                        stmts.add(JassIm.ImSet(trace, JassIm.ImVarAccess(temp), te));
×
460
                        result = JassIm.ImVarAccess(temp);
×
461
                    }
462
                }
463
            }
464
        }
465
        assert result != null;
×
466

467
        return JassIm.ImStatementExpr(stmts, result);
×
468
    }
469

470
    /**
471
     * extracts all side effects into the list of statements
472
     */
473
    private static ImExpr extractSideEffect(ImExpr e, List<ImStmt> into) {
474
        if (e instanceof ImStatementExpr) {
1✔
475
            ImStatementExpr se = (ImStatementExpr) e;
1✔
476
            for (ImStmt s : se.getStatements()) {
1✔
477
                s.setParent(null);
1✔
478
                into.add(s);
1✔
479
            }
1✔
480
            ImExpr expr = se.getExpr();
1✔
481
            expr.setParent(null);
1✔
482
            return extractSideEffect(expr, into);
1✔
483
        } else if (e instanceof ImTupleExpr) {
1✔
484
            ImTupleExpr te = (ImTupleExpr) e;
1✔
485
            if (!te.getExprs().isEmpty()) {
1✔
486
                ImExpr firstExpr = te.getExprs().get(0);
1✔
487
                ImExpr newFirstExpr = extractSideEffect(firstExpr, into);
1✔
488
                if (newFirstExpr != firstExpr) {
1✔
489
                    te.getExprs().set(0, newFirstExpr);
1✔
490
                }
491
            }
492
        }
493
        return e;
1✔
494
    }
495

496

497
    private static ImExprs accessVars(List<ImVar> tempIndexes) {
498
        return tempIndexes.stream()
×
499
                .map(JassIm::ImVarAccess)
×
500
                .collect(Collectors.toCollection(JassIm::ImExprs));
×
501
    }
502

503

504
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc