Sfondo
Sto scrivendo un compilatore per una lingua personalizzata per un progetto scolastico e sta andando davvero bene per me.
Se dovessi ricominciare tutto da capo avrei fatto molte architetture software diverse, ma al momento non ho tempo o motivazione per una completa riscrittura, quindi mi concentrerò solo sulla conclusione di questo progetto.
C'è una cosa nel mio codice che voglio refactoring in questo momento ed è il modo in cui organizzo le mie asserzioni.
Sto scrivendo questo compilatore nel C usando flex e bison per generare il mio lexer e il parser. Il codice in questo progetto non è molto pulito e abbastanza disordinato, ma non me ne vergogno (i compilatori sono piuttosto complessi e questo è il mio primo tentativo). Ma ho aggiunto delle asserzioni nel codice per far sì che i bug vengano individuati in tempo e più facili da rintracciare.
Il problema
Il problema che sto avendo con assert
in questo progetto è che c'è semplicemente troppo da controllare.
Ecco un esempio di una funzione che aggiunge i parametri della funzione alla tabella dei simboli durante la compilazione di una definizione di funzione:
static void add_function_params_to_symbols(struct context *con,
const struct ast_op *function_def) {
struct ast *child = function_def->childs[0];
assert(child);
assert(child->type == SYN_DECLARATOR);
assert(child->val_type == AST_OP);
child = child->val.op.childs[1];
assert(child);
assert(child->type == SYN_FUNCTION);
assert(child->val_type == AST_OP);
child = child->val.op.childs[1];
assert(child);
assert(child->type == SYN_PARAMETER_LIST);
assert(child->val_type == AST_OP);
parameter_list_to_symbols(con, &child->val.op);
}
Questa funzione ha tre volte tante asserzioni come istruzioni di codice!
Questo è un esempio estremo perché questa funzione deve scendere di tre livelli fino alla fine dell'albero per trovare il PARAMETER_LIST
che poi passa a parameter_list_to_symbols
che aggiunge i parametri alla tabella dei simboli.
L'albero di analisi per una definizione di funzione nel mio compilatore assomiglia un po 'a questo:
FUNCTION_DEF
DECLARATOR
IDENTIFIER [function_name]
FUNCTION
[return type]
PARAMETER_LIST
[declarators containing type and name for each parameter]
[...]
FUNCTION_BODY
[statements in function body]
[...]
Per me sono quattro le cose da testare per ogni nodo che utilizzo nel programma.
-
assert(node)
per verificare che il nodo non siaNULL
-
assert(node->type == SYN_FOOBAR)
per verificare che il nodo sia del tipo previsto (comeSYN_EXPRESSION
oSYN_VARIABLE
). -
assert(child->val_type == AST_OP)
per verificare che il nodo abbia il giusto tipo di valoreunion
. I nodi possono contenere anche altri valori. Ad esempio nei nodi che memorizzano un valore di stringachild->val_type
sarebbe uguale aAST_STRING
.AST_OP
significa che il nodo è un nodo operatore che è un nodo che ha figli. -
Probabilmente dovrei anche affermare che ogni nodo ha il giusto numero di bambini con
assert(child->val.op.n_childs == XXX)
ma non l'ho fatto in questo esempio di codice perché è già pieno di asserzioni e affermando che anche questo non sembra aggiungi molto valore.
Probabilmente ho scritto centinaia di asserzioni come questa nel mio codice e sembra che questo stia diventando difficile da mantenere, ma non sono sicuro di come dovrei riorganizzarlo.
La domanda
Sto "affermando" cose nel mio codice? Quali cose dovrei affermare e quali cose posso assumere sono OK ?
Come dovrei strutturare le mie asserzioni per renderle più facili da mantenere?
Se hai altri suggerimenti sull'utilizzo di asserzioni e test simili in codice, per favore menzionali nella risposta.