Dela via


Förbättrade #line direktiv

Anteckning

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta anteckningarna från Language Design Meeting (LDM) .

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-fråga: https://github.com/dotnet/csharplang/issues/4747

Sammanfattning

Kompilatorn tillämpar mappningen som definierats av #line direktiv på diagnostikplatser och sekvenspunkter som skickas till PDB.

För närvarande kan endast radnumret och filsökvägen mappas medan starttecknet härleds från källkoden. Förslaget är att tillåta att fullständig omfattningskartläggning anges.

Motivation

DSL:er som genererar C#-källkod (till exempel ASP.NET Razor) kan för närvarande inte skapa exakt källmappning med hjälp av #line direktiv. Detta resulterar i försämrad felsökningsupplevelse i vissa fall eftersom sekvenspunkterna som skickas till PDB inte kan mappas till den exakta platsen i den ursprungliga källkoden.

Till exempel, följande Razor-kod

@page "/"
Time: @DateTime.Now

genererar kod så här (förenklad):

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

Ovanstående direktiv skulle mappa sekvenspunkten som genereras av kompilatorn för _builder.Add(DateTime.Now);-instruktionen till rad 2, men kolumnen skulle bli fel (16 i stället för 7).

Razor-källgeneratorn genererar faktiskt fel kod:

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

Avsikten var att bevara starttecknet och det fungerar för mappning av diagnostikplatser. Detta fungerar dock inte för sekvenspunkter eftersom #line direktiv endast gäller för de sekvenspunkter som följer det. Det finns ingen sekvenspunkt i mitten av _builder.Add(DateTime.Now); -instruktionen (sekvenspunkter kan endast genereras i IL-instruktioner med tom utvärderingsstack). Direktivet #line 2 i ovanstående kod har alltså ingen effekt på den genererade PDB och det felsökningsprogrammet kommer inte att placera någon brytpunkt eller stoppa vid kodfragmentet @DateTime.Now på Razor-sidan.

Problem som tas upp i det här förslaget: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526

Detaljerad design

Vi ändrar syntaxen för line_indicator som används i pp_line direktiv så här:

Aktuell

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

Föreslagit:

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'
    ;

Det vill: #line-direktivet skulle acceptera antingen 5 decimaltal (startrad, starttecken, slutlinje, sluttecken, teckenförskjutning), 4 decimaltal (startrad, starttecken, slutlinje, sluttecken) eller ett enda (rad).

Om teckenförskjutning inte anges är standardvärdet 0, annars anges antalet UTF-16 tecken. Talet måste vara icke-negativt och mindre än längden på raden som följer #line-direktivet i den icke-mappade filen.

(startrad, starttecken)-(slutlinje, sluttecken) anger ett intervall i den mappade filen. startrad och slutlinje är positiva heltal som anger radnummer. startteckensluttecken är positiva heltal som anger UTF-16-teckennummer. startrad, starttecken, slutlinje, sluttecken är 1-baserade, vilket innebär att den första raden i filen och det första UTF-16-tecknet på varje rad tilldelas nummer 1.

Implementeringen kommer att begränsa dessa tal så att de anger ett giltigt källområde för sekvenspunkten :

  • startrad – 1 ligger inom intervallet [0, 0x20000000) och är inte lika med 0xfeefee.
  • slutlinje – 1 ligger inom intervallet [0, 0x20000000) och inte lika med 0xfeefee.
  • starttecken – 1 ligger inom intervallet [0, 0x10000)
  • sluttecken – 1 ligger inom intervallet [0, 0x10000)
  • slutlinje är större eller lika med startrad.
  • startrad är lika med slutrad och då är sluttecken större än starttecken.

Observera att talen som anges i direktivsyntaxen är 1-baserade tal, men de faktiska intervallen i PDB är nollbaserade. Därav justeringarna -1 ovan.

De mappade intervallen för sekvenspunkter och platserna för diagnostik som #line direktiv gäller för beräknas på följande sätt.

Låt d vara det nollbaserade numret för den ommappade rad som innehåller #line-direktivet. Let span L = (start: (startrad - 1, starttecken - 1), slut: (slutlinje - 1, sluttecken - 1)) vara nollbaserat spann som anges i direktivet.

Funktion M som mappar en position (rad, tecken) inom omfånget för #line-direktivet i källfilen som innehåller #line-direktivet till en mappad position (mappad linje, mappat tecken) definieras på följande sätt:

M(l, c) =

l == d + 1 => (L.start.line + ld – 1, L.start.character + max(cteckenförskjutning, 0))

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

Syntaxkonstruktionerna som sekvenspunkter är associerade med bestäms av kompilatorimplementeringen och omfattas inte av den här specifikationen. Kompilatorn bestämmer också för varje sekvenspunkt sin omappade spann. Det här intervallet kan delvis eller helt täcka den associerade syntaxkonstruktionen.

När de ommappade intervallen bestäms av kompilatorn tillämpas funktionen M som definierats ovan på deras start- och slutpositioner, med undantag för slutpositionen för alla sekvenspunkter inom omfånget för #line-direktivet vars ommappade plats ligger på rad d + 1 och tecken mindre än teckenförskjutning. Slutpositionen för alla dessa sekvenspunkter är L.end-.

Exempel [5.i] visar varför det är nödvändigt att ange slutpositionen för det första sekvenspunktsintervallet.

Ovanstående definition gör det möjligt för generatorn av den ommappade källkoden att undvika intim kunskap om vilka exakta källkonstruktioner av C#-språket som producerar sekvenspunkter. De mappade intervallen för sekvenspunkterna i omfånget för #line-direktivet härleds från den relativa positionen för motsvarande ommappade intervall till det första ommappade intervallet.

Genom att ange teckenförskjutning tillåts generatorn att infoga vilket enkelradsprefix som helst på den första raden. Den här prefixkoden genereras och finns inte i den mappade filen. Ett sådant infogat prefix påverkar värdet för det första ommappade sekvenspunktsintervallet. Därför måste starttecknet för efterföljande sekvenspunktsområde förskjutas med prefixets längd (teckenförskjutning). Se exempel [2].

bild

Exempel

För tydlighetens skull använder exemplen spanof('...') och lineof('...') pseudosyntax för att uttrycka det mappade intervallets startposition respektive radnummer för det angivna kodfragmentet.

1. Första och efterföljande intervall

Överväg följande kod med nollbaserade radnummer som inte är mappade, listade till höger:

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

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

Det finns fyra sekvensintervall som direktivet gäller för med följande omappade och mappade spann: (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. Teckenförskjutning

Razor genererar _builder.Add(-prefix med längden 15 (inklusive de två inledande blankstegen).

Rakkniv:

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

Genererad 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) teckenförskjutning = 15

Spänner över:

  • _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: Enkelradsintervall

Rakkniv:

@page "/"
Time: @DateTime.Now

Genererad C#:

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

4. Razor: Flerradsintervall

Rakkniv:

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

Genererad 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: blockkonstruktioner

i. block som innehåller uttryck

I det här exemplet måste det mappade intervallet för den första sekvenspunkten som är associerad med IL-instruktionen som genereras för _builder.Add(Html.Helper(() => -instruktionen täcka hela uttrycket för Html.Helper(...) i den genererade filen a.razor. Detta uppnås genom tillämpning av regel [1] till sekvenspunktens slutposition.

@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 som innehåller satser

Använder befintligt #line line file formulär sedan

a) Razor lägger inte till något prefix, b) { finns inte i den genererade filen och det kan inte finnas någon sekvenspunkt placerad på den, därför är intervallet för den första ommappade sekvenspunkten okänd för Razor.

Starttecknet för Console i den genererade filen måste vara justerat med Razor-filen.

@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
  Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. block som innehåller toppnivåkod (@code, @functions)

Använder befintligt #line line file formulär sedan

a) Razor lägger inte till något prefix, b) { finns inte i den genererade filen och det kan inte finnas någon sekvenspunkt placerad på den, därför är intervallet för den första ommappade sekvenspunkten okänd för Razor.

Starttecknet för [Parameter] i den genererade filen måste stämma överens med Razor-filen.

@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

Använder befintligt #line line file formulär eftersom a) Razor inte lägger till något prefix. b) Omfånget för den första omappade sekvenspunkten kanske inte är känt för Razor (eller behöver kanske inte vara känt).

Starttecknet för nyckelordet i den genererade filen måste vara anpassat med Razor-filen.

@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