Progettazione di una gerarchia di classi gestibili per la rappresentazione di AST

0

Ho bisogno di progettare una gerarchia di classi simile a AST per l'introspezione del codice (come l'API Java Element utilizzata durante l'elaborazione delle annotazioni). Ma non sono sicuro di come renderlo manutenibile e facile da usare.

Il mio istinto è quello di modellare il linguaggio il più vicino possibile e sfruttare appieno il sistema dei tipi. Per fare ciò, utilizzerei l'ereditarietà per modellare l'intuitivo "è un" - rapporto tra elementi di sintassi per ottenere controlli di esaurimento in switch -espressioni. Cioè MethodElement s e ConstructorElement s sono ExecutableElement s, TypeAliasElement s, ClassElement s e InterfaceElement s sono TypeElement s e così via. Ciò porta a una gerarchia di classi piuttosto profonda.

Tuttavia, il comportamento condiviso non rispetta questa gerarchia. Ad esempio: PackageElement s, ClassElement s e InterfaceElement s possono racchiudere altri elementi. Ma PackageElement s racchiude sempre solo PackageElement s o TypeElement s e solo ClassElement s racchiude mai ConstructorElement s. Le interfacce Mixin in combinazione con i delegati per l'implementazione possono essere utilizzate per condividere il comportamento tra le classi. Tuttavia, diventa complesso molto rapidamente con gerarchie di classi profonde e molte interfacce di mixaggio. Dovrebbe esserci una singola interfaccia per elementi che sono in grado di racchiudere altri elementi? O un'interfaccia per essere in grado di racchiudere pacchetti, un'altra interfaccia per essere in grado di racchiudere tipi, un'altra per racchiudere funzioni ecc. Dovrebbero esserci proprietà separate per visibilità e modalità o semplicemente una che contenga tutti i modificatori applicabili?

L'approccio opposto (quello scelto dall'API Java Element) è quello di rendere la gerarchia piatta con interfacce meno specifiche: tutte le classi implementano la funzione getEnclosedElements: List<Element> , con quegli elementi che non racchiudono nulla semplicemente restituendo una lista vuota . Allo stesso modo non ci sono tipi diversi per classi e interfacce. Sono raggruppati in TypeElement con una proprietà enum che specifica di quale tipo sia realmente. Ovviamente questo rende tutto molto più semplice per me, ma non è ovvio all'utente dall'interfaccia solo quali classi possono racchiudere quali tipi di altri elementi, quindi gli utenti devono leggere la documentazione API molto più spesso e il loro codice è meno sicuro.

Quale approccio è preferibile qui? Temo che le gerarchie di classi profonde renderanno l'API non rintracciabile, difficile da mantenere (perché sono necessari così tanti mixin per condividere il comportamento tra classi simili) e fastidiosi da usare. Ad esempio: l'utente potrebbe voler scrivere una funzione per stampare tutti i membri di ClassElement o InterfacElement . Non possono utilizzare l'interfaccia EnclosesElements perché un PackageElement racchiude anche gli elementi e inoltre non può utilizzare la classe base TypeElement poiché include TypeAliasElement che non include mai membri. Non esiste un'interfaccia o una classe base che includa solo InterfaceElement e ClassElement .

    
posta Grisu47 22.11.2018 - 22:26
fonte

1 risposta

1

È sull'equilibrio.

La codifica di una maggior parte della lingua del client nel modello di tipo della lingua dell'host importerà il codice host per giocare con le lingue client. Sarà anche una fonte di complessità in quanto il codice host deve essere testato ed esteso compatibilmente con la lingua del client.

Codificare meno informazioni sulla lingua del client nella lingua dell'host aprirà le opportunità per il codice host di abusare della lingua del client. Inoltre, renderà più semplice l'implementazione di più lingue client, comprese eventuali estensioni proprietarie. Consentirà un design più modulare, migliorerà il riutilizzo e ridurrà la duplicazione.

La mia domanda sarebbe come stai usando questo AST? È mirato a:

  • fornendo un DSL all'interno della lingua dell'host - quindi preferisci le informazioni di tipo complesso poiché il codice è scritto nella lingua dell'host. È compilato insieme al codice host e si può presumibilmente essere fissato a questo punto. Ottenere errori di compilazione e fornire la tracciabilità dello stack è l'approccio migliore per il debug e la manutenzione a lungo termine.
  • fornire una funzione di script Host per consentire modifiche del comportamento dopo la compilazione. In questo caso, basti sul sistema di tipi per semplificare l'integrazione di dati e funzioni scritti all'interno della lingua di Host da utilizzare con lo script e, al contrario, fornire tipi che adattano facilmente le funzioni ei dati dei linguaggi del client per l'utilizzo da parte della lingua dell'host. Alcuni tipi comuni e le firme delle funzioni dovrebbero essere sufficienti, ma vedere cosa funziona. Non sarai in grado di evitare completamente il controllo e gli errori di run-time, in particolare se la lingua dell'Host e del Client ha una semantica diversa.
  • fornire un compilatore o un interprete. Preferisci implementazioni più semplici e generiche. In questo caso, la lingua host non si cura di integrarsi con la lingua del client, quindi semplificarla rende sia più veloce sia più flessibile. Queste soluzioni inoltre non tendono a fermarsi con un AST ma ulteriormente ottimizzare, semplificare e compilare il codice Client. Quindi avere un AST complicato ha dei dubbi e costi definitivi. Faresti meglio a fornire servizi come il debug interattivo, JIT e il trasferimento di file.
risposta data 23.11.2018 - 07:57
fonte

Leggi altre domande sui tag