Skip to content

Commit fc9cc8a

Browse files
committed
[ls-python] expose range values at boundary
1 parent 632032e commit fc9cc8a

12 files changed

Lines changed: 64 additions & 34 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ change is listed here under **Changed** or **Removed**.
3636
differential suite now has **2,664 / 3,025 rows = 88.1% matched**, or
3737
**88.1% complete**, with **100% agreement among decided rows**.
3838

39+
- `range` values now cross the public call boundary as structural
40+
`Val.range` snapshots. The boundary preserves each constructor field's
41+
integer-or-Boolean form while the runner compares CPython's normalized
42+
integer attributes. The differential suite now has **2,665 / 3,025 rows =
43+
88.1% matched**, or **88.1% complete** (proof interpreter: **2,592 / 3,025
44+
= 85.7%**), with **100% agreement among decided rows**.
45+
3946
## [0.1.9] — 2026-10-11
4047

4148
### Added

‎Examples/python/seq_lab/seq_lab.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
33
Tuple/namedtuple slices, tuple repetition, sum(it[, start]), tuple(it),
44
and range as an immediate value (materialize-per-use). Range equality now
5-
compares sequences without materialization; the remaining loud frontier is
6-
pinned by refusal probes: boundary and list slices. String repetition now
5+
compares sequences without materialization; range values cross the public
6+
boundary, while list slices remain pinned by a refusal probe. String repetition now
77
has its own acceptance tier.
88
"""
99

‎Examples/python/seq_lab/spec.lean‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ repetition, `sum(it[, start])` (the element fold IS `evalBinOp .add`; a
88
str start is CPython's special-cased `TypeError`), `tuple(it)`, and
99
`range` as the immediate `RVal.rangeV` — materialize-per-use, which is
1010
what makes RE-ITERATION exact (`range_reiter`). Range equality compares the
11-
integer sequence without materializing it. The remaining loud frontier is
12-
pinned below: range indexing/membership/unpacking/boundary, list slices —
13-
every one `unsupported`, never a guessed value
11+
integer sequence without materializing it. Range values also cross the public
12+
boundary as `Val.range`; list slices remain pinned below as `unsupported`,
13+
never a guessed value
1414
and never a fake `TypeError`.
1515
-/
1616

@@ -93,10 +93,10 @@ load_program seq_lab from "Examples/python/seq_lab/seq_lab.json"
9393
#py_check seq_lab.range_in_float() = true
9494
#py_check seq_lab.range_in_fraction() = false
9595

96-
/-! ### the remaining loud range frontier (unsupported, never guessed) -/
96+
/-! ### range values at the public boundary -/
9797

98-
#guard (match callFunction seq_lab "range_boundary" #[.int 3] 4096 with
99-
| .unsupported _ => true | _ => false)
98+
#guard callFunction seq_lab "range_boundary" #[.int 3] 4096 ==
99+
.ok (.range (.int 0) (.int 3) (.int 1))
100100
#py_check seq_lab.range_unpack() = 0
101101
#py_check seq_lab.range_unpack_step() = 5
102102
#py_check seq_lab.range_unpack_bad() raises

‎LeanModels/Python/Ast.lean‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,19 @@ def benignImportBinds : String → Option (String × Bool)
421421

422422
/-! ## Core value / result types (DESIGN.md, normative) -/
423423

424+
/-- The integer object retained by a public `range` value, including bool
425+
subclasses. Runtime range values use the same representation, so freezing
426+
and thawing preserve the constructor fields exactly. -/
427+
inductive RangeBound where
428+
| int (value : Int)
429+
| bool (value : Bool)
430+
deriving Repr, Inhabited, BEq
431+
432+
/-- The numerical interpretation used by range consumers. -/
433+
def RangeBound.toInt : RangeBound → Int
434+
| .int value => value
435+
| .bool value => if value then 1 else 0
436+
424437
/-- Runtime values. -/
425438
inductive Val where
426439
| none
@@ -429,6 +442,8 @@ inductive Val where
429442
| int (n : Int)
430443
| float (value : Float64)
431444
| str (s : String)
445+
/-- A Python `range` value at the public call boundary. -/
446+
| range (lo hi step : RangeBound)
432447
| list (xs : Array Val)
433448
| tuple (xs : Array Val)
434449
deriving Repr, Inhabited, BEq

‎LeanModels/Python/Runtime.lean‎

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,6 @@ namespace LeanModels.Python
2727
/-- A heap address (the index into the heap array). -/
2828
abbrev Addr := Nat
2929

30-
/-- The integer object retained by a range constructor, including bool subclasses. -/
31-
inductive RangeBound where
32-
| int (value : Int)
33-
| bool (value : Bool)
34-
deriving Repr, Inhabited, BEq
35-
36-
/-- The numerical interpretation used by range consumers. -/
37-
def RangeBound.toInt : RangeBound → Int
38-
| .int value => value
39-
| .bool value => if value then 1 else 0
40-
4130
/-- Runtime values (docs/memory-model.md v2). -/
4231
inductive RVal where
4332
| none
@@ -622,6 +611,7 @@ mutual
622611
| .int n => .int n
623612
| .float value => .float value
624613
| .str s => .str s
614+
| .range lo hi step => .rangeV lo hi step
625615
| .list xs => .listV (RVal.thawList xs.toList).toArray
626616
| .tuple xs => .tuple (RVal.thawList xs.toList).toArray
627617

@@ -646,6 +636,7 @@ mutual
646636
| .int n => .ok (.int n)
647637
| .float value => .ok (.float value)
648638
| .str s => .ok (.str s)
639+
| .rangeV lo hi step => .ok (.range lo hi step)
649640
| .listV xs => do
650641
let vs ← RVal.freezeList xs.toList
651642
return .list vs.toArray
@@ -654,8 +645,6 @@ mutual
654645
return .tuple vs.toArray
655646
| .ntuple _ _ _ =>
656647
.unsupported "returning a namedtuple through the call boundary is outside the tier (no namedtuple observation form in `Val`; a tuple snapshot would silently forget the class — docs/memory-model.md §class semantics)"
657-
| .rangeV .. =>
658-
.unsupported "returning a range through the call boundary is outside the tier (no range observation form in `Val`; docs/memory-model.md §module-init execution)"
659648
| .ref _ =>
660649
.unsupported "returning a heap object through the call boundary is outside the tier (H1-proper freeze, docs/memory-model.md)"
661650

@@ -684,6 +673,7 @@ mutual
684673
| .int _ => rfl
685674
| .float _ => rfl
686675
| .str _ => rfl
676+
| .range _ _ _ => rfl
687677
| .list xs => by
688678
simp [RVal.thaw, RVal.freeze, RVal.freezeList_thawList xs.toList]
689679
| .tuple xs => by
@@ -716,6 +706,8 @@ mutual
716706
cases (Res.ok.inj h); rfl
717707
| .str _, v, h => by
718708
cases (Res.ok.inj h); rfl
709+
| .rangeV lo hi step, v, h => by
710+
cases (Res.ok.inj h); rfl
719711
| .listV xs, v, h => by
720712
simp only [RVal.freeze, Res.bind_eq_ok, Res.pure_eq] at h
721713
obtain ⟨vs, hl, hv⟩ := h
@@ -865,7 +857,7 @@ mutual
865857
recursively). On this fragment thawing is pure — the bridge every
866858
pre-H2 boundary statement rides. Kernel-computable (list-structural). -/
867859
def Val.listFree : Val → Bool
868-
| .none | .ellipsis | .bool _ | .int _ | .float _ | .str _ => true
860+
| .none | .ellipsis | .bool _ | .int _ | .float _ | .str _ | .range _ _ _ => true
869861
| .list _ => false
870862
| .tuple xs => Val.listFreeList xs.toList
871863

@@ -893,6 +885,7 @@ mutual
893885
| .int n => (h, .int n)
894886
| .float value => (h, .float value)
895887
| .str s => (h, .str s)
888+
| .range lo hi step => (h, .rangeV lo hi step)
896889
| .tuple xs =>
897890
match RVal.thawListH h xs.toList with
898891
| (h, vs) => (h, .tuple vs.toArray)
@@ -928,6 +921,7 @@ mutual
928921
| .int _, _, h => rfl
929922
| .float _, _, h => rfl
930923
| .str _, _, h => rfl
924+
| .range _ _ _, _, h => rfl
931925
| .tuple xs, hf, h => by
932926
simp only [RVal.thawH, RVal.thaw,
933927
RVal.thawListH_of_listFree xs.toList (by simpa [Val.listFree] using hf) h]
@@ -1002,8 +996,7 @@ mutual
1002996
return .tuple vs.toArray
1003997
| .ntuple _ _ _ =>
1004998
.unsupported "returning a namedtuple through the call boundary is outside the tier (no namedtuple observation form in `Val`; a tuple snapshot would silently forget the class — docs/memory-model.md §class semantics)"
1005-
| .rangeV .. =>
1006-
.unsupported "returning a range through the call boundary is outside the tier (no range observation form in `Val`; docs/memory-model.md §module-init execution)"
999+
| .rangeV lo hi step => .ok (.range lo hi step)
10071000
| .ref a =>
10081001
if path.contains a then
10091002
.unsupported "returning a CYCLIC heap object through the call boundary is outside the tier (no cyclic observation form in `Val`; docs/memory-model.md §call layering)"
@@ -1065,8 +1058,7 @@ mutual
10651058
return .tuple vs.toArray
10661059
| .ntuple _ _ _ =>
10671060
.unsupported "returning a namedtuple through the call boundary is outside the tier (no namedtuple observation form in `Val`; a tuple snapshot would silently forget the class — docs/memory-model.md §class semantics)"
1068-
| .rangeV .. =>
1069-
.unsupported "returning a range through the call boundary is outside the tier (no range observation form in `Val`; docs/memory-model.md §module-init execution)"
1061+
| .rangeV lo hi step => .ok (.range lo hi step)
10701062
| .ref a => RVal.freezeH h fuel [] (.ref a)
10711063

10721064
/-- Elementwise `freezeB`, first refusal wins. -/
@@ -1092,6 +1084,7 @@ mutual
10921084
| .int _ => rfl
10931085
| .float _ => rfl
10941086
| .str _ => rfl
1087+
| .range _ _ _ => rfl
10951088
| .list xs => by
10961089
simp only [RVal.thaw, RVal.freezeB,
10971090
RVal.freezeListB_thawList h fuel xs.toList, Res.ok_bind, Res.pure_eq]
@@ -1146,6 +1139,7 @@ mutual
11461139
| .int _ => rfl
11471140
| .float _ => rfl
11481141
| .str _ => rfl
1142+
| .range _ _ _ => rfl
11491143
| .list xs => by
11501144
simpa [RVal.thaw, RVal.refFree] using
11511145
RVal.refFreeList_thawList xs.toList
@@ -1334,7 +1328,10 @@ theorem RVal.eq_thaw_of_freezeH_pair (h : Heap) (fuel : Nat) :
13341328
| int n => simp only [RVal.freezeH] at hc; cases (Res.ok.inj hc); rfl
13351329
| float value => simp only [RVal.freezeH] at hc; cases (Res.ok.inj hc); rfl
13361330
| str s => simp only [RVal.freezeH] at hc; cases (Res.ok.inj hc); rfl
1337-
| rangeV lo hi step => simp [RVal.freezeH] at hc
1331+
| rangeV lo hi step =>
1332+
simp only [RVal.freezeH] at hc
1333+
cases (Res.ok.inj hc)
1334+
rfl
13381335
| listV xs =>
13391336
-- the snapshot is a `.list` — excluded by `hlf`
13401337
simp only [RVal.freezeH, Res.bind_eq_ok, Res.pure_eq] at hc
@@ -1401,6 +1398,7 @@ mutual
14011398
| .int _, v, hc, _ => by cases (Res.ok.inj hc); rfl
14021399
| .float _, v, hc, _ => by cases (Res.ok.inj hc); rfl
14031400
| .str _, v, hc, _ => by cases (Res.ok.inj hc); rfl
1401+
| .rangeV lo hi step, v, hc, _ => by cases (Res.ok.inj hc); rfl
14041402
| .listV xs, v, hc, hlf => by
14051403
simp only [RVal.freezeB, Res.bind_eq_ok, Res.pure_eq] at hc
14061404
obtain ⟨vs, _, hv⟩ := hc

‎LeanModels/Python/Semantics.lean‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def Val.typeName : Val → String
9898
| .int _ => "int"
9999
| .float _ => "float"
100100
| .str _ => "str"
101+
| .range _ _ _ => "range"
101102
| .list _ => "list"
102103
| .tuple _ => "tuple"
103104

‎Main.lean‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ partial def valJson : Val → String
228228
| .int n => "{\"t\":\"int\",\"v\":" ++ jsonStr (toString n) ++ "}"
229229
| .float value => "{\"t\":\"float\",\"bits\":" ++ jsonStr (toString value.bits.toNat) ++ "}"
230230
| .str s => "{\"t\":\"str\",\"v\":" ++ jsonStr s ++ "}"
231+
| .range lo hi step =>
232+
"{\"t\":\"range\",\"lo\":" ++ jsonStr (toString lo.toInt) ++
233+
",\"hi\":" ++ jsonStr (toString hi.toInt) ++
234+
",\"step\":" ++ jsonStr (toString step.toInt) ++ "}"
231235
| .list xs => "{\"t\":\"list\",\"v\":[" ++ ",".intercalate (xs.toList.map valJson) ++ "]}"
232236
| .tuple xs => "{\"t\":\"tuple\",\"v\":[" ++ ",".intercalate (xs.toList.map valJson) ++ "]}"
233237

‎docs/memory-model.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3105,8 +3105,9 @@ each construct differential, refusals loud):
31053105
scalar renderer; numerical consumers use `RangeBound.toInt`. Thus
31063106
`range(False, True)` renders those Boolean names but yields integer `0`.
31073107
A retained Boolean `True` step is numerically one and omitted from rendering;
3108-
`False` still fails construction. Attribute access, slicing,
3109-
range-valued `%` conversions, and the public boundary are not newly admitted.
3108+
`False` still fails construction. Attribute access, slicing, and
3109+
range-valued `%` conversions remain loud; ranges now cross the public
3110+
boundary as a structural `Val.range` snapshot.
31103111

31113112
**What stays out, loudly:** `.items()` outside the init shell;
31123113
`dict.keys/.values`; the call-mutation gap where an attempt was REFUSED

‎docs/python-coverage.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ What the Lean model of Python runs, measured. Everything outside the modelled ti
1010

1111
| rows | agree with CPython | disagree | recorded refusals |
1212
|---:|---:|---:|---:|
13-
| 3025 | 2664 | 0 | 361 |
13+
| 3025 | 2665 | 0 | 360 |
1414

15-
Of the 2664 rows the model decides, 2664 agree with CPython (100.0%). A *recorded refusal* is a row kept in the suite to pin a known gap: the model must refuse it, and the suite fails if it ever answers instead.
15+
Of the 2665 rows the model decides, 2665 agree with CPython (100.0%). A *recorded refusal* is a row kept in the suite to pin a known gap: the model must refuse it, and the suite fails if it ever answers instead.
1616

1717
### What the theorems can see
1818

1919
The rows above run through the runner's interpreter (`LeanModels/Python/Monadic/`). Theorems are stated about a second definition, `LeanModels/Python/Semantics.lean`, whose tier is narrower ([python-architecture.md](python-architecture.md)). `diff_test.py --proof-interpreter` runs the same rows through it; a refusal there is allowed, a wrong answer is not.
2020

2121
| rows | agree with CPython | disagree | refused |
2222
|---:|---:|---:|---:|
23-
| 3025 | 2591 | 0 | 434 |
23+
| 3025 | 2592 | 0 | 433 |
2424

2525
## Grammar
2626

‎harness/cases.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@
408408
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_in_step", "args": [[]], "expect": "match"},
409409
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_in_float", "args": [[]], "expect": "match"},
410410
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_in_fraction", "args": [[]], "expect": "match"},
411-
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_boundary", "args": [[3]], "expect": "unsupported"},
411+
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_boundary", "args": [[3]], "expect": "match"},
412412
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_unpack", "args": [[]], "expect": "match"},
413413
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_unpack_step", "args": [[]], "expect": "match"},
414414
{"file": "Examples/python/seq_lab/seq_lab.py", "function": "range_unpack_bad", "args": [[]], "expect": "match"},

0 commit comments

Comments
 (0)