增強的 #line 指示符
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異已經在語言設計會議(LDM)的相關會議記錄中捕捉到。
冠軍問題:https://github.com/dotnet/csharplang/issues/4747
總結
編譯程式會將 #line
指示詞所定義的對應套用到輸出至 PDB 的診斷位置和時序點。
目前只有行號和檔案路徑可以被對應,而起始字元則由原始程式碼推斷得出。 提議是允許指定完整的範圍對應。
動機
產生 C# 原始程式碼的 DSL(例如 ASP.NET Razor)目前無法使用 #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
指示詞範圍內的位置(行、字元)對應到對應位置(對應行、對應字元)。
M(l, c) =
l == d + 1 => (L.start.line + l – d – 1, L.start.character + max(c – 字元位移, 0) )
l>d + 1 => (L.start.line + l – d – 1, c)
順序點相關聯的語法建構是由編譯程序實作所決定,而且此規格未涵蓋。 編譯程式也會針對每個序列決定其未對應的範圍。 此範圍可能部分或完全涵蓋相關聯的語法建構。
一旦編譯器確定未對應的範圍,上述定義的函式 M 會套用到這些範圍的起始和結束位置,但 #line 指示詞範圍內所有順序點的結束位置除外,其未對應的位置位於第 d + 1 行且字元小於字符偏移。 所有這些序列點的結束位置都在 L.end。
範例 [5.i] 示範為何必須提供指定第一個序列點範圍結束位置的能力。
上述定義允許未映射原始碼的生成器避免深入了解哪種 C# 語言的源構造產生序列點。
#line
指令範圍中的映射時序點範圍,是根據相應未映射範圍與第一個未映射範圍之間的相對位置衍生而來的。
指定 字元位移 可讓產生器在第一行插入任何單行前綴。 此前綴是未出現在映射檔案中的生成程式碼。 這類插入的字首會影響第一個未映射的序列點的值。 因此,後續序列點範圍的起始字元必須依前綴長度進行位移(字元位移)。 請參閱範例 [2]。
例子
為了清楚起見,範例會使用 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(
(包括兩個前綴空格)。
剃刀:
@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:單行範圍
剃刀
@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:多行範圍
剃刀:
@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:區塊建構
一 包含表達式的區塊
在本例中,與 _builder.Add(Html.Helper(() =>
語句發出的 IL 指令相關聯的第一個序列點所對應的範圍,必須涵蓋在所產生的檔案 a.razor
中 Html.Helper(...)
的完整表達式。 這是藉由將規則 [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
)
第二。 區塊包含語句
使用現有的 #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
第三。 區塊,包含最上層程式代碼 (@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
窗體,因為 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