diff --git a/docs/SPEC.md b/docs/SPEC.md index ee8b217e..ec0edc75 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -892,9 +892,16 @@ All in `PM/commonTest`, so they run wherever the interpreter loads. `min(a, b)` compares with `<` and keeps the first element in iteration order on ties (including `+0.0` versus `-0.0`), NaN behaviour depends on position because comparisons with NaN are false, and chunked combination must reproduce that chain bit for bit; if an implementation cannot, they - are left to a later step. Step 2 implements `+` and `*` only (min/max follow in step 3 below): the compiled subset has no bitwise - operator (the front end skips a function using `&`, `|` or `^` on ints, there is no IR operation), - so `& | ^` are left until the subset has them. + are left to a later step. Step 2 implements `+` and `*` (min/max follow in step 3, `& | ^` in step 4 below). + **Bitwise (step 4).** `acc = acc & e`, `acc | e`, `acc ^ e` (either operand order; the augmented + `acc &= e` lowers to the same shape) on an i64 accumulator, with `e` the step 2 i64 expression set + extended with the N-16 operators `& | ^` and `~`. Shifts are not accepted in `e`: their overflow + and negative-count deopt is not a worker failure, so such a loop stays sequential. A chunk folds + from the identity (-1 for `&`, 0 for `|` and `^`) and the partials combine in chunk order; these + operators are exactly associative and commutative and never overflow, so the combination cannot + fail. Only an overflow inside `e` (for example `xs[i] * c`) discards the region and reruns the loop + sequentially, as for `+`. They may share a loop with `+ *` and min/max accumulators. The accumulator + rules are step 2's (read nowhere else in the body, one operator per accumulator). **`min`/`max` (step 3).** The compiled subset has no `min(a, b)` / `max(a, b)` call (it stays a generic object call, sequential). What it does compile is Python's own definition of `min(acc, e)`, the conditional update `if e < acc: acc = e` (`e > acc` for max; `acc > e` and @@ -941,7 +948,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads. chunk, NaN/inf/-0.0), identical bytes for every thread count, failure at first/middle/last iteration, negative qualification cases, disabled-path test, reductions, race and GC tests, TSan/ASan, N-14, signals, fork and shutdown, and measurement tests for the threshold. - `Status: steps 1, 2 and 3a implemented (#277): element-wise F64 stores a[i] = into list copy-in or local arrays (no failure path: eligible loops cannot fail or deopt); i64 reductions acc += e and acc *= e with the overflow rerun above (stores and reductions never in one loop); i64 and f64 min/max as `if e < acc: acc = e` (f64 with a NaN reruns sequentially); fork, signal and shutdown tests; & | ^ reductions, float + and * reductions, the poll inside a region and the rest planned (design)`. + `Status: steps 1, 2, 3a and 4 implemented (#277): element-wise F64 stores a[i] = into list copy-in or local arrays (no failure path: eligible loops cannot fail or deopt); i64 reductions acc += e and acc *= e with the overflow rerun above (stores and reductions never in one loop); i64 and f64 min/max as `if e < acc: acc = e` (f64 with a NaN reruns sequentially); i64 bitwise reductions acc &= e, |= e, ^= e (identity -1 for &, 0 for | and ^; no shifts in e; test_parallel_loops.py bitwise tests); fork, signal and shutdown tests; float + and * reductions, the poll inside a region and the rest planned (design)`. - **N-16** TypedPython bitwise operators on ints (#289; INTENT ยง1.4: compiled code computes what CPython computes). `&`, `|`, `^`, unary `~`, `<<`, `>>` and the augmented forms `&= |= ^= <<= >>=` are in the compiled diff --git a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py index 3f13c31d..e26730ee 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py @@ -2188,7 +2188,7 @@ def key(x): continue v = st.value if isinstance(st, ir.Assign) else None if (not isinstance(v, ir.BinOp) or st.target != acc or v.op.name != opn - or opn not in ("ADD", "MUL") or v.type is not Type.I64 + or opn not in ("ADD", "MUL", "AND", "OR", "XOR") or v.type is not Type.I64 or self.types.get(acc) is not Type.I64 or acc == s.var): raise CGenError(f"{f.name}: parallel reduction statement is not `acc = acc op e`") if isinstance(v.left, ir.Local) and v.left.name == acc: @@ -2282,7 +2282,7 @@ def key(x): " uint64_t tp_k;", *decls] if reds: out += [" int tp_rc = 0;", " int tp_s = 0;"] - out += [f" {_CT[self.types[acc]]} {_ident('l', acc)} = {'1' if opn == 'MUL' else '0'};" + out += [f" {_CT[self.types[acc]]} {_ident('l', acc)} = { {'MUL': '1', 'AND': '-1'}.get(opn, '0') };" for acc, opn, _ in reds] out += [f" int tp_has{j} = 0;" for j, (_, opn, _) in enumerate(reds) if opn in ("MIN", "MAX")] out.append(" (void)tp_rc; (void)tp_s;") diff --git a/python-multiplatform-ksp/src/main/python/typedpython/ir.py b/python-multiplatform-ksp/src/main/python/typedpython/ir.py index 7dab13c5..f367380b 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/ir.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/ir.py @@ -674,7 +674,7 @@ class ParInfo: read: tuple[str, ...] weight: int = 1 # Step 2 (#277): the i64 reductions `acc = acc op e` of a loop whose body is only such statements, - # as sorted (acc, BinOpKind name) pairs, op ADD or MUL. Then `stored` is empty. Each chunk computes + # as sorted (acc, BinOpKind name) pairs, op ADD, MUL, AND, OR or XOR (min/max as MIN, MAX). Then `stored` is empty. Each chunk computes # its partial from the op's identity with the sequential code's overflow checks; an overflow in any # chunk or in the combination discards the region and the sequential loop runs from its start. reduce: tuple[tuple[str, str], ...] = () diff --git a/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md b/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md index c75482d4..3426d8e2 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md +++ b/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md @@ -497,7 +497,7 @@ for the epoch's increment. `tp_runtime.h` needs the GCC/Clang `__atomic` builtin ## Parallel regions (SPEC N-15, issues #151, #277) A `ForRange` the verifier marked `par` (`ir.ParInfo`; marked function, `a[i] = F64 expression` stores -or i64 `+ *` reductions, or min/max `if e < acc: acc = e` on an i64 or f64) runs its iterations on per-region C threads above a work threshold. Workers touch no Python +or i64 `+ * & | ^` reductions, or min/max `if e < acc: acc = e` on an i64 or f64) runs its iterations on per-region C threads above a work threshold. Workers touch no Python object and no Python API, are not Python threads and never outlive the call. #define TP_PAR_MIN_WORK 65536 /* PLACEHOLDER, to be measured on #151: weighted iterations (trip count * ParInfo.weight) */ #define TP_PAR_MAX_THREADS 8 /* default cap; TYPEDPYTHON_THREADS overrides (1 disables), hard cap 64 */ @@ -512,7 +512,7 @@ object and no Python API, are not Python threads and never outlive the call. void tp_par_stats(int64_t out[4]); /* regions with workers, workers started, failed creations, reduction reruns */ #define TP_PAR_NONE 0 / TP_PAR_OK 1 / TP_PAR_BAD 2 /* per-chunk state of a reduction chunk (zero-initialised array of TP_PAR_HARD_CAP) */ #define TP_PAR_OP_ADD 0 / TP_PAR_OP_MUL 1 - int tp_par_combine(int op, int64_t *acc, const int64_t *part, const unsigned char *st); /* acc op= part[0], part[1], ... in chunk order, checked; 1 if a chunk was BAD or it overflowed (acc untouched) */ + int tp_par_combine(int op, int64_t *acc, const int64_t *part, const unsigned char *st); /* op: TP_PAR_OP_ADD/MUL/AND/OR/XOR; acc op= part[0], part[1], ... in chunk order, checked (AND/OR/XOR never overflow); 1 if a chunk was BAD or it overflowed (acc untouched) */ void tp_par_rerun_note(void); /* counts a discarded reduction region */ `tp_par_run` always completes the whole range. The calling thread runs the first chunk; a chunk whose `pthread_create` failed runs on the calling thread after it (fewer workers, down to none: the sequential diff --git a/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h b/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h index 64e32696..159ad2a1 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h +++ b/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h @@ -3170,8 +3170,14 @@ static inline int tp_par_run(tp_par_fn fn, const void *ctx, uint64_t n) #define TP_PAR_BAD 2 #define TP_PAR_OP_ADD 0 #define TP_PAR_OP_MUL 1 +#define TP_PAR_OP_AND 2 +#define TP_PAR_OP_OR 3 +#define TP_PAR_OP_XOR 4 -static inline int64_t tp_par_identity(int op) { return op == TP_PAR_OP_MUL ? 1 : 0; } +static inline int64_t tp_par_identity(int op) +{ + return op == TP_PAR_OP_MUL ? 1 : op == TP_PAR_OP_AND ? -1 : 0; +} /* acc = acc op part[0] op part[1] ... in chunk order, checked like the sequential code. Returns 0 and * stores the result in *acc, or 1 (a chunk failed or the combination overflowed) leaving *acc alone. */ @@ -3185,6 +3191,10 @@ static inline int tp_par_combine(int op, int64_t *acc, const int64_t *part, cons } } for (k = 0; k < TP_PAR_HARD_CAP && st[k] == TP_PAR_OK; k++) { + if (op >= TP_PAR_OP_AND) { /* bitwise: exact, never overflows */ + v = op == TP_PAR_OP_AND ? (v & part[k]) : op == TP_PAR_OP_OR ? (v | part[k]) : (v ^ part[k]); + continue; + } if (op == TP_PAR_OP_MUL ? tp_mul_i64(v, part[k], &w) : tp_add_i64(v, part[k], &w)) { return 1; } diff --git a/python-multiplatform-ksp/src/main/python/typedpython/verify.py b/python-multiplatform-ksp/src/main/python/typedpython/verify.py index 8f8db7a1..6afd8667 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/verify.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/verify.py @@ -215,13 +215,13 @@ distinct lists by the entry guard, local arrays are distinct allocations), and a read of the stored array itself is the same element the iteration writes. * Step 2 (reductions): or the body is a non-empty sequence of `acc = acc op e` (`_par_reduce`): op - ADD or MUL, each `acc` a distinct i64 local, definitely assigned before the loop, not an array, + ADD, MUL, AND, OR or XOR, each `acc` a distinct i64 local, definitely assigned before the loop, not an array, loop variable or entry global, and read nowhere else in the body (not in any `e`); `e` built from i64 Const, i64 Local (the loop variable, or a scalar nothing in the body assigns), `b[var]` - proven on an i64 non-buffer list or local array, and i64 + - *. Stores and reductions are never + proven on an i64 non-buffer list or local array, and i64 + - * & | ^ and ~ (step 4; no shifts). Stores and reductions are never mixed in one loop. Those nodes can overflow, which is not a failure of the region: the runtime - discards it and reruns the loop sequentially. (& | ^ are not in the compiled subset: the front - end lowers no bitwise operator, so there is no such reduction to mark.) + discards it and reruns the loop sequentially. Step 4: acc & | ^ e never overflow (identity -1 for &, + 0 for | and ^); only an overflow inside `e` reruns. * Step 3 (min/max): a statement of the reduction body may be `if e < acc: acc = e` / `e > acc` (either operand order, no else), `acc` an i64 or f64 local, `e` of its type (`_par_minmax`). That is Python's `min(acc, e)`; the f64 NaN case reruns sequentially at run time (SPEC N-15). @@ -310,6 +310,12 @@ PAR_LOOP = "verify/par-loop" +# SPEC N-15 steps 2 and 4: the reduction operators, and the i64 operators allowed in an element expression +# (no shifts: their deopt is not a worker failure). +_PAR_RED_OPS = (BinOpKind.ADD, BinOpKind.MUL, BinOpKind.AND, BinOpKind.OR, BinOpKind.XOR) +_PAR_I64_OPS = (BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL, BinOpKind.AND, BinOpKind.OR, BinOpKind.XOR) + + @dataclass(frozen=True) class Diagnostic: function: str @@ -1754,7 +1760,7 @@ def _par_reduce(self, s: ForRange, body: tuple, state: _State, weight: list, arr mm[hit[0]] = hit[2] continue if (type(st) is not Assign or type(st.value) is not BinOp or st.value.type is not Type.I64 - or st.value.op not in (BinOpKind.ADD, BinOpKind.MUL) or st.value.np + or st.value.op not in _PAR_RED_OPS or st.value.np or st.target in accs): return None accs[st.target] = st.value.op.name @@ -1781,8 +1787,10 @@ def ok(e: object) -> bool: return False if t is BinOp: return (e.type is Type.I64 and e.left.type is Type.I64 and e.right.type is Type.I64 - and e.op in (BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL) and not e.np + and e.op in _PAR_I64_OPS and not e.np and ok(e.left) and ok(e.right)) + if t is UnaryOp: # ~x of an i64: cannot fail (SPEC N-16) + return e.type is Type.I64 and e.op is UnaryOpKind.INVERT and ok(e.operand) return False def reads_acc(e: object) -> bool: diff --git a/python-multiplatform-ksp/src/test/python/test_parallel_loops.py b/python-multiplatform-ksp/src/test/python/test_parallel_loops.py index 80fd6bf2..b7591fe5 100644 --- a/python-multiplatform-ksp/src/test/python/test_parallel_loops.py +++ b/python-multiplatform-ksp/src/test/python/test_parallel_loops.py @@ -1470,3 +1470,310 @@ def spin(): sys.stdout.flush() """ % BIG, _threads_env(), timeout=300) assert r == {"exiting": True} + + +# ===================================================================================================== +# N-15 step 4: i64 bitwise reductions `acc = acc & e`, `| e`, `^ e` (#277). +# +# Written before the implementation (red). `& | ^` are exactly associative and commutative and never +# overflow: a chunk folds from the identity (-1 for &, 0 for | and ^) and the partials combine in chunk +# order. An overflow in an element expression (`xs[i] * c`) still discards the region and reruns the +# loop sequentially. Shifts are not accepted in `e` (their deopt is not a worker failure in v1). +# Red record: before the change none of the BOK loops was `par` (_par_reduce accepted ADD/MUL only). +# ===================================================================================================== + +BSOURCE = '''\ +# typedpython: compiled + +# typedpython: parallel +def b_and(xs: list[int]) -> int: + s: int = -1 + for i in range(len(xs)): + s &= xs[i] + return s + + +# typedpython: parallel +def b_or(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s |= xs[i] + return s + + +# typedpython: parallel +def b_xor(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s ^= xs[i] + return s + + +# typedpython: parallel +def b_and_swapped(xs: list[int], s0: int) -> int: + s: int = s0 + for i in range(len(xs)): + s = xs[i] & s + return s + + +# typedpython: parallel +def b_or_swapped(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s = xs[i] | s + return s + + +# typedpython: parallel +def b_xor_expr(xs: list[int], c: int) -> int: + s: int = 0 + for i in range(len(xs)): + s = s ^ ((xs[i] * c) + (i & 255)) + return s + + +# typedpython: parallel +def b_or_not(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s |= ~xs[i] + return s + + +# typedpython: parallel +def b_mix(xs: list[int]) -> int: + a: int = 0 + x: int = 5 + o: int = 0 + m: int = 1000000 + for i in range(len(xs)): + a += xs[i] + x ^= xs[i] & 4095 + o |= xs[i] + if xs[i] < m: + m = xs[i] + return a + x * 3 + o * 5 + m * 7 + + +# typedpython: parallel +def b_local(n: int) -> int: + t = [0] * n + for i in range(len(t)): + t[i] = i * 7 - 3 + s: int = 0 + for i in range(len(t)): + s ^= t[i] + return s + + +def b_unmarked(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s ^= xs[i] + return s + + +# typedpython: parallel +def nb_acc_read(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s |= xs[i] ^ s + return s + + +# typedpython: parallel +def nb_two_ops(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s |= xs[i] + s &= xs[i] + return s + + +# typedpython: parallel +def nb_and_then_add(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s ^= xs[i] + s += 1 + return s + + +# typedpython: parallel +def nb_shift(xs: list[int]) -> int: + s: int = 0 + for i in range(len(xs)): + s ^= xs[i] << 1 + return s + + +# typedpython: parallel +def nb_rshift(xs: list[int], k: int) -> int: + s: int = 0 + for i in range(len(xs)): + s |= xs[i] >> k + return s + + +# typedpython: parallel +def nb_copy(xs: list[int]) -> int: + s: int = 0 + w: int = 0 + for i in range(len(xs)): + s ^= xs[i] + w = s + return s + w + + +# typedpython: parallel +def nb_div(xs: list[int], d: int) -> int: + s: int = 0 + for i in range(len(xs)): + s ^= xs[i] // d + return s +''' + +BOK = { + "b_and": (("s", "AND"),), "b_or": (("s", "OR"),), "b_xor": (("s", "XOR"),), + "b_and_swapped": (("s", "AND"),), "b_or_swapped": (("s", "OR"),), "b_xor_expr": (("s", "XOR"),), + "b_or_not": (("s", "OR"),), "b_local": (("s", "XOR"),), + "b_mix": (("a", "ADD"), ("m", "MIN"), ("o", "OR"), ("x", "XOR")), +} +BNO = ["nb_acc_read", "nb_two_ops", "nb_and_then_add", "nb_shift", "nb_rshift", "nb_copy", "nb_div"] + + +@pytest.fixture(scope="module") +def bbuilt(tmp_path_factory): + return build(tmp_path_factory.mktemp("bpar"), BSOURCE, name="bparmod") + + +@pytest.fixture(scope="module") +def bmod(bbuilt): + return cbuild.load("bparmod", bbuilt[2]) + + +@pytest.mark.parametrize("name", sorted(BOK)) +def test_bitwise_reduction_loops_are_marked_par(bbuilt, name): + loops = [l for l in par_loops(fn(bbuilt, name)) if l.par is not None] + assert loops, name + assert loops[-1].par.reduce == BOK[name] and loops[-1].par.stored == () + + +@pytest.mark.parametrize("name", BNO) +def test_ineligible_bitwise_shapes_stay_sequential(bbuilt, name): + f = fn(bbuilt, name) + assert f.parallel is True + loops = par_loops(f) + assert loops and all(l.par is None for l in loops), name + + +def test_unmarked_bitwise_never_parallel(bbuilt): + assert all(l.par is None for l in par_loops(fn(bbuilt, "b_unmarked"))) + + +def test_bitwise_c_regions_only_for_eligible(bbuilt): + import re + owners = set(re.findall(r"static void tp_par_(\w+?)(?:_fast)?_\d+_body\(", bbuilt[1])) + assert owners == set(BOK), owners + + +def test_verifier_rejects_a_forged_bitwise_reduction(bbuilt): + good = par_loops(fn(bbuilt, "b_xor"))[-1].par + proved, diags = _forge(bbuilt, "b_xor", dataclasses.replace(good, reduce=(("s", "OR"),))) + assert not proved.functions and any(d.rule == PAR_RULE for d in diags) + proved, diags = _forge(bbuilt, "b_xor", dataclasses.replace(good, reduce=())) + assert not proved.functions and any(d.rule == PAR_RULE for d in diags) + for name, op in (("nb_acc_read", "OR"), ("nb_two_ops", "OR"), ("nb_shift", "XOR"), ("nb_rshift", "OR"), + ("nb_copy", "XOR"), ("nb_and_then_add", "XOR")): + proved, diags = _forge(bbuilt, name, ir.ParInfo((), (), 3, (("s", op),))) + assert not proved.functions and any(d.rule == PAR_RULE for d in diags), name + proved, diags = _forge(bbuilt, "b_xor", good) + assert proved.functions and not diags + + +def _bsame(m, name, *args): + want = m.__typedpython_interpreted__[name](*[list(a) if isinstance(a, list) else a for a in args]) + got = getattr(m, name)(*[list(a) if isinstance(a, list) else a for a in args]) + assert got == want and type(got) is type(want), (name, got, want) + + +@pytest.mark.parametrize("n", RSIZES) +def test_bitwise_reductions_match_interpreter(bmod, n): + for lo, hi in ((-1000, 1000), (I64_MIN, I64_MAX), (0, 255)): + xs = rdata(n, 3, lo, hi) + for nm in ("b_and", "b_or", "b_xor", "b_or_swapped", "b_or_not", "b_mix"): + _bsame(bmod, nm, xs) + _bsame(bmod, "b_and_swapped", xs, -1) + _bsame(bmod, "b_and_swapped", xs, 0x0F0F0F0F0F0F0F0F) + _bsame(bmod, "b_xor_expr", xs, 1) + _bsame(bmod, "b_local", n) + + +def test_bitwise_identity_edge_values(bmod): + n = 300001 + _bsame(bmod, "b_and", [-1] * n) + _bsame(bmod, "b_and", [I64_MIN] * n) + _bsame(bmod, "b_or", [I64_MIN] * n) + _bsame(bmod, "b_xor", [I64_MAX] * n) + _bsame(bmod, "b_xor", [I64_MAX] * (n - 1)) + z = [-1] * n + z[n // 2] = 0 + _bsame(bmod, "b_and", z) + + +def test_bitwise_region_counter_and_no_rerun(bmod): + if (os.cpu_count() or 1) < 2 or os.environ.get("TYPEDPYTHON_THREADS") == "1": + pytest.skip("single thread") + before = bmod.__typedpython_parallel__() + xs = rdata(300001, 4, I64_MIN, I64_MAX) + bmod.b_and(list(xs)); bmod.b_or(list(xs)); bmod.b_xor(list(xs)) + after = bmod.__typedpython_parallel__() + assert after[0] - before[0] == 3 and after[3] == before[3] + + +def test_bitwise_element_expression_overflow_reruns_and_equals_python(bmod): + n = 300001 + xs = [1] * n + xs[n // 2] = 2**40 + before = bmod.__typedpython_parallel__()[3] + _bsame(bmod, "b_xor_expr", xs, 2**30) # 2**70 overflows i64: rerun, then promote + if (os.cpu_count() or 1) > 1 and os.environ.get("TYPEDPYTHON_THREADS") != "1": + assert bmod.__typedpython_parallel__()[3] > before + + +BCHILD_BODY = """ + import random + I = m.__typedpython_interpreted__ + bad = [] + def check(name, *args): + want = I[name](*[list(a) if isinstance(a, list) else a for a in args]) + got = getattr(m, name)(*[list(a) if isinstance(a, list) else a for a in args]) + if not (got == want and type(got) is type(want)): + bad.append([name, repr(got), repr(want)]) + r = random.Random(9) + n = 300001 + for lo, hi in ((-1000, 1000), (-2**63, 2**63 - 1), (0, 255)): + xs = [r.randint(lo, hi) for _ in range(n)] + for nm in ("b_and", "b_or", "b_xor", "b_or_swapped", "b_or_not", "b_mix"): + check(nm, xs) + check("b_and_swapped", xs, -1); check("b_xor_expr", xs, 3) + for k in (0, 1, 2, 3, 7, 1000): + for nm in ("b_and", "b_or", "b_xor", "b_mix"): + check(nm, xs[:k]) + ov = [1] * n; ov[n // 2] = 2**40 + check("b_xor_expr", ov, 2**30) + check("b_local", n) + par = m.__typedpython_parallel__() + print(json.dumps({"bad": bad, "regions": par[0], "reruns": par[3]})) +""" + + +@pytest.mark.parametrize("threads", [1, 2, 3, 7]) +def test_bitwise_equals_interpreter_for_every_thread_count(bbuilt, threads): + r = run_child(bbuilt[2], BCHILD_BODY, {"TYPEDPYTHON_THREADS": str(threads)}, timeout=900) + assert r["bad"] == [], r["bad"][:5] + if threads == 1: + assert r["regions"] == 0 and r["reruns"] == 0 + elif (os.cpu_count() or 1) > 1: + assert r["regions"] > 0 and r["reruns"] >= 1