Skip to content

Commit 93691de

Browse files
pirapiraclaude
andcommitted
Complete Phase 9e: Fork choice — Store, LMD GHOST, ancestors
Add interpreter features needed for fork choice: - max()/min() with key= parameter via builtinMinWithKey/builtinMaxWithKey - set.update() method for in-place set merging - time.perf_counter() function - zip() with strict=True kwarg for length validation - Tuple lexicographic comparison (<, <=, >, >=) Tests cover: ancestors generator, walrus-in-while pattern, LMD GHOST algorithm with forking block tree, and full Store end-to-end with from_anchor, on_block, update_head, and attestation processing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a2489c8 commit 93691de

5 files changed

Lines changed: 127 additions & 8 deletions

File tree

LeanPython/Interpreter/Eval.lean

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ open LeanPython.Stdlib.Hmac
2020
open LeanPython.Stdlib.Secrets
2121
open LeanPython.Stdlib.Sys
2222
open LeanPython.Stdlib.Os
23-
open LeanPython.Stdlib.Time (timeTime timeMonotonic timeSleep)
23+
open LeanPython.Stdlib.Time (timeTime timeMonotonic timePerfCounter timeSleep)
2424
open LeanPython.Stdlib.Datetime
2525
open LeanPython.Stdlib.Pathlib
2626
open LeanPython.Stdlib.Logging
@@ -138,7 +138,7 @@ private def knownDictMethods : List String :=
138138
["get", "keys", "values", "items", "pop", "update", "clear", "copy", "setdefault"]
139139

140140
private def knownSetMethods : List String :=
141-
["add", "remove", "discard", "clear", "copy", "union", "intersection",
141+
["add", "remove", "discard", "clear", "copy", "update", "union", "intersection",
142142
"difference", "symmetric_difference", "issubset", "issuperset", "isdisjoint"]
143143

144144
private def knownIntMethods : List String :=
@@ -759,6 +759,16 @@ partial def callValueDispatch (callee : Value) (args : List Value)
759759
match name with
760760
| "map" => builtinMap args
761761
| "filter" => builtinFilter args
762+
| "min" => do
763+
let keyKw := kwargs.find? (fun (k, _) => k == "key")
764+
match keyKw with
765+
| some (_, keyFn) => builtinMinWithKey args keyFn
766+
| none => callBuiltin name args kwargs
767+
| "max" => do
768+
let keyKw := kwargs.find? (fun (k, _) => k == "key")
769+
match keyKw with
770+
| some (_, keyFn) => builtinMaxWithKey args keyFn
771+
| none => callBuiltin name args kwargs
762772
| "functools.reduce" => builtinFunctoolsReduce args
763773
| "itertools.accumulate" => builtinItertoolsAccumulate args kwargs
764774
| "collections.defaultdict" => do
@@ -3135,9 +3145,10 @@ partial def getBuiltinModule (name : String) : InterpM (Option Value) := do
31353145
some <$> mkMod ns
31363146
| "time" =>
31373147
let mut ns : Std.HashMap String Value := {}
3138-
ns := ns.insert "time" (.builtin "time.time")
3139-
ns := ns.insert "monotonic" (.builtin "time.monotonic")
3140-
ns := ns.insert "sleep" (.builtin "time.sleep")
3148+
ns := ns.insert "time" (.builtin "time.time")
3149+
ns := ns.insert "monotonic" (.builtin "time.monotonic")
3150+
ns := ns.insert "perf_counter" (.builtin "time.perf_counter")
3151+
ns := ns.insert "sleep" (.builtin "time.sleep")
31413152
some <$> mkMod ns
31423153
| "datetime" =>
31433154
let mut ns : Std.HashMap String Value := {}
@@ -4182,6 +4193,42 @@ partial def builtinFilter (args : List Value) : InterpM Value := do
41824193
allocList result
41834194
| _ => throwTypeError "filter() requires exactly two arguments"
41844195

4196+
-- ============================================================
4197+
-- min/max with key= (need callValueDispatch, so must be in mutual block)
4198+
-- ============================================================
4199+
4200+
partial def builtinMinWithKey (args : List Value) (keyFn : Value) : InterpM Value := do
4201+
let items : List Value ← match args with
4202+
| [v] => do let arr ← iterValuesExt v; pure arr.toList
4203+
| xs => pure xs
4204+
match items with
4205+
| [] => throwValueError "min() arg is an empty sequence"
4206+
| first :: rest => do
4207+
let mut best := first
4208+
let mut bestKey ← callValueDispatch keyFn [first] []
4209+
for v in rest do
4210+
let vKey ← callValueDispatch keyFn [v] []
4211+
if ← evalCmpOp .lt vKey bestKey then
4212+
best := v
4213+
bestKey := vKey
4214+
return best
4215+
4216+
partial def builtinMaxWithKey (args : List Value) (keyFn : Value) : InterpM Value := do
4217+
let items : List Value ← match args with
4218+
| [v] => do let arr ← iterValuesExt v; pure arr.toList
4219+
| xs => pure xs
4220+
match items with
4221+
| [] => throwValueError "max() arg is an empty sequence"
4222+
| first :: rest => do
4223+
let mut best := first
4224+
let mut bestKey ← callValueDispatch keyFn [first] []
4225+
for v in rest do
4226+
let vKey ← callValueDispatch keyFn [v] []
4227+
if ← evalCmpOp .gt vKey bestKey then
4228+
best := v
4229+
bestKey := vKey
4230+
return best
4231+
41854232
-- ============================================================
41864233
-- Enum support: check if any base class is an Enum
41874234
-- ============================================================
@@ -4891,6 +4938,18 @@ partial def callSetMethod (ref : HeapRef) (method : String) (args : List Value)
48914938
if !found then result := result.push elem
48924939
allocSet result
48934940
| _ => throwTypeError "union() takes exactly one argument"
4941+
| "update" =>
4942+
match args with
4943+
| [other] => do
4944+
let mut arr ← heapGetSet ref
4945+
let b ← iterValuesExt other
4946+
for elem in b do
4947+
let mut found := false
4948+
for existing in arr do
4949+
if ← valueEq existing elem then found := true; break
4950+
if !found then arr := arr.push elem
4951+
heapSetSet ref arr; return .none
4952+
| _ => throwTypeError "update() takes exactly one argument"
48944953
| "intersection" =>
48954954
match args with
48964955
| [other] => do

LeanPython/Runtime/Builtins.lean

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,20 @@ partial def builtinEnumerate (args : List Value) : InterpM Value := do
300300
idx := idx + 1
301301
allocList result
302302

303-
/-- `zip(*iterables)` - returns list of tuples. -/
304-
partial def builtinZip (args : List Value) : InterpM Value := do
303+
/-- `zip(*iterables, strict=False)` - returns list of tuples. -/
304+
partial def builtinZip (args : List Value) (kwargs : List (String × Value)) : InterpM Value := do
305305
let iterables ← args.mapM iterValues
306306
if iterables.isEmpty then
307307
allocList #[]
308308
else
309+
let strict := match kwargs.find? (fun (k, _) => k == "strict") with
310+
| some (_, .bool true) => true
311+
| _ => false
312+
if strict then
313+
let firstLen := iterables.head!.size
314+
for iter in iterables do
315+
if iter.size != firstLen then
316+
throwValueError "zip() has arguments with different lengths"
309317
let minLen := iterables.foldl (fun acc arr => min acc arr.size) iterables.head!.size
310318
let mut result : Array Value := #[]
311319
for i in [:minLen] do
@@ -622,7 +630,7 @@ partial def callBuiltin (name : String) (args : List Value)
622630
| "sorted" => builtinSorted args
623631
| "reversed" => builtinReversed args
624632
| "enumerate" => builtinEnumerate args
625-
| "zip" => builtinZip args
633+
| "zip" => builtinZip args kwargs
626634
| "list" => builtinList args
627635
| "tuple" => builtinTuple args
628636
| "isinstance" => builtinIsinstance args
@@ -1076,6 +1084,7 @@ partial def callBuiltin (name : String) (args : List Value)
10761084
-- ============================================================
10771085
| "time.time" => timeTime args
10781086
| "time.monotonic" => timeMonotonic args
1087+
| "time.perf_counter" => timePerfCounter args
10791088
| "time.sleep" => timeSleep args
10801089
-- ============================================================
10811090
-- logging module functions

LeanPython/Runtime/Ops.lean

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,7 @@ partial def evalCmpOp (op : CmpOp) (left right : Value) : InterpM Bool := do
639639
match left', right' with
640640
| .str a, .str b => return (a < b)
641641
| .bytes a, .bytes b => return (a.toList < b.toList)
642+
| .tuple a, .tuple b => tupleLt a b
642643
| _, _ => throwTypeError s!"'<' not supported between instances of '{typeName left}' and '{typeName right}'"
643644
| .ltE => do
644645
match ← numericCompare left' right' with
@@ -648,6 +649,11 @@ partial def evalCmpOp (op : CmpOp) (left right : Value) : InterpM Bool := do
648649
match left', right' with
649650
| .str a, .str b => return (decide (a ≤ b))
650651
| .bytes a, .bytes b => return (a.toList < b.toList || a == b)
652+
| .tuple a, .tuple b => do
653+
let lt ← tupleLt a b
654+
if lt then return true
655+
let gt ← tupleLt b a
656+
return !gt
651657
| _, _ => throwTypeError s!"'<=' not supported between instances of '{typeName left}' and '{typeName right}'"
652658
| .gt => do
653659
match ← numericCompare left' right' with
@@ -657,6 +663,7 @@ partial def evalCmpOp (op : CmpOp) (left right : Value) : InterpM Bool := do
657663
match left', right' with
658664
| .str a, .str b => return (decide (a > b))
659665
| .bytes a, .bytes b => return (b.toList < a.toList)
666+
| .tuple a, .tuple b => tupleLt b a
660667
| _, _ => throwTypeError s!"'>' not supported between instances of '{typeName left}' and '{typeName right}'"
661668
| .gtE => do
662669
match ← numericCompare left' right' with
@@ -666,11 +673,25 @@ partial def evalCmpOp (op : CmpOp) (left right : Value) : InterpM Bool := do
666673
match left', right' with
667674
| .str a, .str b => return (decide (a ≥ b))
668675
| .bytes a, .bytes b => return (b.toList < a.toList || a == b)
676+
| .tuple a, .tuple b => do
677+
let lt ← tupleLt b a
678+
if lt then return true
679+
let gt ← tupleLt a b
680+
return !gt
669681
| _, _ => throwTypeError s!"'>=' not supported between instances of '{typeName left}' and '{typeName right}'"
670682
| .is_ => return (isIdentical left right)
671683
| .isNot => return (!isIdentical left right)
672684
| .in_ => valueContains right left
673685
| .notIn => do return !(← valueContains right left)
686+
where
687+
/-- Lexicographic less-than for tuples. -/
688+
tupleLt (a b : Array Value) : InterpM Bool := do
689+
let minLen := min a.size b.size
690+
for i in [:minLen] do
691+
let eq ← valueEq a[i]! b[i]!
692+
if !eq then
693+
return ← evalCmpOp .lt a[i]! b[i]!
694+
return (a.size < b.size)
674695

675696
-- ============================================================
676697
-- Bool operators (short-circuit)

LeanPython/Stdlib/Time.lean

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ partial def timeMonotonic (_args : List Value) : InterpM Value := do
2121
let ms ← (IO.monoMsNow : BaseIO Nat)
2222
return .float (Nat.toFloat ms / 1000.0)
2323

24+
/-- Python time.perf_counter(): return performance counter as float seconds. -/
25+
partial def timePerfCounter (_args : List Value) : InterpM Value := do
26+
let ms ← (IO.monoMsNow : BaseIO Nat)
27+
return .float (Nat.toFloat ms / 1000.0)
28+
2429
/-- Python time.sleep(seconds): sleep for given duration. -/
2530
partial def timeSleep (args : List Value) : InterpM Value := do
2631
let ms : UInt32 := match args with

0 commit comments

Comments
 (0)