Skip to content

Commit 6599975

Browse files
pirapiraclaude
andcommitted
Continue Phase 9a: fix interpreter gaps for leanSpec type patterns
Fix kwargs forwarding in callBoundMethod (instance/super/classObj branches), add bytes dunder dispatch engine, bytes multiplication, bytes(instance) wrappedValue extraction, bytearray builtin, isinstance with tuple of builtins, pow(base,exp,None), and kwargs-to-positional conversion for int.to_bytes. 15 new tests covering BaseUint encode/decode round-trip, BaseBytes with length validation, and real leanSpec operator/serialization patterns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fea24b1 commit 6599975

6 files changed

Lines changed: 270 additions & 22 deletions

File tree

LeanPython/Interpreter/Eval.lean

Lines changed: 151 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,18 +1401,32 @@ partial def callValueDispatch (callee : Value) (args : List Value)
14011401
| _ => throwTypeError s!"int.__new__: cannot convert {typeName val} to int"
14021402
| other => throwTypeError s!"int.__new__: cannot convert {typeName other} to int"
14031403
else if name == "bytes.__new__" then do
1404-
match args with
1405-
| [cls@(.classObj _), .bytes b] =>
1404+
-- bytes.__new__(cls, value) — may be called via super().__new__(cls, value)
1405+
-- which prepends inst, giving [inst, cls, value]. Extract last class and value.
1406+
let (cls, mval) ← match args with
1407+
| [cls@(.classObj _), v] => pure (cls, some v)
1408+
| [_, cls@(.classObj _), v] => pure (cls, some v) -- super() prepends inst
1409+
| [cls@(.classObj _)] => pure (cls, none)
1410+
| [_, cls@(.classObj _)] => pure (cls, none) -- super() prepends inst, no value
1411+
| _ => throwTypeError "bytes.__new__(cls, value) requires a class and bytes"
1412+
match mval with
1413+
| some (.bytes b) =>
14061414
allocInstance { cls := cls, attrs := {}, wrappedValue := some (.bytes b) }
1407-
| [cls@(.classObj _), .int n] => do
1415+
| some (.int n) => do
14081416
-- bytes(n) creates n zero bytes
14091417
let mut buf := ByteArray.empty
14101418
for _ in List.range n.toNat do
14111419
buf := buf.push 0
14121420
allocInstance { cls := cls, attrs := {}, wrappedValue := some (.bytes buf) }
1413-
| [_cls@(.classObj _)] =>
1414-
throwTypeError "bytes.__new__() missing required argument"
1415-
| _ => throwTypeError "bytes.__new__(cls, value) requires a class and bytes"
1421+
| some (.instance iref) => do
1422+
let id_ ← heapGetInstanceData iref
1423+
match id_.wrappedValue with
1424+
| some (.bytes b) =>
1425+
allocInstance { cls := cls, attrs := {}, wrappedValue := some (.bytes b) }
1426+
| _ => throwTypeError s!"bytes.__new__: cannot convert {typeName (.instance iref)} to bytes"
1427+
| none =>
1428+
allocInstance { cls := cls, attrs := {}, wrappedValue := some (.bytes ByteArray.empty) }
1429+
| some other => throwTypeError s!"bytes.__new__: cannot convert {typeName other} to bytes"
14161430
else if name.startsWith "int." then do
14171431
-- Dispatch int dunder methods: extract wrapped int values
14181432
let methodName := String.ofList (name.toList.drop "int.".length)
@@ -1608,6 +1622,114 @@ partial def callValueDispatch (callee : Value) (args : List Value)
16081622
return .int bits
16091623
| _ => throwTypeError "int.bit_length takes 1 argument"
16101624
| _ => throwTypeError s!"int.{methodName} is not implemented"
1625+
else if name.startsWith "bytes." && name != "bytes.__new__" then do
1626+
-- Dispatch bytes dunder methods: extract wrapped bytes values
1627+
let methodName := String.ofList (name.toList.drop "bytes.".length)
1628+
let extractBytes : Value → InterpM ByteArray := fun v =>
1629+
match v with
1630+
| .bytes b => pure b
1631+
| .instance iref => do
1632+
let id_ ← heapGetInstanceData iref
1633+
match id_.wrappedValue with
1634+
| some (.bytes b) => pure b
1635+
| _ => throwTypeError s!"expected bytes, got {typeName v}"
1636+
| other => throwTypeError s!"expected bytes, got {typeName other}"
1637+
match methodName with
1638+
| "__len__" => match args with
1639+
| [a] => return .int (← extractBytes a).size
1640+
| _ => throwTypeError "bytes.__len__ takes 1 argument"
1641+
| "__getitem__" => match args with
1642+
| [a, .int idx] => do
1643+
let b ← extractBytes a
1644+
let i : Int := if idx < 0 then (b.size : Int) + idx else idx
1645+
if i < 0 || i >= b.size then throwTypeError "index out of range"
1646+
return .int (b[i.toNat]!.toNat : Int)
1647+
| _ => throwTypeError "bytes.__getitem__ takes 2 arguments"
1648+
| "__contains__" => match args with
1649+
| [a, .int byte_] => do
1650+
let b ← extractBytes a
1651+
return .bool (b.toList.any (fun x => x.toNat == byte_.toNat))
1652+
| _ => return .bool false
1653+
| "__iter__" => match args with
1654+
| [a] => do
1655+
let b ← extractBytes a
1656+
let items := b.toList.map (fun byte => Value.int byte.toNat)
1657+
allocGenerator items.toArray
1658+
| _ => throwTypeError "bytes.__iter__ takes 1 argument"
1659+
| "__add__" => match args with
1660+
| [a, b] => return .bytes ((← extractBytes a) ++ (← extractBytes b))
1661+
| _ => throwTypeError "bytes.__add__ takes 2 arguments"
1662+
| "__mul__" | "__rmul__" => match args with
1663+
| [a, .int n] => do
1664+
let b ← extractBytes a
1665+
if n <= 0 then return .bytes ByteArray.empty
1666+
else
1667+
let mut result := ByteArray.empty
1668+
for _ in [:n.toNat] do result := result ++ b
1669+
return .bytes result
1670+
| _ => throwTypeError "bytes.__mul__ takes 2 arguments"
1671+
| "__eq__" => match args with
1672+
| [a, b] => return .bool ((← extractBytes a) == (← extractBytes b))
1673+
| _ => throwTypeError "bytes.__eq__ takes 2 arguments"
1674+
| "__ne__" => match args with
1675+
| [a, b] => return .bool ((← extractBytes a) != (← extractBytes b))
1676+
| _ => throwTypeError "bytes.__ne__ takes 2 arguments"
1677+
| "__hash__" => match args with
1678+
| [a] => do
1679+
let b ← extractBytes a
1680+
let h := hash b.toList
1681+
return .int h.toNat
1682+
| _ => throwTypeError "bytes.__hash__ takes 1 argument"
1683+
| "__repr__" | "__str__" => match args with
1684+
| [a] => do
1685+
let b ← extractBytes a
1686+
-- Simple hex representation
1687+
let hexDigit (n : Nat) : Char := if n < 10 then Char.ofNat (48 + n) else Char.ofNat (87 + n)
1688+
let mut s := "b'"
1689+
for byte in b.toList do
1690+
let hi := byte.toNat / 16
1691+
let lo := byte.toNat % 16
1692+
s := s ++ s!"\\x{String.ofList [hexDigit hi, hexDigit lo]}"
1693+
s := s ++ "'"
1694+
return .str s
1695+
| _ => throwTypeError "bytes.__repr__ takes 1 argument"
1696+
| "hex" => match args with
1697+
| [a] => do
1698+
let b ← extractBytes a
1699+
let mut result := ""
1700+
for byte in b.toList do
1701+
let hi := byte.toNat / 16
1702+
let lo := byte.toNat % 16
1703+
let hexDigit (n : Nat) : Char := if n < 10 then Char.ofNat (48 + n) else Char.ofNat (87 + n)
1704+
result := result ++ String.ofList [hexDigit hi, hexDigit lo]
1705+
return .str result
1706+
| _ => throwTypeError "bytes.hex takes 1 argument"
1707+
| "fromhex" => match args with
1708+
| [.str hex] => do
1709+
let cleaned := hex.toList.filter (· != ' ')
1710+
let mut result := ByteArray.empty
1711+
let mut i := 0
1712+
while i + 1 < cleaned.length do
1713+
let hi := cleaned[i]!
1714+
let lo := cleaned[i+1]!
1715+
let hexVal (c : Char) : Nat :=
1716+
if c.val >= 48 && c.val <= 57 then c.val.toNat - 48
1717+
else if c.val >= 65 && c.val <= 70 then c.val.toNat - 55
1718+
else if c.val >= 97 && c.val <= 102 then c.val.toNat - 87
1719+
else 0
1720+
result := result.push (hexVal hi * 16 + hexVal lo).toUInt8
1721+
i := i + 2
1722+
return .bytes result
1723+
| _ => throwTypeError "bytes.fromhex takes 1 argument"
1724+
| "decode" => match args with
1725+
| [a] => do
1726+
let b ← extractBytes a
1727+
return .str (String.ofList (b.toList.map (fun byte => Char.ofNat byte.toNat)))
1728+
| [a, _encoding] => do
1729+
let b ← extractBytes a
1730+
return .str (String.ofList (b.toList.map (fun byte => Char.ofNat byte.toNat)))
1731+
| _ => throwTypeError "bytes.decode takes at most 1 argument"
1732+
| _ => throwTypeError s!"bytes.{methodName} is not implemented"
16111733
else
16121734
callBuiltin name args kwargs
16131735
| .function ref => do
@@ -3598,7 +3720,16 @@ partial def callBoundMethod (receiver : Value) (method : String) (args : List Va
35983720
| .dict ref => callDictMethod ref method args
35993721
| .str s => callStrMethod s method args
36003722
| .set ref => callSetMethod ref method args
3601-
| .int n => callIntMethod n method args
3723+
| .int n => do
3724+
-- Convert known kwargs to positional args for builtin int methods
3725+
let args' ← if kwargs.isEmpty then pure args else
3726+
match method with
3727+
| "to_bytes" =>
3728+
let length := kwargs.find? (·.1 == "length") |>.map (·.2) |>.getD (.int 1)
3729+
let byteorder := kwargs.find? (·.1 == "byteorder") |>.map (·.2) |>.getD (.str "big")
3730+
pure (args ++ [length, byteorder])
3731+
| _ => pure args
3732+
callIntMethod n method args'
36023733
| .bytes b => callBytesMethod b method args
36033734
| .tuple arr => callTupleMethod arr method args
36043735
| .builtin name => callBuiltinTypeMethod name method args
@@ -3629,15 +3760,15 @@ partial def callBoundMethod (receiver : Value) (method : String) (args : List Va
36293760
| _ => pure ()
36303761
match found with
36313762
| some (.classMethod innerFn) =>
3632-
callValueDispatch innerFn (receiver :: args) []
3763+
callValueDispatch innerFn (receiver :: args) kwargs
36333764
| some (.staticMethod innerFn) =>
3634-
callValueDispatch innerFn args []
3765+
callValueDispatch innerFn args kwargs
36353766
| some (.function fref) => do
36363767
-- Regular function called on class (no self binding)
36373768
let fd ← heapGetFunc fref
3638-
let scope ← bindFuncParams fd.params args [] fd.defaults fd.kwDefaults
3769+
let scope ← bindFuncParams fd.params args kwargs fd.defaults fd.kwDefaults
36393770
callRegularFunc fd scope
3640-
| some fn => callValueDispatch fn args []
3771+
| some fn => callValueDispatch fn args kwargs
36413772
| none => throwAttributeError s!"type object '{cd.name}' has no attribute '{method}'"
36423773
| .instance iref => do
36433774
-- Look up method in instance's class MRO and call with self
@@ -3703,16 +3834,16 @@ partial def callBoundMethod (receiver : Value) (method : String) (args : List Va
37033834
match fn with
37043835
| .classMethod innerFn =>
37053836
-- Call with class as first arg instead of instance
3706-
callValueDispatch innerFn (id_.cls :: args) []
3837+
callValueDispatch innerFn (id_.cls :: args) kwargs
37073838
| .staticMethod innerFn =>
37083839
-- Call without self
3709-
callValueDispatch innerFn args []
3840+
callValueDispatch innerFn args kwargs
37103841
| .function fref => do
37113842
let fd ← heapGetFunc fref
3712-
let scope ← bindFuncParams fd.params (receiver :: args) [] fd.defaults fd.kwDefaults
3843+
let scope ← bindFuncParams fd.params (receiver :: args) kwargs fd.defaults fd.kwDefaults
37133844
let scopeWithClass := scope.insert "__class__" definingCls
37143845
callRegularFunc fd scopeWithClass
3715-
| _ => callValueDispatch fn (receiver :: args) []
3846+
| _ => callValueDispatch fn (receiver :: args) kwargs
37163847
| none => throwAttributeError s!"'{cd.name}' object has no attribute '{method}'"
37173848
| _ => throwAttributeError s!"instance has no attribute '{method}'"
37183849
| .superObj startAfterCls inst => do
@@ -3744,19 +3875,19 @@ partial def callBoundMethod (receiver : Value) (method : String) (args : List Va
37443875
match fn with
37453876
| .function fref => do
37463877
let fd ← heapGetFunc fref
3747-
let scope ← bindFuncParams fd.params (inst :: args) [] fd.defaults fd.kwDefaults
3878+
let scope ← bindFuncParams fd.params (inst :: args) kwargs fd.defaults fd.kwDefaults
37483879
let scopeWithClass := scope.insert "__class__" definingCls
37493880
callRegularFunc fd scopeWithClass
37503881
| .classMethod innerFn =>
37513882
-- classmethod: call with class as first arg
3752-
callValueDispatch innerFn (inst :: args) []
3883+
callValueDispatch innerFn (inst :: args) kwargs
37533884
| .staticMethod innerFn =>
37543885
-- staticmethod: call without self
3755-
callValueDispatch innerFn args []
3886+
callValueDispatch innerFn args kwargs
37563887
| .builtin _ =>
37573888
-- Builtins in synthetic type classes: prepend inst (self/cls)
3758-
callValueDispatch fn (inst :: args) []
3759-
| _ => callValueDispatch fn (inst :: args) []
3889+
callValueDispatch fn (inst :: args) kwargs
3890+
| _ => callValueDispatch fn (inst :: args) kwargs
37603891
| none => throwAttributeError s!"'super' object has no attribute '{method}'"
37613892
| _ => throwAttributeError s!"'super' object has no attribute '{method}'"
37623893
| _ => throwAttributeError s!"'{typeName receiver}' object has no attribute '{method}'"

LeanPython/Runtime/Builtins.lean

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ partial def builtinIsinstance (args : List Value) : InterpM Value := do
338338
| "dict" => tn == "dict"
339339
| "set" => tn == "set"
340340
| "bytes" => tn == "bytes"
341+
| "bytearray" => tn == "bytearray"
341342
| _ => false
342343
if isMatch then return .bool true
343344
-- For instances, check if any class in MRO is the synthetic built-in type
@@ -381,6 +382,28 @@ partial def builtinIsinstance (args : List Value) : InterpM Value := do
381382
| _ => pure ()
382383
| _ => pure ()
383384
return .bool false
385+
| [obj, .tuple classes] => do
386+
-- isinstance(non-instance, (ClassA, ClassB, ...))
387+
let tn := typeName obj
388+
for cls in classes do
389+
match cls with
390+
| .builtin bname =>
391+
if bname == "object" then return .bool true
392+
let isMatch := match bname with
393+
| "int" => tn == "int" || tn == "bool"
394+
| "float" => tn == "float"
395+
| "str" => tn == "str"
396+
| "bool" => tn == "bool"
397+
| "list" => tn == "list"
398+
| "tuple" => tn == "tuple"
399+
| "dict" => tn == "dict"
400+
| "set" => tn == "set"
401+
| "bytes" => tn == "bytes"
402+
| "bytearray" => tn == "bytearray"
403+
| _ => false
404+
if isMatch then return .bool true
405+
| _ => pure ()
406+
return .bool false
384407
| [_, .classObj _] => return .bool false -- non-instance is not an instance of a custom class
385408
| _ => throwTypeError "isinstance() takes 2 arguments"
386409

@@ -442,6 +465,7 @@ def builtinChr (args : List Value) : InterpM Value := do
442465
def builtinPow (args : List Value) : InterpM Value := do
443466
match args with
444467
| [base, exp] => evalBinOp .pow base exp
468+
| [base, exp, .none] => evalBinOp .pow base exp
445469
| [.int base, .int exp, .int m] =>
446470
if m == 0 then throwValueError "pow() 3rd argument cannot be 0"
447471
else if exp < 0 then throwValueError "pow() 2nd argument cannot be negative when 3rd argument specified"
@@ -647,6 +671,28 @@ partial def callBuiltin (name : String) (args : List Value)
647671
for _ in [:n.toNat] do ba := ba.push 0
648672
return .bytes ba
649673
| [.bytes b] => return .bytes b
674+
| [.instance iref] => do
675+
let id_ ← heapGetInstanceData iref
676+
match id_.wrappedValue with
677+
| some (.bytes b) => return .bytes b
678+
| some (.int n) =>
679+
-- bytes(int_subclass_instance) - like bytes(n) for zero-filled
680+
if n < 0 then throwValueError "negative count"
681+
else
682+
let mut ba := ByteArray.empty
683+
for _ in [:n.toNat] do ba := ba.push 0
684+
return .bytes ba
685+
| _ =>
686+
-- Try iterating over the instance
687+
let items ← iterValues (.instance iref)
688+
let mut result := ByteArray.empty
689+
for item in items do
690+
match item with
691+
| .int n =>
692+
if n < 0 || n > 255 then throwValueError "bytes must be in range(0, 256)"
693+
else result := result.push n.toNat.toUInt8
694+
| _ => throwTypeError "cannot convert to bytes"
695+
return .bytes result
650696
| [v] => do
651697
let items ← iterValues v
652698
let mut result := ByteArray.empty
@@ -658,6 +704,28 @@ partial def callBuiltin (name : String) (args : List Value)
658704
| _ => throwTypeError "cannot convert to bytes"
659705
return .bytes result
660706
| _ => throwTypeError "bytes() takes at most 1 argument"
707+
| "bytearray" => do
708+
-- bytearray behaves like bytes for construction, returns bytes
709+
match args with
710+
| [] => return .bytes ByteArray.empty
711+
| [.int n] =>
712+
if n < 0 then throwValueError "negative count"
713+
else
714+
let mut ba := ByteArray.empty
715+
for _ in [:n.toNat] do ba := ba.push 0
716+
return .bytes ba
717+
| [.bytes b] => return .bytes b
718+
| [v] => do
719+
let items ← iterValues v
720+
let mut result := ByteArray.empty
721+
for item in items do
722+
match item with
723+
| .int n =>
724+
if n < 0 || n > 255 then throwValueError "bytes must be in range(0, 256)"
725+
else result := result.push n.toNat.toUInt8
726+
| _ => throwTypeError "cannot convert to bytes"
727+
return .bytes result
728+
| _ => throwTypeError "bytearray() takes at most 1 argument"
661729
| "iter" => do
662730
match args with
663731
| [.generator ref] => return .generator ref

LeanPython/Runtime/Ops.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,13 @@ partial def evalBinOp (op : BinOp) (left right : Value) : InterpM Value := do
396396
| .str s, .int n | .int n, .str s =>
397397
if n <= 0 then return .str ""
398398
else return .str (String.join (List.replicate n.toNat s))
399+
| .bytes b, .int n | .int n, .bytes b =>
400+
if n <= 0 then return .bytes ByteArray.empty
401+
else
402+
let mut result := ByteArray.empty
403+
for _ in [:n.toNat] do
404+
result := result ++ b
405+
return .bytes result
399406
| .list ref, .int n | .int n, .list ref => do
400407
let arr ← heapGetList ref
401408
if n <= 0 then allocList #[]

LeanPython/Runtime/Types.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ def builtinNames : List String :=
348348
"sum", "any", "all", "hash", "id", "input", "ord", "chr",
349349
"hex", "oct", "bin", "round", "pow", "divmod", "map", "filter",
350350
"iter", "next", "hasattr", "getattr", "setattr", "callable",
351-
"issubclass", "super", "object", "bytes",
351+
"issubclass", "super", "object", "bytes", "bytearray",
352352
"staticmethod", "classmethod", "property",
353353
-- Dataclass
354354
"dataclass",

0 commit comments

Comments
 (0)