Prova che il codice morto non può essere rilevato dai compilatori


32

Sto programmando di tenere un corso invernale su un numero variabile di argomenti, uno dei quali sarà compilatore. Ora, mi sono imbattuto in questo problema mentre pensavo agli incarichi da dare durante il trimestre, ma mi ha lasciato perplesso, quindi potrei usarlo come esempio.

public class DeadCode {
  public static void main(String[] args) {
     return;
     System.out.println("This line won't print.");
  }
}

Nel programma sopra, è ovvio che l'istruzione print non verrà mai eseguita a causa di return. I compilatori a volte forniscono avvisi o errori sul codice morto. Ad esempio, il codice sopra non verrà compilato in Java. Il compilatore javac, tuttavia, non rileverà tutte le istanze di codice morto in ogni programma. Come potrei dimostrare che nessun compilatore può farlo?


29
Qual è il tuo background e qual è il contesto in cui insegnerai? Ad essere sincero, sono leggermente preoccupato che tu debba chiedere questo, visto che stai per insegnare. Ma buona chiamata chiedendo qui!
Raffaello


9
@ MichaelKjörling Il rilevamento del codice morto è impossibile anche senza queste considerazioni.
David Richerby,

2
BigInteger i = 0; while(isCollatzConjectureTrueFor(i)) i++; printf("Hello world\n");
user253751

2
@immibis La domanda richiede una prova dell'impossibilità di rilevare il codice morto . Hai fornito un esempio in cui il corretto rilevamento del codice morto richiede la risoluzione di un problema aperto in matematica. Ciò non dimostra che il rilevamento del codice morto sia impossibile .
David Richerby,

Risposte:


57

Tutto deriva dall'indecidibilità del problema di arresto. Supponiamo di avere una funzione di codice morto "perfetta", alcune Turing Machine M e alcune stringhe di input x e una procedura simile a questa:

Run M on input x;
print "Finished running input";

Se M funziona per sempre, eliminiamo l'istruzione print, poiché non la raggiungeremo mai. Se M non funziona per sempre, allora dobbiamo conservare la dichiarazione di stampa. Quindi, se abbiamo un dispositivo di rimozione del codice morto, ci consente anche di risolvere il problema di Halting, quindi sappiamo che non può esserci un dispositivo di rimozione del codice morto.

Il modo per aggirare questo è attraverso "l'approssimazione conservatrice". Quindi, nel mio esempio di Turing Machine sopra, possiamo supporre che l'esecuzione di M su x potrebbe finire, quindi la riproduciamo in modo sicuro e non rimuoviamo l'istruzione print. Nel tuo esempio, sappiamo che, indipendentemente dalle funzioni che fanno o non si fermano, che non riusciremo mai a raggiungere quell'istruzione stampata.

Di solito, questo viene fatto costruendo un "grafico del flusso di controllo". Facciamo ipotesi di semplificazione, come "la fine di un ciclo while è collegata all'inizio e l'istruzione dopo", anche se funziona per sempre o funziona solo una volta e non visita entrambi. Allo stesso modo, supponiamo che un'istruzione if possa raggiungere tutti i suoi rami, anche se in realtà alcuni non vengono mai utilizzati. Questo tipo di semplificazioni ci consente di rimuovere "ovviamente codice morto" come nell'esempio che dai, pur rimanendo decidibile.

Per chiarire alcune confusioni dai commenti:

  1. Nitpick: per M fisso, questo è sempre decidibile. M deve essere l'input

    Come dice Raphael, nel mio esempio, consideriamo la Turing Machine come un input. L'idea è che, se avessimo un algoritmo DCE perfetto, saremmo in grado di costruire lo snippet di codice che do per qualsiasi macchina di Turing e avere un DCE risolverebbe il problema di arresto.

  2. non convinto. restituire come una dichiarazione contundente in un'esecuzione diretta senza ramo non è difficile da decidere. (e il mio compilatore mi dice che è in grado di capirlo)

    Per il problema che njzk2 solleva: hai assolutamente ragione, in questo caso puoi determinare che non è possibile ottenere una dichiarazione dopo il ritorno. Questo perché è abbastanza semplice da poter descrivere la sua irraggiungibilità usando i vincoli del grafico del flusso di controllo (ovvero non ci sono bordi in uscita da un'istruzione return). Ma non esiste un perfetto eliminatore di codice morto, che elimina tutto il codice inutilizzato.

  3. Non prendo una prova dipendente dall'input per una prova. Se esiste un tale tipo di input dell'utente che può consentire al codice di essere finito, è corretto per il compilatore supporre che il seguente ramo non sia morto. Non riesco a vedere a cosa servano tutti questi voti, è sia ovvio (es. Infinito stdin) che sbagliato.

    Per TomášZato: in realtà non è una prova dipendente dall'input. Piuttosto, interpretalo come un "forall". Funziona come segue: supponiamo di avere un algoritmo DCE perfetto. Se mi dai una Turing Machine M arbitraria e inserisci x, posso usare il mio algoritmo DCE per determinare se M si ferma, costruendo lo snippet di codice sopra e vedendo se l'istruzione print viene rimossa. Questa tecnica, di lasciare un parametro arbitrario per dimostrare un'istruzione forall, è comune in matematica e logica.

    Non capisco fino in fondo il punto di TomášZato sul fatto che il codice sia finito. Sicuramente il codice è finito, ma un algoritmo DCE perfetto deve essere applicato a tutto il codice, che è un set infinte. Allo stesso modo, mentre il codice stesso è finito, i potenziali insiemi di input sono infinte, così come il potenziale tempo di esecuzione del codice.

    Per quanto riguarda considerare il ramo finale non morto: è sicuro in termini di "approssimazione conservativa" di cui parlo, ma non è sufficiente rilevare tutte le istanze di codice morto come richiesto dall'OP.

Prendi in considerazione un codice come questo:

while (true)
  print "Hello"
print "goodbye"

Chiaramente possiamo rimuovere print "goodbye"senza cambiare il comportamento del programma. Quindi, è un codice morto. Ma se c'è una chiamata di funzione diversa invece che (true)nella whilecondizione, allora non sappiamo se possiamo rimuoverla o meno, portando all'indecidibilità.

Nota che non me ne occuperò da solo. È un risultato ben noto nella teoria dei compilatori. È discusso in The Tiger Book . (Potresti riuscire a vedere di cosa parlano nei libri di Google .


1
@ njzk2: stiamo cercando di dimostrare che è impossibile creare un eliminatore di codice morto che elimini tutto il codice morto, non che sia impossibile costruire un eliminatore di codice morto che elimini un codice morto. L'esempio print-after-return può essere eliminato facilmente usando le tecniche del grafico del flusso di controllo, ma non tutti i codici morti possono essere eliminati in questo modo.
user2357112 supporta Monica l'

4
Questa risposta fa riferimento a commenti. Mentre leggo la risposta, devo saltare nei commenti, quindi tornare alla risposta. Questo è confuso (doppiamente se si considera che i commenti sono fragili e potrebbero andare persi). Una risposta autonoma sarebbe molto più facile da leggere.
TRiG

1
@ TomášZato - considera il programma che incrementa una variabile e controlla se è un numero dispari perfetto, terminando solo quando trova un tale numero. Chiaramente questo programma non dipende da alcun input esterno. Stai affermando che si può facilmente determinare se questo programma termina o no? nnn
Gregory J. Puleo,

3
@ TomášZato Ti sbagli nella comprensione del problema di arresto. Data una macchina di Turing finita e un input finito , è impossibile determinare se muove all'infinito mentre si esegue su . Non l'ho provato rigorosamente perché è stato dimostrato più volte ed è un principio fondamentale dell'informatica. C'è un bel disegno della prova su Wikipediax M xMxMx
jmite,

1
jmite, ti preghiamo di inserire commenti validi nella risposta in modo che la risposta sia autonoma. Quindi contrassegna tutti i commenti che sono obsoleti in quanto tali in modo da poter ripulire. Grazie!
Raffaello

14

Questa è una svolta alla risposta di jmite che elude la potenziale confusione sulla non terminazione. Darò un programma che si ferma sempre, può avere un codice morto ma non possiamo (sempre) algoritmicamente decidere se lo ha.

Considera la seguente classe di input per l'identificatore del codice morto:

simulateMx(n) {
  simulate TM M on input x for n steps
  if M did halt
    return 0
  else
    return 1
}

Poiché Me xsono corretti, simulateMsha un codice morto con return 0if e solo se Mnon si ferma x.

Questo ci dà immediatamente una riduzione dal problema di arresto al controllo del codice morto: dato TM come istanza del problema di arresto, crea un programma sopra con il codice di - ha un codice morto se e solo se non si ferma da solo codice.M MMxMM

Quindi, il controllo del codice morto non è calcolabile.

Nel caso in cui non si abbia familiarità con la riduzione come tecnica di prova in questo contesto, consiglio il nostro materiale di riferimento .


5

Un modo semplice per dimostrare questo tipo di proprietà senza impantanarsi nei dettagli è utilizzare il seguente lemma:

Lemma: per qualsiasi compilatore C per un linguaggio completo di Turing, esiste una funzione undecidable_but_true()che non accetta argomenti e restituisce il vero booleano, in modo che C non possa prevedere se undecidable_but_true()restituisce vero o falso.

Si noti che la funzione dipende dal compilatore. Data una funzione undecidable_but_true1(), un compilatore può sempre essere aumentato con la consapevolezza se questa funzione restituisce vero o falso; ma c'è sempre qualche altra funzione undecidable_but_true2()che non sarà coperta.

Prova: secondo il teorema di Rice , la proprietà "questa funzione ritorna vera" è indecidibile. Pertanto, qualsiasi algoritmo di analisi statica non è in grado di decidere questa proprietà per tutte le possibili funzioni.

Corollario: dato un compilatore C, il seguente programma contiene un codice morto che non può essere rilevato:

if (!undecidable_but_true()) {
    do_stuff();
}

Una nota su Java: il linguaggio Java impone che i compilatori rifiutino determinati programmi che contengono codice non raggiungibile, mentre richiede in modo ragionevole che il codice sia fornito in tutti i punti raggiungibili (ad es. Il flusso di controllo in una funzione non nulla deve terminare con returnun'istruzione). La lingua specifica esattamente come viene eseguita l'analisi del codice non raggiungibile; in caso contrario, sarebbe impossibile scrivere programmi portatili. Dato un programma del modulo

some_method () {
    <code whose continuation is unreachable>
    // is throw InternalError() needed here?
}

è necessario specificare in quali casi il codice non raggiungibile deve essere seguito da qualche altro codice e in quali casi non deve essere seguito da alcun codice. Un esempio di un programma Java che contiene codice che è irraggiungibile, ma non in un modo che i compilatori Java possono notare, viene fornito in Java 101:

String day_of_week(int n) {
    switch (n % 7) {
    case 0: return "Sunday";
    case 1: case -6: return "Monday";
    …
    case 6: case -1: return "Saturday";
    }
    // return or throw is required here, even though this point is unreachable
}

Si noti che alcuni compilatori per alcune lingue potrebbero essere in grado di rilevare che la fine di day_of_weeknon è raggiungibile.
user253751

@immibis Sì, per esempio gli studenti CS101 possono farlo nella mia esperienza (anche se è vero che gli studenti CS101 non sono un buon analizzatore statico, di solito dimenticano i casi negativi). Questo è parte del mio punto: è un esempio di un programma con codice irraggiungibile che un compilatore Java non rileverà (almeno, può avvisare, ma potrebbe non rifiutare).
Gilles 'SO- smetti di essere malvagio' il

1
Temo che il fraseggio del Lemma sia fuorviante nella migliore delle ipotesi, con una sfumatura di errore. L'indecidibilità ha senso solo se la si definisce in termini di (infiniti) insiemi di istanze. (Il compilatore fa produrre una risposta per ogni funzione, e sappiamo che non può essere sempre corretto, ma dire che c'è una singola istanza indecidibile è spento.) Il suo punto tra il Lemma e la prova (che non corrispondono del tutto il Lemma come detto) cerca di risolvere questo problema, ma penso che sarebbe meglio formulare un lemma chiaramente corretto.
Raffaello

@Raphael Uh? No, il compilatore non deve produrre una risposta alla domanda "questa funzione è costante?". Non è necessario distinguere "Non lo so" da "no" per produrre codice funzionante, ma non è rilevante qui poiché siamo interessati solo alla parte di analisi statica del compilatore, non alla parte di traduzione del codice. Non capisco cosa trovi fuorviante o errato sull'affermazione del lemma - a meno che il tuo punto sia che dovrei scrivere "analizzatore statico" anziché "compilatore"?
Gilles 'SO- smetti di essere malvagio' il

L'affermazione suona come "indecidibilità significa che esiste un'istanza che non può essere risolta", che è errata. (So ​​che non intendi dirlo, ma è così che può leggere agli incauti / novizi, imho.)
Raffaello

3

La risposta di jmite si applica se il programma uscirà mai da un calcolo - solo perché è infinito non chiamerei il codice dopo che è morto.

Tuttavia, esiste un altro approccio: un problema per il quale esiste una risposta ma non è noto:

public void Demo()
{
  if (Chess.Evaluate(new Chessboard(), int.MaxValue) != 0)
    MessageBox.Show("Chess is unfair!");
  else
    MessageBox.Show("Chess is fair!");
}

public class chess
{
  public Int64 Evaluate(Chessboard Board, int SearchDepth)
  {
  ...
  }
}

Questa routine senza dubbio non contiene codice morto - la funzione restituisce una risposta che esegue un percorso ma non l'altro. Buona fortuna a trovarlo, però! La mia memoria non è un computer teorico in grado di risolverlo durante la vita dell'universo.

Più in dettaglio:

La Evaluate()funzione calcola da quale parte vince una partita a scacchi se entrambe le parti giocano perfettamente (con la massima profondità di ricerca).

I valutatori di scacchi normalmente guardano avanti ad ogni possibile mossa di una certa profondità specificata e quindi tentano di segnare la scacchiera in quel punto (a volte espandere certi rami più lontano mentre guardare a metà di uno scambio o simili può produrre una percezione molto distorta.) Dal momento che la profondità massima effettiva sono 17695 mezze mosse la ricerca è esaustiva, attraverserà ogni possibile partita a scacchi. Dato che tutti i giochi finiscono, non c'è alcun problema a provare a decidere quanto sia buona una posizione in ogni board (e quindi non c'è motivo di guardare alla logica di valutazione del board - non verrà mai chiamata), il risultato è una vittoria, una perdita o un pareggio. Se il risultato è un pareggio il gioco è giusto, se il risultato non è un pareggio è un gioco ingiusto. Per espanderlo un po 'otteniamo:

public Int64 Evaluate(Chessboard Board, int SearchDepth)
{
  foreach (ChessMove Move in Board.GetPossibleMoves())
    {
      Chessboard NewBoard = Board.MakeMove(Move);
      if (NewBoard.Checkmate()) return int.MaxValue;
      if (NewBoard.Draw()) return 0;
      if (SearchDepth == 0) return NewBoard.Score();
      return -Evaluate(NewBoard, SearchDepth - 1);
    }
}

Si noti inoltre che sarà praticamente impossibile per il compilatore rendersi conto che Chessboard.Score () è un codice morto. Una conoscenza delle regole degli scacchi consente a noi umani di capirlo, ma per capirlo devi sapere che MakeMove non potrà mai aumentare il conteggio dei pezzi e che Chessboard.Draw () tornerà vero se il conteggio dei pezzi rimane statico per troppo tempo .

Si noti che la profondità di ricerca è in mezze mosse, non in mosse intere. Questo è normale per questo tipo di routine AI in quanto è una routine O (x ^ n) - l'aggiunta di un altro strato di ricerca ha un effetto importante sul tempo necessario per l'esecuzione.


8
Supponi che un algoritmo di controllo debba eseguire il calcolo. Un errore comune! No, non puoi assumere nulla sul funzionamento di una pedina, altrimenti non puoi confutare la sua esistenza.
Raffaello

6
La domanda richiede una prova che è impossibile rilevare il codice morto. Il tuo post contiene un esempio di un caso in cui sospetti che sarebbe difficile rilevare un codice morto. Questa non è una risposta alla domanda in corso.
David Richerby,

2
@LorenPechtel Non lo so, ma non è una prova. Vedi anche qui ; un esempio più chiaro del tuo malinteso.
Raffaello

3
Se aiuta, considera che teoricamente non c'è niente che impedisce a qualcuno di eseguire il proprio compilatore per più della vita dell'universo; l'unica limitazione è la praticità. Un problema decidibile è un problema decidibile, anche se appartiene alla classe di complessità NONELEMENTARY.
Pseudonimo del

4
In altre parole, questa risposta è nella migliore delle ipotesi euristica intesa a dimostrare perché probabilmente non è facile costruire un compilatore che rilevi tutto il codice morto, ma non è una prova di impossibilità. Questo tipo di esempio potrebbe essere utile come modo per costruire l'intuizione per gli studenti, ma non è una prova. Presentandosi come una prova, fa un disservizio. La risposta dovrebbe essere modificata per affermare che si tratta di un esempio di costruzione dell'intuizione, ma non di una prova di impossibilità.
DW

-3

Penso che in un corso di informatica, la nozione di codice morto sia interessante nel contesto della comprensione della differenza tra tempo di compilazione e tempo di esecuzione!

Un compilatore può determinare quando si dispone di codice che non può mai essere attraversato in nessuno scenario di compilazione, ma non può farlo per il runtime. un semplice ciclo continuo con input dell'utente per il test di interruzione del ciclo lo dimostra.

Se un compilatore potrebbe effettivamente determinare il codice morto di runtime (ovvero discernere Turing completo), c'è un argomento secondo cui il codice non deve mai essere eseguito, perché il lavoro è già fatto!

Se non altro, l'esistenza di codice che supera i controlli del codice morto in fase di compilazione illustra la necessità di un controllo pragmatico dei limiti sugli input e dell'igiene generale della codifica (nel mondo reale dei progetti reali).


1
La domanda richiede una prova che è impossibile rilevare il codice morto. Non hai risposto a questa domanda.
David Richerby,

Inoltre, la tua affermazione che "un compilatore può determinare quando hai codice che non può mai essere attraversato in uno scenario di compilazione" è errata e contraddice direttamente ciò che la domanda ti chiede di provare.
David Richerby,

@ David Richerby, penso che potresti avermi letto male. Non sto suggerendo che il controllo in fase di compilazione possa trovare TUTTO il codice morto, assolutamente no. Sto suggerendo che esiste un sottoinsieme dell'insieme di tutto il codice morto che è riconoscibile in fase di compilazione. Se scrivo: if (true == false) {print ("qualcosa");}, quell'istruzione print sarà riconoscibile al momento della compilazione per essere codice morto. Non sei d'accordo sul fatto che questo è un controesempio alla tua affermazione?
2

Certo, puoi determinare un codice morto. Ma se hai intenzione di dire "determina quando [hai un codice morto]" senza qualifiche, allora per me significa trovare tutto il codice morto, non solo parte di esso.
David Richerby,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.