Direttive #line migliorate
Nota
Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.
Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono acquisite nelle note pertinenti del language design meeting (LDM) .
Altre informazioni sul processo di adozione degli speclet delle funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche di .
Problema del campione: https://github.com/dotnet/csharplang/issues/4747
Sommario
Il compilatore applica il mapping definito dalle direttive #line
alle posizioni diagnostiche e ai punti di sequenza emessi al PDB.
Attualmente è possibile mappare solo il numero di riga e il percorso del file mentre il carattere iniziale viene dedotto dal codice sorgente. La proposta è di permettere di specificare la mappatura completa dell'intervallo.
Motivazione
I DSLS che generano codice sorgente C# (ad esempio ASP.NET Razor) non possono attualmente produrre mapping di origine precisi usando direttive #line
. Ciò comporta un'esperienza di debug compromessa in alcuni casi perché i punti di sequenza generati nel PDB non possono mappare alla posizione precisa nel codice sorgente originale.
Ad esempio, il codice Razor seguente
@page "/"
Time: @DateTime.Now
genera codice come in questo modo (semplificato):
#line hidden
void Render()
{
_builder.Add("Time:");
#line 2 "page.razor"
_builder.Add(DateTime.Now);
#line hidden
}
La direttiva precedente mapperebbe il punto di sequenza generato dal compilatore per l'istruzione _builder.Add(DateTime.Now);
alla riga 2, ma la colonna sarebbe errata (16 anziché 7).
Il generatore di origine Razor genera effettivamente in modo errato il codice seguente:
#line hidden
void Render()
{
_builder.Add("Time:");
_builder.Add(
#line 2 "page.razor"
DateTime.Now
#line hidden
);
}
Lo scopo era quello di mantenere il carattere iniziale e funziona per la mappatura della posizione diagnostica. Tuttavia, questo non funziona per i punti di sequenza poiché la direttiva #line
si applica solo ai punti di sequenza che la seguono. Non esiste alcun punto di sequenza al centro dell'istruzione _builder.Add(DateTime.Now);
(i punti di sequenza possono essere generati solo in istruzioni IL con stack di valutazione vuoto). La direttiva #line 2
nel codice precedente non ha quindi alcun effetto sul PDB generato e il debugger non inserisce un punto di interruzione né si arresta sul frammento @DateTime.Now
nella pagina Razor.
Problemi risolti da questa proposta: https://github.com/dotnet/roslyn/issues/43432https://github.com/dotnet/roslyn/issues/46526
Progettazione dettagliata
Si modifica la sintassi di line_indicator
usata nella direttiva pp_line
in questo modo:
Corrente:
line_indicator
: decimal_digit+ whitespace file_name
| decimal_digit+
| 'default'
| 'hidden'
;
Proposto:
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'
;
Cioè, La direttiva #line
accetterebbe 5 numeri decimali (riga iniziale, carattere iniziale, riga finale, carattere finale, offset carattere), 4 numeri decimali (riga iniziale, carattere iniziale, riga finale, carattere finale) o un singolo carattere (riga).
Se offset di caratteri non viene specificato il valore predefinito è 0, altrimenti specifica il numero di caratteri UTF-16. Il numero deve essere non negativo e minore della lunghezza della riga che segue la direttiva #line nel file non mappato.
(riga iniziale, carattere iniziale)-(riga finale, carattere finale) specifica un intervallo nel file mappato. riga iniziale e riga finale sono numeri interi positivi che specificano i numeri di riga. carattere iniziale, carattere finale sono numeri interi positivi che specificano numeri di carattere UTF-16. riga iniziale, carattere iniziale, riga finale, carattere finale sono in base 1, ovvero la prima riga del file e il primo carattere UTF-16 su ogni riga viene assegnato numero 1.
L'implementazione vincoli questi numeri in modo che specifichino un intervallo di origine del punto di sequenza valido:
- riga iniziale - 1 è compreso nell'intervallo [0, 0x20000000) e non uguale a 0xfeefee.
- riga finale - 1 è compreso nell'intervallo [0, 0x20000000) e non è uguale a 0xfeefee.
- inizio del carattere - 1 è compreso nell'intervallo [0, 0x10000)
- carattere finale - 1 è compreso nell'intervallo [0, 0x10000)
- riga finale è maggiore o uguale a riga iniziale.
- inizio riga è uguale a fine riga, allora fine carattere è maggiore di inizio carattere.
Si noti che i numeri specificati nella sintassi della direttiva sono numeri basati su 1, ma gli intervalli effettivi nel PDB sono in base zero. Di conseguenza, le precedenti rettifiche -1.
Gli intervalli mappati di punti di sequenza e le posizioni della diagnostica a cui la direttiva #line
si applica sono calcolati come segue.
Lasciare d essere il numero in base zero della linea non mappata contenente la direttiva #line
.
Intervallo L = (inizio: (riga iniziale - 1, carattere iniziale - 1), fine: (riga finale - 1, carattere finale - 1)) deve essere in base zero specificato dalla direttiva.
La funzione M che esegue il mapping di una posizione (riga, carattere) nell'ambito della direttiva #line
nel file di origine contenente la direttiva #line a una posizione mappata (riga mappata, carattere mappato) viene definita come segue:
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)
I costrutti di sintassi associati ai punti di sequenza sono determinati dall'implementazione del compilatore e non coperti da questa specifica. Il compilatore decide anche per ogni punto di sequenza il relativo intervallo non mappato. Questo intervallo può coprire parzialmente o completamente il costrutto di sintassi associato.
Dopo che gli intervalli non mappati vengono determinati dal compilatore, la funzione M definita in precedenza viene applicata alle posizioni di inizio e fine, eccetto per la posizione finale di tutti i punti sequenza e nell'ambito della direttiva #line la cui posizione non mappata è in corrispondenza della riga d + 1 e carattere inferiore all'offset del carattere. La posizione finale di tutti questi punti di sequenza è L.end.
L'esempio [5.i] dimostra perché è necessario fornire la possibilità di specificare la posizione finale del primo intervallo di punti di sequenza.
La definizione precedente consente al generatore del codice sorgente non mappato di evitare una conoscenza approfondita su quali esatti costrutti linguistici del C# producono punti di sequenza. Gli intervalli mappati dei punti di sequenza nell'ambito della direttiva
#line
derivano dalla posizione relativa degli intervalli non mappati corrispondenti al primo intervallo non mappato.
Se si specifica l'offset di caratteri , il generatore può inserire qualsiasi prefisso a riga singola nella prima riga. Questo prefisso è un codice generato che non è presente nel file mappato. Tale prefisso inserito influisce sul valore del primo intervallo di punti di sequenza non mappati. Pertanto, il carattere iniziale degli intervalli di punti di sequenza successivi deve essere sfalsato dalla lunghezza del prefisso (offset di caratteri). Vedere l'esempio [2].
Esempi
Per maggiore chiarezza, gli esempi usano spanof('...')
e lineof('...')
pseudosintassi per esprimere rispettivamente la posizione iniziale dell'intervallo mappato e il numero di riga del frammento di codice specificato.
1. Primo e successivo intervallo
Si consideri il codice seguente con numeri di riga non mappati con base zero indicati a destra.
#line (1,10)-(1,15) "a" // 3
A();B( // 4
);C(); // 5
D(); // 6
d = 3 L = (0, 9).. (0, 14)
La direttiva si applica a quattro intervalli di punti di sequenza con i seguenti intervalli non mappati e mappati: (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. Offset carattere
Razor genera il prefisso _builder.Add(
di lunghezza 15 (inclusi due spazi iniziali).
Rasoio:
@page "/"
@F(() => 1+1,
() => 2+2
)
Generato 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) offset di caratteri = 15
Si estende:
-
_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: intervallo a riga singola
Rasoio:
@page "/"
Time: @DateTime.Now
Generated C#:
#line hidden
void Render()
{
_builder.Add("Time:");
#line spanof('DateTime.Now') 15 "page.razor"
_builder.Add(DateTime.Now);
#line hidden
);
}
4. Razor: intervallo su più righe
Rasoio:
@page "/"
@JsonToHtml(@"
{
""key1"": "value1",
""key2"": "value2"
}")
C# generato:
#line hidden
void Render()
{
_builder.Add("Time:");
#line spanof('JsonToHtml(@"...")') 15 "page.razor"
_builder.Add(JsonToHtml(@"
{
""key1"": "value1",
""key2"": "value2"
}"));
#line hidden
}
);
}
5. Razor: costrutti di blocco
io. blocco contenente espressioni
In questo esempio, l'intervallo mappato del primo punto di sequenza associato all'istruzione IL generata per l'istruzione _builder.Add(Html.Helper(() =>
deve coprire l'intera espressione di Html.Helper(...)
nel file generato a.razor
. Questa operazione viene ottenuta dall'applicazione della regola [1] alla posizione finale del punto di sequenza.
@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. blocco contenente istruzioni
Usa il modulo esistente #line line file
a partire da
a) Razor non aggiunge alcun prefisso, b) {
non è presente nel file generato e non può esserci un punto di sequenza posizionato su di esso, pertanto l'intervallo del primo punto di sequenza non mappato è sconosciuto a Razor.
Il carattere iniziale di Console
nel file generato deve essere allineato al file Razor.
@{Console.WriteLine(1);Console.WriteLine(2);}
#line lineof('@{') "a.razor"
Console.WriteLine(1);Console.WriteLine(2);
#line hidden
iii. blocco contenente codice di primo livello (@code, @functions)
Usa il modulo esistente #line line file
dal
a) Razor non aggiunge alcun prefisso, b) {
non è presente nel file generato e non può esserci un punto di sequenza posizionato su di esso, pertanto l'intervallo del primo punto di sequenza non mappato è sconosciuto a Razor.
Il carattere iniziale di [Parameter]
nel file generato deve essere allineato al file 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
Usa il modulo #line line file
esistente perché a) Razor non aggiunge alcun prefisso.
b) l'intervallo del primo punto di sequenza non mappato potrebbe non essere noto a Razor (o non deve sapere).
Il carattere iniziale della parola chiave nel file generato deve essere allineato al file 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
C# feature specifications