-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: CallerCharacterNumberAttribute #3992
Comments
I would call this CallerCharacterNumber. One terminology issue is that 'Column' is generally an IDE concept (and thus affected by things like 'how many spaces are in a tab?', whereas 'char(acter)' just means 'how many characters along is this in the string representing this line. |
Roslyn also uses the terminology |
A source generator might do this, sure, but I don't really buy that as a use case: C# 10 will hopefully be adding support for |
Another example is given in the linked issue: #3987 This gives more flexibility than CallerArgumentExpression alone. For example, this project https://github.com/SingleAccretion/Accretion.Diagnostics allows you to use this as an extension method: More generally, this is useful if a source generator wants to customize behavior based on the environment it's called in. I can't think of a really good motivating example not related to logging right now, but I'm pretty sure there are some. Will update when I think of any :-). |
I see CallerArgumentExpression will work for extension method arguments. But the point remains the same - this allows you to e.g. customize the formatting, or include only certain parts of the argument, or include the entire line, or in general customize the output based on the context. |
Relates to #87 |
@333fred Another example - consider a function which takes a delegate as a parameter, and caches the result of the delegate, so it's only evaluated once, and stores it in a static field. It then switches on the calling location, to find the correct field to return. e.g. using System;
using System.Runtime.CompilerServices;
Console.WriteLine(Helpers.Cache(() => 1 + 2 + 3));
Console.WriteLine(Helpers.Cache(() => "Hello" + "World"));
Console.WriteLine(Helpers.Cache(() => new object()));
public static partial class Helpers
{
private static int? field1;
private static string field2;
private static object field3;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Cache<T>(Func<T> func, [CallerFilePath] string filePath = default, [CallerLineNumber] int lineNumber = default)
{
return (lineNumber, filePath) switch {
(4, "_") => (T)(object)(field1 ??= (int?)(object)func()),
(5, "_") => (T)(object)(field2 ??= (string)(object)func()),
(6, "_") => (T)(field3 ??= func()),
_ => default,
};
}
} I'm sure that more creative people could come up with many more exciting examples. |
That certainly is more interesting, but I don't believe it's very interesting. Those delegates don't depend on state, so you could just use reference equality in the cache (which would likely be faster). Examples that do depend on state couldn't be cached no matter the caching strategy, so that also doesn't matter. |
That's relying on an implementation detail, and is easy to accidentally break - for example by converting a method to a delegate directly, or capturing a local variable which is going to be the same on all invocations. Still not the most compelling of examples I admit :-) I think what I'm trying to do here is to create a library of techniques which can be used for source generators, and I'm exploring where and when this technique might be useful. I think the example above does show that it has potential for more than just logging, although I don't think I've come up with a killer use case yet. I am pretty sure though there is somebody out there who is doing something with source generators for which this is the perfect solution. I just need to get them to comment here :-) |
Why not just have a
It reduces the number of parameters which makes the Intellisense presentation look less cluttered. |
I don't expect people to be using this manually at all, so I don't think it's worth optimising for style. This is the simplest to design and implement. |
@333fred here's an example I would like to write a generator for, and could benefit from I want to turn a static lambda into a function pointer using source generators. This requires checking what the passed in lambda is by looking at E.g. using System;
using System.Runtime.CompilerServices;
Console.WriteLine(Eval(Helpers.FP((a, b) => (a + b)), 1, 2));
Console.WriteLine(Eval(Helpers.FP((a, b) => (a * b)), 1, 2));
Console.WriteLine(Eval(Helpers.FP((a, b) => (a - b)), 1, 2));
static int Eval(delegate*<int, int, int> del, int a, int b) => del(a, b);
internal static partial class Helpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe delegate*<int, int, int> FP(Func<T, T, T> func, [CallerFilePath] string filePath = default, [CallerLineNumber] int lineNumber = default)
{
return (lineNumber, filePath) switch {
(4, "_") => &M1,
(5, "_") => &M2,
(6, "_") => &M3,
_ => default,
};
}
static int M1(int a, int b) => a + b;
static int M2(int a, int b) => a * b;
static int M3(int a, int b) => a - b;
} |
That would be a fairly complex generator to begin with, provided that it doesn't cause some safety issue. Doesn't it make sense to actually allow lambda assignment to function pointers? (#3476) (I am not against this feature at all, I just think that particular case should be supported by the language) |
@alrz agreed. However one advantage of source generators is to allow you to fill in where the language is missing features. I want to promote techniques using source generators that allow you to do that. |
@YairHalberstadt |
Indeed. This proposal shows one way of working around that limitation using a technique which works in some cases. |
Indeed, @YairHalberstadt, that's my reaction as well.
This is not a goal for me. Source generators aren't about enabling syntax for new languages features, it's about removing boilerplate code. Everything I've seen in this proposal so far is about the former, not the latter, which is why I'm still unenthusastic about it. |
Marking myself as a champion here because it sits in the design space for interceptors, and it's possible this will be an element of the solution we choose. |
This is triaged into the working set to accompany #7009. |
LDM looked at this here https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-06.md#callercharacternumberattribute. We are not pursuing this approach to interceptors, so we are closing this out. |
While I understand the interceptor usecase ( or rather, the lack thereof), this issue feels like it could stand on its own. |
Wouldn't this feature be useful to allow for identifying which value was null in a complex expression when a I've went through this problem recently where a very large LINQ |
Could you expand on that @julealgon ? Also, this likely would not help linq (since those signatures would not be updated). |
@CyrusNajmabadi Sorry, looking back at that example, it wasn't really a LINQ select, just a normal call with many different arguments: Here is a slightly sanitized version of it. Yes... pretty bad code... but the point was that identifying exactly which argument computation was causing the null reference here was hard as that information is just not present anywhere in the exception (it points to the call line only): var response = await service.InsertAsync(submissionRevisionId: Guid.NewGuid(),
submissionId: submissionRevison.SubmissionId, companyId: submissionRevison.CompanyId,
odometer: submissionRevison.Odometer,
vehicleTypeId: (byte?)submissionRevison.VehicleType,
vin: submissionRevison.Vin, tradeInVehicleId: submissionRevison.TradeInVehicleId,
createdBy: submissionRevison.CreatedBy,
createdOn: submissionRevison.CreatedOn,
otherConsiderations: submissionRevison.OtherConsiderations,
exteriorColor: (byte?)submissionRevison.VehicleMaterialAndColors.ExteriorColor,
interiorColor: (byte?)submissionRevison.VehicleMaterialAndColors?.InteriorColor,
interiorMaterial: (byte?)submissionRevison.VehicleMaterialAndColors?.InteriorMaterial,
exteriorColorDetails: submissionRevison.VehicleMaterialAndColors?.ExteriorColorDetails,
interiorColorDetails: submissionRevison.VehicleMaterialAndColors?.InteriorColorDetails,
interiorMaterialDetails: submissionRevison.VehicleMaterialAndColors?.InteriorMaterialDetails,
hasAfterMarketModifications: submissionRevison.VehicleConditions?.HasAfterMarketModifications,
afterMarketModificationsConditionDetails: submissionRevison.VehicleConditions?.AfterMarketModificationsConditionDetails,
carBeenInAccident: (byte?)submissionRevison.VehicleConditions?.CarBeenInAccident,
carBeenInAccidentDetails: submissionRevison.VehicleConditions?.CarBeenInAccidentDetails,
hasCosmeticIssues: submissionRevison.VehicleConditions?.HasCosmeticIssues,
cosmeticConditionDetails: submissionRevison.VehicleConditions?.CosmeticConditionDetails,
hasExteriorDamage: submissionRevison.VehicleConditions?.HasExteriorDamage,
exteriorDamageConditionDetails: submissionRevison.VehicleConditions?.ExteriorDamageConditionDetails,
hasMechanicalIssues: submissionRevison.VehicleConditions?.HasMechanicalIssues,
mechanicalIssueConditionDetails: submissionRevison.VehicleConditions?.MechanicalIssueConditionDetails,
hasPreviousBodyOrPaintWorkInThePanels: submissionRevison.VehicleConditions?.HasPreviousBodyOrPaintWorkInThePanels,
previousBodyOrPaintWorkInThePanelsDetails: submissionRevison.VehicleConditions?.PreviousBodyOrPaintWorkInThePanelsDetails,
hasTheOdometerBeenBrokenReplacedOrModified: submissionRevison.VehicleConditions?.HasTheOdometerBeenBrokenReplacedOrModified,
odometerConditionDetails: submissionRevison.VehicleConditions?.OdometerConditionDetails,
hasOnboardSystemIssues: submissionRevison.VehicleConditions?.HasOnboardSystemIssues,
onboardSystemConditionDetails: submissionRevison.VehicleConditions?.OnboardSystemConditionDetails,
doesTiresNeedToBeReplaced: submissionRevison.VehicleConditions?.DoesTiresNeedToBeReplaced,
tiresConditionDetails: submissionRevison.VehicleConditions?.TiresConditionDetails,
conditionType: (byte?)submissionRevison.VehicleConditions?.ConditionType,
doesVehicleHistoryReportHaveIssuesListed: submissionRevison.VehicleConditions?.DoesVehicleHistoryReportHaveIssuesListed,
vehicleHistoryReportDetails: submissionRevison.VehicleConditions?.VehicleHistoryReportDetails,
hasWarningLightsShownOnTheDashboard: submissionRevison.VehicleConditions?.HasWarningLightsShownOnTheDashboard,
warningLightsConditionDetails: submissionRevison.VehicleConditions?.WarningLightsConditionDetails,
wheelsType: (byte?)submissionRevison.VehicleConditions?.WheelsType,
wheelsCondition: (byte?)submissionRevison.VehicleConditions?.WheelsCondition,
wheelsConditionDetails: submissionRevison.VehicleConditions?.WheelsConditionDetails
); When a coworker sent this asking for guidance I could identify that the issue was likely on the - exteriorColor: (byte?)submissionRevison.VehicleMaterialAndColors.ExteriorColor,
+ exteriorColor: (byte?)submissionRevison.VehicleMaterialAndColors?.ExteriorColor, The exception message that is generated for cases such as this lacks enough detail to be able to pinpoint the line/column/argument position, as it goes something like this:
With a bunch of other frames reaching that specific call but only mentioning the initial call line number. To be clear, this is a NET472 application. The reason I mention this is that I know there have been changes to stack trace information in later releases of the frammework. |
Oh my |
CallerCharacterNumberAttribute
Summary
Add a
CallerCharacterNumberAttribute
which when applied on an optional parameter, the compiler replaces with the caller's character (column) number, similar to https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callerlinenumberattribute?view=netcore-3.1.The character number, is "how many characters along is this in the string representing this line".
Motivation
Consider a source generator that generates a method which acts differently depending on where it's called form. To do so it switches on CallerLineNumber, and CallerFilePath.
For example, a source generator might provide a method to print the expression passed into it as an argument (see it on sharplab):
This approach is actually optimized quite nicely right now by the jit (assuming the method is less than 64kb of IL, but that limit may be lifted by .NET 6, and can be alleviated by creating a tree of methods if necessary).
However it won't work at the moment if
PrintExpression
is called twice on the same line. To differentiate that we'll need access to the caller's character number.Detailed design
Add an attribute
System.Runtime.CompilerServices.CallerCharacterNumberAttribute
:Everything is as for the other
Caller
attributes: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/caller-information, except it provides character number.The above example could now be written as:
Drawbacks
The main question is whether this is a pattern we want to encourage in the first place. Using the trio of
CallerFilePath
,CallerLineNumber
, andCallerCharacterNumber
to distinguish the location something is called from and run something different in each case, is a hacky workaround to get around the no source code rewriting limitation of source generators. It's not clear whether it's any better than source rewriting, and relies heavily on the JIT to do a good job to be efficient.Alternatives
Do nothing
Unresolved questions
Seeing is this is pretty much identical to CallerLineNumber, I can't imagine there are any unresolved design questions.
Relevant issues/discussions
See #3987 which could be solved using this technique.
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-09.md#callercharacternumberattribute
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-06.md#callercharacternumberattribute
The text was updated successfully, but these errors were encountered: