Skip to content

Commit 706eef2

Browse files
pirapiraclaude
andcommitted
Advance leanSpec E2E integration to Tier 5 & 6 (byte_arrays, bitfields, collections, union)
Add support for remaining SSZ type files from leanSpec, bringing E2E tests from 7 to 11 — all passing. Key interpreter fixes: @overload/@runtime_checkable as identity decorators, cast() returning second arg, __init_subclass__ hook, cross-type valueEq for int subclass instances, PEP 604 type unions, inline function bodies, trailing comma in parenthesized imports, and int.to_bytes/ from_bytes keyword argument support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 438ca49 commit 706eef2

6 files changed

Lines changed: 389 additions & 16 deletions

File tree

LeanPython/Interpreter/Eval.lean

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2754,6 +2754,11 @@ partial def evalSubscriptValue (obj idx : Value) : InterpM Value := do
27542754
match cd.ns["__class_getitem__"]? with
27552755
| some fn => callValueDispatch fn [idx] []
27562756
| none => return .none -- default: subscripting a class returns .none (for typing)
2757+
| .builtin name =>
2758+
-- Allow subscripting on builtin type names for type annotations (list[int], dict[str, int], etc.)
2759+
match name with
2760+
| "list" | "dict" | "set" | "tuple" | "frozenset" | "type" => return .none
2761+
| _ => throwTypeError s!"'{typeName obj}' object is not subscriptable"
27572762
| _ => throwTypeError s!"'{typeName obj}' object is not subscriptable"
27582763

27592764
-- Assignment target resolution
@@ -2979,30 +2984,37 @@ partial def getBuiltinModule (name : String) : InterpM (Option Value) := do
29792984
-- Stub common typing names as none so imports don't fail
29802985
for n in ["Any", "Optional", "List", "Dict", "Set", "Tuple", "FrozenSet",
29812986
"Self", "Union", "Callable", "Protocol",
2982-
"runtime_checkable", "NamedTuple",
2987+
"NamedTuple",
29832988
"NoReturn", "SupportsInt", "SupportsIndex",
29842989
"TypeAlias", "Literal", "IO", "Sequence", "Mapping",
29852990
"Iterator", "Iterable", "Generator", "Coroutine",
29862991
"Awaitable", "AsyncIterator", "AsyncGenerator",
29872992
"Type", "Generic", "Annotated",
2988-
"overload", "cast", "no_type_check"] do
2993+
"no_type_check"] do
29892994
ns := ns.insert n .none
29902995
-- ClassVar and Final are distinct markers (not .none) so Pydantic can skip them
29912996
ns := ns.insert "ClassVar" (.builtin "typing.ClassVar")
29922997
ns := ns.insert "Final" (.builtin "typing.Final")
29932998
-- TypeVar is callable (returns .none as a stub type variable)
29942999
ns := ns.insert "TypeVar" (.builtin "typing.TypeVar")
2995-
-- override is a callable identity decorator (not .none)
3000+
-- Identity decorators (return their argument unchanged)
29963001
ns := ns.insert "override" (.builtin "typing.override")
3002+
ns := ns.insert "overload" (.builtin "typing.overload")
3003+
ns := ns.insert "runtime_checkable" (.builtin "typing.runtime_checkable")
3004+
-- cast(type, value) returns the value unchanged
3005+
ns := ns.insert "cast" (.builtin "typing.cast")
29973006
some <$> mkMod ns
29983007
| "typing_extensions" =>
29993008
-- Alias for typing for compatibility
30003009
let mut ns : Std.HashMap String Value := {}
30013010
ns := ns.insert "TYPE_CHECKING" (.bool false)
3002-
for n in ["Self", "Protocol", "runtime_checkable",
3011+
for n in ["Self", "Protocol",
30033012
"Annotated", "TypeAlias", "get_type_hints"] do
30043013
ns := ns.insert n .none
30053014
ns := ns.insert "override" (.builtin "typing.override")
3015+
ns := ns.insert "overload" (.builtin "typing.overload")
3016+
ns := ns.insert "runtime_checkable" (.builtin "typing.runtime_checkable")
3017+
ns := ns.insert "cast" (.builtin "typing.cast")
30063018
some <$> mkMod ns
30073019
| "abc" => do
30083020
-- Create real ABC base class
@@ -3740,6 +3752,30 @@ partial def execStmt (s : Stmt) : InterpM Unit := do
37403752
let cd' ← heapGetClassData cref
37413753
heapSetClassData cref { cd' with mro := mro }
37423754
| _ => pure ()
3755+
-- Call __init_subclass__ on each direct base class (Python semantics)
3756+
for base in bases do
3757+
match base with
3758+
| .classObj bref => do
3759+
let bcd ← heapGetClassData bref
3760+
match bcd.ns["__init_subclass__"]? with
3761+
| some fn => do
3762+
-- __init_subclass__ is a classmethod; call with the new class as cls
3763+
let actualFn := match fn with
3764+
| .classMethod inner => inner
3765+
| other => other
3766+
-- Inject __class__ into the function's scope for super() support
3767+
match actualFn with
3768+
| .function fref => do
3769+
let fd ← heapGetFunc fref
3770+
let scope ← bindFuncParams fd.params [classVal] [] fd.defaults fd.kwDefaults
3771+
let scopeWithClass := scope.insert "__class__" base
3772+
let _ ← callRegularFunc fd scopeWithClass
3773+
pure ()
3774+
| _ =>
3775+
let _ ← callValueDispatch actualFn [classVal] []
3776+
pure ()
3777+
| none => pure ()
3778+
| _ => pure ()
37433779
-- Post-process Pydantic BaseModel subclasses
37443780
let isPydanticModel ← isBaseModelSubclass bases.toArray
37453781
if isPydanticModel then
@@ -4165,14 +4201,28 @@ partial def callBoundMethod (receiver : Value) (method : String) (args : List Va
41654201
let args' ← if kwargs.isEmpty then pure args else
41664202
match method with
41674203
| "to_bytes" =>
4168-
let length := kwargs.find? (·.1 == "length") |>.map (·.2) |>.getD (.int 1)
4169-
let byteorder := kwargs.find? (·.1 == "byteorder") |>.map (·.2) |>.getD (.str "big")
4170-
pure (args ++ [length, byteorder])
4204+
-- Support: .to_bytes(length, byteorder), .to_bytes(length, byteorder=...), .to_bytes(byteorder=..., length=...)
4205+
let posLength := args.head?
4206+
let length := posLength.orElse fun _ => (kwargs.find? (·.1 == "length") |>.map (·.2))
4207+
let posByteorder := if args.length > 1 then args.tail.head? else none
4208+
let byteorder := posByteorder.orElse fun _ => (kwargs.find? (·.1 == "byteorder") |>.map (·.2))
4209+
pure [length.getD (.int 1), byteorder.getD (.str "big")]
41714210
| _ => pure args
41724211
callIntMethod n method args'
41734212
| .bytes b => callBytesMethod b method args
41744213
| .tuple arr => callTupleMethod arr method args
4175-
| .builtin name => callBuiltinTypeMethod name method args
4214+
| .builtin name => do
4215+
-- Convert kwargs to positional for builtin type methods
4216+
let args' ← if kwargs.isEmpty then pure args else
4217+
match name, method with
4218+
| "int", "from_bytes" =>
4219+
let posBytes := args.head?
4220+
let bytes_ := posBytes.orElse fun _ => (kwargs.find? (·.1 == "bytes") |>.map (·.2))
4221+
let posByteorder := if args.length > 1 then args.tail.head? else none
4222+
let byteorder := posByteorder.orElse fun _ => (kwargs.find? (·.1 == "byteorder") |>.map (·.2))
4223+
pure [bytes_.getD (.bytes ByteArray.empty), byteorder.getD (.str "big")]
4224+
| _, _ => pure args
4225+
callBuiltinTypeMethod name method args'
41764226
| .generator ref => callGeneratorMethod ref method args
41774227
| .property getter setter deleter =>
41784228
match method with
@@ -4328,7 +4378,10 @@ partial def callBoundMethod (receiver : Value) (method : String) (args : List Va
43284378
-- Builtins in synthetic type classes: prepend inst (self/cls)
43294379
callValueDispatch fn (inst :: args) kwargs
43304380
| _ => callValueDispatch fn (inst :: args) kwargs
4331-
| none => throwAttributeError s!"'super' object has no attribute '{method}'"
4381+
| none =>
4382+
-- object.__init_subclass__ is a no-op in Python
4383+
if method == "__init_subclass__" then return .none
4384+
else throwAttributeError s!"'super' object has no attribute '{method}'"
43324385
| _ => throwAttributeError s!"'super' object has no attribute '{method}'"
43334386
| _ => throwAttributeError s!"'{typeName receiver}' object has no attribute '{method}'"
43344387

LeanPython/Parser/Stmt.lean

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,18 @@ partial def parseFromImportAlias : ParserM Alias := do
120120

121121
/-- Parse an indented block: NEWLINE INDENT stmt+ DEDENT. -/
122122
partial def parseBlock : ParserM (List Stmt) := do
123-
discard expectNewline
124-
discard expectIndent
125-
let stmts ← parseStatements
126-
discard expectDedent
127-
return stmts
123+
-- Check for inline body (e.g., `def foo(): pass` or `def foo(): ...`)
124+
match ← peekKind with
125+
| some .newline =>
126+
discard expectNewline
127+
discard expectIndent
128+
let stmts ← parseStatements
129+
discard expectDedent
130+
return stmts
131+
| _ =>
132+
-- Inline body: parse simple statements on the same line
133+
let stmts ← parseSimpleStmts
134+
return stmts
128135

129136
/-- Parse one or more statements (until DEDENT or ENDMARKER). -/
130137
partial def parseStatements : ParserM (List Stmt) := do
@@ -269,9 +276,21 @@ partial def parseFromImport : ParserM Stmt := do
269276
let lvl := if level > 0 then some level else none
270277
return .importFrom modName aliases lvl (← spanFrom start)
271278
let hasParen ← if ← isDelimiter .lpar then do discard advance; pure true else pure false
272-
let aliases ← sepBy1 parseFromImportAlias (discard (expectDelimiter .comma))
279+
-- Parse comma-separated import aliases; trailing comma is allowed inside parens
280+
let first ← parseFromImportAlias
281+
let mut aliases := [first]
282+
while true do
283+
match ← attempt (discard (expectDelimiter .comma)) with
284+
| none => break
285+
| some _ =>
286+
-- After comma, try to parse another alias; if it fails (trailing comma), stop
287+
if hasParen then
288+
match ← attempt parseFromImportAlias with
289+
| some a => aliases := aliases ++ [a]
290+
| none => break
291+
else
292+
aliases := aliases ++ [← parseFromImportAlias]
273293
if hasParen then
274-
if ← isDelimiter .comma then discard advance
275294
discard (expectDelimiter .rpar)
276295
let lvl := if level > 0 then some level else none
277296
return .importFrom modName aliases lvl (← spanFrom start)

LeanPython/Runtime/Builtins.lean

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ partial def builtinIsinstance (args : List Value) : InterpM Value := do
448448
| _ => pure ()
449449
return .bool false
450450
| [_, .classObj _] => return .bool false -- non-instance is not an instance of a custom class
451+
| [_, .none] => return .bool false -- isinstance(x, None-stub) gracefully returns False
451452
| _ => throwTypeError "isinstance() takes 2 arguments"
452453

453454
/-- Hash a single value for tuple/object hashing. -/
@@ -985,6 +986,18 @@ partial def callBuiltin (name : String) (args : List Value)
985986
match args with
986987
| [f] => return f
987988
| _ => throwTypeError "override() takes exactly 1 argument"
989+
| "typing.overload" => do
990+
match args with
991+
| [f] => return f
992+
| _ => throwTypeError "overload() takes exactly 1 argument"
993+
| "typing.runtime_checkable" => do
994+
match args with
995+
| [f] => return f
996+
| _ => throwTypeError "runtime_checkable() takes exactly 1 argument"
997+
| "typing.cast" => do
998+
match args with
999+
| [_, v] => return v
1000+
| _ => throwTypeError "cast() takes exactly 2 arguments"
9881001
| "typing.TypeVar" => do
9891002
-- TypeVar('T', bound=SomeType) — stub that returns .none
9901003
return .none

LeanPython/Runtime/Ops.lean

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ partial def valueEq (a b : Value) : InterpM Bool :=
158158
match ad.wrappedValue, bd.wrappedValue with
159159
| some va, some vb => valueEq va vb
160160
| _, _ => return false
161+
-- Cross-type: instance with wrappedValue vs plain value
162+
| .instance iref, other => do
163+
let id_ ← heapGetInstanceData iref
164+
match id_.wrappedValue with
165+
| some wrapped => valueEq wrapped other
166+
| none => return false
167+
| other, .instance iref => do
168+
let id_ ← heapGetInstanceData iref
169+
match id_.wrappedValue with
170+
| some wrapped => valueEq other wrapped
171+
| none => return false
161172
| _, _ => return false
162173

163174
-- ============================================================
@@ -530,6 +541,13 @@ partial def evalBinOp (op : BinOp) (left right : Value) : InterpM Value := do
530541
result := result.set! i (k, v); found := true; break
531542
if !found then result := result.push (k, v)
532543
allocDict result
544+
-- Type union syntax: X | Y in annotations (PEP 604), e.g. int | str
545+
| .none, _ => return .none
546+
| _, .none => return .none
547+
| .builtin _, .builtin _ => return .none
548+
| .classObj _, .classObj _ => return .none
549+
| .builtin _, .classObj _ => return .none
550+
| .classObj _, .builtin _ => return .none
533551
| _, _ => throwTypeError s!"unsupported operand type(s) for |: '{typeName left}' and '{typeName right}'"
534552
| .bitAnd =>
535553
match toInt left, toInt right with

LeanPythonTest/Stdlib.lean

Lines changed: 30 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)