Skip to content

Commit 8541acb

Browse files
pirapiraclaude
andcommitted
Complete Phase 5A: core object model (classes, instances, single inheritance, super)
Add ClassData/InstanceData heap types, Value.classObj/instance/superObj variants. Classes now store namespace and MRO, instances bind self on method calls, and super() correctly tracks the defining class for multi-level inheritance chains. Includes isinstance/issubclass for custom classes, type() returning the class, and refactored with-statement to use generic attribute lookup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 258dd85 commit 8541acb

7 files changed

Lines changed: 389 additions & 70 deletions

File tree

LeanPython/Interpreter/Eval.lean

Lines changed: 176 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,34 @@ partial def callValueDispatch (callee : Value) (args : List Value)
318318
let fd ← heapGetFunc ref
319319
callUserFunc fd args kwargs
320320
| .boundMethod receiver method => callBoundMethod receiver method args
321+
| .classObj cref => do
322+
-- Instance creation: allocate instance, call __init__
323+
let cd ← heapGetClassData cref
324+
let inst ← allocInstance { cls := callee, attrs := {} }
325+
-- Look up __init__ in MRO, tracking which class defines it
326+
let mut initFound : Option (Value × Value) := none -- (function, defining class)
327+
for mroEntry in cd.mro do
328+
match mroEntry with
329+
| .classObj mref => do
330+
let mcd ← heapGetClassData mref
331+
if let some fn := mcd.ns["__init__"]? then
332+
initFound := some (fn, mroEntry)
333+
break
334+
| _ => pure ()
335+
-- Call __init__ if found, injecting __class__ for super() support
336+
match initFound with
337+
| some (fn, definingCls) =>
338+
match fn with
339+
| .function fref => do
340+
let fd ← heapGetFunc fref
341+
let scope ← bindFuncParams fd.params (inst :: args) kwargs fd.defaults fd.kwDefaults
342+
let scopeWithClass := scope.insert "__class__" definingCls
343+
let _ ← callRegularFunc fd scopeWithClass
344+
| _ => let _ ← callValueDispatch fn (inst :: args) kwargs
345+
| none =>
346+
if !args.isEmpty then
347+
throwTypeError s!"{cd.name}() takes no arguments"
348+
return inst
321349
| .exception _ _ =>
322350
-- Exception objects are not callable, but exception classes are handled as builtins
323351
throwTypeError s!"'{typeName callee}' object is not callable"
@@ -523,6 +551,39 @@ partial def getAttributeValue (obj : Value) (attr : String) : InterpM Value := d
523551
if attr == "__next__" || attr == "__iter__" || attr == "close" then
524552
return .boundMethod obj attr
525553
else throwAttributeError s!"'generator' object has no attribute '{attr}'"
554+
| .classObj cref => do
555+
let cd ← heapGetClassData cref
556+
-- Look up in own namespace first, then walk MRO
557+
for mroEntry in cd.mro do
558+
match mroEntry with
559+
| .classObj mref => do
560+
let mcd ← heapGetClassData mref
561+
if let some v := mcd.ns[attr]? then return v
562+
| _ => pure ()
563+
throwAttributeError s!"type object '{cd.name}' has no attribute '{attr}'"
564+
| .instance iref => do
565+
let id_ ← heapGetInstanceData iref
566+
-- Check instance attributes first
567+
if let some v := id_.attrs[attr]? then return v
568+
-- Then walk class MRO
569+
match id_.cls with
570+
| .classObj cref => do
571+
let cd ← heapGetClassData cref
572+
for mroEntry in cd.mro do
573+
match mroEntry with
574+
| .classObj mref => do
575+
let mcd ← heapGetClassData mref
576+
if let some v := mcd.ns[attr]? then
577+
-- If it's a function, return as bound method
578+
match v with
579+
| .function _ => return .boundMethod obj attr
580+
| _ => return v
581+
| _ => pure ()
582+
throwAttributeError s!"'{cd.name}' object has no attribute '{attr}'"
583+
| _ => throwAttributeError s!"instance has no attribute '{attr}'"
584+
| .superObj _startAfterCls _inst =>
585+
-- super().attr — resolved in callBoundMethod
586+
return .boundMethod obj attr
526587
| _ => throwAttributeError s!"'{typeName obj}' object has no attribute '{attr}'"
527588

528589
-- Subscript access
@@ -625,6 +686,12 @@ partial def assignToTarget (target : Expr) (value : Value) : InterpM Unit := do
625686
break
626687
if !found then newPairs := newPairs.push (key, value)
627688
heapSetDict ref newPairs
689+
| .instance iref => do
690+
let id_ ← heapGetInstanceData iref
691+
heapSetInstanceData iref { id_ with attrs := id_.attrs.insert attr value }
692+
| .classObj cref => do
693+
let cd ← heapGetClassData cref
694+
heapSetClassData cref { cd with ns := cd.ns.insert attr value }
628695
| _ => throwAttributeError s!"'{typeName objVal}' object attribute '{attr}' is read-only"
629696
| .starred inner _ => assignToTarget inner value
630697
| _ => throwRuntimeError (.runtimeError "invalid assignment target")
@@ -758,7 +825,9 @@ partial def execStmt (s : Stmt) : InterpM Unit := do
758825
for n in names do declareNonlocal n
759826

760827
| .classDef cd => do
761-
-- Minimal: run body in a scope, store as dict
828+
-- Evaluate base classes
829+
let bases ← cd.bases.mapM evalExpr
830+
-- Execute class body in a new scope
762831
pushScope {}
763832
try execStmts cd.body
764833
catch
@@ -769,11 +838,30 @@ partial def execStmt (s : Stmt) : InterpM Unit := do
769838
| s :: _ => s
770839
| [] => ({} : Scope)
771840
popScope
772-
let mut entries : Array (Value × Value) := #[]
773-
entries := entries.push (.str "__name__", .str cd.name)
841+
-- Build namespace from class body scope
842+
let mut ns : Std.HashMap String Value := {}
774843
for (k, v) in classScope do
775-
entries := entries.push (.str k, v)
776-
let classVal ← allocDict entries
844+
ns := ns.insert k v
845+
-- Compute linear MRO: [self, ...bases' MROs flattened]
846+
let selfPlaceholder := Value.none -- placeholder, replaced after allocation
847+
let mut mro : Array Value := #[selfPlaceholder]
848+
for base in bases do
849+
match base with
850+
| .classObj bref => do
851+
let bcd ← heapGetClassData bref
852+
for entry in bcd.mro do
853+
-- Avoid duplicates
854+
let isDup := mro.any fun e => Value.beq e entry
855+
if !isDup then mro := mro.push entry
856+
| _ => pure () -- ignore non-class bases for now
857+
-- Allocate the class
858+
let classVal ← allocClassObj { name := cd.name, bases := bases.toArray, mro := mro, ns := ns }
859+
-- Fix MRO[0] to point to the actual class
860+
match classVal with
861+
| .classObj cref => do
862+
let cd' ← heapGetClassData cref
863+
heapSetClassData cref { cd' with mro := cd'.mro.set! 0 classVal }
864+
| _ => pure ()
777865
setVariable cd.name classVal
778866

779867
| .raise_ exprOpt cause _ => do
@@ -869,23 +957,21 @@ partial def execStmt (s : Stmt) : InterpM Unit := do
869957
| some other => do execStmts finally_; throw other
870958

871959
| .with_ items body _ => do
960+
-- Helper: try to get a named attribute from an object
961+
let getAttr := fun (obj : Value) (name : String) => do
962+
try
963+
let v ← getAttributeValue obj name
964+
return some v
965+
catch
966+
| .error (.attributeError _) => return (none : Option Value)
967+
| other => throw other
872968
-- Evaluate context managers and call __enter__
873969
let mut managers : List (Value × Value) := []
874970
for item in items do
875971
let mgr ← evalExpr item.contextExpr
876-
-- Try to call __enter__ on the manager
877-
let entered ← match mgr with
878-
| .dict ref => do
879-
let pairs ← heapGetDict ref
880-
let mut enterFn : Option Value := none
881-
for (k, v) in pairs do
882-
match k with
883-
| .str s => if s == "__enter__" then enterFn := some v
884-
| _ => pure ()
885-
match enterFn with
886-
| some fn => callValueDispatch fn [mgr] []
887-
| none => pure mgr
888-
| _ => pure mgr
972+
let entered ← match ← getAttr mgr "__enter__" with
973+
| some fn => callValueDispatch fn [mgr] []
974+
| none => pure mgr
889975
if let some target := item.optionalVars then
890976
assignToTarget target entered
891977
managers := managers ++ [(mgr, entered)]
@@ -895,43 +981,23 @@ partial def execStmt (s : Stmt) : InterpM Unit := do
895981
catch
896982
| sig@(.error _) => bodyError := some sig
897983
| other => do
898-
-- For control flow signals (return/break), still call __exit__ then re-throw
899984
for (mgr, _) in managers.reverse do
900-
match mgr with
901-
| .dict ref => do
902-
let pairs ← heapGetDict ref
903-
for (k, v) in pairs do
904-
match k with
905-
| .str s =>
906-
if s == "__exit__" then do
907-
let _ ← callValueDispatch v [mgr, .none, .none, .none] []
908-
| _ => pure ()
909-
| _ => pure ()
985+
if let some fn ← getAttr mgr "__exit__" then
986+
let _ ← callValueDispatch fn [mgr, .none, .none, .none] []
910987
throw other
911988
-- Call __exit__ on each manager in reverse order
912989
let mut suppressed := false
913990
for (mgr, _) in managers.reverse do
914-
match mgr with
915-
| .dict ref => do
916-
let pairs ← heapGetDict ref
917-
let mut exitFn : Option Value := none
918-
for (k, v) in pairs do
919-
match k with
920-
| .str s => if s == "__exit__" then exitFn := some v
921-
| _ => pure ()
922-
match exitFn with
923-
| some fn =>
924-
let exitArgs := match bodyError with
925-
| none => [mgr, .none, .none, .none]
926-
| some (.error e) =>
927-
let excType := Value.str (runtimeErrorTypeName e)
928-
let excVal := Value.exception (runtimeErrorTypeName e) (runtimeErrorMessage e)
929-
[mgr, excType, excVal, .none]
930-
| _ => [mgr, .none, .none, .none]
931-
let result ← callValueDispatch fn exitArgs []
932-
if ← isTruthy result then suppressed := true
933-
| none => pure ()
934-
| _ => pure ()
991+
if let some fn ← getAttr mgr "__exit__" then
992+
let exitArgs := match bodyError with
993+
| none => [mgr, .none, .none, .none]
994+
| some (.error e) =>
995+
let excType := Value.str (runtimeErrorTypeName e)
996+
let excVal := Value.exception (runtimeErrorTypeName e) (runtimeErrorMessage e)
997+
[mgr, excType, excVal, .none]
998+
| _ => [mgr, .none, .none, .none]
999+
let result ← callValueDispatch fn exitArgs []
1000+
if ← isTruthy result then suppressed := true
9351001
-- Re-raise body error if not suppressed
9361002
match bodyError with
9371003
| some sig => if !suppressed then throw sig
@@ -1013,6 +1079,66 @@ partial def callBoundMethod (receiver : Value) (method : String) (args : List Va
10131079
| .tuple arr => callTupleMethod arr method args
10141080
| .builtin name => callBuiltinTypeMethod name method args
10151081
| .generator ref => callGeneratorMethod ref method args
1082+
| .instance iref => do
1083+
-- Look up method in instance's class MRO and call with self
1084+
let id_ ← heapGetInstanceData iref
1085+
match id_.cls with
1086+
| .classObj cref => do
1087+
let cd ← heapGetClassData cref
1088+
let mut found : Option (Value × Value) := none -- (function, defining class)
1089+
for mroEntry in cd.mro do
1090+
if found.isSome then break
1091+
match mroEntry with
1092+
| .classObj mref => do
1093+
let mcd ← heapGetClassData mref
1094+
if let some fn := mcd.ns[method]? then
1095+
found := some (fn, mroEntry)
1096+
| _ => pure ()
1097+
match found with
1098+
| some (fn, definingCls) =>
1099+
match fn with
1100+
| .function fref => do
1101+
let fd ← heapGetFunc fref
1102+
let scope ← bindFuncParams fd.params (receiver :: args) [] fd.defaults fd.kwDefaults
1103+
let scopeWithClass := scope.insert "__class__" definingCls
1104+
callRegularFunc fd scopeWithClass
1105+
| _ => callValueDispatch fn (receiver :: args) []
1106+
| none => throwAttributeError s!"'{cd.name}' object has no attribute '{method}'"
1107+
| _ => throwAttributeError s!"instance has no attribute '{method}'"
1108+
| .superObj startAfterCls inst => do
1109+
-- Look up method in MRO starting after startAfterCls
1110+
let instCls ← match inst with
1111+
| .instance iref => do
1112+
let id_ ← heapGetInstanceData iref
1113+
pure id_.cls
1114+
| _ => pure Value.none
1115+
match instCls with
1116+
| .classObj cref => do
1117+
let cd ← heapGetClassData cref
1118+
let mut pastStart := false
1119+
let mut found : Option (Value × Value) := none -- (function, defining class)
1120+
for mroEntry in cd.mro do
1121+
if found.isSome then break
1122+
if !pastStart then
1123+
if Value.beq mroEntry startAfterCls then pastStart := true
1124+
continue
1125+
match mroEntry with
1126+
| .classObj mref => do
1127+
let mcd ← heapGetClassData mref
1128+
if let some fn := mcd.ns[method]? then
1129+
found := some (fn, mroEntry)
1130+
| _ => pure ()
1131+
match found with
1132+
| some (fn, definingCls) =>
1133+
match fn with
1134+
| .function fref => do
1135+
let fd ← heapGetFunc fref
1136+
let scope ← bindFuncParams fd.params (inst :: args) [] fd.defaults fd.kwDefaults
1137+
let scopeWithClass := scope.insert "__class__" definingCls
1138+
callRegularFunc fd scopeWithClass
1139+
| _ => callValueDispatch fn (inst :: args) []
1140+
| none => throwAttributeError s!"'super' object has no attribute '{method}'"
1141+
| _ => throwAttributeError s!"'super' object has no attribute '{method}'"
10161142
| _ => throwAttributeError s!"'{typeName receiver}' object has no attribute '{method}'"
10171143

10181144
-- ============================================================

LeanPython/Interpreter/Types.lean

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,43 @@ def typeName : Value → String
324324
| .boundMethod _ _ => "method"
325325
| .exception tn _ => tn
326326
| .generator _ => "generator"
327+
| .classObj _ => "type"
328+
| .instance _ => "instance"
329+
| .superObj _ _ => "super"
330+
331+
-- ============================================================
332+
-- Class heap operations
333+
-- ============================================================
334+
335+
/-- Get class data from the heap. -/
336+
def heapGetClassData (ref : HeapRef) : InterpM ClassData := do
337+
match ← heapGet ref with
338+
| .classObjData cd => return cd
339+
| _ => throwRuntimeError (.runtimeError "heap object is not a class")
340+
341+
/-- Update class data on the heap. -/
342+
def heapSetClassData (ref : HeapRef) (cd : ClassData) : InterpM Unit :=
343+
heapSet ref (.classObjData cd)
344+
345+
/-- Get instance data from the heap. -/
346+
def heapGetInstanceData (ref : HeapRef) : InterpM InstanceData := do
347+
match ← heapGet ref with
348+
| .instanceObjData id_ => return id_
349+
| _ => throwRuntimeError (.runtimeError "heap object is not an instance")
350+
351+
/-- Update instance data on the heap. -/
352+
def heapSetInstanceData (ref : HeapRef) (id_ : InstanceData) : InterpM Unit :=
353+
heapSet ref (.instanceObjData id_)
354+
355+
/-- Allocate a class object on the heap. -/
356+
def allocClassObj (cd : ClassData) : InterpM Value := do
357+
let ref ← heapAlloc (.classObjData cd)
358+
return .classObj ref
359+
360+
/-- Allocate an instance on the heap. -/
361+
def allocInstance (id_ : InstanceData) : InterpM Value := do
362+
let ref ← heapAlloc (.instanceObjData id_)
363+
return .instance ref
327364

328365
-- ============================================================
329366
-- Generator heap operations

0 commit comments

Comments
 (0)