[Proposal] Partial Local Functions #9131
Replies: 3 comments 17 replies
-
I see a lot of theoretical use cases in your motivation, but very little in actual examples. What's a real-world example of where you would have wanted this? |
Beta Was this translation helpful? Give feedback.
-
User Code (MyStateMachine.cs) In this file, the user implements the state functions as partial methods with custom logic. The user code then calls the generated state machine runner. public partial class MyStateMachine
{
// The user code calls the generated state machine runner.
private bool RunInlineWithStateMachine(Span<Events> inputs)
{
// Example of shared state that might be used by the state functions.
int i;
// The user provides the implementations for the state functions.
// These methods must match the signatures declared in the generated code.
partial void S(StateS.Input value)
{
if (value is StateS.Input.Epsilon)
{
i = 0;
}
}
partial void A(StateA.Input value)
{
if (i > 10)
{
// Custom logic when 'i' is greater than 10.
Console.WriteLine("Value is high in state A.");
}
else
{
// Alternative logic.
Console.WriteLine("Value is low in state A.");
}
}
partial void B(StateB.Input value)
{
i++;
}
partial void C(StateC.Input value)
{
// Custom logic for state C.
Console.WriteLine("Executing state C.");
}
return Run(inputs);
}
} Generated Code (MyStateMachine.Generated.cs) This generated file provides the state machine logic and declares the partial methods for the state functions. The generated code processes the events, handles state transitions, and calls the user-implemented state functions. public partial class MyStateMachine
{
// --- Generated State Machine Logic ---
// This function processes a sequence of events, transitions between states,
// and calls the user-implemented state functions.
private bool RunInlineWithStateMachine(Span<Events> inputs)
{
// --- Generated Partial Declarations for State Functions ---
// These declarations force the user to provide implementations.
partial void S(StateS.Input value);
partial void A(StateA.Input value);
partial void B(StateB.Input value);
partial void C(StateC.Input value);
partial bool Run(Span<Events> inputs, States state = States.StateS, Events @event = Events.Epsilon)
{
// Build an event path: prepend the initial event to the input events.
var path = new Events[inputs.Length + 1];
path[0] = @event;
inputs.CopyTo(path.AsSpan(1));
// Process each event in the path.
for (int i = 0; i < path.Length; i++)
{
var currentEvent = path[i];
switch (state)
{
case States.StateS:
switch (currentEvent)
{
case Events.Epsilon:
S(StateS.Input.Epsilon);
break;
case Events.EventA:
A(StateA.Input.EventA);
state = States.StateA;
break;
default:
return false;
}
break;
case States.StateA:
switch (currentEvent)
{
case Events.EventB:
B(StateB.Input.EventB);
state = States.StateB;
break;
case Events.EventD:
C(StateC.Input.EventD);
state = States.StateC;
break;
default:
return false;
}
break;
case States.StateB:
switch (currentEvent)
{
case Events.EventC:
S(StateS.Input.EventC);
state = States.StateS;
break;
case Events.Finish:
return true;
default:
return false;
}
break;
case States.StateC:
switch (currentEvent)
{
case Events.Finish:
return true;
default:
return false;
}
break;
default:
return false;
}
}
return false;
}
}
} By inlining both the state machine logic and the user-defined state functions, this approach minimizes overhead and enables high-performance throughput. The potential exists for similar inlining of iterators, which could further enhance performance if the use case demands it. |
Beta Was this translation helpful? Give feedback.
-
I'm missing why existing partial methods are not sufficient for your use cases here. Could you expand on that? |
Beta Was this translation helpful? Give feedback.
-
1. Abstract
This proposal introduces partial local functions as a new language feature in C#. Similar in spirit to partial methods in partial classes, partial local functions allow a programmer to declare a local function within a method body without immediately providing an implementation. A code generator (or another part of the system) can later supply the implementation in a separate “partial” part.
2. Motivation
Modern software systems often blend handwritten code with automatically generated code. In many scenarios, it is useful to expose extension points within a method without forcing the user to provide all implementation details upfront. Specifically, partial local functions:
3. Design Overview
3.1. Declaration and Implementation
A partial local function is declared inside a method with the partial modifier. There are two parts:
User Declaration: The user declares the function’s signature. For non-void functions that might be left unimplemented by a code generator, an optional default return expression may be provided.
Generated Implementation: A code generator (or a manually maintained “partial part”) supplies the function body later in the same method.
Example with a Void Return Type:
Example with a Non-Void Return Type and a Default Value:
In a separate “partial part” (for example, in a generated code region or file), an implementation may be provided:
or maybe like this
If both a default and a generated implementation exist, the generated implementation takes precedence.
3.2. Syntax and Placement
3.3. Semantics and Restrictions
Return Types:
Non-Void Functions: Partial local functions with non-void return types are allowed. For safety, one of the following must hold:
This requirement avoids ambiguity about the value returned when the function is invoked.
Accessibility: As local functions, partial local functions inherit the containing method’s scope and cannot be declared with additional access modifiers.
Call Elision and Inlining:
Error Handling:
4. Performance Considerations
While similar extension points could be achieved using method delegates (or lambda expressions), partial local functions are designed with performance in mind:
Compile-Time Resolution:
Partial local functions are resolved and merged at compile time. This eliminates the runtime indirection and overhead typically associated with delegate invocations.
Inlining and Optimization:
When an implementation is present (or when a default clause is used), the compiler treats the function like any other local function. It can inline the function or apply other optimizations, ensuring high-performance execution.
Zero-Overhead Abstraction:
For void partial local functions, if no implementation is provided, the compiler can remove calls entirely. For non-void functions with a default clause, calls may be replaced with a constant expression. In both cases, the performance penalty common to delegate-based approaches (such as closure allocations or invocation indirection) is avoided.
Avoiding Closure Allocations:
Local functions that do not capture local state incur no extra allocations. In contrast, delegates—especially those capturing context—often require heap allocations for closures.
5. Conclusion
The introduction of partial local functions in C# is motivated by the need to seamlessly integrate generated code with handwritten user code at a local scope. Importantly, the design leverages the performance benefits of local functions over delegate-based solutions, ensuring that these extension points incur minimal runtime overhead.
Beta Was this translation helpful? Give feedback.
All reactions