Erweiterte #line-Direktiven
Anmerkung
Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Champion-Problem: https://github.com/dotnet/csharplang/issues/4747
Zusammenfassung
Der Compiler wendet die durch #line
-Direktiven definierte Zuordnung auf Diagnosepositionen und Sequenzpunkte an, die in die PDB ausgegeben werden.
Derzeit kann nur die Zeilennummer und der Dateipfad zugeordnet werden, während das Startzeichen aus dem Quellcode abgeleitet wird. Der Vorschlag besteht darin, die Angabe der vollständigen Bereichszuordnung zuzulassen.
Motivation
DSLs, die C#-Quellcode generieren (z. B. ASP.NET Razor), können derzeit keine präzise Quellzuordnung mit #line
Direktiven erstellen. Dies führt zu beeinträchtigter Debugerfahrung in einigen Fällen, da die sequenzierten Punkte, die an den PDB ausgegeben werden, nicht der genauen Position im ursprünglichen Quellcode zugeordnet werden können.
Beispiel: Der folgende Razor-Code
@page "/"
Time: @DateTime.Now
generiert Code wie so (vereinfacht):
#line hidden
void Render()
{
_builder.Add("Time:");
#line 2 "page.razor"
_builder.Add(DateTime.Now);
#line hidden
}
Die obige Direktive würde den vom Compiler ausgegebenen Sequenzpunkt der _builder.Add(DateTime.Now);
-Anweisung der Zeile 2 zuordnen, aber die Spalte wäre falsch (16 statt 7).
Der Razor-Quellgenerator generiert tatsächlich falsch den folgenden Code:
#line hidden
void Render()
{
_builder.Add("Time:");
_builder.Add(
#line 2 "page.razor"
DateTime.Now
#line hidden
);
}
Die Absicht war, das erste Zeichen und seine Funktionsweise bei der Zuordnung von Diagnosepositionen beizubehalten. Dies funktioniert jedoch nicht für Sequenzpunkte, da #line
Direktive nur für die Sequenzpunkte gilt, die darauf folgen. Es gibt keinen Sequenzpunkt in der Mitte der _builder.Add(DateTime.Now);
-Anweisung (Sequenzpunkte können nur bei IL-Anweisungen mit leerem Auswertungsstapel ausgegeben werden). Die #line 2
-Anweisung im Code oben hat somit keine Auswirkungen auf die generierte PDB-Datei, und der Debugger setzt auf der Razor-Seite keinen Haltepunkt oder Stopp im Codeschnipsel @DateTime.Now
.
Von diesem Vorschlag angesprochene Probleme: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526
Detailentwurf
Wir ändern die Syntax von line_indicator
, wie sie in der pp_line
-Richtlinie verwendet wird, wie folgt:
Current:
line_indicator
: decimal_digit+ whitespace file_name
| decimal_digit+
| 'default'
| 'hidden'
;
Vorgeschlagen:
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'
;
Das heißt, die #line
Richtlinie akzeptiert entweder 5 Dezimalzahlen (Anfangszeile, Anfangszeichen, Endzeile, Endzeichen, Zeichenversatz), 4 Dezimalzahlen (Anfangszeile, Anfangszeichen, Endzeile, Endzeichen) oder ein einzelnes Zeichen (Zeile).
Wenn kein Zeichenoffset angegeben ist, ist der Standardwert 0, andernfalls wird die Anzahl der UTF-16-Zeichen angegeben. Die Zahl muss nichtnegativ und weniger als die Zeilenlänge sein, die in der nicht zugeordneten Datei auf die #line-Anweisung folgt.
(Startzeile, Startzeichen)-(Endzeile, Endzeichen) gibt eine Spanne in der zugeordneten Datei an. Anfangszeile und Endlinie sind positive ganze Zahlen, die Zeilennummern angeben. AnfangszeichenEndzeichen sind positive ganze Zahlen, die UTF-16-Zeichenzahlen angeben. Anfangszeile, Anfangszeichen, Endzeile, Endzeichen sind 1-basiert, was bedeutet, dass die erste Zeile der Datei und das erste UTF-16-Zeichen in jeder Zeile Nummer 1 zugewiesen wird.
Durch die Implementierung werden diese Zahlen begrenzt, wodurch ein gültiger Sequenzpunkt-Quellbereich angegeben wird:
- erste Zeile: 1 liegt innerhalb des Bereichs [0, 0x20000000) und ist nicht gleich 0xfeefee.
- letzte Zeile: 1 liegt innerhalb des Bereichs [0, 0x20000000) und ist nicht gleich 0xfeefee.
- Anfangszeichen - 1 liegt innerhalb des Bereichs [0, 0x10000)
- Endzeichen - 1 befindet sich innerhalb des Bereichs [0, 0x10000)
- Die letzte Zeile ist größer oder gleich der ersten Zeile.
- Anfangszeile entspricht Endzeile, dann ist Endzeichen größer als Anfangszeichen.
Beachten Sie, dass die in der Direktivensyntax angegebenen Zahlen 1-basierte Zahlen sind, die tatsächlichen Spannen im PDB jedoch nullbasiert sind. Daher wurden die -1-Anpassungen oben vorgenommen.
Die zugeordneten Sequenzpunktbereiche und die Positionen der Diagnosen, auf die die #line
-Anweisung angewendet wird, werden wie folgt berechnet.
Angenommen, d ist die nullbasierte Zahl der nicht zugeordneten Zeile mit der #line
-Anweisung.
Span L = (Start: (Startzeile - 1, Anfangszeichen - 1), Ende: (Endzeile - 1, Endzeichen - 1)) ist nullbasierte Spanne, die durch die Direktive angegeben wird.
Funktion M, die eine Position (Zeile, Zeichen) innerhalb des Bereichs der #line
-Direktive in der Quelldatei zuordnet, die die #line Direktive enthält, einer zugeordneten Position (zugeordnete Linie, zugeordnetes Zeichen) wird wie folgt definiert:
M(l, c) =
l == d + 1 => (L.start.line + l – d – 1, L.start.character + max(c – Zeichenoffset, 0))
l>d + 1 => (L.start.line + l – d – 1, c)
Die Syntaxkonstrukte, denen Sequenzpunkte zugeordnet sind, werden von der Compilerimplementierung bestimmt und nicht von dieser Spezifikation abgedeckt. Der Compiler bestimmt auch für jeden Sequenzpunkt dessen nicht abgebildeten Bereich. Dieser Bereich kann teilweise oder vollständig das zugehörige Syntaxkonstrukt abdecken.
Sobald die nicht zugeordneten Bereiche vom Compiler ermittelt wurden, wird die oben definierte Funktion M auf die Anfangs- und Endpositionen dieser Bereiche angewendet, mit Ausnahme der Endposition aller Sequenzpunkte innerhalb des Geltungsbereichs der #line-Anweisung, deren nicht zugeordnete Position sich in Zeile d + 1 befindet und deren Zeichen weniger als der Zeichenoffset sind. Die Endposition aller diese Sequenzpunkte ist L.end.
Das Beispiel [5.i] veranschaulicht, warum es erforderlich ist, die Endposition der ersten Sequenzpunktspanne anzugeben.
Durch die Definition oben benötigt der Generator des nicht zugeordneten Quellcodes keine genauen Kenntnisse darüber, welche Quellkonstrukte der Programmiersprache C# Sequenzpunkte erzeugen. Die zugeordneten Sequenzpunktbereiche im Geltungsbereich der
#line
-Anweisung werden von der relativen Position der entsprechenden nicht zugeordneten Bereiche im Verhältnis zum ersten nicht zugeordneten Bereich abgeleitet.
Die Angabe des Zeichenoffsets ermöglicht es dem Generator, ein beliebiges Einzeilenpräfix in die erste Zeile einzufügen. Dieses Präfix ist generierter Code, der in der zugeordneten Datei nicht vorhanden ist. Ein solches eingefügtes Präfix wirkt sich auf den Wert des ersten nicht zugeordneten Sequenzpunktbereichs aus. Daher muss das erste Zeichen der nachfolgenden Sequenzpunktbereiche um die Länge des Präfixes (Zeichenoffset) versetzt werden. Siehe Beispiel [2].
Beispiele
Aus Gründen der Übersichtlichkeit verwenden die Beispiele die Pseudosyntaxen spanof('...')
und lineof('...')
, um die zugeordnete Anfangsposition und Zeilennummer des jeweiligen Bereichs im entsprechenden Codeschnipsel darzustellen.
1. Erste und nachfolgende Bereiche
Im Folgenden sehen Sie einen Code mit nicht zugeordneten nullbasierten Zeilennummern rechts:
#line (1,10)-(1,15) "a" // 3
A();B( // 4
);C(); // 5
D(); // 6
d = 3 L = (0, 9).. (0, 14)
Die Anweisung mit folgenden nicht zugeordneten und zugeordneten Bereichen wird auf vier Sequenzpunktbereiche angewendet: (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. Zeichenoffset
Razor generiert das _builder.Add(
-Präfix der Länge 15 (einschließlich zwei vorangestellter Leerzeichen).
Rasierer:
@page "/"
@F(() => 1+1,
() => 2+2
)
Generiertes 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) Zeichenoffset = 15
Spannweiten:
_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: Einzeiliger Bereich
Rasierer:
@page "/"
Time: @DateTime.Now
Generiertes C#:
#line hidden
void Render()
{
_builder.Add("Time:");
#line spanof('DateTime.Now') 15 "page.razor"
_builder.Add(DateTime.Now);
#line hidden
);
}
4. Razor: Mehrzeiliger Bereich
Rasierer:
@page "/"
@JsonToHtml(@"
{
""key1"": "value1",
""key2"": "value2"
}")
Generiertes 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: Blockkonstrukte
i. Block mit Ausdrücken
In diesem Beispiel muss der zugeordnete Bereich des ersten Sequenzpunkts, der mit der für die Anweisung _builder.Add(Html.Helper(() =>
ausgegebenen IL-Anweisung verknüpft ist, den gesamten Ausdruck von Html.Helper(...)
in der generierten Datei a.razor
abdecken. Dies wird durch Anwendung der Regel [1] auf die Endposition des Sequenzpunkts erreicht.
@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. Block mit Anweisungen
Verwendet vorhandenes #line line file
Formular seit
a) Razor fügt kein Präfix hinzu, b) {
in der generierten Datei nicht vorhanden ist und kein Sequenzpunkt darin platziert werden kann, daher ist die Spanne des ersten nicht zugeordneten Sequenzpunkts für Razor unbekannt.
Das Anfangszeichen von Console
in der generierten Datei muss mit der Razor-Datei abgeglichen werden.
@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. Block mit Code der obersten Ebene (@code, @functions)
Verwendet vorhandenes #line line file
Formular seit
a) Razor fügt kein Präfix hinzu, b) {
in der generierten Datei nicht vorhanden ist und kein Sequenzpunkt darin platziert werden kann, daher ist die Spanne des ersten nicht zugeordneten Sequenzpunkts für Razor unbekannt.
Das Startzeichen [Parameter]
in der generierten Datei muss mit der Razor-Datei übereinstimmen.
@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
Verwendet das bestehende Format #line line file
, da a) Razor kein Präfix hinzufügt.
b) Razor der Bereich des ersten nicht zugeordneten Sequenzpunkts möglicherweise nicht bekannt ist (bzw. dies nicht erforderlich ist).
Das Anfangszeichen des Schlüsselworts in der generierten Datei muss mit der Razor-Datei abgeglichen werden.
@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
C# feature specifications