slicecompose 0.1.0__tar.gz
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.
- slicecompose-0.1.0/.github/workflows/python-publish.yml +35 -0
- slicecompose-0.1.0/.gitignore +7 -0
- slicecompose-0.1.0/CLAUDE.md +69 -0
- slicecompose-0.1.0/EXPLANATION.md +405 -0
- slicecompose-0.1.0/LICENSE +21 -0
- slicecompose-0.1.0/PKG-INFO +206 -0
- slicecompose-0.1.0/PROBLEM.md +167 -0
- slicecompose-0.1.0/README.md +148 -0
- slicecompose-0.1.0/pyproject.toml +86 -0
- slicecompose-0.1.0/setup.cfg +4 -0
- slicecompose-0.1.0/src/slicecompose/__init__.py +872 -0
- slicecompose-0.1.0/src/slicecompose/_version.py +24 -0
- slicecompose-0.1.0/src/slicecompose.egg-info/PKG-INFO +206 -0
- slicecompose-0.1.0/src/slicecompose.egg-info/SOURCES.txt +19 -0
- slicecompose-0.1.0/src/slicecompose.egg-info/dependency_links.txt +1 -0
- slicecompose-0.1.0/src/slicecompose.egg-info/requires.txt +11 -0
- slicecompose-0.1.0/src/slicecompose.egg-info/scm_file_list.json +15 -0
- slicecompose-0.1.0/src/slicecompose.egg-info/scm_version.json +8 -0
- slicecompose-0.1.0/src/slicecompose.egg-info/top_level.txt +1 -0
- slicecompose-0.1.0/tests/test_slicecompose.py +447 -0
- slicecompose-0.1.0/verify_examples.py +135 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Upload Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
pypi-publish:
|
|
12
|
+
name: Upload release to PyPI
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- name: Check out repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.x"
|
|
27
|
+
|
|
28
|
+
- name: Install build dependencies
|
|
29
|
+
run: python -m pip install --upgrade build
|
|
30
|
+
|
|
31
|
+
- name: Build package distribution
|
|
32
|
+
run: python -m build
|
|
33
|
+
|
|
34
|
+
- name: Publish package distributions to PyPI
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# slicecompose — project notes
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
Exact slice-composition library: `compose(s1, s2)` returns a slice equivalent
|
|
6
|
+
to `arr[s1][s2]` for **all** sequence lengths, or `None` exactly when none
|
|
7
|
+
exists. Sound AND complete — both directions are hard guarantees. Never
|
|
8
|
+
weaken either one to make a test pass.
|
|
9
|
+
|
|
10
|
+
## Environment
|
|
11
|
+
|
|
12
|
+
- micromamba env: **py10** (pure-stdlib library; only pytest is needed).
|
|
13
|
+
|
|
14
|
+
## Layout / packaging
|
|
15
|
+
|
|
16
|
+
- src layout per projtemplate: library in `src/slicecompose/__init__.py`,
|
|
17
|
+
tests in `tests/`. `verify_examples.py` stays at root (tied to PROBLEM.md).
|
|
18
|
+
- `pyproject.toml` uses setuptools + setuptools_scm (version from git tags;
|
|
19
|
+
`fallback_version = "0.0.0"` keeps builds working before the first tag).
|
|
20
|
+
`src/slicecompose/_version.py` is generated — never edit or commit it.
|
|
21
|
+
- pytest works from the repo root without installing (pytest `pythonpath =
|
|
22
|
+
["src"]` in pyproject) — no editable install into py10 needed.
|
|
23
|
+
- ruff is configured in pyproject (line-length 99, double quotes).
|
|
24
|
+
|
|
25
|
+
## Testing
|
|
26
|
+
|
|
27
|
+
- Quick tier: `micromamba run -n py10 python -m pytest -q` from the repo root (~30 s)
|
|
28
|
+
- Exhaustive tier: prefix `SLICECOMPOSE_FULL=1` (~35 min; measured 36 min)
|
|
29
|
+
- Run heavy tiers with `ulimit -v` and `nice`.
|
|
30
|
+
- Ground truth for any manual check is CPython `range` slicing:
|
|
31
|
+
`range(n)[s1][s2] == range(n)[s3]` is exact, O(1) even for astronomical
|
|
32
|
+
`n`, and ranges compare by value. Prefer it over the library's own
|
|
33
|
+
`_eval1`/`_eval2` when independence matters.
|
|
34
|
+
|
|
35
|
+
## Architecture invariants
|
|
36
|
+
|
|
37
|
+
- **Soundness never depends on candidate generation**: every candidate
|
|
38
|
+
(including ±1 wiggles) must pass `_verify`, which compares only
|
|
39
|
+
ground-truth point evaluations. Keep it that way — new candidate families
|
|
40
|
+
are cheap to add and cannot break soundness.
|
|
41
|
+
- **Completeness** lives in the forcing lemmas (EXPLANATION.md §5.2) and the
|
|
42
|
+
candidate generators. This is the risky part: any change there requires
|
|
43
|
+
re-running the exhaustive grid audit (FULL tier) — it has caught a real
|
|
44
|
+
forcing-lemma bug before (the K<0 window stop pin).
|
|
45
|
+
- No code path may branch on field *magnitudes* (only signs, `None`-ness,
|
|
46
|
+
and exact comparisons); this uniformity is what lets the exhaustive
|
|
47
|
+
small-field audits stand in for all magnitudes.
|
|
48
|
+
- `SlicePlan` must keep delegating reduction to the `SliceChain` greedy
|
|
49
|
+
stack: the stack is left-to-right, so a collapsed prefix + new tail
|
|
50
|
+
reduces identically to the raw history and plan results are independent
|
|
51
|
+
of when queries happened. An "optimal" interval-DP reduction would break
|
|
52
|
+
that path-independence unless the raw history were kept alongside.
|
|
53
|
+
|
|
54
|
+
## Documentation sync
|
|
55
|
+
|
|
56
|
+
- EXPLANATION.md §8 quotes the FULL-tier grid/table sizes; keep them in sync
|
|
57
|
+
with the parameters in `test_slicecompose.py` when grids change.
|
|
58
|
+
- The runtime estimates in the test module docstring are measured values;
|
|
59
|
+
update them if the grids change.
|
|
60
|
+
|
|
61
|
+
## Known theory facts (verified, documented in EXPLANATION.md §7)
|
|
62
|
+
|
|
63
|
+
- Adjacent-pair chain reduction is **not confluent**: merge order can change
|
|
64
|
+
the final chain length, and the greedy stack does not always reach a
|
|
65
|
+
shortest fixpoint (spec only requires *a* fixpoint, so this is not a bug).
|
|
66
|
+
- Pairwise merging is inherently incomplete for chains ≥ 3: e.g.
|
|
67
|
+
`[2::-1], [:2], [:-1]` equals the single slice `[2:0:-2]` for every `n`,
|
|
68
|
+
yet neither adjacent pair merges. A k-ary `compose` would be needed to
|
|
69
|
+
detect such chains.
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# Why `compose` is sound and complete for every length
|
|
2
|
+
|
|
3
|
+
`compose(s1, s2)` must return a slice equivalent to `arr[s1][s2]` for **all**
|
|
4
|
+
lengths `n >= 0`, or `None` exactly when no such slice exists. Testing any
|
|
5
|
+
finite set of lengths cannot establish this by itself; the argument below
|
|
6
|
+
shows how the implementation reduces the infinite quantification over `n` to
|
|
7
|
+
finitely many exact checks, without ever enumerating ranges (fields may be
|
|
8
|
+
astronomically large integers).
|
|
9
|
+
|
|
10
|
+
Throughout, `s1 = (a1, b1, k1)`, `s2 = (a2, b2, k2)` are the input slices and
|
|
11
|
+
`q = k1` after the mirror reduction of §2 (so `q >= 1`), `K = k1*k2`,
|
|
12
|
+
`D = q*|k2|`. `ceil(x/y)` denotes ceiling division.
|
|
13
|
+
|
|
14
|
+
## 1. The action of a slice, and the equality criterion
|
|
15
|
+
|
|
16
|
+
For a slice `s` and length `n`, Python's semantics (`slice.indices`) yield
|
|
17
|
+
normalized `(alpha(n), beta(n), k)` and select the index progression
|
|
18
|
+
|
|
19
|
+
I_j(n) = A(n) + k*j, 0 <= j < L(n),
|
|
20
|
+
|
|
21
|
+
where `A = alpha` and `L(n) = max(0, ceil((beta - alpha)/k))`. The clamped
|
|
22
|
+
anchor forms are (with `[x]+ = max(0, x)`):
|
|
23
|
+
|
|
24
|
+
| step | field | normalized form |
|
|
25
|
+
|-------|--------------|------------------------------------------------------|
|
|
26
|
+
| k > 0 | start `None` | `alpha = 0` |
|
|
27
|
+
| k > 0 | start `s>=0` | `alpha = min(s, n)` |
|
|
28
|
+
| k > 0 | start `s<0` | `alpha = [n+s]+` |
|
|
29
|
+
| k > 0 | stop `None` | `beta = n` |
|
|
30
|
+
| k > 0 | stop `e>=0` | `beta = min(e, n)` |
|
|
31
|
+
| k > 0 | stop `e<0` | `beta = [n+e]+` |
|
|
32
|
+
| k < 0 | start `None` | `alpha = n-1` |
|
|
33
|
+
| k < 0 | start `s>=0` | `alpha = min(s, n-1)` |
|
|
34
|
+
| k < 0 | start `s<0` | `alpha = max(-1, n+s)` |
|
|
35
|
+
| k < 0 | stop `None` | `beta = -1` |
|
|
36
|
+
| k < 0 | stop `e>=0` | `beta = min(e, n-1)` |
|
|
37
|
+
| k < 0 | stop `e<0` | `beta = max(-1, n+e)` |
|
|
38
|
+
|
|
39
|
+
`_indices`/`_eval1` implement exactly these formulas (unit-tested against
|
|
40
|
+
CPython's own slicing for thousands of random slices and lengths).
|
|
41
|
+
|
|
42
|
+
Every anchor is either **front-anchored** (`alpha`/`beta` eventually a
|
|
43
|
+
constant) or **back-anchored** (eventually `n + const`). Two virtual
|
|
44
|
+
positions exist only via `None`: "the whole back" (`beta = n`, k > 0) and
|
|
45
|
+
"before the beginning" (`beta = -1`, k < 0).
|
|
46
|
+
|
|
47
|
+
**Equality criterion.** Two actions `(A, k, L)` and `(A', k', L')` at length
|
|
48
|
+
`n` select the same subsequence iff
|
|
49
|
+
|
|
50
|
+
L = L' and (L = 0 or A = A') and (L <= 1 or k = k').
|
|
51
|
+
|
|
52
|
+
Since `range(n)` has pairwise-distinct elements, per-`n` equality of index
|
|
53
|
+
selections is exactly the problem's equivalence. `_pt_eq` implements this.
|
|
54
|
+
|
|
55
|
+
The composition of `s1` then `s2` acts as
|
|
56
|
+
|
|
57
|
+
m(n) = L1(n),
|
|
58
|
+
A(n) = alpha1(n) + k1 * A2(m(n)), K = k1*k2, L(n) = L2(m(n)),
|
|
59
|
+
|
|
60
|
+
because selecting `j`-th of `s2` inside the `s1`-selection composes the two
|
|
61
|
+
affine index maps (`_eval2`). So `compose` must decide: *is there a slice
|
|
62
|
+
whose `(A, L)` functions agree with the composed ones in the sense above for
|
|
63
|
+
every `n`, with step `K` wherever some `L >= 2`?*
|
|
64
|
+
|
|
65
|
+
## 2. Mirror reduction: assume `k1 >= 1`
|
|
66
|
+
|
|
67
|
+
Define `~x = -x-1` and `mirror(s) = slice(~start, ~stop, -step)` with `None`
|
|
68
|
+
kept. **Lemma (mirror).** For every slice `u` and every `n`:
|
|
69
|
+
|
|
70
|
+
arr[::-1][u] == arr[mirror(u)].
|
|
71
|
+
|
|
72
|
+
*Proof.* Reversal maps position `p` to `n-1-p`. The `j`-th selected index of
|
|
73
|
+
`u` on the reversed sequence is `A_u(n) + k_u j` in reversed coordinates,
|
|
74
|
+
i.e. `n-1-A_u(n) - k_u j` in original coordinates: an action with step
|
|
75
|
+
`-k_u`. Applying `p -> n-1-p` to the table above maps each anchor form to the
|
|
76
|
+
corresponding form with the field negated bitwise (`min(s,n)`-type maps to
|
|
77
|
+
`max(-1, n+~s)`-type and vice versa, front and back swap), and the two virtual
|
|
78
|
+
`None` positions map to each other. Hence the transformed action is exactly
|
|
79
|
+
the action of `mirror(u)`. ∎
|
|
80
|
+
|
|
81
|
+
Since `rev . rev = id`, also `u = rev . mirror(u)`. Consequently
|
|
82
|
+
|
|
83
|
+
s1 . s2 = rev . (mirror(s1) . s2),
|
|
84
|
+
|
|
85
|
+
and applying the lemma once more: `s1 . s2` is expressible as a single slice
|
|
86
|
+
iff `mirror(s1) . s2` is, and the results correspond via `mirror`. If
|
|
87
|
+
`k1 < 0`, `mirror(s1)` has positive step, so `compose` recurses once with
|
|
88
|
+
`q = |k1|` and mirrors the result back. From here on `q >= 1`.
|
|
89
|
+
|
|
90
|
+
## 3. Structure of the composed action
|
|
91
|
+
|
|
92
|
+
### 3.1 Breakpoints (regime boundaries)
|
|
93
|
+
|
|
94
|
+
Every `min`, `max`, `[.]+` branch switch and every sign change of
|
|
95
|
+
`beta - alpha` in §1 is a comparison between two terms of the form `c` or
|
|
96
|
+
`n + c` with `c ∈ {0, ±start, ±stop}`. Hence:
|
|
97
|
+
|
|
98
|
+
**Lemma B1.** All regime boundaries of a slice's action, as functions of the
|
|
99
|
+
length, lie in `{x + y + d : x, y ∈ {0, ±fields}, |d| <= 2}`.
|
|
100
|
+
|
|
101
|
+
`_combo_vals` takes exactly this generous superset. For the composed action,
|
|
102
|
+
`s2`'s regimes live in `m`-space; they are pulled back to `n`-space:
|
|
103
|
+
|
|
104
|
+
**Lemma B2.** `m(n) = L1(n)` changes by at most 1 when `n` grows by 1 (one
|
|
105
|
+
added element extends any slice result by at most one element; immediate from
|
|
106
|
+
the formulas), and is monotone between consecutive breakpoints of `s1` (there
|
|
107
|
+
`beta1 - alpha1` is affine). Hence `m` crosses each critical value `V` of
|
|
108
|
+
`s2` at most once per piece, cannot skip it, and the crossing is found
|
|
109
|
+
exactly by binary search (`_pullback_breaks`). On the unbounded final piece,
|
|
110
|
+
a crossing of `V`, if any, occurs within `q*(V+4) + 4*Mag + 64` of the piece
|
|
111
|
+
start (`m` moves with slope `1/q` and offsets bounded by the field
|
|
112
|
+
magnitudes `Mag`), so the bounded search window loses nothing.
|
|
113
|
+
|
|
114
|
+
The union of `s1`-breakpoints, pulled-back `s2`-breakpoints, the candidate's
|
|
115
|
+
own breakpoints (Lemma B1 applied to it), and `{0..3}`, each with ±2 guards,
|
|
116
|
+
cuts `n >= 0` into **stretches** (`_stretches`); by construction every
|
|
117
|
+
involved function is regime-constant on every stretch. The count is O(1)
|
|
118
|
+
(a few hundred at most), independent of field magnitudes.
|
|
119
|
+
|
|
120
|
+
### 3.2 Per-stretch normal forms
|
|
121
|
+
|
|
122
|
+
**Lemma S1 (lengths).** On a stretch, `m` is constant or
|
|
123
|
+
`ceil((±n + c)/q)`; `L2` as a function of `m` is constant or
|
|
124
|
+
`ceil((±m + c')/k2')` with `k2' = |k2|` (possibly clamped at 0, which only
|
|
125
|
+
truncates the staircase). Composing and using the exact identities
|
|
126
|
+
|
|
127
|
+
ceil((ceil(x/p) + c)/r) = ceil((x + p*c)/(p*r)),
|
|
128
|
+
ceil((c - ceil(x/p))/r) = ceil((p*(c-1) + 1 - x)/(p*r)),
|
|
129
|
+
|
|
130
|
+
the composed `L(n)` on a stretch is **constant, or
|
|
131
|
+
`max(0, ceil((±n + c'')/D))` with `D = q*|k2|`**: a monotone staircase with
|
|
132
|
+
unit jumps spaced exactly `D`. The same holds for a candidate slice with
|
|
133
|
+
divisor `|k3|`.
|
|
134
|
+
|
|
135
|
+
**Lemma S2 (first index).** On a stretch, `A(n) = alpha1(n) + q*A2(m(n))`
|
|
136
|
+
where `alpha1` is affine (slope 0 or 1) and `A2` is affine in `m` with slope
|
|
137
|
+
`tau ∈ {0, 1}` (slope 1 iff `s2`'s start is back-anchored in the active
|
|
138
|
+
regime). Since `q*ceil((±n+c)/q) = ±n + c + ((∓n - c) mod q)`,
|
|
139
|
+
|
|
140
|
+
A(n) = affine(n) + tau * sawtooth_q(n),
|
|
141
|
+
|
|
142
|
+
where the sawtooth has period `q` and amplitude `q - 1`. It is present
|
|
143
|
+
(non-degenerate) iff `tau = 1`, `m` non-constant on the stretch, and `q > 1`.
|
|
144
|
+
A candidate slice's `A` is affine on every stretch (its regime switches are
|
|
145
|
+
breakpoints).
|
|
146
|
+
|
|
147
|
+
**Lemma S3 (monotonicity).** On a stretch, `m` is monotone and `L2` is
|
|
148
|
+
monotone in `m` over the traversed range (no regime change inside), so the
|
|
149
|
+
composed `L` is monotone on every stretch; likewise the candidate's `L`.
|
|
150
|
+
Consequently `{n in stretch : L(n) >= t}` is a contiguous prefix or suffix.
|
|
151
|
+
|
|
152
|
+
**Lemma S4 (final stretch).** Beyond the last breakpoint no clamp changes
|
|
153
|
+
state anymore, so `L` is constant or a growing staircase. A strictly
|
|
154
|
+
decreasing regime always terminates at its zero-crossing, which is a
|
|
155
|
+
pulled-back death threshold (a 2-field combination, Lemma B1) and hence a
|
|
156
|
+
breakpoint; the code still guards the impossible case (`tail_mode ==
|
|
157
|
+
'shrink'`) conservatively.
|
|
158
|
+
|
|
159
|
+
## 4. The verifier is exact (soundness)
|
|
160
|
+
|
|
161
|
+
`_verify` compares the composed action `f` with a candidate's action `g` on
|
|
162
|
+
every stretch. All probes evaluate **ground truth** (`_eval2`, `_eval1`
|
|
163
|
+
implement §1 directly), so no derived formula can silently disagree with
|
|
164
|
+
Python's semantics; the lemmas are only needed to show the probes *suffice*.
|
|
165
|
+
|
|
166
|
+
On a **finite stretch** `[u, v]` (`_check_stretch_finite`):
|
|
167
|
+
|
|
168
|
+
1. *Support and step-relevance regions.* For `t ∈ {1, 2}` the regions
|
|
169
|
+
`{L_f >= t}` and `{L_g >= t}` are contiguous (S3) and are computed exactly
|
|
170
|
+
by monotone binary search; they must coincide. This pins where results
|
|
171
|
+
are empty, where they have one element, and where steps matter.
|
|
172
|
+
2. *Lengths on the `L>=2` region `[w1, w2]`.* Point comparisons at `w1`
|
|
173
|
+
(which has `L >= 2`) force `k3 = K`, hence both staircases have the same
|
|
174
|
+
divisor `D` (S1). Two monotone unit-jump staircases that are exactly
|
|
175
|
+
`D`-periodic, agree at `w1`, and have the same first jump position (found
|
|
176
|
+
by binary search, compared, and value-checked at `jump-1, jump, jump+1`)
|
|
177
|
+
are identical on the region: level and phase determine an exact staircase.
|
|
178
|
+
The `L ∈ {0, 1}` parts are already fixed by step 1.
|
|
179
|
+
3. *First indices on the `L>=1` region.* `g`'s `A` is affine (S2). If the
|
|
180
|
+
composed sawtooth is active (measured via `tau` and `m` at the region's
|
|
181
|
+
endpoints — exact because regimes are constant) and the region spans at
|
|
182
|
+
least `q` points, `f`'s `A` contains a full sawtooth period, is not
|
|
183
|
+
affine, and cannot equal any slice's `A`: reject. Otherwise the region
|
|
184
|
+
contains at most one sawtooth wrap. The checked points include `w1`,
|
|
185
|
+
`w1+1`, midpoint, `w2-1`, `w2`: if there is no wrap both sides are affine
|
|
186
|
+
and agreement at two adjacent points implies agreement everywhere; if
|
|
187
|
+
there is one wrap, the composed values after the wrap deviate from the
|
|
188
|
+
affine function through `(w1, w1+1)` by exactly `±q ≠ 0`, which the
|
|
189
|
+
endpoint checks expose.
|
|
190
|
+
4. All curated points are additionally compared with the full criterion of
|
|
191
|
+
§1 (length, first index, step where relevant).
|
|
192
|
+
|
|
193
|
+
On the **final stretch** `[u, ∞)` (`_check_stretch_inf`): by S4 both sides
|
|
194
|
+
are constant or growing exact staircases with divisor at most
|
|
195
|
+
`dmax = max(D, |k3|)`, so every threshold and the first jump lie within
|
|
196
|
+
`u + 2*dmax + 5`; the same finite reasoning applies there, far probes at
|
|
197
|
+
`u + 3*dmax + 7` (and `+ dmax` beyond) pin constant levels and growth, and
|
|
198
|
+
exact `D`-periodicity (S1) extends the jump alignment to all `n`. An active
|
|
199
|
+
sawtooth necessarily wraps on an infinite support region and is rejected
|
|
200
|
+
outright — no slice can match it (this is what kills, e.g., `[::2][::-1]`).
|
|
201
|
+
|
|
202
|
+
**Soundness theorem.** If `_verify(t1, t2, cand)` returns `True`, then the
|
|
203
|
+
candidate is equivalent to the composition for every `n >= 0`: the stretches
|
|
204
|
+
cover all `n`, and on each stretch steps 1–4 establish pointwise equality of
|
|
205
|
+
the selections in the sense of §1. Since `compose` only ever returns a
|
|
206
|
+
verified candidate, every returned slice is correct — independently of any
|
|
207
|
+
property of the candidate generator.
|
|
208
|
+
|
|
209
|
+
## 5. Candidate generation is exhaustive (completeness)
|
|
210
|
+
|
|
211
|
+
Suppose *some* equivalent slice `w = (s, e, k3)` exists. We show the
|
|
212
|
+
generator (`_candidates`) emits a candidate that is pointwise-equal to `w`
|
|
213
|
+
for all `n`; the verifier then accepts it, so `compose` cannot return `None`.
|
|
214
|
+
|
|
215
|
+
### 5.1 Every slice belongs to one of finitely many families
|
|
216
|
+
|
|
217
|
+
From the table in §1, classify `w` by its support `{n : L_w(n) >= 1}` and
|
|
218
|
+
tail behavior:
|
|
219
|
+
|
|
220
|
+
- **always empty**;
|
|
221
|
+
- **bounded support**: only back-start/front-stop with `k3 > 0`, or
|
|
222
|
+
front-start/back-stop with `k3 < 0` (every other anchor combination has
|
|
223
|
+
`beta - alpha` eventually constant or growing, giving tail support or
|
|
224
|
+
emptiness);
|
|
225
|
+
- **tail support, `L` eventually constant** ("windows"): front/front or
|
|
226
|
+
back/back anchors, either step sign;
|
|
227
|
+
- **tail support, `L` growing**: front-start/back-stop with `k3 > 0`, or
|
|
228
|
+
back-start/front-stop with `k3 < 0` (the growth forces the stop to recede
|
|
229
|
+
with `n` while the start stays anchored).
|
|
230
|
+
|
|
231
|
+
The profile (§3 machinery without a candidate) measures the composition's
|
|
232
|
+
support bounds `N0`, `N1`, its tail mode (empty / constant `P` / growing),
|
|
233
|
+
`max_len`, and tail probes `A(n̂)`, `sigma = A(n̂+1) - A(n̂)`. These
|
|
234
|
+
measurements are exact: on the final stretch the forms of S1/S2 hold, so
|
|
235
|
+
constancy vs. growth is decided by probes `2D+5` apart, and monotone binary
|
|
236
|
+
searches locate the support boundaries. Equivalence forces `w` to reproduce
|
|
237
|
+
exactly these measured quantities, which places `w` in exactly one family
|
|
238
|
+
and, as shown next, forces its parameters.
|
|
239
|
+
|
|
240
|
+
### 5.2 Parameter forcing per family
|
|
241
|
+
|
|
242
|
+
*Step.* If `max_len >= 2`, some `n` has two selected elements whose distance
|
|
243
|
+
is `q*k2`, so `k3 = K` (generated as such). If `max_len <= 1`, the step
|
|
244
|
+
never influences the selection beyond enforcing `L <= 1`: for the emitted
|
|
245
|
+
shapes, `L >= 1` is step-independent (`beta - alpha >= 1`) and the single
|
|
246
|
+
selected element is `alpha(n)`; any `kappa >= max(beta - alpha)` works and
|
|
247
|
+
all such slices are pointwise identical, so emitting the safe bound
|
|
248
|
+
`max(1, ...)` loses nothing.
|
|
249
|
+
|
|
250
|
+
*Start.* On the tail, `sigma ∈ {0, 1}` for any slice (S2 without sawtooth).
|
|
251
|
+
`sigma = 0` forces a front anchor with `s = A(n̂)` (on its support a
|
|
252
|
+
front-anchored start equals its field); `sigma = 1` forces a back anchor with
|
|
253
|
+
`s = A(n̂) - n̂`. If the measured `sigma` is anything else, the composed `A`
|
|
254
|
+
is not affine on the tail (it carries a sawtooth) and **no** slice matches —
|
|
255
|
+
correctly yielding `None`. For bounded support the same reading is done at
|
|
256
|
+
the support ends `N0`, `N1`.
|
|
257
|
+
|
|
258
|
+
*Stop.*
|
|
259
|
+
- *Growing `L`, `K > 0`* (front/back): the tail lengths are
|
|
260
|
+
`ceil((n + e - s)/K)`; the position `n_j` of the first length jump past
|
|
261
|
+
`n̂` (binary search) and the value `L(n_j)` pin `e` exactly:
|
|
262
|
+
`e = s + K*(L(n_j)-1) + 1 - n_j`, with `e = 0` meaning `stop=None`.
|
|
263
|
+
Distinct `e` give distinct tail length functions, so `e` is unique.
|
|
264
|
+
- *Growing `L`, `K < 0`* (back/front): symmetrically
|
|
265
|
+
`e = n_j + s + K*(L(n_j)-1) - 1`, with `e = -1` meaning `stop=None`.
|
|
266
|
+
- *Window, `K > 0`, front/front*: `e` may vary within one step bucket
|
|
267
|
+
`(s + K(P-1), s + K*P]`. **Bucket invariance:** for any `n`, replacing `e`
|
|
268
|
+
by another bucket member changes `beta = min(e, n)` only when `beta = n`
|
|
269
|
+
for both or when `n` lies between them, in which case `ceil((n - s)/K)`
|
|
270
|
+
equals `P` as well — the selection is unchanged *for every* `n`. Both
|
|
271
|
+
bucket ends are emitted.
|
|
272
|
+
- *Window, `K > 0`, back/back*: the support start of
|
|
273
|
+
`[s:e:K]` (`s < e <= 0`) is `max(1, 1 - e)` (while the start clamps at 0,
|
|
274
|
+
`L >= 1` iff `n + e >= 1`), so `e = 1 - N0`, unique.
|
|
275
|
+
- *Window, `K < 0`, front/front*: the support start of `[s:e:K]` is `e + 2`
|
|
276
|
+
(`beta = min(e, n-1) < alpha` first happens there), or `1` for
|
|
277
|
+
`stop=None`; so `e = N0 - 2`, unique. (The tail bucket does *not* pin `e`
|
|
278
|
+
here: bucket members differ at small `n` while the start is clamped —
|
|
279
|
+
found and fixed by the exhaustive grid audit.)
|
|
280
|
+
- *Window, `K < 0`, back/back*: bucket freedom again; the clamp
|
|
281
|
+
`beta = max(-1, n+e)` hands over seamlessly (`L = min(ceil((n+s+1)/|K|), P)`
|
|
282
|
+
independent of the bucket member), so both bucket ends are emitted.
|
|
283
|
+
- *Bounded support:* emptiness first fails at `N1 + 1`, giving
|
|
284
|
+
`e = N1 + s + 1` (`K > 0`) resp. `e = s - N1 - 1` (`K < 0`); the start is
|
|
285
|
+
read at the support ends as above.
|
|
286
|
+
|
|
287
|
+
*`max_len <= 1` shapes.* The selected-element function `E(n) = A(n)` of any
|
|
288
|
+
`L <= 1` slice is, on its support: constant `c` (front start; support is then
|
|
289
|
+
forced to be exactly `{n > c}` for every such slice, so one representative
|
|
290
|
+
`slice(c, c+1)` suffices); `[n+s]+` (back start, tail support; support start
|
|
291
|
+
`N0` pins the stop via `e = 1 - N0` as above, `kappa = max(1, e - s)`);
|
|
292
|
+
`min(s, n-1)` (negative-step front start; `e = N0 - 2`,
|
|
293
|
+
`kappa = max(1, s - e)`); or the bounded-support versions handled with the
|
|
294
|
+
bounded pins. Anything else (e.g. the parity-dependent `E` of `[::2][-1:]`)
|
|
295
|
+
matches no slice and correctly yields `None`.
|
|
296
|
+
|
|
297
|
+
±1 wiggles around every derived stop (and the alternative start readings for
|
|
298
|
+
bounded support) are also emitted purely as armor against off-by-one
|
|
299
|
+
conventions; the exact verifier filters them, so they cannot affect
|
|
300
|
+
soundness.
|
|
301
|
+
|
|
302
|
+
**Completeness theorem.** If an equivalent slice `w` exists, the profile
|
|
303
|
+
places it in one family; the forcing lemmas show the generator emits a
|
|
304
|
+
candidate pointwise-equal to `w` (either `w`'s exact parameters or an
|
|
305
|
+
equivalent bucket/`kappa` representative). The verifier, being exact (§4),
|
|
306
|
+
accepts it. Hence `compose` returns `None` only when no equivalent slice
|
|
307
|
+
exists.
|
|
308
|
+
|
|
309
|
+
## 6. Always-empty compositions and edge cases
|
|
310
|
+
|
|
311
|
+
- If `max_len = 0` (e.g. `[5:3][::-1]`), `slice(0, 0, 1)` is emitted and
|
|
312
|
+
trivially verified — any always-empty slice is acceptable.
|
|
313
|
+
- `n = 0` imposes no constraint (every slice selects nothing), and all
|
|
314
|
+
formulas above are exact from `n = 0` on.
|
|
315
|
+
- Emitted slices are cosmetically normalized (`start 0 -> None` for positive
|
|
316
|
+
step, `start -1 -> None` for negative step, `step 1 -> None`); each rewrite
|
|
317
|
+
is a strict semantic identity from the table in §1, and it is the
|
|
318
|
+
normalized triple that gets verified.
|
|
319
|
+
|
|
320
|
+
## 7. `SliceChain`
|
|
321
|
+
|
|
322
|
+
`SliceChain(*slices)` reduces with a stack: each incoming slice is repeatedly
|
|
323
|
+
merged with the top of the stack while `compose` succeeds. A successful
|
|
324
|
+
merge replaces the top, so the new top is retried against its own left
|
|
325
|
+
neighbor — this finds cascading reductions such as
|
|
326
|
+
|
|
327
|
+
[::2], [::-1], [::-1] -> [::2], [:] -> [::2]
|
|
328
|
+
|
|
329
|
+
(the problem's chain example: the *last two* merge first, then again).
|
|
330
|
+
Invariant: adjacent stack entries never merge; therefore one pass reaches a
|
|
331
|
+
state where **no adjacent pair merges**, which is the required fixpoint.
|
|
332
|
+
The invariant is maintained because mergeability of a pair depends only on
|
|
333
|
+
the pair's composed function, and a merge leaves the composed function of
|
|
334
|
+
every disjoint pair unchanged — so the already-checked pairs to the left
|
|
335
|
+
stay unmergeable.
|
|
336
|
+
|
|
337
|
+
The fixpoint is not canonical, though: adjacent-pair reduction is **not
|
|
338
|
+
confluent**, and merge order can even affect the length of the result. A
|
|
339
|
+
merge consumes its two blocks, which can destroy a pairing the optimum
|
|
340
|
+
needed. Example (all claims machine-verified):
|
|
341
|
+
|
|
342
|
+
[-2:2], [0:2], [:0:-1], [1:0:-1]
|
|
343
|
+
|
|
344
|
+
The stack eagerly merges the first pair and then gets stuck at three slices,
|
|
345
|
+
while merging the middle pair first (`[0:2][:0:-1] == [1:2]`) collapses the
|
|
346
|
+
whole chain to a single always-empty slice. So the stack reaches *a*
|
|
347
|
+
fixpoint (as the problem statement requires) but not always a shortest one;
|
|
348
|
+
finding a shortest reachable fixpoint would take an interval DP over
|
|
349
|
+
mergeable blocks, matrix-chain style. Pairwise merging is moreover
|
|
350
|
+
inherently incomplete for chains of three or more:
|
|
351
|
+
|
|
352
|
+
[2::-1], [:2], [:-1] == [2:0:-2] for every n,
|
|
353
|
+
|
|
354
|
+
yet neither adjacent pair is expressible on its own, so no merge order can
|
|
355
|
+
reduce this chain at all; deciding such cases would require a k-ary
|
|
356
|
+
`compose`.
|
|
357
|
+
|
|
358
|
+
`apply` simply applies the reduced slices in order, which is correct whether
|
|
359
|
+
or not any merging happened (each merge preserved the function). The
|
|
360
|
+
reduced tuple is exposed as `.slices`.
|
|
361
|
+
|
|
362
|
+
`SlicePlan` is a lazy wrapper over the same reduction: it gathers slices
|
|
363
|
+
unreduced (each bracket application returns a new plan) and on first query
|
|
364
|
+
replaces its stored chain by the `SliceChain` fixpoint, flagging itself
|
|
365
|
+
reduced. Because the stack processes slices left to right, its state after
|
|
366
|
+
the prefix *is* the collapsed prefix, so reducing a collapsed prefix plus
|
|
367
|
+
new tail equals reducing the raw history — query timing never affects a
|
|
368
|
+
plan's result, and everything above applies to `SlicePlan` unchanged.
|
|
369
|
+
|
|
370
|
+
## 8. Validation performed
|
|
371
|
+
|
|
372
|
+
The proofs above are the argument for all `n`; the test suite additionally
|
|
373
|
+
hammers every code path empirically:
|
|
374
|
+
|
|
375
|
+
- the problem statement's 17 merge examples, 6 non-merge examples, and the
|
|
376
|
+
chain examples;
|
|
377
|
+
- an **exhaustive** grid of 147,456 pairs (fields in `{None, 0, ±1, ±2, ±3}`,
|
|
378
|
+
steps up to `±3`) and 1,000,000 pairs with steps up to `±6` and fields up
|
|
379
|
+
to `±7`: every merge is checked against brute force for `n <= 500` plus
|
|
380
|
+
large spot lengths, and every `None` is audited for completeness against
|
|
381
|
+
brute-force candidate tables of 18,000 resp. ~221,000 slices (both
|
|
382
|
+
audits found zero defects; an earlier run of the audit is what caught
|
|
383
|
+
the `K < 0` window stop-pin bug described in §5.2);
|
|
384
|
+
- randomized soundness checks with fields up to `10^30` and steps up to
|
|
385
|
+
`10^12`, compared at random lengths up to `10^60`;
|
|
386
|
+
- a structural completeness probe at huge magnitudes: for random triples,
|
|
387
|
+
`(s1∘s2)∘s3` and `s1∘(s2∘s3)` decide expressibility of the same function,
|
|
388
|
+
so their `None`-ness must agree whenever both pair-merges exist;
|
|
389
|
+
- the mirror lemma tested pointwise for thousands of random slices,
|
|
390
|
+
including huge fields;
|
|
391
|
+
- `_indices`/`_eval1` tested against CPython's own slicing.
|
|
392
|
+
|
|
393
|
+
Because no branch of the implementation depends on the *magnitude* of any
|
|
394
|
+
field (only on signs, `None`-ness, and exact comparisons), the exhaustive
|
|
395
|
+
small-field audits exercise every regime combination that large-field inputs
|
|
396
|
+
can produce; large-field correctness then follows from the magnitude-uniform
|
|
397
|
+
lemmas, and is additionally spot-checked directly.
|
|
398
|
+
|
|
399
|
+
## 9. Complexity
|
|
400
|
+
|
|
401
|
+
`compose` performs O(1) profile/verification stretches, each with O(1)
|
|
402
|
+
probes plus binary searches over value ranges bounded by the field
|
|
403
|
+
magnitudes — O(polylog(magnitude)) big-integer operations total
|
|
404
|
+
(~0.1 ms for small fields, ~1 ms for 100-digit fields). No operation ever
|
|
405
|
+
materializes a range, so memory is O(1).
|
|
@@ -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.
|