Skip to content

Proposal: CallerCharacterNumberAttribute #3992

Closed as not planned
Closed as not planned

Description

CallerCharacterNumberAttribute

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

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):

using System;
using System.Runtime.CompilerServices;

Console.WriteLine(Helpers.PrintExpression(1 + 3 + 7));
Console.WriteLine(Helpers.PrintExpression(new object()));
Console.WriteLine(Helpers.PrintExpression(5 == 7));

// This code is all generated by a source generator
public static partial class Helpers 
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string PrintExpression(object expression, [CallerFilePath] string filePath = default, [CallerLineNumber] int lineNumber = default)
    {
        return (lineNumber, filePath) switch {
                (4, "Main.cs") =>  "1 + 3 + 7" ,
                (5, "Main.cs")  => "new object()",
                (6, "Main.cs") => "5 == 7",
                _ => ""
        };    
    }
}

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:

namespace System.Runtime.CompilerServices
{
    [System.AttributeUsage(System.AttributeTargets.Parameter, Inherited=false)]
    public sealed class CallerCharacterNumberAttribute : Attribute {}
}

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:

using System;
using System.Runtime.CompilerServices;

Console.WriteLine(Helpers.PrintExpression(1 + 3 + 7));
Console.WriteLine(Helpers.PrintExpression(new object()));
Console.WriteLine(Helpers.PrintExpression(5 == 7));

// This code is all generated by a source generator
public static partial class Helpers 
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string PrintExpression(object expression, [CallerFilePath] string filePath = default, [CallerLineNumber] int lineNumber = default, [CallerCharacterNumber] int characterNumber = default)
    {
        return (lineNumber, characterNumber, filePath) switch {
                (4, 42, "Main.cs") =>  "1 + 3 + 7" ,
                (5, 42, "Main.cs")  => "new object()",
                (6, 42, "Main.cs") => "5 == 7",
                _ => ""
        };    
    }
}

Drawbacks

The main question is whether this is a pattern we want to encourage in the first place. Using the trio of CallerFilePath, CallerLineNumber, and CallerCharacterNumber 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

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

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions