di  -  mercoledì 30 marzo 2011

Introduzione

In questo articolo analizzeremo i metodi statici che sono presenti nella classe pdp8. Prima però vorrei parlare di qualcosa di più generale: se avete seguito i commenti del precedente post, potete aver notato come alcune soluzioni, se pur funzionanti, possano essere più facili o difficili, oppure meno performanti di altre.

Quando iniziate un progetto, dovete tener bene in mente dei punti fondamentali :

  • Fare un’attenta analisi del problema da risolvere (problem setting)
  • Cercare di modellizzare il problema (visualizzandolo logicamente, magari con strutture a blocchi. Questa parte dipende molto dalla natura del problema)
  • Focalizzare i reali obbiettivi del problema (eliminando eventuali dettagli inutili)

Pensate poi che le specifiche di un progetto, qualsiasi esso sia, presenteranno delle problematiche scomponibili in sottoproblemi da risolvere sempre tenendo a mente gli stessi punti.

Fatto questo potete avere delle varie opzioni di risoluzione (problem solving) delle quali stavamo parlando nei commenti del precedente articolo. Se iniziata una soluzione si nota che la cosa è troppo complicata e prolissa, sarà opportuno ricominciare da capo (se necessario, altrimenti, se si procede per step, basta tornare a quello immediatamente precedente).

Ottimizzare il lavoro per rendere il progetto maneggevole è tutto un altro paio di maniche e dipende molto dalle scelte iniziali fatte. Per esempio in questo caso, riconvertire il core per l’utilizzo degli interi comporterebbe una riscrittura quasi completa del codice, ma in termini di prestazioni e di leggibilità ne guadagnerebbe molto.

Anche la soluzione che utilizzare una classe con comportamenti prestabiliti inerenti alle esigenze del progetto non può essere adottata se non riscrivendo gran parte del codice. Queste soluzioni, purtroppo, sono venute fuori dopo la stesura di una versione “completa” del programma e per questo difficili da adattare.

Per non incorrere in queste problematiche, come già detto in precedenza, occorre FOCALIZZARE gli obbiettivi. Nel mio caso, quando ho iniziato a scrivere questo programma, erano due:

  • Portabilità : programma multipiattaforma per gli OS più diffusi al momento
  • Emulazione corretta del funzionamento del calcolatore didattico pdp8

Raggiunti questi obbiettivi, non mi sono preoccupato moltissimo della forma e delle ottimizzazioni, ma bensì delle cose necessarie all’uso didattico. Ora che le ultime modifiche al programma sono state apportate, non ci dovrebbero più essere cambiamenti per quanto riguarda il funzionamento, se non per migliorare l’esecuzione (velocità) e correggere qualche bug.

Le questioni appena sollevate sono di particolare importanza per qualsiasi programma, videogioco o problema che dovete risolvere ed implementare. Come mio primo progetto, sono molto contento di condividere con voi queste idee perché credo siano basilari per la formazione di un qualsiasi programmatore.

Ora torniamo al pdp8 ed ai suoi metodi statici :

Codice

    @staticmethod
    def purge(lista):
        """
        Rimuove dalla lista caratteri indesiderati
        """
        vuoti = lista.count('')
        spazi = lista.count(' ')
        tab = lista.count('\t')
        newline = lista.count('\n')

        for x in range(0,vuoti):
            lista.remove('')
        for x in range(0,spazi):
            lista.remove(' ')
        for x in range(0,tab):
            lista.remove('\t')
        for x in range(0,newline):
            lista.remove('\n')
        for x in range(0,len(lista)):
            lista[x] = lista[x].lstrip()

    @staticmethod
    def purgestr(stringa):
        """
        Rimuove dalla stringa caratteri indesiderati
        """
        stringa = stringa.strip('\t')
        stringa = stringa.strip(' ')
        stringa = stringa.strip('')
        stringa = stringa.strip('\n')
        stringa = stringa.strip('\r')
        return stringa

    @staticmethod
    def range(i):
        """
        Converte i nell'intervallo di rappresentabilit? degli interi.
        """
        temp = i%65536
        if i>32767:
            return (temp-65536)
        else:
            return temp

    @staticmethod
    def elws(opcode):
        """
        Elimina gli spazi bianchi alla fine dei comandi e
        ritorna la stringa corretta
        """
        temp = ''
        whites = True
        for x in range(0,len(opcode)):
            if whites is True and opcode[-1-x]==' ':
                pass
            else:
                temp += opcode[-1-x]
                whites = False
        return temp[::-1]

    @staticmethod
    def binario(x):
        """
        Coverte un numero intero in una stringa binaria e
        ritorna una stringa binaria senza '0b' in testa
        """
        if x<0:
            temp = bin((2**16)+x)
        else:
            temp = bin(x)
        return temp[2:]

    @staticmethod
    def extendpositive(x,fine = 16):
        """
        Estende il campo dei numeri positivi se rappresentati con meno di 16
        cifre
        """
        if len(x)            num = fine-len(x)
            temp = ''
            for y in range(0,num):
                temp+= '0'
            x = temp+x
        return x

    @staticmethod
    def extendlabel(x):
        """
        Estende il campo dei degli indirizzi di memoria dei label
        """
        if len(x)<12:
            num = 12-len(x)
            temp = ''
            for y in range(0,num):
                temp+= '0'
            x = temp+x
        return x

    @staticmethod
    def esadecimale(x):
        """
        Converte un numero intero in una stringa esadecimale e
        ritorna una stringa esadecimale senza '0x' in testa
        """
        temp = hex(x)
        return temp[2:]

    @staticmethod
    def hex2dec(x):
        """
        Coverte un numero esadecimale in un numero intero
        """
        return int(x,16)

    @staticmethod
    def bin2dec(x):
        """
        Converte un numero binario in un numero intero
        """
        return int(x,2)

    @staticmethod
    def strand(a,b):
        """
        Ritorna l'and tra i caratteri binari a e b
        """
        if a == '1' and b == '1':
            return ('1')
        else :
            return ('0')

    @staticmethod
    def dec2char(x):
        """
        Ritorna il carattere ascii del numero decimale passato
        """
        return chr(x)

    @staticmethod
    def char2dec(x):
        """
        Ritorna il numero decimale del carattere ascii passato
        """
        return ord(x)

Analisi

Queste funzioni non hanno niente di speciale da spiegare, se non il motivo per il quale sono state create:

  • La prima funzione ci consente di eliminare caratteri superflui da una lista. In particolare viene utilizzata dopo una split per cancellare quello che non serve, come spaziature iniziali, caratteri di fine carrello ecc… Da notare che si opera direttamente sulla lista, quindi non si restituisce nulla.
  • La seconda funzione effettua le stesse rimozioni della prima ma su una stringa generica. Infatti restituisce la stringa stessa una volta ripulita. Queste due funzioni sono state molto utili per quanto riguarda la lettura (o parsing) del file assembly che viene caricato. Magari sono un pò eccessive come soluzioni, quindi sarà un piacere leggere qualche valida alternativa, se qualcuno ne ha.
  • Tra le funzioni di pulizia delle stringhe rientra anche elws, che elimina esclusivamente gli spazi bianchi dalla fine della stringa e la ritorna.
  • La funzione range converte l’intero passato in complemento a 2 su 16 bit. Quindi l’intervallo di rappresentabilità va da -32768 a 32767.
  • Binario ed esadecimale ritornano una stringa in base due o sedici senza 0b o 0x in testa, che non sono necessari nella visualizzazione dei dati (si elimina anche l’eventuale segno negativo). In binario si distinguono anche valori negativi, mentre per esadecimale non è necessario in quanto viene utilizzato solo per gli indirizzi, che sono solo positivi.
  • Extendpositive e extendlabel hanno lo stesso compito, estendere la stringa su 16 e 12 cifre rispettivamente. Come potete notare, extendlabel è praticamente inutile, visto che, quando si estendono i positivi, si può indicare il numero di bit (o cifre) che si dovranno avere in extendpositive, quindi si può anche passare 12. Nelle prossime versioni eliminerò questo metodo che era stato utilizzato solo ad inizio progetto perché, nel parsing del file assembly, i labels hanno la priorità rispetto alla conversione del codice, in quanto rappresentano veri e propri indirizzi di memeria e quindi sono stati affrontati per primi.
  • strand è la funzione che esercita l’AND tra due caratteri, necessaria perché fa parte delle istruzioni della ALU. ADD invece ha bisogno solamente che le stringhe vengano riconvertite in interi per operare. Questi meccanismi sono necessari per la scelta iniziale da me fatta, ovvero di utilizzare le stringhe. Come potete vedere però, non ci sono altre istruzioni da implementare perché il processore è molto essenziale sotto questo punto di vista.

Conclusioni

Con questo abbiamo visualizzato i metodi che contornano il pdp8. Essi sono indispensabili per il funzionamento della macchina che poggia le sue basi sulle stringhe.

Tornando al discorso delle possibili implementazioni, potete pensare che, se si utilizzavano solo gli interi, queste funzioni erano comunque necessarie per la rappresentazione della macchina (nell’interfaccia utente). Magari se si organizzavano degli oggetti per rappresentare i registri, questi metodi potevano fare parte dei registri stessi per essere utilizzati quando necessario (si poteva creare anche un “dato generale” con questi metodi).

Nei prossimi articoli continuerò ad illustrarvi il codice fino ad arrivare all’interfaccia grafica. Spero che, come nei primi post, i vostri commenti portino nuove e brillanti idee per migliorare questo programma.

PS: Ho aggiunto la possibilità di inserire dei break all’interno del codice ed il controllo sintattico del file assembly. Inoltre la macchina ora non esegue più secondo i cicli (fetch …), ma l’esecuzione è scandita dal clock (a quattro tempi in questo caso) e quindi si simula l’esecuzione di ogni singola micro-istruzione. Quest’ultima modifica, che è la più corposa, ha esclusivamenti fini didattici e non sarebbe necessaria per un semplice simulatore/emulatore; però non badate troppo alle istruzioni di input ed output perché sono state simulate correttamente solo nel funzionamento e non nelle micro-istruzioni.

22 Commenti »

I commenti inseriti dai lettori di AppuntiDigitali non sono oggetto di moderazione preventiva, ma solo di eventuale filtro antispam. Qualora si ravvisi un contenuto non consono (offensivo o diffamatorio) si prega di contattare l'amministrazione di Appunti Digitali all'indirizzo info@appuntidigitali.it, specificando quale sia il commento in oggetto.

  • # 1
    Antonio Barba (TheKaneB)
     scrive: 

    PS: Ho aggiunto la possibilità di inserire dei break all’interno del codice ed il controllo sintattico del file assembly.

    Fantastico!

    Il miglior utilizzo che si possa fare di un emulatore è proprio quello del debugging veloce del codice, che sulla macchina originale sarebbe molto difficile (se non quasi impossibile) da fare :-)

  • # 2
    Mirco Tracolli
     scrive: 

    @ Antonio

    Naturalmente è una cosa molto semplice : si può mettere o togliere un break da una linea di codice, che poi corrisponde a una cella di memoria :D. L’esecuzione si “arresta” per poi riprendere al segnale dell’utente (che deve premere continua).

    Con l’iserimento delle micro-istruzioni, sicuramente questo aspetto è più curato, in quanto si può procedere passo passo nell’esecuzione :P.

  • # 3
    simock85
     scrive: 

    Ciao.
    Non ho ancora trovato il tempo per guardare a fondo l’intero sorgente di questo progetto, ma mi permetto un piccolo appunto sul codice.
    Non mi piace molto l’uso degli staticmetods, da quando scrivo in py li ho abbandonati. Credo che esistano solamente per i programmatori che approdano a py da altri linguaggi; nulla vieta di scrivere le funzioni fuori dalla classe, visto che non dipendono in nessun modo da questa e non portano alcun vantaggio in classi ereditate.
    A mio avviso invece, sono utili i classmetods ma questo è un altro discorso.

    Simo

  • # 4
    David
     scrive: 

    Perdonami, non ho intento polemico, ma questo non è python ma Java scritto in python: la sfilza di @statimethod (che in python non si usano così) ne è un chiaro segno.
    Mi pare inoltre che ci sia un errore in purge(): la funzione non ritorna alcun valore.

    In python si scriverebbe

    def purge(lista):
    “””
    Rimuove dalla lista caratteri indesiderati
    “””
    for car in (”, ‘ ‘, ‘\t’, ‘\n’):
    while 1:
    try:
    lista.remove(car)
    except:
    break
    return [item.strip() for item in lista]

  • # 5
    Cesare Di Mauro
     scrive: 

    Premesso che per delle indicazioni migliori sarebbe meglio vedere anche in che modo vengono usate le funzioni, provo a darne qualcuna.

    purge & purgestr: se hai una stringa e vuoi eliminare tutti i “whitespace”, ti basta eseguire il metodo split senza parametri.
    Quindi nel primo caso è sufficiente eseguire .split() per trovarti la lista di parole. Nel secondo ti basta una ”.join(s.split()) per ottenere la stringa ripulita.

    elws: basta .rstrip().

    binario: se t’interessa l’efficienza, è meglio non usare l’operatore **, quindi al posto di 2 ** 16 userei 1 << 16; ma visto che è una costante nota, piazzerei direttamente 65536. extendpositive & extendlabel: basta .zfill(16) e .zfill(12). hex2dec & bin2dec: personalmente li eliminerei, perché int(x, 16) e int(x, 2) sono sufficientemente chiari e compatti. dec2char & char2dec: idem come sopra; trovo sia meglio utilizzare le funzioni standard di Python. In generale trovo più compatto e leggibile l’operatore ternario di Python (che è anche un po’ più efficiente). Ad esempio, la funzione binario la riscriverei così: [python]def binario(x): """ Coverte un numero intero in una stringa binaria e ritorna una stringa binaria senza ‘0b’ in testa """ Temp = bin((x + 65536) if x < 0 else x) return Temp[2 : ][/python] Comunque qui si va sempre per una questione di gusti: c’è a chi piace il classico if else, c’è chi l’operatore ternario. ;) Riguardo ai break all’interno del codice devo capire bene cosa intendi, e vedere l’implementazione. A naso mi viene la parola “generatori”, ma potrebbe non c’entrare un tubo. :D Per il resto ottimo articolo. Aspetto con ansia gli altri. :P

  • # 6
    Cesare Di Mauro
     scrive: 

    @David: purge non deve tornare niente, perché opera “inplace”, cioè lavorando direttamente sulla stringa.

    Riguardo agli @staticmethod, saranno sicuramente usati all’interno di una classe, e magari Mirco ha voluto soltanto estrapolarli.

  • # 7
    Mirco Tracolli
     scrive: 

    @ Cesare

    Per i break memorizzo soltanto la riga di codice dove si deve fermare l’esecuzione (che in poche parole corrisponde ad una cella di memoria). Il tutto utilizzando un dizionario per controllare se in una particolare cella si deve fermare l’esecuzione. Non è niente di speciale :D.

    Per quanto riguarda le risposte sugli staticmethod è come se mi avessi letto nella mente, lo stavo per scrivere poco prima che rispondessi…

    Non conoscevo l’esistenza di .rstrip() e zfill(), provvederò alla sostituzione, così anche per la costante, vi prego di perdonarmi. Naturalmente adotterò anche le funzioni standard, tanto questa operazione di pulizia prima o poi la dovevo fare :D.

    Come hai già detto, l’operatore ternario è più una questione di gusti, purtroppo sono abituato a scrivere così.

    Ho solo una perplessità: ho avuto dei problemi con la split quando divido le parole; a prescindere se il testo sia in unicode o in ascii, ho dei caratteri superflui dopo una split() semplice tra i quali ”, ‘ ‘, ‘\r’. Quelle due funzioni sono state create proprio per questo motivo, magari quando analizzeremo il loader (che trasforma il codice assembly in codice macchina), il problema sarà più chiaro e se ne verrà a monte.

    Purtroppo, anche rileggendo il codice, ho solo il mio punto di vista a far da padrone, quindi ringrazio tutti per gli interventi e mi scuso se sono stato poco chiaro e preciso.

  • # 8
    simock85
     scrive: 

    Si gli staticsmetods sono all’interno di una classe, ma continuano a non piacermi. Degli oggetti che non hanno riferimenti alla classe o a una sua istanza, secondo me, non devono stare nella classe (in python). Ma è una opinione personale.

    Per quanto riguarda l’operatore ternario, l’ho imparato leggendo un bel libro (di cui non ricordo il nome, dovrei andare a spulciare la libreria), come list comprehension. Davvero comodi, anche se occorre un po discostarsi dall’approccio alla programmazione standard :D

  • # 9
    Cesare Di Mauro
     scrive: 

    Se il codice è generico, si può mettere fuori dalla classe racchiudendolo in una funzione.

    Altrimenti potrebbe essere comodo lasciarlo nella classe, che possiamo considerare come un “namespace” che racchiude tutto quello che è di suo dominio.

    @Mirco. E’ strano, perché ho fatto diverse delle prove e lo split() elimina proprio tutto il whitespace. Comunque lo vedremo meglio quando ci sarà il codice che lo richiede. ;)

  • # 10
    Mirco Tracolli
     scrive: 

    @ simock85

    Comunque sia, nessuno vieta di metterle fuori dalla classe, ma visto che python possiede questa proprietà, perché non usarla a proprio piacimento?

    @ Cesare

    Sicuramente con le modifiche che farò ora (zfill e rstrip), qualcosa cambierà e magari trovo il “problema”. Dopo tutto, anche se il programma funziona correttamente, non è detto che non ci sia un modo migliore e più veloce di fare le cose… :D

  • # 11
    simock85
     scrive: 

    @Mirco
    Non sto assolutamente dicendo che non sono da utilizzare. Solo che non mi piacciono :D
    Ho visto della robaccia scritta in py (chi sono io per giudicare?), il tuo codice invece è leggibilissimo, anche per chi come me non conosce l’architettura del PDP-8.

  • # 12
    Mirco Tracolli
     scrive: 

    @ simock85

    Scusami, ho frainteso :D. Sono contento che i codici che scrivo sono di facile lettura (sono abbastanza fissato con questo, si vede? :P) e spero sempre di migliorare (anche per quanto riguarda le parti più tecniche). Sarà una strada lunga, ma con i vostri consigli sarà tutti più divertente!

  • # 13
    David
     scrive: 

    @Cesare

    @David: purge non deve tornare niente, perché opera “inplace”, cioè lavorando direttamente sulla stringa.

    Si, l’avevo capito, ma in python operare inplace su una variabile globale senza mettere il “global” sotto l’intestazione delle funzione è una cattiva pratica.

    “Explicit is better than implicit”

  • # 14
    Cesare Di Mauro
     scrive: 

    Veramente non mi risulta proprio questa pratica. Ti passo un po’ di riferimenti:
    http://docs.python.org/tutorial/controlflow.html#defining-functions
    http://docs.python.org/reference/simple_stmts.html#global
    http://www.python.org/dev/peps/pep-0008/

    In particolare quest’ultimo è il PEP che si segue in termini di convenzioni.

    La frase che hai riportato è un mantra di Python, ma non implica che si debba esplicitare l’uso di una variabile globale. E’ messa lì in particolare per l’annosa questione del self obbligatorio come primo parametro nella definizione dei metodi. E anche per evitare robe strane come la variabile speciale _ tanto cara a Perl. :P

    Tra l’altro non penso che “lista” sia una variabile globale, quanto di una variabile locale o d’istanza della classe, che viene passata a quella funzione per essere ripulita. Ma qui penso sia meglio che intervenga Mirco. :)

  • # 15
    Mirco Tracolli
     scrive: 

    Confermo che lista non è globale, ma è una variabile locale che rappresenta il testo assembly passato alla funzione (metodo) di load della classe.

    La classe lavorerà poi su questa lista, magari creandone anche altre di appoggio, ma comunque non sono globali, al massimo sono temporanee e quindi una volta usciti dal metodo vengono perse come variabili locali. Quello che rimane è solo l’elaborazione finale :D.

  • # 16
    simock85
     scrive: 

    Stavo riguardando ora, in effetti i caratteri che vai a rimuovere dalla lista vengono tutti rimossi da un ”.join(split(stringa)), strano che ti rimangano dei pezzi.

    Se ti interessa, per rimuovere elementi da una lista, io preferisco inserirli in una tupla, ciclare l’intera lista e se l’elemento non è nella tupla appenderlo ad una nuova lista. Ho verificato con cprofile che questo è il metodo più efficiente, anche se nel caso di liste molto grosse è conveniente trasformarle prima in tuple.

    exclude = (‘pippo’, ‘pluto’)
    list = [‘pippo’, ‘zio paperone’, ‘pluto’]
    list_n = []
    for item in list:
    if item not in exclude:
    list_n.append(item)
    return list_n

    ovviamente puoi effettuare qualsiasi operazione sull’oggetto prima dell’append e ti risparmi qualche ciclo.

    Ho inoltre verificato che nelle stringhe è più efficiente il metodo replace(char, ”) piuttosto che lo strip(char).

    Ho fatto queste prove tempo fa, ho scritto una libreria per la manipolazione e hashing di stringhe e sono riuscito ad ottenere performances equivalenti a codici scritti in c#.

  • # 17
    Mirco Tracolli
     scrive: 

    @ simock85

    Ti ringrazio molto. Utilizzerò questo materiale per migliorare la funzione di parsing del file, naturalmente ci saranno ancora degli affinamenti da fare, ma già con queste piccole modifiche si dovrebbe guadagnare qualcosa.

  • # 18
    Mirco Tracolli
     scrive: 

    @ ALL :D

    Volevo informarvi che ho apportato le modifiche che stavamo discutendo nei commenti ed ho anche trovato l’origine di tutti quei caratteri in più! Magari nel prossimo articolo si vedrà meglio come affinare il codice, comunque l’errore deriva da questo:

    Se per esempio si ha la stringa ‘HLT\n’, anche con spazi o con il ritorno di carrello e basta; prima di analizzare istruzione per istruzione, divido tutto il codice per le righe con split(‘\n’).

    Questa operazione, prendendo in considerazione l’esempio, da il seguente risultato : [‘HLT’, ”]

    Ecco l’origine di tutti quegli spazi, compresi i caratteri di stringa vuota (”). Naturalmente non potevate conoscere il motivo, visto che non ho trattato ancora questa parte, ma come ho già detto, lo vedremo nel prossimo articolo…

  • # 19
    Cesare Di Mauro
     scrive: 

    Mumble. Quando mi capitano questi casi faccio così: prendo il testo e lo converto in una lista di stringhe, eliminando quelle vuote, poi passo a processare una riga alla volta, convertendola in parole. Qualcosa del tipo:

    for Line in s.split('\n'):
      if Line:
        Words = Line.split()
        [...] Qui processo l'elenco di parole

    Comunque aspettiamo il prossimo articolo per capire meglio quale soluzione si potrebbe applicare per risolvere velocemente e (soprattutto, visto che parliamo di Python :D) elegantemente il problema. ;)

  • # 20
    Mirco Tracolli
     scrive: 

    @ Cesare

    Penso di lasciare solo le modifiche che ho fatto, ma senza toccare la parte di parsing del file, così magari, vedendolo complessivamente, si può apportare una soluzione migliore e più efficente. Magari rilascio questa parte in Beta sui download del sito, giusto finché non sono finite le modifiche più importanti.

    Tra l’altro, eliminando hex2dec ecc… il codice si presenta molto meglio per una conversione agli interi (basta infatti “eliminare” la funzione binario, cosa che non avevo notato in un primo momento), quindi se vorrò ancora migliorarlo, ci saranno solide basi di partenza :D.

  • # 21
    Cesare Di Mauro
     scrive: 

    Il progetto è tuo: non ti devi sentire di cambiarlo a seconda delle indicazioni che ti vengono date, a meno che ovviamente non ti vada di farlo. ;)

    Per quanto mi riguarda ritengo che l’obiettivo di questi articoli sia la divulgazione, e questo lo si fa sia con l’articolo (il quale rappresenta la base di partenza) che con gli eventuali commenti che possono saltare fuori ed essere utili agli altri.

    A parte che l’argomento dev’essere interessante, e questo credo sia fondamentale per tutti: per l’autore che deve scrivere il pezzo, e per chi dovrà leggerlo.

  • # 22
    Mirco Tracolli
     scrive: 

    @ Cesare

    Io sono molto soddisfatto del risultato del progetto, visto che, per quanto riguarda il funzionamento, fa tutto quello che dovrebbe fare :D. Però se vedo che una cosa può essere migliorata, mi rimane in testa finché non la miglioro, non so se mi sono spiegato… :P

    Mi fa comunque piacere che queste idee (e bisogna ancora arrivare all’interfaccia grafica :D) siano di aiuto a tutta la comunità (sopratutto per i pythoniani!!!) ed interessanti da trattare. Come hai già detto però, se l’argomento non è interessante di per se, non ne ricava nulla nemmeno chi lo scrive!!!

Scrivi un commento!

Aggiungi il commento, oppure trackback dal tuo sito.

I commenti inseriti dai lettori di AppuntiDigitali non sono oggetto di moderazione preventiva, ma solo di eventuale filtro antispam. Qualora si ravvisi un contenuto non consono (offensivo o diffamatorio) si prega di contattare l'amministrazione di Appunti Digitali all'indirizzo info@appuntidigitali.it, specificando quale sia il commento in oggetto.