Skip to content

Finalize single-expression light handlers#2381

Open
TUPM96 wants to merge 1 commit into
Lightprotocol:mainfrom
TUPM96:codex/light-program-single-expression-1290
Open

Finalize single-expression light handlers#2381
TUPM96 wants to merge 1 commit into
Lightprotocol:mainfrom
TUPM96:codex/light-program-single-expression-1290

Conversation

@TUPM96
Copy link
Copy Markdown

@TUPM96 TUPM96 commented May 25, 2026

Closes #1290.

Summary

  • Treat a single-expression light handler as delegated only when the context is actually moved into the inner call.
  • Keep &ctx / &mut ctx handlers on the wrapper path so __user_result and light_finalize are emitted.
  • Add regression coverage for both the borrowed-context finalize path and the moved-context delegation path.

Validation

  • cargo fmt --package light-sdk-macros --check
  • cargo test -p light-sdk-macros test_wrap_single_expression_mut_context_runs_finalize
  • cargo test -p light-sdk-macros test_wrap_moved_context_delegation_skips_finalize
  • cargo test -p light-sdk-macros light_pdas_tests::parsing_tests
  • git diff --check

Summary by CodeRabbit

  • Bug Fixes

    • Improved context argument detection in program handlers to accurately determine whether context is borrowed or moved, ensuring the wrapper applies proper finalization logic only when appropriate.
  • Tests

    • Added test cases validating correct behavior when context is passed by reference versus moved into handler functions.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

The PR refines how the macro detects whether a handler function consumes its context argument. A new helper function (call_moves_ctx_arg) distinguishes direct context moves from reference patterns, allowing the wrapper to conditionally inject finalization code only when the context remains available after the handler returns.

Changes

Context-move detection refinement

Layer / File(s) Summary
Refined delegation detection with context-move helper
sdk-libs/macros/src/light_pdas/program/parsing.rs
call_moves_ctx_arg helper classifies call arguments as consuming context (direct usage or into transformation) while excluding references (&ctx, &mut ctx). is_delegation_body switches from the broader call_has_ctx_arg check to this refined helper, ensuring finalization code is only injected when safe.
Test coverage for reference and move delegation cases
sdk-libs/macros/src/light_pdas_tests/parsing_tests.rs
Two test cases validate wrap_function_with_light: one confirms finalization markers appear for &mut ctx patterns, and another confirms they are absent when context is moved into instructions::handler(ctx). Import section adjusted for test support.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

The changes are focused and consistent: a straightforward helper function addition, a localized logic substitution, and parallel test cases that directly mirror the implementation logic. No complex branching or semantic interactions across distant code paths.

Suggested reviewers

  • SwenSchaeferjohann

Poem

A context that moves must not be moved twice—
The macro now sees through its crystal disguise.
References stay put, their shadows persist,
While consumed arguments vanish like mist. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: distinguishing between borrowed-context handlers (which need finalize logic) and moved-context handlers (which delegate), enabling single-expression handlers to work correctly.
Linked Issues check ✅ Passed The PR directly addresses issue #1290 by refining delegation detection to only treat handlers as delegated when context is actually moved, ensuring finalize logic is emitted for borrowed-context handlers and modifications are properly reflected.
Out of Scope Changes check ✅ Passed All changes are focused on fixing delegation detection logic and adding regression tests for the finalize and delegation paths, staying within the scope of issue #1290's requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk-libs/macros/src/light_pdas/program/parsing.rs`:
- Around line 621-650: is_delegation_body currently only inspects
MethodCall.args via call_moves_ctx_arg and misses cases where the context is
moved as the MethodCall.receiver (e.g., ctx.delegate(...)); update the logic to
also check the MethodCall.receiver for moved ctx. Specifically, modify the
MethodCall branch in is_delegation_body to test call.receiver for being an
identifier equal to ctx_name or being a MethodCall "into" whose receiver is that
identifier (analogous to the existing checks in call_moves_ctx_arg), or refactor
call_moves_ctx_arg to accept and check a receiver Expr as well and reuse the
same moved-ctx detection for receiver and args; ensure you reference
is_delegation_body, call_moves_ctx_arg, syn::Expr::MethodCall and call.receiver
when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75ba3b19-bf26-4005-8ed3-a2be612873dd

📥 Commits

Reviewing files that changed from the base of the PR and between 5d58e11 and 1b522d6.

📒 Files selected for processing (2)
  • sdk-libs/macros/src/light_pdas/program/parsing.rs
  • sdk-libs/macros/src/light_pdas_tests/parsing_tests.rs

Comment on lines +621 to +650
syn::Expr::MethodCall(call) => call_moves_ctx_arg(&call.args, ctx_name),
_ => false,
}
}
_ => false,
}
}

/// Check if any argument consumes the context param.
///
/// Passing `&ctx` or `&mut ctx` lets the wrapper continue using `ctx` for
/// finalization after the handler returns, so those are intentionally excluded.
fn call_moves_ctx_arg(
args: &syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>,
ctx_name: &str,
) -> bool {
for arg in args {
match arg {
syn::Expr::Path(path) if path.path.is_ident(ctx_name) => return true,
syn::Expr::MethodCall(method_call)
if method_call.method == "into"
&& matches!(&*method_call.receiver, syn::Expr::Path(path) if path.path.is_ident(ctx_name)) =>
{
return true;
}
_ => {}
}
}
false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'is_delegation_body|Expr::MethodCall|call_moves_ctx_arg|call\\.receiver' sdk-libs/macros/src/light_pdas/program/parsing.rs
rg -n -C2 'test_wrap_.*finalize|instructions::handler\\(&mut ctx\\)|instructions::handler\\(ctx\\)' sdk-libs/macros/src/light_pdas_tests/parsing_tests.rs

Repository: Lightprotocol/light-protocol

Length of output: 3518


Fix delegation detection to account for moved ctx used as a method-call receiver

In sdk-libs/macros/src/light_pdas/program/parsing.rs (~lines 611-623), is_delegation_body checks syn::Expr::MethodCall(call) for delegation using only call.args via call_moves_ctx_arg, ignoring call.receiver. If a handler consumes ctx as the receiver (e.g., ctx.delegate(...)), delegation can be misclassified and the wrapper may incorrectly skip/emit the finalize path. sdk-libs/macros/src/light_pdas_tests/parsing_tests.rs only covers instructions::handler(ctx) / instructions::handler(&mut ctx) (arg-style Expr::Call), not receiver consumption.

Suggested fix
 fn is_delegation_body(block: &syn::Block, ctx_name: &str) -> bool {
@@
         syn::Stmt::Expr(expr, _) => {
             // Check if it's a function call that takes ctx as an argument
             match expr {
                 syn::Expr::Call(call) => call_moves_ctx_arg(&call.args, ctx_name),
-                syn::Expr::MethodCall(call) => call_moves_ctx_arg(&call.args, ctx_name),
+                syn::Expr::MethodCall(call) => {
+                    expr_moves_ctx(&call.receiver, ctx_name)
+                        || call_moves_ctx_arg(&call.args, ctx_name)
+                }
                 _ => false,
             }
         }
@@
 }
 
+fn expr_moves_ctx(expr: &syn::Expr, ctx_name: &str) -> bool {
+    match expr {
+        syn::Expr::Path(path) if path.path.is_ident(ctx_name) => true,
+        syn::Expr::MethodCall(method_call)
+            if method_call.method == "into"
+                && matches!(&*method_call.receiver, syn::Expr::Path(path) if path.path.is_ident(ctx_name)) =>
+        {
+            true
+        }
+        _ => false,
+    }
+}
+
 fn call_moves_ctx_arg(
     args: &syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>,
     ctx_name: &str,
 ) -> bool {
     for arg in args {
-        match arg {
-            syn::Expr::Path(path) if path.path.is_ident(ctx_name) => return true,
-            syn::Expr::MethodCall(method_call)
-                if method_call.method == "into"
-                    && matches!(&*method_call.receiver, syn::Expr::Path(path) if path.path.is_ident(ctx_name)) =>
-            {
-                return true;
-            }
-            _ => {}
+        if expr_moves_ctx(arg, ctx_name) {
+            return true;
         }
     }
     false
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
syn::Expr::MethodCall(call) => call_moves_ctx_arg(&call.args, ctx_name),
_ => false,
}
}
_ => false,
}
}
/// Check if any argument consumes the context param.
///
/// Passing `&ctx` or `&mut ctx` lets the wrapper continue using `ctx` for
/// finalization after the handler returns, so those are intentionally excluded.
fn call_moves_ctx_arg(
args: &syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>,
ctx_name: &str,
) -> bool {
for arg in args {
match arg {
syn::Expr::Path(path) if path.path.is_ident(ctx_name) => return true,
syn::Expr::MethodCall(method_call)
if method_call.method == "into"
&& matches!(&*method_call.receiver, syn::Expr::Path(path) if path.path.is_ident(ctx_name)) =>
{
return true;
}
_ => {}
}
}
false
}
syn::Expr::MethodCall(call) => {
expr_moves_ctx(&call.receiver, ctx_name)
|| call_moves_ctx_arg(&call.args, ctx_name)
}
_ => false,
}
}
_ => false,
}
}
fn expr_moves_ctx(expr: &syn::Expr, ctx_name: &str) -> bool {
match expr {
syn::Expr::Path(path) if path.path.is_ident(ctx_name) => true,
syn::Expr::MethodCall(method_call)
if method_call.method == "into"
&& matches!(&*method_call.receiver, syn::Expr::Path(path) if path.path.is_ident(ctx_name)) =>
{
true
}
_ => false,
}
}
/// Check if any argument consumes the context param.
///
/// Passing `&ctx` or `&mut ctx` lets the wrapper continue using `ctx` for
/// finalization after the handler returns, so those are intentionally excluded.
fn call_moves_ctx_arg(
args: &syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>,
ctx_name: &str,
) -> bool {
for arg in args {
if expr_moves_ctx(arg, ctx_name) {
return true;
}
}
false
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-libs/macros/src/light_pdas/program/parsing.rs` around lines 621 - 650,
is_delegation_body currently only inspects MethodCall.args via
call_moves_ctx_arg and misses cases where the context is moved as the
MethodCall.receiver (e.g., ctx.delegate(...)); update the logic to also check
the MethodCall.receiver for moved ctx. Specifically, modify the MethodCall
branch in is_delegation_body to test call.receiver for being an identifier equal
to ctx_name or being a MethodCall "into" whose receiver is that identifier
(analogous to the existing checks in call_moves_ctx_arg), or refactor
call_moves_ctx_arg to accept and check a receiver Expr as well and reuse the
same moved-ctx detection for receiver and args; ensure you reference
is_delegation_body, call_moves_ctx_arg, syn::Expr::MethodCall and call.receiver
when making the change.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improper macro expansion for instruction handler functions with a single expression

2 participants