slicecompose 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,872 @@
1
+ """Exact composition of Python slices.
2
+
3
+ ``compose(s1, s2)`` returns a slice ``s3`` such that ``arr[s1][s2] == arr[s3]``
4
+ for *every* sequence ``arr`` (equivalently, for every length ``n >= 0``), or
5
+ ``None`` if no such slice exists. Both directions are exact:
6
+
7
+ - soundness: a returned slice is equivalent for all ``n`` (not just tested ones),
8
+ - completeness: ``None`` is returned only if no equivalent slice exists.
9
+
10
+ ``SliceChain(*slices)`` holds a sequence of slices applied in order and reduces
11
+ adjacent pairs with ``compose`` until no adjacent pair merges.
12
+
13
+ ``SlicePlan`` (with the ready-made empty plan ``s_``) gathers slices lazily
14
+ through ordinary bracket notation -- ``s_[2:][3:]`` -- and reduces its stored
15
+ chain in place on first query.
16
+
17
+ See EXPLANATION.md for the full soundness/completeness argument. In brief:
18
+
19
+ - The action of a slice on a length-``n`` sequence is the index progression
20
+ ``A(n) + k*j`` for ``j < L(n)``. For a composition, ``A``/``L`` are, on each
21
+ of finitely many computable intervals of ``n`` ("stretches"), either affine,
22
+ an exact ceiling-division staircase, or affine plus a period-``|step1|``
23
+ sawtooth. Single slices produce the same forms without the sawtooth.
24
+ - Equality of two such functions on a stretch is decided exactly from a small
25
+ set of probe evaluations (endpoints, support/length thresholds, staircase
26
+ jump alignment) -- valid for arbitrarily large field values, no enumeration.
27
+ - If an equivalent slice exists, its anchor structure (front/back for start
28
+ and stop, step sign) and its parameters are forced by the composition's tail
29
+ behavior, staircase jump phase, and support boundaries; the generator emits
30
+ the forced candidates (plus tolerance wiggles) and the verifier filters.
31
+
32
+ Standard library only.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ __all__ = ["compose", "SlicePlan", "s_", "SliceChain"]
38
+
39
+ _INF = None # sentinel for "unbounded stretch end"
40
+
41
+
42
+ # --------------------------------------------------------------------------
43
+ # Ground-truth evaluation (exact Python slice semantics, arbitrary ints)
44
+ # --------------------------------------------------------------------------
45
+
46
+
47
+ def _norm3(s):
48
+ """slice -> (start, stop, step) with step a nonzero int."""
49
+ if not isinstance(s, slice):
50
+ raise TypeError(f"expected a slice, got {type(s).__name__}")
51
+ a, b, k = s.start, s.stop, s.step
52
+ k = 1 if k is None else int(k)
53
+ if k == 0:
54
+ raise ValueError("slice step cannot be zero")
55
+ a = None if a is None else int(a)
56
+ b = None if b is None else int(b)
57
+ return a, b, k
58
+
59
+
60
+ def _to_slice(t):
61
+ a, b, k = t
62
+ return slice(a, b, None if k == 1 else k)
63
+
64
+
65
+ def _indices(t, n):
66
+ """Exact equivalent of slice(*t).indices(n) for arbitrary ints."""
67
+ a, b, k = t
68
+ if k > 0:
69
+ if a is None:
70
+ a2 = 0
71
+ elif a >= 0:
72
+ a2 = a if a < n else n
73
+ else:
74
+ a2 = a + n if a + n > 0 else 0
75
+ if b is None:
76
+ b2 = n
77
+ elif b >= 0:
78
+ b2 = b if b < n else n
79
+ else:
80
+ b2 = b + n if b + n > 0 else 0
81
+ else:
82
+ if a is None:
83
+ a2 = n - 1
84
+ elif a >= 0:
85
+ a2 = a if a < n else n - 1
86
+ else:
87
+ a2 = a + n if a + n > -1 else -1
88
+ if b is None:
89
+ b2 = -1
90
+ elif b >= 0:
91
+ b2 = b if b < n else n - 1
92
+ else:
93
+ b2 = b + n if b + n > -1 else -1
94
+ return a2, b2, k
95
+
96
+
97
+ def _eval1(t, n):
98
+ """(first_index, step, length) of slice-triple t applied to length n."""
99
+ a, b, k = _indices(t, n)
100
+ if k > 0:
101
+ length = (b - a + k - 1) // k
102
+ else:
103
+ length = (b - a + k + 1) // k
104
+ return a, k, (length if length > 0 else 0)
105
+
106
+
107
+ def _eval2(t1, t2, n):
108
+ """(first_index, step, length) of applying t1 then t2 to length n."""
109
+ a1, k1, m = _eval1(t1, n)
110
+ a2, k2, length = _eval1(t2, m)
111
+ return a1 + k1 * a2, k1 * k2, length
112
+
113
+
114
+ def _pt_eq(x, y):
115
+ """Do two (first, step, length) actions select the same elements?"""
116
+ a1, k1, n1 = x
117
+ a2, k2, n2 = y
118
+ if n1 != n2:
119
+ return False
120
+ if n1 == 0:
121
+ return True
122
+ if a1 != a2:
123
+ return False
124
+ return n1 == 1 or k1 == k2
125
+
126
+
127
+ # --------------------------------------------------------------------------
128
+ # Mirror reduction: arr[::-1][u] == arr[mirror(u)] for every u and arr.
129
+ # Mirroring start/stop fields is bitwise-not (None stays None), step negates.
130
+ # Applying it to s1 reduces every problem to step1 >= 1:
131
+ # s1 = rev ; mirror(s1) hence s1 . s2 merges iff mirror(s1) . s2 does,
132
+ # and the merged results correspond via mirror as well.
133
+ # --------------------------------------------------------------------------
134
+
135
+
136
+ def _mirror(t):
137
+ a, b, k = t
138
+ return (None if a is None else ~a, None if b is None else ~b, -k)
139
+
140
+
141
+ # --------------------------------------------------------------------------
142
+ # Monotone searches (predicates guaranteed monotone by the structure lemmas)
143
+ # --------------------------------------------------------------------------
144
+
145
+
146
+ def _first_true(lo, hi, pred):
147
+ """Smallest n in [lo, hi] with pred(n), else None; pred monotone F->T."""
148
+ if lo > hi:
149
+ return None
150
+ if pred(lo):
151
+ return lo
152
+ if not pred(hi):
153
+ return None
154
+ while hi - lo > 1:
155
+ mid = (lo + hi) // 2
156
+ if pred(mid):
157
+ hi = mid
158
+ else:
159
+ lo = mid
160
+ return hi
161
+
162
+
163
+ def _last_true(lo, hi, pred):
164
+ """Largest n in [lo, hi] with pred(n), else None; pred monotone T->F."""
165
+ if lo > hi:
166
+ return None
167
+ if pred(hi):
168
+ return hi
169
+ if not pred(lo):
170
+ return None
171
+ while hi - lo > 1:
172
+ mid = (lo + hi) // 2
173
+ if pred(mid):
174
+ lo = mid
175
+ else:
176
+ hi = mid
177
+ return lo
178
+
179
+
180
+ # --------------------------------------------------------------------------
181
+ # Breakpoints and stretches.
182
+ #
183
+ # Every regime boundary of a slice's normalized (start, stop, length) as a
184
+ # function of the sequence length is of the form x + y (+/- 2 guard) where
185
+ # x, y range over {0, +-field values}: each boundary is one comparison between
186
+ # two clamped anchor forms, each involving at most one field. We therefore
187
+ # take all pairwise combinations -- a generous, provably sufficient superset.
188
+ #
189
+ # For the composed map the intermediate length m(n) = len1(n) feeds slice s2,
190
+ # so s2's critical values live in m-space and are pulled back to n-space by
191
+ # monotone search (m is monotone between s1's own breakpoints, and changes by
192
+ # at most 1 per step, so it cannot skip a critical value).
193
+ # --------------------------------------------------------------------------
194
+
195
+
196
+ def _combo_vals(t):
197
+ """Pairwise +-field combinations (with guards) for one slice triple."""
198
+ base = {0, 1, 2, 3}
199
+ for f in (t[0], t[1]):
200
+ if f is not None:
201
+ base.add(f)
202
+ base.add(-f)
203
+ vals = set()
204
+ for x in base:
205
+ for y in base:
206
+ for d in (-2, -1, 0, 1, 2):
207
+ vals.add(x + y + d)
208
+ return vals
209
+
210
+
211
+ def _magnitude(t1, t2):
212
+ m = 8
213
+ for t in (t1, t2):
214
+ for f in t:
215
+ if f is not None:
216
+ m += abs(f)
217
+ return m
218
+
219
+
220
+ def _pullback_breaks(t1, t2):
221
+ """n-space breakpoints of the composed action of (t1, t2), t1.step >= 1."""
222
+ q = t1[2]
223
+ n_breaks = {v for v in _combo_vals(t1) if v >= 0}
224
+ v_set = _combo_vals(t2)
225
+ v_max = max(abs(v) for v in v_set)
226
+ mag = _magnitude(t1, t2)
227
+ span = q * (v_max + 4) + 4 * mag + 64
228
+
229
+ def m_of(n):
230
+ return _eval1(t1, n)[2]
231
+
232
+ pieces = sorted(n_breaks | {0})
233
+ out = set(n_breaks)
234
+ for i, lo in enumerate(pieces):
235
+ hi = pieces[i + 1] - 1 if i + 1 < len(pieces) else lo + span
236
+ if hi < lo:
237
+ continue
238
+ m_lo, m_hi = m_of(lo), m_of(hi)
239
+ if m_lo == m_hi:
240
+ continue # m constant on the piece: no crossings inside
241
+ inc = m_hi > m_lo
242
+ for v in v_set:
243
+ if inc:
244
+ if not (m_lo < v <= m_hi):
245
+ continue
246
+ hit = _first_true(lo, hi, lambda n, v=v: m_of(n) >= v)
247
+ else:
248
+ if not (m_hi < v <= m_lo):
249
+ continue
250
+ hit = _first_true(lo, hi, lambda n, v=v: m_of(n) < v)
251
+ if hit is not None:
252
+ out.update((hit - 2, hit - 1, hit, hit + 1, hit + 2))
253
+ return {v for v in out if v >= 0} | {0, 1, 2, 3}
254
+
255
+
256
+ def _stretches(breaks, extra=()):
257
+ """Sorted breakpoints -> list of (u, v) closed integer stretches.
258
+
259
+ The last stretch is (u, _INF). Regimes of all involved functions are
260
+ constant on each stretch.
261
+ """
262
+ pts = sorted(set(breaks) | {v for v in extra if v is not None and v >= 0} | {0})
263
+ out = []
264
+ for i, u in enumerate(pts):
265
+ if i + 1 < len(pts):
266
+ if pts[i + 1] - 1 >= u:
267
+ out.append((u, pts[i + 1] - 1))
268
+ else:
269
+ out.append((u, _INF))
270
+ return out
271
+
272
+
273
+ # --------------------------------------------------------------------------
274
+ # Exact verifier.
275
+ #
276
+ # On each stretch, both the composed action and a candidate slice have:
277
+ # length L(n): constant, or an exact monotone staircase ceil((+-n + c)/D)
278
+ # clamped at 0 (composed D = step1 * |step2|, candidate D = |step3|);
279
+ # first index A(n): affine, or affine plus a period-step1 sawtooth (composed
280
+ # only; present only when s2's start is back-anchored, the
281
+ # intermediate length is non-constant on the stretch and step1 > 1).
282
+ # Equality of such functions on a stretch follows from finitely many checks:
283
+ # - equal L-support thresholds (first/last n with L >= 1 and with L >= 2),
284
+ # - equal values at stretch endpoints / far probes,
285
+ # - equal first staircase-jump position plus equal step divisor D
286
+ # (two exact same-D monotone unit-jump staircases agreeing at a jump and
287
+ # in level are identical; divisors match because step3 must equal
288
+ # step1*step2 wherever some L >= 2 exists),
289
+ # - A: both sides affine -> two points suffice; a sawtooth with a wrap
290
+ # inside the checked span deviates from any affine by +-step1 at one of
291
+ # the checked points; spans >= step1 with an active sawtooth are rejected
292
+ # structurally (a full wrap makes A non-affine, no slice can match).
293
+ # --------------------------------------------------------------------------
294
+
295
+
296
+ def _l_range(evalf, u, v, lim):
297
+ """Contiguous {n in [u, v]: L(n) >= lim} for monotone-L stretch, or None.
298
+
299
+ Returns (w1, w2). L is monotone on the stretch (structure lemma), so the
300
+ region is a prefix or suffix of [u, v].
301
+ """
302
+ lu = evalf(u)[2] >= lim
303
+ lv = evalf(v)[2] >= lim
304
+ if lu and lv:
305
+ return (u, v)
306
+ if not lu and not lv:
307
+ return None
308
+ if lv: # increasing: suffix
309
+ w = _first_true(u, v, lambda n: evalf(n)[2] >= lim)
310
+ return (w, v)
311
+ w = _last_true(u, v, lambda n: evalf(n)[2] >= lim)
312
+ return (u, w)
313
+
314
+
315
+ def _first_jump(evalf, w1, w2):
316
+ """First n in (w1, w2] where L(n) != L(w1); L monotone on [w1, w2]."""
317
+ l0 = evalf(w1)[2]
318
+ if evalf(w2)[2] == l0:
319
+ return None
320
+ return _first_true(w1 + 1, w2, lambda n: evalf(n)[2] != l0)
321
+
322
+
323
+ def _check_stretch_finite(fc, fx, u, v, q, t2):
324
+ """Exact equality of composed fc and candidate fx on finite [u, v]."""
325
+
326
+ def same(n):
327
+ return _pt_eq(fc(n), fx(n))
328
+
329
+ pts = {u, u + 1, (u + v) // 2, v - 1, v}
330
+ # support thresholds (L >= 1) and step-relevance thresholds (L >= 2)
331
+ for lim in (1, 2):
332
+ rc = _l_range(fc, u, v, lim)
333
+ rx = _l_range(fx, u, v, lim)
334
+ if (rc is None) != (rx is None):
335
+ return False
336
+ if rc is None:
337
+ continue
338
+ if rc != rx:
339
+ return False
340
+ w1, w2 = rc
341
+ pts.update((w1, w1 + 1, (w1 + w2) // 2, w2 - 1, w2))
342
+ if lim == 2:
343
+ # staircase jump alignment inside the L>=2 region
344
+ jc = _first_jump(fc, w1, w2)
345
+ jx = _first_jump(fx, w1, w2)
346
+ if jc != jx:
347
+ return False
348
+ if jc is not None:
349
+ pts.update((jc - 1, jc, jc + 1))
350
+ # sawtooth wrap rejection for A on the L>=1 region
351
+ r1 = _l_range(fc, u, v, 1)
352
+ if r1 is not None:
353
+ w1, w2 = r1
354
+ if w2 - w1 + 1 >= max(q, 2) and _sawtooth_active(fc, w1, w2, q, t2):
355
+ return False
356
+ for n in pts:
357
+ if u <= n <= v and not same(n):
358
+ return False
359
+ return True
360
+
361
+
362
+ def _sawtooth_active(fc, w1, w2, q, t2):
363
+ """Is the composed A of form affine + nontrivial sawtooth on [w1, w2]?
364
+
365
+ True iff step1 = q > 1, s2's start is effectively back-anchored on this
366
+ stretch (slope of a2 in m equals 1) and the intermediate length m is
367
+ non-constant here. Regimes are constant per stretch, so endpoint
368
+ measurements are exact.
369
+ """
370
+ if q <= 1:
371
+ return False
372
+ m1 = fc.m(w1)
373
+ m2 = fc.m(w2)
374
+ if m1 == m2:
375
+ return False
376
+ a21 = _eval1(t2, m1)[0]
377
+ a22 = _eval1(t2, m2)[0]
378
+ # a2 is affine in m on the stretch with slope 0 or 1
379
+ return (a22 - a21) == (m2 - m1)
380
+
381
+
382
+ class _ComposedEval:
383
+ """Composed ground-truth evaluator, also exposing the intermediate m."""
384
+
385
+ __slots__ = ("t1", "t2")
386
+
387
+ def __init__(self, t1, t2):
388
+ self.t1 = t1
389
+ self.t2 = t2
390
+
391
+ def __call__(self, n):
392
+ return _eval2(self.t1, self.t2, n)
393
+
394
+ def m(self, n):
395
+ return _eval1(self.t1, n)[2]
396
+
397
+
398
+ def _check_stretch_inf(fc, fx, u, q, t2, dmax):
399
+ """Exact equality of fc and fx on [u, infinity).
400
+
401
+ On the final stretch both sides are constant or exactly-periodic
402
+ staircases with divisor <= dmax; all thresholds and the first jump lie
403
+ within u + 2*dmax + 5, and levels/growth are pinned by far probes.
404
+ """
405
+
406
+ def same(n):
407
+ return _pt_eq(fc(n), fx(n))
408
+
409
+ hi = u + 2 * dmax + 5
410
+ far = u + 3 * dmax + 7
411
+ for n in (u, u + 1, u + 2, hi, far, far + 1, far + dmax):
412
+ if not same(n):
413
+ return False
414
+ for lim in (1, 2):
415
+ rc = _l_range(fc, u, hi, lim)
416
+ rx = _l_range(fx, u, hi, lim)
417
+ if (rc is None) != (rx is None):
418
+ return False
419
+ if rc is None:
420
+ continue # thresholds beyond hi impossible: growth shows by hi
421
+ if rc != rx:
422
+ return False
423
+ for n in (rc[0], rc[0] + 1, rc[1]):
424
+ if not same(n):
425
+ return False
426
+ if lim == 2:
427
+ jc = _first_jump(fc, rc[0], hi)
428
+ jx = _first_jump(fx, rc[0], hi)
429
+ if jc != jx:
430
+ return False
431
+ if jc is not None:
432
+ for n in (jc - 1, jc, jc + 1):
433
+ if not same(n):
434
+ return False
435
+ # A-sawtooth on the (infinite) support region: any active sawtooth wraps,
436
+ # making A non-affine there, which no single slice can match.
437
+ if fc(far)[2] >= 1 and _sawtooth_active(fc, u, far, q, t2):
438
+ return False
439
+ return True
440
+
441
+
442
+ def _verify(t1, t2, cand, base_breaks):
443
+ """Exact check: does slice-triple cand equal the composition of t1, t2?"""
444
+ q = t1[2]
445
+ d_comp = q * abs(t2[2])
446
+ dmax = max(d_comp, abs(cand[2]), 1)
447
+ breaks = set(base_breaks) | {v for v in _combo_vals(cand) if v >= 0}
448
+ fc = _ComposedEval(t1, t2)
449
+
450
+ def fx(n):
451
+ return _eval1(cand, n)
452
+
453
+ for u, v in _stretches(breaks):
454
+ if v is _INF:
455
+ if not _check_stretch_inf(fc, fx, u, q, t2, dmax):
456
+ return False
457
+ else:
458
+ if not _check_stretch_finite(fc, fx, u, v, q, t2):
459
+ return False
460
+ return True
461
+
462
+
463
+ # --------------------------------------------------------------------------
464
+ # Profile: measured structural facts about the composed action, from which
465
+ # candidate parameters are forced (see EXPLANATION.md for the forcing lemmas).
466
+ # --------------------------------------------------------------------------
467
+
468
+ _UNBOUNDED = float("inf")
469
+
470
+
471
+ class _Profile:
472
+ __slots__ = (
473
+ "q",
474
+ "k2",
475
+ "K",
476
+ "D",
477
+ "breaks",
478
+ "stretches",
479
+ "fc",
480
+ "tail_mode",
481
+ "P",
482
+ "max_len",
483
+ "N0",
484
+ "N1",
485
+ "n_hat",
486
+ "A_hat",
487
+ "sigma",
488
+ "n_j",
489
+ "L_j",
490
+ )
491
+
492
+
493
+ def _profile(t1, t2):
494
+ pro = _Profile()
495
+ pro.q = t1[2]
496
+ pro.k2 = t2[2]
497
+ pro.K = pro.q * pro.k2
498
+ pro.D = pro.q * abs(pro.k2)
499
+ pro.breaks = _pullback_breaks(t1, t2)
500
+ pro.stretches = _stretches(pro.breaks)
501
+ fc = pro.fc = _ComposedEval(t1, t2)
502
+ D = pro.D
503
+
504
+ # tail classification on the final (regime-settled) stretch
505
+ u_inf = pro.stretches[-1][0]
506
+ f1 = u_inf + 2 * D + 5
507
+ f2 = f1 + 2 * D + 5
508
+ l1, l2 = fc(f1)[2], fc(f2)[2]
509
+ if l2 > l1:
510
+ pro.tail_mode, pro.P = "grow", None
511
+ elif l1 == 0:
512
+ pro.tail_mode, pro.P = "empty", 0
513
+ elif l2 == l1:
514
+ pro.tail_mode, pro.P = "const", l1
515
+ else: # decreasing final stretch: excluded by the structure lemmas
516
+ pro.tail_mode, pro.P = "shrink", None
517
+
518
+ # support bounds (first / last n with nonempty result)
519
+ pro.N0 = pro.N1 = None
520
+ for u, v in pro.stretches:
521
+ vv = f2 if v is _INF else v
522
+ r = _l_range(fc, u, vv, 1)
523
+ if r is not None:
524
+ if pro.N0 is None:
525
+ pro.N0 = r[0]
526
+ pro.N1 = r[1]
527
+ if pro.tail_mode in ("grow", "const"):
528
+ pro.N1 = _UNBOUNDED
529
+
530
+ # maximum result length over all n (monotone per stretch -> endpoints)
531
+ if pro.tail_mode == "grow":
532
+ pro.max_len = _UNBOUNDED
533
+ else:
534
+ best = 0
535
+ for u, v in pro.stretches:
536
+ vv = f2 if v is _INF else v
537
+ best = max(best, fc(u)[2], fc(vv)[2])
538
+ pro.max_len = best
539
+
540
+ # tail probes: first index A and its slope sigma at a settled point
541
+ pro.n_hat = f1
542
+ pro.A_hat = pro.sigma = pro.n_j = pro.L_j = None
543
+ if pro.tail_mode in ("grow", "const"):
544
+ pro.A_hat = fc(f1)[0]
545
+ pro.sigma = fc(f1 + 1)[0] - pro.A_hat
546
+ if pro.tail_mode == "grow":
547
+ lf1 = fc(f1)[2]
548
+ pro.n_j = _first_true(f1 + 1, f1 + D + 2, lambda n: fc(n)[2] != lf1)
549
+ pro.L_j = fc(pro.n_j)[2] if pro.n_j is not None else None
550
+ return pro
551
+
552
+
553
+ # --------------------------------------------------------------------------
554
+ # Candidate generation. Anchor structure and parameters of any equivalent
555
+ # slice are forced by the profile (tail anchors, staircase jump phase,
556
+ # support boundaries); +-1 wiggles are included as convention armor and are
557
+ # filtered by the exact verifier, so they never affect soundness.
558
+ # --------------------------------------------------------------------------
559
+
560
+
561
+ def _wig(v):
562
+ return (v - 1, v, v + 1)
563
+
564
+
565
+ def _stop_pos(cands, s, e, k):
566
+ """Append (s, stop, k) for k > 0 families with back-anchored stop e <= 0."""
567
+ if e < 0:
568
+ cands.append((s, e, k))
569
+ elif e == 0:
570
+ cands.append((s, None, k))
571
+
572
+
573
+ def _stop_negstep_front(cands, s, e, k):
574
+ """Append (s, stop, k) for k < 0 families with front-anchored stop e >= -1."""
575
+ if e >= 0:
576
+ cands.append((s, e, k))
577
+ elif e == -1:
578
+ cands.append((s, None, k))
579
+
580
+
581
+ def _cand_grow(pro):
582
+ K, sig, a_hat, n_hat, n_j, l_j = (
583
+ pro.K,
584
+ pro.sigma,
585
+ pro.A_hat,
586
+ pro.n_hat,
587
+ pro.n_j,
588
+ pro.L_j,
589
+ )
590
+ cands = []
591
+ if n_j is None:
592
+ return cands
593
+ if sig == 0 and K > 0 and a_hat >= 0:
594
+ s = a_hat
595
+ for e in _wig(s + K * (l_j - 1) + 1 - n_j):
596
+ _stop_pos(cands, s, e, K)
597
+ elif sig == 1 and K < 0:
598
+ s = a_hat - n_hat
599
+ if s < 0:
600
+ for e in _wig(n_j + s + K * (l_j - 1) - 1):
601
+ _stop_negstep_front(cands, s, e, K)
602
+ return cands
603
+
604
+
605
+ def _cand_window(pro):
606
+ K, P, sig, a_hat, n_hat, n0 = pro.K, pro.P, pro.sigma, pro.A_hat, pro.n_hat, pro.N0
607
+ cands = []
608
+ if sig == 0 and a_hat >= 0:
609
+ s = a_hat
610
+ if K > 0:
611
+ for e in (s + K * (P - 1) + 1, s + K * P):
612
+ if e > s:
613
+ cands.append((s, e, K))
614
+ else:
615
+ # the support of [s:e:K] starts at e + 2 (at 1 for stop None),
616
+ # which pins e via the measured support start N0
617
+ for e in _wig(n0 - 2):
618
+ _stop_negstep_front(cands, s, e, K)
619
+ elif sig == 1:
620
+ s = a_hat - n_hat
621
+ if s < 0:
622
+ if K > 0:
623
+ for e in _wig(1 - n0):
624
+ _stop_pos(cands, s, e, K)
625
+ else:
626
+ for e in (s + K * (P - 1) - 1, s + K * P):
627
+ if e < s:
628
+ cands.append((s, e, K))
629
+ return cands
630
+
631
+
632
+ def _cand_single_tail(pro):
633
+ sig, n_hat, n0 = pro.sigma, pro.n_hat, pro.N0
634
+ e_val = pro.A_hat # the single selected index at the tail probe
635
+ cands = []
636
+ if sig == 0 and e_val >= 0:
637
+ c = e_val
638
+ cands.append((c, c + 1, 1))
639
+ # "min-shape": select min(c, n-1) via a negative-step slice
640
+ for e in _wig(n0 - 2):
641
+ kappa = max(1, c - e)
642
+ _stop_negstep_front(cands, c, e, -kappa)
643
+ elif sig == 1:
644
+ s = e_val - n_hat
645
+ if s < 0:
646
+ if s + 1 < 0:
647
+ cands.append((s, s + 1, 1))
648
+ else:
649
+ cands.append((s, None, 1))
650
+ for e in _wig(1 - n0):
651
+ kappa = max(1, e - s)
652
+ _stop_pos(cands, s, e, kappa)
653
+ return cands
654
+
655
+
656
+ def _cand_single_bounded(pro):
657
+ fc, n0, n1 = pro.fc, pro.N0, pro.N1
658
+ e0, e1 = fc(n0)[0], fc(n1)[0]
659
+ cands = []
660
+ # forward bump family: selects [n+s]+ while it stays below stop
661
+ for s in {e1 - n1, e0 - n0}:
662
+ if s < 0:
663
+ for e in _wig(n1 + s + 1):
664
+ if e >= 1:
665
+ cands.append((s, e, max(1, e - s)))
666
+ # backward bump family: selects min(s', n-1) while it stays above stop
667
+ for sp in {e1, e0}:
668
+ if sp >= 0:
669
+ for ep in _wig(sp - n1 - 1):
670
+ if ep <= -1:
671
+ cands.append((sp, ep, -max(1, sp + 1)))
672
+ return cands
673
+
674
+
675
+ def _cand_bounded(pro):
676
+ K, fc, n0, n1 = pro.K, pro.fc, pro.N0, pro.N1
677
+ a0, a1 = fc(n0)[0], fc(n1)[0]
678
+ cands = []
679
+ if K > 0:
680
+ for s in {a1 - n1, a0 - n0}:
681
+ if s < 0:
682
+ for e in _wig(n1 + s + 1):
683
+ if e >= 1:
684
+ cands.append((s, e, K))
685
+ else:
686
+ for sp in {a1, a0}:
687
+ if sp >= 0:
688
+ for ep in _wig(sp - n1 - 1):
689
+ if ep <= -1:
690
+ cands.append((sp, ep, K))
691
+ return cands
692
+
693
+
694
+ def _candidates(pro):
695
+ if pro.max_len == 0:
696
+ raw = [(0, 0, 1)]
697
+ elif pro.max_len == 1:
698
+ raw = _cand_single_tail(pro) if pro.N1 is _UNBOUNDED else _cand_single_bounded(pro)
699
+ elif pro.tail_mode == "grow":
700
+ raw = _cand_grow(pro)
701
+ elif pro.tail_mode == "const":
702
+ raw = _cand_window(pro)
703
+ elif pro.tail_mode == "empty":
704
+ raw = _cand_bounded(pro)
705
+ else: # 'shrink': excluded by the structure lemmas
706
+ raw = []
707
+ out, seen = [], set()
708
+ for c in raw:
709
+ c = _prettify(c)
710
+ if c not in seen:
711
+ seen.add(c)
712
+ out.append(c)
713
+ return out
714
+
715
+
716
+ def _prettify(t):
717
+ """Semantics-preserving cosmetic normalization of a slice triple."""
718
+ a, b, k = t
719
+ if k > 0 and a == 0:
720
+ a = None
721
+ if k < 0 and a == -1:
722
+ a = None
723
+ return (a, b, k)
724
+
725
+
726
+ # --------------------------------------------------------------------------
727
+ # Public API
728
+ # --------------------------------------------------------------------------
729
+
730
+
731
+ def compose(s1, s2):
732
+ """Return a slice s3 with arr[s1][s2] == arr[s3] for every sequence arr,
733
+ or None if no such slice exists.
734
+ """
735
+ t1, t2 = _norm3(s1), _norm3(s2)
736
+ if t1[2] < 0:
737
+ # arr[s1] == arr[::-1][mirror(s1)] with mirror(s1).step > 0, and
738
+ # rev-first compositions transfer exactly through mirroring.
739
+ r = _compose_triples(_mirror(t1), t2)
740
+ return None if r is None else _to_slice(_prettify(_mirror(r)))
741
+ r = _compose_triples(t1, t2)
742
+ return None if r is None else _to_slice(r)
743
+
744
+
745
+ def _compose_triples(t1, t2):
746
+ pro = _profile(t1, t2)
747
+ for cand in _candidates(pro):
748
+ if _verify(t1, t2, cand, pro.breaks):
749
+ return cand
750
+ return None
751
+
752
+
753
+ class SlicePlan:
754
+ """A lazily reduced chain of slices, built with bracket notation.
755
+
756
+ A plan gathers slices without doing any work: ``plan[1:][::2]`` returns
757
+ a new plan remembering both slices and leaves ``plan`` unchanged. ``s_``
758
+ is the ready-made empty plan, so ``s_[2:][3:]`` builds a plan directly
759
+ from slicing syntax. On first query (``slices``, ``slice``, ``apply``,
760
+ ``==`` or ``hash``) the stored chain is reduced like a
761
+ :class:`SliceChain` and replaced in place by the reduced form, and the
762
+ plan marks itself reduced, so the work happens at most once per batch
763
+ of gathered slices. A plan derived from an already-reduced plan
764
+ inherits the collapsed prefix and only has its new tail to reduce;
765
+ because the reduction stack runs left to right, this yields the same
766
+ result as reducing the raw history, so query timing never affects the
767
+ outcome. ``repr`` shows the stored chain as-is without triggering
768
+ reduction.
769
+ """
770
+
771
+ __slots__ = ("_slices", "_reduced")
772
+
773
+ def __init__(self, *slices):
774
+ self._slices = tuple(_to_slice(_prettify(_norm3(s))) for s in slices)
775
+ self._reduced = len(self._slices) <= 1
776
+
777
+ def __getitem__(self, item):
778
+ if not isinstance(item, slice):
779
+ raise TypeError("SlicePlan supports slice indices only")
780
+ return SlicePlan(*self._slices, item)
781
+
782
+ @property
783
+ def slices(self):
784
+ """The reduced tuple of slices; reduces (once) on first access."""
785
+ if not self._reduced:
786
+ self._slices = SliceChain(*self._slices).slices
787
+ self._reduced = True
788
+ return self._slices
789
+
790
+ @property
791
+ def slice(self):
792
+ """The single equivalent slice, or None if the chain does not
793
+ reduce to one. The empty plan gives ``slice(None)``."""
794
+ red = self.slices
795
+ if len(red) > 1:
796
+ return None
797
+ return red[0] if red else slice(None)
798
+
799
+ def apply(self, seq):
800
+ """Apply the (reduced) slices to seq in order."""
801
+ for s in self.slices:
802
+ seq = seq[s]
803
+ return seq
804
+
805
+ def __eq__(self, other):
806
+ if not isinstance(other, SlicePlan):
807
+ return NotImplemented
808
+ return self.slices == other.slices
809
+
810
+ def __hash__(self):
811
+ return hash(tuple((s.start, s.stop, s.step) for s in self.slices))
812
+
813
+ def __repr__(self):
814
+ inner = ", ".join(repr(s) for s in self._slices)
815
+ return f"SlicePlan({inner})"
816
+
817
+
818
+ s_ = SlicePlan()
819
+
820
+
821
+ class SliceChain:
822
+ """A sequence of slices applied in order, e.g. seq[s0][s1]...[sk].
823
+
824
+ On construction, adjacent pairs are merged with :func:`compose` until no
825
+ adjacent pair merges (a merge is retried against the new left neighbor,
826
+ so cascading reductions are found). ``apply`` works the same whether or
827
+ not any merging happened.
828
+ """
829
+
830
+ __slots__ = ("_slices",)
831
+
832
+ def __init__(self, *slices):
833
+ stack = []
834
+ for s in slices:
835
+ cur = _to_slice(_prettify(_norm3(s)))
836
+ while stack:
837
+ merged = compose(stack[-1], cur)
838
+ if merged is None:
839
+ break
840
+ stack.pop()
841
+ cur = merged
842
+ stack.append(cur)
843
+ self._slices = tuple(stack)
844
+
845
+ @property
846
+ def slices(self):
847
+ """The reduced tuple of slices."""
848
+ return self._slices
849
+
850
+ def apply(self, seq):
851
+ """Apply the (reduced) slices to seq in order."""
852
+ for s in self._slices:
853
+ seq = seq[s]
854
+ return seq
855
+
856
+ def __len__(self):
857
+ return len(self._slices)
858
+
859
+ def __iter__(self):
860
+ return iter(self._slices)
861
+
862
+ def __eq__(self, other):
863
+ if not isinstance(other, SliceChain):
864
+ return NotImplemented
865
+ return self._slices == other._slices
866
+
867
+ def __hash__(self):
868
+ return hash(tuple((s.start, s.stop, s.step) for s in self._slices))
869
+
870
+ def __repr__(self):
871
+ inner = ", ".join(repr(s) for s in self._slices)
872
+ return f"SliceChain({inner})"
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: slicecompose
3
+ Version: 0.1.0
4
+ Summary: Exact composition of Python slices
5
+ Author-email: István Sárándi <istvan.sarandi@uni-tuebingen.de>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 István Sárándi
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/isarandi/slicecompose
29
+ Project-URL: Repository, https://github.com/isarandi/slicecompose
30
+ Project-URL: Issues, https://github.com/isarandi/slicecompose/issues
31
+ Project-URL: Changelog, https://github.com/isarandi/slicecompose/releases
32
+ Project-URL: Author, https://istvansarandi.com
33
+ Keywords: slice,slicing,composition,indexing,sequences,lazy
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Topic :: Software Development :: Libraries
38
+ Classifier: Programming Language :: Python
39
+ Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Programming Language :: Python :: 3.13
44
+ Classifier: Programming Language :: Python :: 3.14
45
+ Classifier: Operating System :: OS Independent
46
+ Requires-Python: >=3.10
47
+ Description-Content-Type: text/markdown
48
+ License-File: LICENSE
49
+ Provides-Extra: test
50
+ Requires-Dist: pytest>=7.0; extra == "test"
51
+ Requires-Dist: pytest-cov; extra == "test"
52
+ Provides-Extra: lint
53
+ Requires-Dist: ruff; extra == "lint"
54
+ Requires-Dist: pre-commit; extra == "lint"
55
+ Provides-Extra: dev
56
+ Requires-Dist: slicecompose[lint,test]; extra == "dev"
57
+ Dynamic: license-file
58
+
59
+ # slicecompose
60
+
61
+ Exact composition of Python slices — pure Python, standard library only.
62
+
63
+ `compose(s1, s2)` returns a slice `s3` such that `arr[s1][s2] == arr[s3]` for
64
+ **every** sequence `arr` (every length, including 0), or `None` if no such
65
+ slice exists. Both directions of the decision are exact:
66
+
67
+ - **Sound** — a returned slice is equivalent for *all* lengths, not just
68
+ tested ones.
69
+ - **Complete** — `None` is returned only when no equivalent single slice
70
+ exists.
71
+
72
+ ```python
73
+ >>> from slicecompose import compose, SliceChain
74
+
75
+ >>> compose(slice(2, None), slice(3, None)) # arr[2:][3:] == arr[5:]
76
+ slice(5, None, None)
77
+
78
+ >>> compose(slice(None, None, -1), slice(None, 1)) # arr[::-1][:1] == arr[-1:]
79
+ slice(-1, None, None)
80
+
81
+ >>> compose(slice(-2, None), slice(None, 1)) # arr[-2:][:1] == arr[-2::2]
82
+ slice(-2, None, 2)
83
+
84
+ >>> compose(slice(None, None, 2), slice(None, None, -1)) is None
85
+ True
86
+ ```
87
+
88
+ The last pair really has no equivalent: `arr[::2][::-1]` starts at index
89
+ `n-1` for odd `n` but `n-2` for even `n`, and no fixed slice can depend on
90
+ the parity of the length. The third example shows the opposite surprise: the
91
+ result must select the single element at index `max(0, n-2)`, which no
92
+ "obvious" candidate like `[-2:-1]` does (it is empty at `n == 1`) — but
93
+ `[-2::2]` does, for every `n`.
94
+
95
+ Why this is harder than plugging into `slice.indices()`: the answer must be
96
+ a fixed slice that works *universally*, without knowing the length of the
97
+ sequence it will be applied to. Clamping, negative indices, and the
98
+ sign-dependent defaults make the composed selection a piecewise function of
99
+ the length; `compose` decides exactly whether that function is realizable by
100
+ a single slice.
101
+
102
+ ## API
103
+
104
+ ### `compose(s1, s2) -> slice | None`
105
+
106
+ The core decision procedure described above. Accepts any valid slice
107
+ objects, including huge field values (no ranges are ever materialized;
108
+ everything is decided arithmetically).
109
+
110
+ ### `SliceChain(*slices)`
111
+
112
+ Container for a sequence of slices applied in order, e.g.
113
+ `seq[s0][s1]...[sk]`.
114
+
115
+ - On construction, adjacent pairs are merged with `compose` until no
116
+ adjacent pair merges; a successful merge is retried against its new left
117
+ neighbor, so cascading reductions are found (e.g. `[::2], [::-1], [::-1]`
118
+ reduces to `[::2]` — the last two merge first).
119
+ - `.slices` — the reduced tuple of slices.
120
+ - `.apply(seq)` — applies the (reduced) slices in order; correct whether or
121
+ not any merging happened.
122
+
123
+ ```python
124
+ >>> ch = SliceChain(slice(None, None, 2), slice(None, None, -1), slice(None, None, -1))
125
+ >>> ch.slices
126
+ (slice(None, None, 2),)
127
+ >>> SliceChain(slice(2, None), slice(3, None)).apply("abcdefgh")
128
+ 'fgh'
129
+ ```
130
+
131
+ Note that the reduced chain is *a* fixpoint of adjacent-pair merging, not
132
+ necessarily the shortest one: pairwise reduction is not confluent, and there
133
+ are chains whose total effect is a single slice even though no adjacent pair
134
+ merges at all. See `EXPLANATION.md` §7 for verified examples and details.
135
+
136
+ ### `SlicePlan` / `s_`
137
+
138
+ Lazy plan building with ordinary bracket notation. `s_` is the ready-made
139
+ empty plan; slicing a plan returns a *new* plan (the original is unchanged),
140
+ and nothing is computed while slices are gathered. On the first query —
141
+ `.slices`, `.slice`, `.apply(seq)`, `==`/`hash` — the plan reduces its
142
+ stored chain in place and remembers that it did, so the reduction runs at
143
+ most once per batch of gathered slices.
144
+
145
+ ```python
146
+ >>> from slicecompose import s_
147
+ >>> s_[2:][3:].slice
148
+ slice(5, None, None)
149
+ >>> plan = s_[100:]
150
+ >>> plan[::2].slices # branching; plan itself is unchanged
151
+ (slice(100, None, 2),)
152
+ >>> s_[::2][::-1].slice is None # irreducible pairs stay a chain
153
+ True
154
+ >>> s_[::2][::-1].apply(tuple(range(9)))
155
+ (8, 6, 4, 2, 0)
156
+ ```
157
+
158
+ This is aimed at expensive media (video streams, remote arrays): build the
159
+ access plan symbolically with normal slicing syntax, reduce it, and only
160
+ then touch the data. A reduced slice needs no length to be executed — its
161
+ anchors are only "offset from the front" and "offset from the end" — so the
162
+ plan works even when the element count is unknown or unreliable.
163
+
164
+ ## Correctness
165
+
166
+ `EXPLANATION.md` contains the full argument for why the implementation is
167
+ sound and complete for every length — the infinite quantification over `n`
168
+ is reduced to finitely many exact checks (regime breakpoints, per-stretch
169
+ staircase/sawtooth normal forms, an exact verifier, and profile-forced
170
+ candidate generation). Returned slices are always validated by the exact
171
+ verifier, so soundness does not rest on the candidate generator.
172
+
173
+ The test suite (`test_slicecompose.py`) additionally audits both failure
174
+ modes empirically: exhaustive small-field grids checked for wrong merges by
175
+ brute force and for missed merges against large candidate tables, huge-field
176
+ randomized soundness checks, structural consistency probes, and the problem
177
+ statement's examples.
178
+
179
+ ## Installation
180
+
181
+ ```bash
182
+ pip install slicecompose # from PyPI, once published
183
+ pip install . # from a checkout
184
+ ```
185
+
186
+ Pure Python, no dependencies.
187
+
188
+ ## Running the tests
189
+
190
+ ```bash
191
+ pytest # quick tier, ~30 s
192
+ SLICECOMPOSE_FULL=1 pytest # exhaustive grids, ~35 min
193
+ ```
194
+
195
+ `verify_examples.py` independently re-checks the examples claimed in
196
+ `PROBLEM.md` by direct evaluation.
197
+
198
+ ## Files
199
+
200
+ | file | contents |
201
+ |------------------------------|----------------------------------------------------------|
202
+ | `src/slicecompose/` | the library (`compose`, `SlicePlan`/`s_`, `SliceChain`) |
203
+ | `tests/test_slicecompose.py` | test suite (quick and exhaustive tiers) |
204
+ | `PROBLEM.md` | the problem statement |
205
+ | `EXPLANATION.md` | soundness/completeness argument |
206
+ | `verify_examples.py` | standalone check of the problem's examples |
@@ -0,0 +1,9 @@
1
+ slicecompose/__init__.py,sha256=Vs66wRek-2ezMIoYsVvx83L5Iw74kSNoLkh7Kz53hXA,28149
2
+ slicecompose/_version.py,sha256=n_5vdJsPNu7wZ57LGuRL585uvll-hiuvZUBWzdG0RQU,520
3
+ slicecompose-0.1.0.dist-info/licenses/LICENSE,sha256=Yo-WMDNvrjL9OitVRBuKaYeo33FiSJLpONpT68bvA-s,1074
4
+ slicecompose-0.1.0.dist-info/METADATA,sha256=2TyUJcOuUn-wWwI-b7dKJ_ydQfsgchy8vbbfmhl0N8w,8753
5
+ slicecompose-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ slicecompose-0.1.0.dist-info/scm_file_list.json,sha256=etrWSXBGntF9qzj2pRZ43j2X3RjZJ3K9dLJigVy7uT8,289
7
+ slicecompose-0.1.0.dist-info/scm_version.json,sha256=GBIDAdGgdV59UHaoI0hz1u_QOuisrSMw_jyTixf-Rpk,160
8
+ slicecompose-0.1.0.dist-info/top_level.txt,sha256=nnony279CCiGALJDWZB3u4KZ9GeSHCeeUTbqyU92S28,13
9
+ slicecompose-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 István Sárándi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ {
2
+ "files": [
3
+ ".gitignore",
4
+ "README.md",
5
+ "EXPLANATION.md",
6
+ "CLAUDE.md",
7
+ "pyproject.toml",
8
+ "PROBLEM.md",
9
+ "LICENSE",
10
+ "verify_examples.py",
11
+ "src/slicecompose/__init__.py",
12
+ ".github/workflows/python-publish.yml",
13
+ "tests/test_slicecompose.py"
14
+ ]
15
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.1.0",
3
+ "distance": 0,
4
+ "node": "g933f131d8ec6bd93de56d5173807f6ed02281585",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-16"
8
+ }
@@ -0,0 +1 @@
1
+ slicecompose