增强型 #line 指令

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。

功能规范与已完成的实现之间可能存在一些差异。 这些差异被记录在相关的 语言设计会议(LDM)说明中。

可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/4747

总结

编译器将 #line 指令定义的映射应用于发送到 PDB 的诊断位置和序列点。

目前,从源代码推断起始字符时,只能映射行号和文件路径。 提议允许指定全跨度映射。

动机

生成 C# 源代码(如 ASP.NET Razor)的 DSL 当前无法使用 #line 指令生成精确的源映射。 在某些情况下,这会导致调试体验下降,因为发送到 PDB 的序列点无法映射到原始源代码中的精确位置。

例如,以下 Razor 代码:

@page "/"
Time: @DateTime.Now

生成如下所示的代码(简化):

#line hidden
void Render()
{
   _builder.Add("Time:");
#line 2 "page.razor"
   _builder.Add(DateTime.Now);
#line hidden
}

上述指令会将编译器针对 _builder.Add(DateTime.Now); 语句发出的序列点映射到第 2 行,但列位置有误(是 16 而不是 7)。

Razor 源生成器实际上错误地生成以下代码:

#line hidden
void Render()
{
   _builder.Add("Time:");
   _builder.Add(
#line 2 "page.razor"
      DateTime.Now
#line hidden
);
}

目的是保留起始字符,它适用于诊断位置映射。 但是,这不适用于序列点,因为 #line 指令仅适用于其后面的序列点。 _builder.Add(DateTime.Now); 语句中间没有序列点(序列点只能在具有空计算堆栈的 IL 指令中发出)。 因此,上述代码中的 #line 2 指令对生成的 PDB 没有影响,调试器不会在 Razor 页面中的 @DateTime.Now 代码片段上放置断点或停止。

此建议解决的问题:https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526

详细设计

我们可以这样修改 pp_line 指令中使用的 line_indicator 语法:

当前:

line_indicator
    : decimal_digit+ whitespace file_name
    | decimal_digit+
    | 'default'
    | 'hidden'
    ;

提议:

line_indicator
    : '(' decimal_digit+ ',' decimal_digit+ ')' '-' '(' decimal_digit+ ',' decimal_digit+ ')' whitespace decimal_digit+ whitespace file_name
    | '(' decimal_digit+ ',' decimal_digit+ ')' '-' '(' decimal_digit+ ',' decimal_digit+ ')' whitespace file_name
    | decimal_digit+ whitespace file_name
    | decimal_digit+
    | 'default'
    | 'hidden'
    ;

也就是说, #line 指令将接受 5 个小数(起始行起始字符结束行结束字符字符偏移)、4 个小数(起始行起始字符结束行结束字符),或单个行()。

如果未指定 字符偏移量,其默认值为 0;否则,它将指定 UTF-16 字符的数量。 该数字必须是非负数,且小于未映射文件中 #line 指令后面的行数。

起始行起始字符)-(结束行结束字符)指定映射文件中的跨度。 起始行结束行 是指定行号的正整数。 起始字符结束字符 是指定 UTF-16 字符数的正整数。 起始行起始字符结束行结束字符 基于 1,这意味着文件的第一行和每行的第一个 UTF-16 字符都分配了数字 1。

实施时将对这些数字进行限制,使其指定有效的序列点源跨度

  • 起始行 - 1 在 [0, 0x20000000) 范围内,且不等于 0xfeefee。
  • 结束行 - 1 在 [0, 0x20000000) 范围内,且不等于 0xfeefee。
  • 起始字符 - 1 在 [0, 0x10000] 范围内
  • 结束字符 - 1 在 [0, 0x10000) 范围内
  • 结束线 大于或等于 起始线
  • 起始行 等于 结束行,则 结束字符 大于 起始字符

请注意,指令语法中指定的数字是基于 1 的数字,但 PDB 中的实际跨度是从零开始的。 因此,上述调整为 -1。

序列点的映射跨度和 #line 指令适用的诊断位置计算如下。

d 为包含 #line 指令的未映射行的从零开始编号。 令跨度L = (起始: (起始行 - 1, 起始字符 - 1), 结束: (结束行 - 1, 结束字符 - 1)),这是指令指定的从零开始的跨度。

函数 M 的定义如下:它将源文件中 #line 指令范围内的位置(行、字符)映射到包含 #line 指令的映射位置(映射行、映射字符)。

Mlc) =

l == d + 1 => (L.start.line + ld – 1, L.start.character + 最大(c字符偏移量, 0))

l>d + 1 => (L.start.line + ld – 1, c

序列点关联的语法构造由编译器实现确定,此规范未涵盖。 编译器还决定每个序列点的未映射跨度。 此范围可能部分或完全涵盖关联的语法构造。

一旦编译器确定了未映射跨度,上述定义的函数 M 就会应用到它们的起始和终止位置,但 #line 指令范围内所有序列点的终止位置除外,这些序列点的未映射位置位于行 d + 1 和小于字符偏移量的字符处。 所有这些序列点的结束位置是 L.end

示例 [5.i] 演示如何提供指定第一个序列点跨度结束位置的能力。

上述定义允许未映射源代码的生成器无需详细了解 C# 语言中哪些具体源码结构会生成序列点。 #line 指令范围内序列点的映射跨度派生自相应未映射跨度相对于第一个未映射跨度的相对位置。

指定 字符偏移量 允许生成器在第一行中插入任何单行前缀。 此前缀是生成的代码,它在映射文件中不存在。 此类插入的前缀会影响第一个未映射序列点范围的值。 因此,后续序列点跨度的起始字符需要偏移前缀的长度(字符偏移量)。 请参阅示例 [2]。

image

例子

为了清楚起见,这些示例使用 spanof('...')lineof('...') 伪语法分别表示指定代码片段的映射范围起始位置和行号。

1. 第一个和后续跨度

请考虑下面的代码,右侧列出了未映射的从零开始的行号:

#line (1,10)-(1,15) "a" // 3
  A();B(                // 4
);C();                  // 5
    D();                // 6

d = 3 L = (0, 9)。。(0, 14)

该指令适用于 4 个序列点跨度,未映射和映射跨度如下:(4, 2)..(4, 5) => (0, 9)..(0, 14) (4, 6)..(5, 1) => (0, 15)..(1, 1) (5, 2)..(5, 5) => (1, 2)..(1, 5) (6, 4)..(6, 7) => (2, 4)..(2, 7)

2. 字符偏移量

Razor 生成长度为 15(包括两个前导空格)的 _builder.Add( 前缀。

Razor:

@page "/"                                  
@F(() => 1+1, 
   () => 2+2
) 

生成的 C#代码:

#line hidden
void Render()            
{ 
#line spanof('F(...)') 15 "page.razor"  // 4
  _builder.Add(F(() => 1+1,            // 5
  () => 2+2                            // 6
));                                    // 7
#line hidden
}
);
}

d = 4 L = (1, 1)。。(3,0) 字符偏移量 = 15

跨度:

  • _builder.Add(F(…)); =>F(…): (5, 2)。。(7, 2) => (1, 1)。(3, 0)
  • 1+1 =>1+1: (5, 23)。(5, 25) => (1, 9)。(1, 11)
  • 2+2 =>2+2: (6, 7)..(6, 9) => (2, 7)..(2, 9)

3. Razor:单行跨度

Razor:

@page "/"
Time: @DateTime.Now

生成的 C#:

#line hidden
void Render()
{
  _builder.Add("Time:");
#line spanof('DateTime.Now') 15 "page.razor"
  _builder.Add(DateTime.Now);
#line hidden
);
}

4. Razor:多行跨度

Razor:

@page "/"                                  
@JsonToHtml(@"
{
  ""key1"": "value1",
  ""key2"": "value2"
}") 

生成的 C# 代码:

#line hidden
void Render()
{
  _builder.Add("Time:");
#line spanof('JsonToHtml(@"...")') 15 "page.razor"
  _builder.Add(JsonToHtml(@"
{
  ""key1"": "value1",
  ""key2"": "value2"
}"));
#line hidden
}
);
}

5. Razor:块构造

i. 包含表达式的块

在此示例中,与为 _builder.Add(Html.Helper(() => 语句发出的 IL 指令关联的第一个序列点的映射范围需要涵盖生成的文件中 Html.Helper(...)a.razor的整个表达式。 这可以通过将规则 [1] 应用到序列点的结束位置来实现。

@Html.Helper(() => 
{
    <p>Hello World</p>
    @DateTime.Now
})
#line spanof('Html.Helper(() => { ... })') 13 "a.razor"
_builder.Add(Html.Helper(() => 
#line lineof('{') "a.razor"
{
#line spanof('DateTime.Now') 13 "a.razor"
_builder.Add(DateTime.Now);
#line lineof('}') "a.razor"
}
#line hidden
)
ii. 包含语句的块

使用现有 #line line file 窗体,因为

a) Razor 不添加任何前缀,b) { 不存在于生成的文件中,并且无法放置序列点,因此第一个未映射的序列点的跨度对 Razor 未知。

生成的文件中 Console 的起始字符必须与 Razor 文件保持一致。

@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
  Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. 包含顶级代码的块 (@code, @functions)

使用现有 #line line file 窗体,因为

a) Razor 不添加任何前缀,b) { 不存在于生成的文件中,并且无法放置序列点,因此第一个未映射的序列点的跨度对 Razor 未知。

生成的文件中 [Parameter] 的起始字符必须与 Razor 文件保持一致。

@code {
    [Parameter]
    public int IncrementAmount { get; set; }
}
#line lineof('[') "a.razor"
    [Parameter]
    public int IncrementAmount { get; set; }
#line hidden

6. Razor:@for@foreach@while@do@if@switch@using@try@lock

使用现有的 #line line file 窗体,因为 a) Razor 不添加任何前缀。 b) Razor 可能不知道第一个未映射序列点的范围(或不应该知道)。

生成的文件中关键字的起始字符必须与 Razor 文件保持一致。

@for (var i = 0; i < 10; i++)
{
}
@if (condition)
{
}
else
{
}
#line lineof('for') "a.razor"
 for (var i = 0; i < 10; i++)
{
}
#line lineof('if') "a.razor"
 if (condition)
{
}
else
{
}
#line hidden