Linguaggi di Scripting

Paolo Medici
NOTA: Questo articolo contiene ARIA FRITTA. Nel senso che comunque sono considerazioni fatte nel lontano 2000, oramai sorpassate.
Visto che in questo periodo si parla tanto di linguaggi di scripting, era ovvio che io dovessi dire la mia anche se nessuno ha chiesto il mio parere... ma cosa volete farci... sono fatto così. Tralasciamo subito l'arida discussione se i linguaggi di scripting servono... a parere mio no. Cioè... se il linguaggio di scripting deve avere una sintassi complicata e deve essere compilato, tanto vale scrivere una DLL che è mille volte più performante. Non ho seguito il corso di InformaticaII dove magari si parlava di queste fregnaccie dei linguaggi di programmazione, e mi ritengo fortunato.
Tuttavia proprio per arginare la complessità di un linguaggio di programmazione (ed estenderlo, renderlo più flessibile) che i linguaggi di scripting sono nati.
I linguaggi di scripting sono nati sulla possibilità di sfruttari dati di tipo variant, ovvero di tipo non staticamente assegnato, o dalla possibilità di estendere il linguaggio dinamicamente.

Comunque ognuno è libero di farsi male come vuole perciò cercherò con questo documento di fare in modo che si faccia meno male possibile.
Un linguaggio di scripting può essere utile (più che utile, direi performante) al momento che compilato in bytecode sia eseguito in una Virtual Machine e perciò legato a una particolare target platform. Un semplice linguaggio di scripting può anche essere utile se non presenta problemi nell'essere eseguito durante la fase di interpretazione, senza bisogno della fase di compilazione e generazione del bytecode.
Spero che ognuno sia in grado di leggere e interpretare un file di testo dove è contenuto lo script di partenza. La difficoltà di questo processo dipende da quanto è complicato il linguaggio che si progetta ovviamente, e ovviamente il target di tutto questo discorso è creare una versione compilata dello script, tale che possa essere eseguita sulla virtual machine in modo abbastanza veloce. Voglio solo dare l'idea nel modo in cui va affrontato il problema non dare soluzioni. Anzi più che altro do idee confuse e farneticanti... ma magari qualcosa di utile c'è...

Uno script interpretato in esecuzione ha un senso se deve essere eseguito solo una volta. Se deve essere eseguito più di una volta, o contiene delle sottoprocedure che verrano chiamate più di una volta allora è meglio compilarlo.

Per creare istruzioni che siano allo stesso tempo complesse, ma veloci da esecuzione bisogna progettare un codice simile all'assembler (RISC o CISC che sia).

Dunque è necessario programmare una Virtual Machine che esamini e interpreti degli opportuni OpCode (tipo chiamate, cicli, e operazioni aritmetiche).

A mio avviso la soluzione migliore è la seguente:
 
CODICE FUNZIONE VARIABILE DESTINAZIONE PARAMETRI


Ogni funzione deve sapere la lungezza dei parametri ad essa associata (quantità parametri implicita). Questo valore potrebbe però essere esplicito. In questo modo se la funzione non è conosciuta verrà saltata visto che si conosce la sua dimensione (nei rari casi di linguaggio che possa essere interpretato da diverse versioni della VM).

Al momento della compilazione a ogni funzione definita dall'utente sarà assegnato un ID che non dovrà sovrapporsi agli ID implementati dall VM. Allo stesso modo le stringhe verra assegnato un ID per riferirsi come variabile.

Le variabili possono essere senza tipo o con tipo. L'unica cosa che cambia in quelle con tipo e la generazione di un messaggio di errore se non esiste una conversione di tipo.

Le variabili possono essere oggetti. Usare script a oggetti permette di ampliare in modo esponenziale le potenzialità dello script elevando all'infinito la complessità di interpretazione, compilazione ed esecuzione (e di errore?).

Orientare comunque a oggetti la variabili è sempre una buona cosa invece, non permettendo ne variabili ne classi definite dal'utente. Se il linguaggio di script è sufficientemente ampio questo è possibile.

Semantica di base

Facciamo un esempio adesso per mostrare quali sfaccettature saranno necessarie nello sviluppo del programma:
 
; I commenti servono. ; è comodo perchè indica di saltare interamente la linea
@A=CreateRectangle(100,100,200,200); Una sintassi colorita aiuta molto la scrittura del codice, mentre complica l'interpretazione. uso il simbolo @ per indicare le variabili per esempio.
@A CreateRectangle 100 100 200 200 E' molto più semplice da compilare, ma meno bello da vedere...
@A.Draw Vederlo come oggetto
Draw @A o no. E' identico. E' solo una questione di estetica. La procedura non richiede un return. Implementare una variabile void (0)
DrawText 5 5 "Script di prova" La stringa andrà salvata a parte e codificato solo un indicatore alla stringa. uso il costrutto "..." per indicare il testo, visto che come ho scelto io gli spazi separano le variabili
Release @A Una visione abbastanza Object Oriented. ma non troppo.
Proc ShadowText @X @Y @T
...
EndProc
Le procedure sono una parte fondamentale per uno script. Vanno cercate inizialmente per assegnargli gli ID e una semantica, poi compilate con il resto.
ShadowText(int @X, int @Y, string @T)
...
ShadowText End
Altro esempio di possibile implementazione... un po' meno interpreter oriented e un po' più user oriented.

Ci sarebbe da implementare anche lo stack se se ne ha voglia...
Faccio poi notare che normalmente in un linguaggio di script si fa senza dei puntatori (Basic, Perl, JavaScript etc etc...). Una strada da seguire per non impazzire e non combinare eccessivi danni. L'implementazione di array (ovviamente dinamici) risolve questo problema in una maniere elegante. Questo obbliga perciò a aggiungere la terza forma di indirzzamento: quella con scostamento (BASE + OFFSET). Ovviamente per generalizzare si potrebbe pensare ogni variabile come un array e fare riferimento normalmente all'entry 0.

Esempio:
 
Script OpCode Espanso:
DrawText @X @Y "Prova" DRAWTEXT VOID mem[@X+0] mem[@Y+0] mem[@String0+0]
DrawText @X[1] @Y[1] @Text[1] DRAWTEXT VOID mem[@X+1] mem[@Y+1] mem[@Text+1]
DrawText 5 5 "Prova" DRAWTEXT VOID immediato[5] immediato[5] mem[@String0+0]

Espressioni

Le espressioni dello script vanno scomposte o trasmesse a mo di stringa a un interprete. Se scomposte occuperanno un po' più di codice ma saranno più veloci da eseguire ed è una soluzione certamente più omogenea.
Io suggerirei per esempio di scriverle direttamente (o farlo fare al compilatore... faticaccia) in notazione ungherese inversa (o era polacca o non era notazione..era sintassi... mmmh..... DWORD dwStatus; <- questa è la notazione ungherese o polacca... boh? ma a me cosa ne viene... mah...). Comunque quella che è... che attraverso uno stack numerico è possibile risolvere le espressioni in modo relativamente compatto.
Altrimenti si può implementare attraverso funzioni molto di base tipo: ADD, SUB, MUL, DIV etc. etc...
 
Script
@C = (@A+@B)*2 ADD @temp1 @A @B
MUL @C @temp1 immediate(2)

Diciamo che in generale istruzioni Assembler Like sono molto più semplici da implementare che funzioni più Complesse, ma a scapito di una crescita di complessità del compilatore.

Istruzioni di flusso

Le equivalenti di for, while, if, then, else sono molto importanti per la completezza dello script. Ovviamente istruzioni tipo Jump o Goto sono da preferirsi per semplicità.
E' possibile considerare istruzioni ricorsive come for, while allo stesso modo di chiamate a sottofunzioni, usando allo stesso modo lo stack locale della riga.
Quando si fa una chiamata a una sottofunzione, si mette nello stack la riga di codice successiva. Il programma salta alla sotto funzione, e quando incontra una istruzione di end estrare dallo stack la riga dove tornare.
Quando si compiono azioni ricorsive si mette nello stack la riga dopo il for per esempio e all'arrivo del end si legge questo valore e si ricomincia il ciclo. L'unico problema sarà quello di incrementare le variabili e di rimuovere l'elemento dallo stack all'uscita del ciclo.

A volte comunque non sono necessarie azioni complesse (come incapsulamento di oggetti e roba simile...) e si possono definire script molto semplici.
Vogliamo gestire gli eventi di una semplice avventura grafica. Per esempio... per aprire una porta è necessario inserire una chiave nella serratura.

Quando clicchiamo la chiave sulla porta o quando cerchiamo successivamente di aprirla, viene chiamata la procedura porta con due variabili: tipo di azione e oggetto.
Queste variabili saranno globali (per semplicità). Chiamate per esempio @Action e @Object e un parametro Object Oriented @This
Un linguaggio evoluto sarebbe una cosa simile
 
Codice Spiegazione Decompiled OpCode
proc Porta(@Action, @Object,@This) Solo per motivi di semplicità di lettura Nessuna istruzione
if @Action==Use Use è una costante definita nel compilatore IFEQUIV [VAR/COST1] [VAR/COST2]
if @Object==Key  Key è una costante definita nel compilatore
@KeyFlag=true Assegnamento: true è definita nel compiulatore MOV @KeyFlag 1
endif Fine dell'IF... se la condizione è sbagliata da qui in poi ricomincia a controllare. Attenzione all'annidamento degli IF ENDIF
if @Object==None None è una costate definita nel compilatore IFEQUIV @Object 0
if @KeyFlag==true IFEQUIV @KeyFlag 1
DoAction OpenDoor @This DoAction è un qualsiasi procedura esposta dal programma principale e implementata direttamente. OpenDoor è una costante DOACTION <codice di OpenDoor> @This
else Se if non è vero può trovare un else ELSE
Say "La porta è chiusa a chiave" Say è un metodo esposto dal programma... SAY @String0001
endif Chisura dell'ultimo if. l'annidamento scende di un livello ENDIF
endif ENDIF
endif ENDIF
endproc Restituisce il controllo al programma RET

Un problema è ovviamento l'annidamento degli IF/ELSE/ENDIF. Usare l'annidamento di funzioni potrebbe complicare il codice ma è una soluzione opinabile. Altrimenti si può creare uno stack locale gestito dall'utente che si ricorda se l'espressione è vera o falsa e esegue di conseguenza il codice
Codice Stack
if @Object==None La condizione è VERA (per esempio) AGGIUNGIAMO VERO Allo stack
if @KeyFlag==true L'ultimo oggetto dello  stack corrente è vero. Eseguiamo l'istruzione. L'istruzione è falsa per esempio. Inseriamo FALSO nello stack
... L'ultimo oggetto dello stack è FALSO, l'istruzione non verrà eseguita (ulteriori if verranno marcati con un ulteriore stato... DUMMY)
else Else: Si Invere la flag dello stack. adesso da FALSE diventa TRUE.
... Lo stack mostra TRUE... eseguiamo l'istruzione
endif Sia che sia True che sia False (o dummy) si elimina l'ultimo oggetto dallo stack.Ora è True
endif Si elimina l'oggetto dallo stack

Perchè è stato necessario aggiungere lo stato Dummy? Per evitare che endif annidati in stati FALSE forzino l'uscita dallo stato.

 
Esempio di possibile codice per un elaboratore di immagini:

#main
$A=Create 130 90

for $I 0 90
wsprintf $N "flag%04u" $I
$X=calc "($I%10)*13"
$Y=calc "($I/10)*9"
&Flagged
end

Save $A "JPG:90" "flag.jpg"
Release $A
end


#Flagged
$B=Load $N
Resize $B 13 9
Blit $A $X $Y $B
Release $B
end

Non reinventare la ruota

Questa è una cosa che si ripete sempre a chi vuole cominciare a sviluppare qualsiasi progetto. Attualmente ci sono in circolazione un centinaio di linguaggi di Scripting (interpretati) che potrebbero fare il caso vostro.
Alcuni esempi sono:
Si possono trovare alcuni link anche da http://www.ddjembedded.com/languages/

Paolo Medici, che ultimamente aveva del tempo da perdere in retorica. Dalla Serie delle Guide Veloci per fare Software Miliardari Visto che queste sono considerazioni personali, gradirei anche qualche altro punto di vista.... la mia email è: software@pmx.it
Questo articolo ha avuto 8935 contatti. Pagina aggiornata il 30 settembre 2006