2016-01-02 13 views
12

Trovo difficile ragionare sulla macro-espansione e mi chiedevo quali fossero le migliori pratiche per testarle.Test unità flash per convenzioni macro e best practice

Quindi, se ho una macro, posso eseguire un livello di espansione macro tramite macroexpand-1.

(defmacro incf-twice (n) 
    `(progn 
    (incf ,n) 
    (incf ,n))) 

ad esempio

(macroexpand-1 '(incf-twice n)) 

viene valutato come

(PROGN (INCF N) (INCF N)) 

Sembra abbastanza semplice da trasformare questo in un test per la macro.

(equalp (macroexpand-1 '(incf-twice n)) 
    '(progn (incf n) (incf n))) 

Esiste una convenzione stabilita per l'organizzazione di test per macro? Inoltre, esiste una libreria per riepilogare le differenze tra le espressioni S?

+3

ho iniziato ad esaminare l'effetto fine del m acro, non l'espansione intermedia. Grande domanda però, non vedo l'ora di ricevere le risposte. –

risposta

6

Generalmente testare macro non è una delle parti forti di Lisp e Common Lisp. Common Lisp (e dialoghi Lisp in generale) utilizza macro procedurali. Le macro possono dipendere dal contesto di runtime, dal contesto di compilazione, dall'implementazione e altro ancora. Possono anche avere effetti collaterali (come la registrazione di cose nell'ambiente di compilazione, la registrazione di cose nell'ambiente di sviluppo e altro).

Così si potrebbe desiderare di prova:

  • che il codice giusto viene generato
  • che il codice generato in realtà fa la cosa giusta
  • che il codice generato funziona davvero nel codice contesti
  • che gli argomenti macro sono effettivamente analizzati correttamente in caso di macro complesse. Think loop, defstruct, ... macro.
  • che la macro rilevi un codice argomento errato. Ancora una volta, pensa a macro come loop e defstruct.
  • gli effetti collaterali

Da questo elenco a dedurre che è meglio per ridurre al minimo tutte queste aree problematiche nello sviluppo di una macro. MA: là fuori ci sono macro davvero molto complesse. Davvero spaventosi. Soprattutto quelli che sono abituati a implementare nuove lingue specifiche del dominio.

L'utilizzo di qualcosa come equalp per confrontare il codice funziona solo per macro relativamente semplici. Le macro spesso introducono simboli nuovi, non integrati e unici. Pertanto, equalp non funzionerà con quelli.

Esempio: (rotatef a b) sembra semplice, ma l'espansione è in realtà complessa:

CL-USER 28 > (pprint (macroexpand-1 '(rotatef a b))) 

(PROGN 
    (LET*() 
    (LET ((#:|Store-Var-1234| A)) 
     (LET*() 
     (LET ((#:|Store-Var-1233| B)) 
      (PROGN 
      (SETQ A #:|Store-Var-1233|) 
      (SETQ B #:|Store-Var-1234|)))))) 
    NIL) 

#:|Store-Var-1233| è un simbolo, che è uninterned e recentemente creato dalla macro.

Un altro semplice modulo macro con un'espansione complessa sarebbe (defstruct s b).

Quindi, per confrontare le espansioni, è necessario un modello di corrispondenza di espressione s. Ce ne sono alcuni disponibili e sarebbero utili qui. È necessario assicurarsi che nei modelli di prova i simboli generati siano identici, laddove necessario.

Esistono anche strumenti di diffusione s-expression. Ad esempio diff-sexp.

3

Sono d'accordo con Rainer Joswig's answer; in generale, questo è un compito molto difficile da risolvere perché i macro possono fare molto. Tuttavia, vorrei sottolineare che in molti casi, il metodo più semplice per testare le macro è fare in modo che le macro facciano il meno possibile. In molti casi, l'implementazione più semplice di una macro è semplicemente lo zucchero sintattico attorno a una funzione più semplice. Ad esempio, c'è un tipico schema di with- & hellip; macro in Common Lisp (ad esempio, con-open-file di), in cui la macro incapsula semplicemente un po 'di codice standard:

(defun make-frob (frob-args) 
    ;; do something and return the resulting frob 
    (list 'frob frob-args)) 

(defun cleanup-frob (frob) 
    (declare (ignore frob)) 
    ;; release the resources associated with the frob 
) 

(defun call-with-frob (frob-args function) 
    (let ((frob (apply 'make-frob frob-args))) 
    (unwind-protect (funcall function frob) 
     (cleanup-frob frob)))) 

(defmacro with-frob ((var &rest frob-args) &body body) 
    `(call-with-frob 
    (list ,@frob-args) 
    (lambda (,var) 
     ,@body))) 

Le prime due funzioni qui, make-frob e pulizia-frob sono relativamente semplici per il test unitario. call-with-frob è un po 'più difficile. L'idea è che si supponga di gestire il codice boilerplate della creazione del frob e assicurare che la chiamata di cleanup avvenga. È un po 'più difficile da controllare, ma se il boilerplate dipende solo da alcune interfacce ben definite, allora probabilmente sarai in grado di creare un finto frob in grado di rilevare se è stato ripulito correttamente. Infine, la macro with-frob è così semplice che è possibile testarla come avevate pensato, ovvero verificarne l'espansione. O potresti dire che è abbastanza semplice che non hai bisogno di testarlo.

D'altra parte, se si sta guardando una macro molto più complessa, come ad esempio ciclo, che è in realtà una sorta di compilatore a sé stante, il gioco è quasi certamente già andando ad avere l'espansione logica in alcune funzioni separate. Ad esempio, si potrebbe avere

(defmacro loop (&body body) 
    (compile-loop body)) 

nel qual caso davvero non hanno bisogno di testare ciclo, è necessario testare compilazione ciclo, e poi sei di nuovo nel regno della vostra unità al solito test.

2

Generalmente testerei solo la funzionalità, non la forma dell'espansione.

Sì, ci sono tutti i tipi di contesti e dintorni che potrebbero influenzare ciò che accade, ma se ci si basa su tali cose, non dovrebbe essere un problema impostarli allo stesso modo per il test.

Alcuni casi comuni:

  • macro vincolanti: prova che le variabili sono vincolati come previsto all'interno e che le variabili esterne ombreggiato sono influenzati
  • svolgitore-proteggere involucri: provocare un'uscita nonlocale dall'interno e verificare che la pulizia sta lavorando
  • definizione/registrazione: test che è possibile definire/registrare ciò che si vuole e utilizzarlo poi