Skip to content

Commit 3f091df

Browse files
pirapiraclaude
andcommitted
Add dict subclass inheritance support (closes #4)
Enable `class BlockLookup(dict[Bytes32, "Block"])` by adding dict as a synthetic builtin type class with full dunder and method dispatch, following the existing int/bytes subclass pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f07d528 commit 3f091df

2 files changed

Lines changed: 208 additions & 2 deletions

File tree

LeanPython/Interpreter/Eval.lean

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,14 @@ private partial def getOrCreateBuiltinTypeClass (name : String) : InterpM Value
399399
"hex", "fromhex", "decode", "join"
400400
] do
401401
ns := ns.insert dunder (.builtin s!"bytes.{dunder}")
402+
else if name == "dict" then
403+
for dunder in [
404+
"__new__", "__init__", "__len__", "__getitem__", "__setitem__", "__delitem__",
405+
"__contains__", "__iter__", "__eq__", "__ne__", "__or__",
406+
"__repr__", "__str__", "__bool__",
407+
"get", "keys", "values", "items", "pop", "update", "clear", "copy", "setdefault"
408+
] do
409+
ns := ns.insert dunder (.builtin s!"dict.{dunder}")
402410
-- Create the class with object as base
403411
let cls ← allocClassObj { name := name, bases := #[objectCls], mro := #[], ns := ns, slots := none }
404412
-- Compute MRO: [self, object]
@@ -414,7 +422,7 @@ private def resolveClassBases (bases : List Value) : InterpM (List Value) := do
414422
bases.mapM fun base =>
415423
match base with
416424
| .builtin name =>
417-
if name == "int" || name == "bytes" || name == "object" || name == "bool" then
425+
if name == "int" || name == "bytes" || name == "object" || name == "bool" || name == "dict" then
418426
getOrCreateBuiltinTypeClass name
419427
else pure base
420428
| _ => pure base
@@ -1527,6 +1535,51 @@ partial def callValueDispatch (callee : Value) (args : List Value)
15271535
| none =>
15281536
allocInstance { cls := cls, attrs := {}, wrappedValue := some (.bytes ByteArray.empty) }
15291537
| some other => throwTypeError s!"bytes.__new__: cannot convert {typeName other} to bytes"
1538+
else if name == "dict.__new__" then do
1539+
-- dict.__new__(cls) — create an instance with an empty wrapped dict
1540+
let cls ← match args with
1541+
| [cls@(.classObj _)] => pure cls
1542+
| [_, cls@(.classObj _)] => pure cls -- super() prepends inst
1543+
| [cls@(.classObj _), _] => pure cls -- cls, init_arg (init_arg handled by __init__)
1544+
| [_, cls@(.classObj _), _] => pure cls -- super() prepends inst + init_arg
1545+
| _ => throwTypeError "dict.__new__(cls) requires a class"
1546+
let dictRef ← heapAlloc (.dictObj #[])
1547+
allocInstance { cls := cls, attrs := {}, wrappedValue := some (.dict dictRef) }
1548+
else if name == "dict.__init__" then do
1549+
-- dict.__init__(self, source?, **kwargs) — populate the wrapped dict
1550+
let (selfVal, initArgs, initKwargs) ← match args with
1551+
| [s] => pure (s, none, kwargs)
1552+
| [s, arg] => pure (s, some arg, kwargs)
1553+
| _ => pure (args.head!, none, kwargs)
1554+
-- Get the wrapped dict ref from self
1555+
let dictRef ← match selfVal with
1556+
| .instance iref => do
1557+
let id_ ← heapGetInstanceData iref
1558+
match id_.wrappedValue with
1559+
| some (.dict ref) => pure ref
1560+
| _ => throwTypeError "dict.__init__: self has no wrapped dict"
1561+
| _ => throwTypeError "dict.__init__: expected instance"
1562+
-- Copy entries from source argument
1563+
match initArgs with
1564+
| some (.dict srcRef) => do
1565+
let srcPairs ← heapGetDict srcRef
1566+
heapSetDict dictRef srcPairs
1567+
| some (.instance iref) => do
1568+
let id_ ← heapGetInstanceData iref
1569+
match id_.wrappedValue with
1570+
| some (.dict srcRef) => do
1571+
let srcPairs ← heapGetDict srcRef
1572+
heapSetDict dictRef srcPairs
1573+
| _ => throwTypeError "dict.__init__: cannot convert to dict"
1574+
| some _ => throwTypeError "dict.__init__: cannot convert to dict"
1575+
| none => pure ()
1576+
-- Add kwargs entries
1577+
if !initKwargs.isEmpty then
1578+
let mut pairs ← heapGetDict dictRef
1579+
for (k, v) in initKwargs do
1580+
pairs := pairs.push (.str k, v)
1581+
heapSetDict dictRef pairs
1582+
return .none
15301583
else if name.startsWith "int." then do
15311584
-- Dispatch int dunder methods: extract wrapped int values
15321585
let methodName := String.ofList (name.toList.drop "int.".length)
@@ -1881,6 +1934,127 @@ partial def callValueDispatch (callee : Value) (args : List Value)
18811934
return .bytes result
18821935
| _ => throwTypeError "bytes.join takes 1 argument"
18831936
| _ => throwTypeError s!"bytes.{methodName} is not implemented"
1937+
else if name.startsWith "dict." then do
1938+
-- Dispatch dict dunder/methods: extract wrapped dict ref
1939+
let methodName := String.ofList (name.toList.drop "dict.".length)
1940+
let extractDictRef : Value → InterpM HeapRef := fun v =>
1941+
match v with
1942+
| .dict ref => pure ref
1943+
| .instance iref => do
1944+
let id_ ← heapGetInstanceData iref
1945+
match id_.wrappedValue with
1946+
| some (.dict ref) => pure ref
1947+
| _ => throwTypeError s!"dict.{methodName}: expected dict, got {typeName v}"
1948+
| _ => throwTypeError s!"dict.{methodName}: expected dict, got {typeName v}"
1949+
match methodName with
1950+
| "__len__" =>
1951+
let ref ← extractDictRef args.head!
1952+
return .int (← heapGetDict ref).size
1953+
| "__getitem__" =>
1954+
let ref ← extractDictRef args.head!
1955+
let idx := args.tail.head!
1956+
let pairs ← heapGetDict ref
1957+
for (k, v) in pairs do
1958+
if ← valueEq k idx then return v
1959+
throwKeyError (← valueRepr idx)
1960+
| "__setitem__" =>
1961+
let ref ← extractDictRef args.head!
1962+
let idx := args.tail.head!
1963+
let value := args.tail.tail.head!
1964+
let pairs ← heapGetDict ref
1965+
let mut newPairs := pairs
1966+
let mut found := false
1967+
for i in [:pairs.size] do
1968+
if ← valueEq pairs[i]!.1 idx then
1969+
newPairs := newPairs.set! i (idx, value); found := true; break
1970+
if !found then newPairs := newPairs.push (idx, value)
1971+
heapSetDict ref newPairs
1972+
return .none
1973+
| "__delitem__" =>
1974+
let ref ← extractDictRef args.head!
1975+
let idx := args.tail.head!
1976+
let pairs ← heapGetDict ref
1977+
let mut newPairs : Array (Value × Value) := #[]
1978+
let mut found := false
1979+
for (k, v) in pairs do
1980+
if !found && (← valueEq k idx) then found := true
1981+
else newPairs := newPairs.push (k, v)
1982+
if !found then throwKeyError (← valueRepr idx)
1983+
heapSetDict ref newPairs
1984+
return .none
1985+
| "__contains__" =>
1986+
let ref ← extractDictRef args.head!
1987+
let elem := args.tail.head!
1988+
let pairs ← heapGetDict ref
1989+
for (k, _) in pairs do
1990+
if ← valueEq k elem then return .bool true
1991+
return .bool false
1992+
| "__iter__" =>
1993+
let ref ← extractDictRef args.head!
1994+
let pairs ← heapGetDict ref
1995+
let keys := pairs.map Prod.fst
1996+
allocGenerator keys
1997+
| "__eq__" =>
1998+
let refA ← extractDictRef args.head!
1999+
let pairsA ← heapGetDict refA
2000+
match args.tail.head! with
2001+
| .dict refB => do
2002+
let pairsB ← heapGetDict refB
2003+
if pairsA.size != pairsB.size then return .bool false
2004+
for (k, v) in pairsA do
2005+
let mut found := false
2006+
for (k2, v2) in pairsB do
2007+
if ← valueEq k k2 then
2008+
if ← valueEq v v2 then found := true
2009+
break
2010+
if !found then return .bool false
2011+
return .bool true
2012+
| .instance iref => do
2013+
let id_ ← heapGetInstanceData iref
2014+
match id_.wrappedValue with
2015+
| some (.dict refB) => do
2016+
let pairsB ← heapGetDict refB
2017+
if pairsA.size != pairsB.size then return .bool false
2018+
for (k, v) in pairsA do
2019+
let mut found := false
2020+
for (k2, v2) in pairsB do
2021+
if ← valueEq k k2 then
2022+
if ← valueEq v v2 then found := true
2023+
break
2024+
if !found then return .bool false
2025+
return .bool true
2026+
| _ => return .bool false
2027+
| _ => return .bool false
2028+
| "__ne__" =>
2029+
let eqResult ← callValueDispatch (.builtin "dict.__eq__") args kwargs
2030+
match eqResult with
2031+
| .bool b => return .bool (!b)
2032+
| _ => return .bool true
2033+
| "__or__" =>
2034+
let refA ← extractDictRef args.head!
2035+
let pairsA ← heapGetDict refA
2036+
let refB ← extractDictRef args.tail.head!
2037+
let pairsB ← heapGetDict refB
2038+
let mut result := pairsA
2039+
for (k, v) in pairsB do
2040+
let mut found := false
2041+
for i in [:result.size] do
2042+
if ← valueEq result[i]!.1 k then
2043+
result := result.set! i (k, v); found := true; break
2044+
if !found then result := result.push (k, v)
2045+
allocDict result
2046+
| "__bool__" =>
2047+
let ref ← extractDictRef args.head!
2048+
return .bool !(← heapGetDict ref).isEmpty
2049+
| "__repr__" | "__str__" =>
2050+
let ref ← extractDictRef args.head!
2051+
let s ← valueRepr (.dict ref)
2052+
return .str s
2053+
| "get" | "keys" | "values" | "items" | "pop" | "update" |
2054+
"clear" | "copy" | "setdefault" =>
2055+
let ref ← extractDictRef args.head!
2056+
callDictMethod ref methodName args.tail
2057+
| _ => throwTypeError s!"dict.{methodName} is not implemented"
18842058
-- ============================================================
18852059
-- functools.singledispatch dispatch and register
18862060
-- ============================================================
@@ -2644,6 +2818,7 @@ partial def getAttributeValue (obj : Value) (attr : String) : InterpM Value := d
26442818
| .function _ => return .boundMethod obj attr
26452819
| .staticMethod fn => return fn
26462820
| .classMethod _ => return .boundMethod id_.cls attr
2821+
| .builtin _ => return .boundMethod obj attr
26472822
| _ => return v
26482823
| _ => pure ()
26492824
-- Fallback: try __getattr__ hook before raising
@@ -2776,7 +2951,7 @@ partial def evalSubscriptValue (obj idx : Value) : InterpM Value := do
27762951
-- Allow subscripting on builtin type names for type annotations (list[int], dict[str, int], etc.)
27772952
match name with
27782953
| "list" | "dict" | "set" | "tuple" | "frozenset" | "type"
2779-
| "memoryview" | "complex" => return .none
2954+
| "memoryview" | "complex" => return (.builtin name)
27802955
| _ => throwTypeError s!"'{typeName obj}' object is not subscriptable"
27812956
| _ => throwTypeError s!"'{typeName obj}' object is not subscriptable"
27822957

LeanPythonTest/Interpreter.lean

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,3 +674,34 @@ private def assertPyError (source errSubstr : String) : IO Unit := do
674674

675675
-- Match/case used as variable name (soft keyword)
676676
#eval assertPy "match = 5\nprint(match)\n" "5\n"
677+
678+
-- ============================================================
679+
-- Dict subclass tests
680+
-- ============================================================
681+
682+
-- Dict subclass: basic construction (empty)
683+
#eval assertPy "class D(dict):\n pass\nd = D()\nprint(len(d))\n" "0\n"
684+
685+
-- Dict subclass: construction with dict arg
686+
#eval assertPy "class D(dict):\n pass\nd = D({1: 'a', 2: 'b'})\nprint(len(d))\nprint(d[1])\n" "2\na\n"
687+
688+
-- Dict subclass: setitem, getitem, contains
689+
#eval assertPy "class D(dict):\n pass\nd = D()\nd[1] = 'x'\nprint(1 in d)\nprint(d[1])\n" "True\nx\n"
690+
691+
-- Dict subclass: iteration
692+
#eval assertPy "class D(dict):\n pass\nd = D({1: 'a', 2: 'b'})\nfor k in d:\n print(k)\n" "1\n2\n"
693+
694+
-- Dict subclass: isinstance
695+
#eval assertPy "class D(dict):\n pass\nd = D()\nprint(isinstance(d, dict))\nprint(isinstance(d, D))\n" "True\nTrue\n"
696+
697+
-- Dict subclass: __or__ via super()
698+
#eval assertPy "class D(dict):\n def __or__(self, other):\n return D(super().__or__(other))\nd = D({1: 'a'})\nresult = d | {2: 'b'}\nprint(len(result))\nprint(isinstance(result, D))\n" "2\nTrue\n"
699+
700+
-- Dict subclass: generic subscript base (dict[str, int])
701+
#eval assertPy "class D(dict[str, int]):\n pass\nd = D()\nd['x'] = 1\nprint(d['x'])\n" "1\n"
702+
703+
-- Dict subclass: .get method
704+
#eval assertPy "class D(dict):\n pass\nd = D({1: 'a'})\nprint(d.get(1))\nprint(d.get(2, 'default'))\n" "a\ndefault\n"
705+
706+
-- Dict subclass: .keys, .values, .items
707+
#eval assertPy "class D(dict):\n pass\nd = D({1: 'a'})\nprint(list(d.keys()))\nprint(list(d.values()))\n" "[1]\n['a']\n"

0 commit comments

Comments
 (0)