L'applicazione di questa domanda è fondamentalmente un traspolatore che contiene molta logica. Il transpiler è scritto in C ++ (che non dovrebbe essere molto rilevante per questa domanda), e traspone un linguaggio speciale (che abbiamo sviluppato) in una lingua diversa.
Per la gestione degli errori (errori di input non validi e errori interni del transpiler), utilizzo le eccezioni. Per mantenere la logica del traspolatore il più semplice possibile, l'input viene convalidato molto tardi, cioè nella maggior parte dei casi non avviene alcuna convalida separata. Ciò significa che la logica di elaborazione genera eccezioni. Alcune funzioni di utilità sono utilizzate globalmente sul codice base e anche quelle possono generare eccezioni.
Ciò significa che le eccezioni sono potenzialmente gettate in una funzione di utilità, in cui non sappiamo da dove proviene l'input. Questo è un problema, perché - simile al modo in cui il compilatore ti dice in quale riga del file si è verificato l'errore di analisi - vorrei dire all'utente del traspolatore in cui è stato generato l'errore. Per mantenere le cose semplici, questo non deve necessariamente essere uno "stack" o "catena" di contesti. Ma le eccezioni dovrebbero contenere almeno alcune informazioni contestuali , quale parte dell'input non è valida e perché, in modo tale che la parte rilevante dell'input possa essere corretta e / o un IDE possa navigare verso la fonte dell'errore .
Per le eccezioni, attualmente utilizzo una sola classe Error
che trasporta tutte le informazioni come membri e vengono utilizzate funzioni statiche per crearle a seconda di quale errore si è verificato, ad esempio:
throw Error::internalError("This should not have happened...");
o un caso più interessante (pseudo codice, questo non è C ++):
var x = ...;
if (!x.isInteger())
throw Error::notAnInteger(x);
Ora vorrei aumentare quell'eccezione (l'oggetto Error
) con una "specifica di contesto / posizione". Ad esempio, nel mio linguaggio transpiled, ho il concetto di variabili, che hanno un valore predefinito. Consideriamo una variabile che ha un ID interno memorizzato come numero intero e un valore che possiamo recuperare. Ora vogliamo assicurarci che il valore sia un intero (di nuovo, pseudo codice):
var variableID = ...;
var value = getValueOfVariable(variableID);
if (!value.isInteger())
throw Error::notAnInteger(value);
Consideriamo ora il caso in cui variableID = 42, value = "foo"
. Quando l'eccezione viene lanciata e presentata all'utente, sa solo che "foo"
non è un numero intero, ma a lei non viene detto che questo valore proviene dalla variabile 42
.
Ecco perché voglio dire al Error
che stiamo cercando di analizzare la variabile il cui ID è variableID
.
Vorrei presentarvi tre idee che mi sono venute in mente, ma non posso decidere quale sia lo stile migliore.
La prima idea consiste nell'aggiungere ulteriori funzioni statiche, ciascuna per un tipo di tale contesto. Nell'esempio sopra, invece di
throw Error::notAnInteger(value);
Allora faccio
throw Error::variableNotAnInteger(value, variableID);
e all'interno di quella funzione memorizzo le informazioni che la fonte dell'errore era "variabile con ID variableID
". Tuttavia, con un numero sempre maggiore di tipi di contesti (non solo analizza il valore di una variabile che può portare a errori "non interi"), il numero di funzioni aumenta rapidamente.
Una seconda idea era di introdurre una classe Location
, che ha lo scopo di descrivere l'origine / contesto dell'errore e di passarla insieme agli altri parametri a queste funzioni statiche:
throw Error::notAnInteger(value, Location::variable(variableID));
Tuttavia, ciò significa che tutte le funzioni di utilità (ad esempio una funzione che ha solo lo scopo di analizzare un valore, indipendentemente da dove proviene il valore), dovrebbero essere aumentate con l'oggetto contestuale Location
, in ordine a
Costruisci in modo appropriato l'oggetto Error
da gettare. Ciò tuttavia rende il codice base un po 'brutto, in particolare per la logica nidificata e ulteriormente nidificata.
Una terza idea è quella di aggiungere la posizione dopo che l'eccezione è stata lanciata . La funzione di utilità genera un'eccezione e il chiamante recupera l'eccezione, aggiunge la posizione appropriata e quindi genera nuovamente l'eccezione aumentata. Questo ha lo svantaggio che quando mi dimentico da qualche parte nel codice di base per aggiungere una tale posizione, l'errore non ne porta uno. (Al contrario, la seconda idea garantisce che venga passato un oggetto posizione.)
Questo apparirebbe (ancora, pseudo codice):
function parseValue(value, type) {
...
if (type == "integer") {
if (!value.isInteger()) {
// Throw an exception without a location / context information:
throw Error::notAnInteger(value);
}
return value.toInteger();
}
...
}
//-----------------------------------------
var variableID = ...;
var value = getValueOfVariable(variableID);
var type = getTypeOfVariable(variableID);
try {
var parsedValue = parseValue(value, type);
} catch (Error e) {
// Add the location of the error, and rethrow:
e.setLocation(Location::variable(variableID));
throw e;
}
Quale dei metodi precedenti può essere considerato un buon stile? C'è anche una quarta opzione che dovrei prendere in considerazione? Com'è fatto tipicamente in trans / / compilatori?