@@ -436,9 +436,16 @@ partial def getAttributeValue (obj : Value) (attr : String) : InterpM Value := d
436436 | .list _ =>
437437 if knownListMethods.contains attr then return .boundMethod obj attr
438438 else throwAttributeError s! "'list' object has no attribute '{ attr} '"
439- | .dict _ =>
439+ | .dict ref =>
440440 if knownDictMethods.contains attr then return .boundMethod obj attr
441- else throwAttributeError s! "'dict' object has no attribute '{ attr} '"
441+ else do
442+ -- Check dict entries as attributes (for class/object dicts)
443+ let pairs ← heapGetDict ref
444+ for (k, v) in pairs do
445+ match k with
446+ | .str s => if s == attr then return v
447+ | _ => pure ()
448+ throwAttributeError s! "'dict' object has no attribute '{ attr} '"
442449 | .set _ =>
443450 if knownSetMethods.contains attr then return .boundMethod obj attr
444451 else throwAttributeError s! "'set' object has no attribute '{ attr} '"
@@ -548,7 +555,22 @@ partial def assignToTarget (target : Expr) (value : Value) : InterpM Unit := do
548555 | _ => pure ()
549556 for i in [:after.length] do
550557 assignToTarget after[i]! items[items.size - after.length + i]!
551- | .attribute _obj _attr _ => throwNotImplemented "attribute assignment"
558+ | .attribute obj attr _ => do
559+ let objVal ← evalExpr obj
560+ match objVal with
561+ | .dict ref => do
562+ let pairs ← heapGetDict ref
563+ let key := Value.str attr
564+ let mut newPairs := pairs
565+ let mut found := false
566+ for i in [:pairs.size] do
567+ if pairs[i]!.1 == key then
568+ newPairs := newPairs.set! i (key, value)
569+ found := true
570+ break
571+ if !found then newPairs := newPairs.push (key, value)
572+ heapSetDict ref newPairs
573+ | _ => throwAttributeError s! "'{ typeName objVal} ' object attribute '{ attr} ' is read-only"
552574 | .starred inner _ => assignToTarget inner value
553575 | _ => throwRuntimeError (.runtimeError "invalid assignment target" )
554576
@@ -699,7 +721,15 @@ partial def execStmt (s : Stmt) : InterpM Unit := do
699721 let classVal ← allocDict entries
700722 setVariable cd.name classVal
701723
702- | .raise_ exprOpt _cause _ => do
724+ | .raise_ exprOpt cause _ => do
725+ -- Evaluate cause if present (for validation; chaining info not stored yet)
726+ if let some causeExpr := cause then
727+ let causeVal ← evalExpr causeExpr
728+ match causeVal with
729+ | .exception _ _ => pure ()
730+ | .none => pure ()
731+ | .builtin _ => pure ()
732+ | _ => throwTypeError "exception cause must be None or derive from BaseException"
703733 match exprOpt with
704734 | some e => do
705735 let v ← evalExpr e
@@ -783,7 +813,74 @@ partial def execStmt (s : Stmt) : InterpM Unit := do
783813 modify fun st => { st with activeException := none }
784814 | some other => do execStmts finally_; throw other
785815
786- | .with_ _items _body _ => throwNotImplemented "with statements"
816+ | .with_ items body _ => do
817+ -- Evaluate context managers and call __enter__
818+ let mut managers : List (Value × Value) := []
819+ for item in items do
820+ let mgr ← evalExpr item.contextExpr
821+ -- Try to call __enter__ on the manager
822+ let entered ← match mgr with
823+ | .dict ref => do
824+ let pairs ← heapGetDict ref
825+ let mut enterFn : Option Value := none
826+ for (k, v) in pairs do
827+ match k with
828+ | .str s => if s == "__enter__" then enterFn := some v
829+ | _ => pure ()
830+ match enterFn with
831+ | some fn => callValueDispatch fn [mgr] []
832+ | none => pure mgr
833+ | _ => pure mgr
834+ if let some target := item.optionalVars then
835+ assignToTarget target entered
836+ managers := managers ++ [(mgr, entered)]
837+ -- Execute body with cleanup
838+ let mut bodyError : Option Signal := none
839+ try execStmts body
840+ catch
841+ | sig@(.error _) => bodyError := some sig
842+ | other => do
843+ -- For control flow signals (return/break), still call __exit__ then re-throw
844+ for (mgr, _) in managers.reverse do
845+ match mgr with
846+ | .dict ref => do
847+ let pairs ← heapGetDict ref
848+ for (k, v) in pairs do
849+ match k with
850+ | .str s =>
851+ if s == "__exit__" then do
852+ let _ ← callValueDispatch v [mgr, .none, .none, .none] []
853+ | _ => pure ()
854+ | _ => pure ()
855+ throw other
856+ -- Call __exit__ on each manager in reverse order
857+ let mut suppressed := false
858+ for (mgr, _) in managers.reverse do
859+ match mgr with
860+ | .dict ref => do
861+ let pairs ← heapGetDict ref
862+ let mut exitFn : Option Value := none
863+ for (k, v) in pairs do
864+ match k with
865+ | .str s => if s == "__exit__" then exitFn := some v
866+ | _ => pure ()
867+ match exitFn with
868+ | some fn =>
869+ let exitArgs := match bodyError with
870+ | none => [mgr, .none, .none, .none]
871+ | some (.error e) =>
872+ let excType := Value.str (runtimeErrorTypeName e)
873+ let excVal := Value.exception (runtimeErrorTypeName e) (runtimeErrorMessage e)
874+ [mgr, excType, excVal, .none]
875+ | _ => [mgr, .none, .none, .none]
876+ let result ← callValueDispatch fn exitArgs []
877+ if ← isTruthy result then suppressed := true
878+ | none => pure ()
879+ | _ => pure ()
880+ -- Re-raise body error if not suppressed
881+ match bodyError with
882+ | some sig => if !suppressed then throw sig
883+ | none => pure ()
787884 | .import_ _aliases _ => throwNotImplemented "import statements"
788885 | .importFrom _ _aliases _ _ => throwNotImplemented "import-from statements"
789886
@@ -1253,7 +1350,53 @@ partial def callStrMethod (s : String) (method : String) (args : List Value)
12531350 let padding := String.ofList (List.replicate (w - s.length) '0' )
12541351 return .str (padding ++ s)
12551352 | _ => throwTypeError "zfill() takes 1 argument"
1256- | "format" => return .str s
1353+ | "format" => do
1354+ -- Python str.format(): supports {}, {0}, {1}, {{, }}
1355+ let chars := s.toList
1356+ let mut result : List Char := []
1357+ let mut i := 0
1358+ let mut autoIdx := 0
1359+ while i < chars.length do
1360+ let c := chars[i]!
1361+ if c == '{' then
1362+ if i + 1 < chars.length && chars[i + 1 ]! == '{' then
1363+ result := result ++ ['{' ]
1364+ i := i + 2
1365+ else
1366+ -- Find closing }
1367+ let mut j := i + 1
1368+ while j < chars.length && chars[j]! != '}' do j := j + 1
1369+ if j >= chars.length then
1370+ throwValueError "Single '{' encountered in format string"
1371+ let fieldContent := String.ofList (chars.drop (i + 1 ) |>.take (j - i - 1 ))
1372+ -- Strip format spec after ':'
1373+ let fieldName := match fieldContent.splitOn ":" with
1374+ | name :: _ => name
1375+ | [] => ""
1376+ let argVal ←
1377+ if fieldName.isEmpty then do
1378+ if autoIdx >= args.length then throwIndexError "Replacement index out of range for positional args tuple"
1379+ let v := args[autoIdx]!
1380+ autoIdx := autoIdx + 1
1381+ pure v
1382+ else match fieldName.toNat? with
1383+ | some idx =>
1384+ if idx >= args.length then throwIndexError s! "Replacement index { idx} out of range for positional args tuple"
1385+ pure args[idx]!
1386+ | none => throwKeyError s! "'{ fieldName} '"
1387+ let valStr ← valueToStr argVal
1388+ result := result ++ valStr.toList
1389+ i := j + 1
1390+ else if c == '}' then
1391+ if i + 1 < chars.length && chars[i + 1 ]! == '}' then
1392+ result := result ++ ['}' ]
1393+ i := i + 2
1394+ else
1395+ throwValueError "Single '}' encountered in format string"
1396+ else
1397+ result := result ++ [c]
1398+ i := i + 1
1399+ return .str (String.ofList result)
12571400 | "count" =>
12581401 match args with
12591402 | [.str sub] => return .int (stringCount s sub)
0 commit comments