Conversation
☂️ Python Coverage
Overall Coverage
New FilesNo new covered files... Modified Files
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR implements lowering support for Python lambda functions to IR, addressing issue #525 which requested the ability to use inline lambda functions with ilist.map and other operations.
Changes:
- Added
lower_Lambdamethod to handle lambda AST nodes and convert them to func.Lambda IR statements with closure capture support - Added comprehensive test coverage for lambda functions including closures, nested lambdas, and integration with ilist.map
- Minor whitespace formatting changes to assign.py
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/lowering/test_py_lambda.py | New test file covering lambda lowering scenarios including closures, nested lambdas, and ilist.map integration |
| src/kirin/dialects/lowering/func.py | Implementation of lower_Lambda method to convert Python lambda AST nodes to IR |
| src/kirin/dialects/py/assign.py | Minor whitespace formatting changes (blank lines added) |
Comments suppressed due to low confidence (5)
src/kirin/dialects/lowering/func.py:166
- The state.frame context manager always creates a next_block (see state.py:192), so blocks[1] will exist. However, this deletion is fragile and relies on internal implementation details of state.frame. Consider adding a comment explaining that blocks[1] is the unused next_block created by state.frame, or checking that len(func_frame.curr_region.blocks) == 2 before deleting to make the code more robust.
func_frame.curr_region.blocks[1].delete()
src/kirin/dialects/lowering/func.py:124
- The output type is hardcoded to types.Any instead of trying to infer it from the lambda body. While this may be intentional for simplicity, it's inconsistent with how lower_FunctionDef handles the output type by calling self.get_hint(state, node.returns). Lambda nodes in Python AST don't have a returns attribute, so types.Any is the correct default, but consider adding a comment explaining this design choice.
output=types.Any,
src/kirin/dialects/lowering/func.py:158
- This commented-out assertion should either be removed or uncommented with proper error handling. If the assertion is valid (lambda should always have a return value), it should be active. If it's not valid, remove the comment. Leaving commented-out code creates confusion about the intended behavior.
# assert hasattr(last_stmt,"result"), "python lambda should always have a return value"
src/kirin/dialects/lowering/func.py:179
- There is significant code duplication between lower_Lambda and the nested function handling in lower_FunctionDef (lines 96-111). The callback function, entries setup, fn_self creation, and lambda statement construction are nearly identical. Consider extracting this shared logic into a helper method to improve maintainability and reduce the risk of inconsistencies when changes are made.
def lower_Lambda(
self, state: lowering.State[ast.AST], node: ast.Lambda
) -> lowering.Result:
frame = state.current_frame
slots = tuple(arg.arg for arg in node.args.args)
self.assert_simple_arguments(node.args)
signature = func.Signature(
inputs=tuple(
self.get_hint(state, arg.annotation) for arg in node.args.args
),
output=types.Any,
)
node_name = f"lambda_0x{id(node)}"
entries: dict[str, ir.SSAValue] = {}
entr_block = ir.Block()
fn_self = entr_block.args.append_from(
types.MethodType[list(signature.inputs), signature.output],
node_name + "_self",
)
entries[node_name] = fn_self
for arg, type in zip(node.args.args, signature.inputs):
entries[arg.arg] = entr_block.args.append_from(type, arg.arg)
def callback(frame: lowering.Frame, value: ir.SSAValue):
first_stmt = entr_block.first_stmt
stmt = func.GetField(obj=fn_self, field=len(frame.captures) - 1)
if value.name:
stmt.result.name = value.name
stmt.result.type = value.type
stmt.source = state.source
if first_stmt:
stmt.insert_before(first_stmt)
else:
entr_block.stmts.append(stmt)
return stmt.result
with state.frame(
[node.body], entr_block=entr_block, capture_callback=callback
) as func_frame:
func_frame.defs.update(entries)
func_frame.exhaust()
last_stmt = func_frame.curr_region.blocks[0].last_stmt
# assert hasattr(last_stmt,"result"), "python lambda should always have a return value"
rtrn_stmt = func.Return(last_stmt)
func_frame.curr_block.stmts.append(rtrn_stmt)
first_stmt = func_frame.curr_region.blocks[0].first_stmt
if first_stmt is None:
raise lowering.BuildError("empty lambda body")
func_frame.curr_region.blocks[1].delete()
lambda_stmt = func.Lambda(
tuple(value for value in func_frame.captures.values()),
sym_name=node_name,
slots=slots,
signature=signature,
body=func_frame.curr_region,
)
lambda_stmt.result.name = node_name
frame.push(lambda_stmt)
frame.defs[node_name] = lambda_stmt.result
return lambda_stmt.result
src/kirin/dialects/lowering/func.py:159
- While func.Return accepts both Statement and SSAValue arguments, passing last_stmt directly is inconsistent with the rest of the codebase. All other uses of func.Return in this file (lines 15, 19, 74) pass .result explicitly. For consistency and clarity, this should be
func.Return(last_stmt.result). Additionally, there's no null check for last_stmt before accessing it, which could cause an AttributeError if the lambda body is empty (though this is checked later at line 163).
rtrn_stmt = func.Return(last_stmt)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This PR address #525