Una VM è un software che offre un'astrazione simile a una macchina. Nel contesto della virtualizzazione, una macchina virtuale potrebbe astrarre risorse hardware. Nella programmazione delle VM del linguaggio, la VM offre un modello di macchina, ad es. con un set di istruzioni specifico, un numero di stack e un modello di memoria.
È vero che possiamo interpretare qualsiasi linguaggio come set di istruzioni, ma in genere le istruzioni VM sono più dettagliate delle direttive di programmazione di alto livello. Ad esempio, il% di alto livello% co_de potrebbe essere stato modificato in queste istruzioni per uno stack machine:
PUSH "baz"
PUSH "bar"
PUSH 2 ; number of arguments
LOAD "foo" ; load the value of a variable onto the stack, here a function
CALL ; the "CALL" op pops a function and number of arguments off the stack,
; then calls it
Il modo in cui queste istruzioni sono eseguite è irrilevante per questa discussione, potrebbero essere interpretate.
Un interprete che "direttamente" esegue foo("bar", "baz")
generalmente non può essere considerato una VM. L'interpretazione diretta è caduta in disgrazia anche per le semplici lingue hobbistiche, ma i primi dialetti BASIC erano interpretati. Oggi, un'interpretazione semplice potrebbe essere ancora utilizzata per i linguaggi di shell semplici.
Il problema con ingenui interpreti di linea è la difficoltà di implementare la programmazione strutturata e procedurale. Supponiamo di voler scrivere foo("bar", "baz")
. Questo potrebbe essere fatto con GOTO condizionali:
10 GOTO 40 IF x
20 z
30 GOTO 50
40 y
50
Questo è molto fragile per un uso serio, ma è molto facile da implementare. Ma Vai a è considerato dannoso e preferiamo piuttosto un if (x) { y } else { z }
costruire. Ad esempio, bash usa if/else
:
if x
then y
else z
fi
Se la condizione è falsa, l'interprete potrebbe passare al if/then/else/fi
corrispondente, ma questo è essenzialmente un problema di parità equilibrato, perché i condizionali possono essere annidati. Un'altra complicazione è che le istruzioni possono anche essere separate tramite else
invece di newline, quindi non si tratta semplicemente di guardare la prima parola in ogni riga.
In breve, qualsiasi linguaggio con sintassi anche tollerabile a distanza dovrebbe essere analizzato in una struttura di dati prima dell'esecuzione. L'output di un parser è chiamato Abstract Syntax Tree. Per esempio. ;
potrebbe produrre questo AST:
FunctionCall
name: "foo"
|
List
length: 2
/ \
String Addition
"bar" / \
Int Variable
4 name: "variable"
Un AST può essere valutato dal basso verso l'alto. Innanzitutto, sostituiamo la variabile con il suo valore (ad esempio foo("bar", 4 + variable)
). Successivamente, la sottostruttura
Addition
/ \
4 38
sarebbe stato valutato, che sostituisce questa sottostruttura con 38
. Infine, viene eseguita la funzione get che sostituisce quella parte dell'albero.
Descrivendo la semantica operazionale di un linguaggio di programmazione usando questi grafici è bello, ma non è un buon modello per l'esecuzione effettiva: questo distrugge l'albero originale, quindi devi fare una copia per cose come corpi in loop o procedure. E se abbiamo variabili, il nostro interprete ha bisogno di un concetto di un ambiente che ci avvicini molto ad essere una VM.
Attraversiamo quel confine per essere una VM se notiamo che i nodi nel nostro AST sono essenzialmente Opcode. Se ordiniamo le operazioni dell'albero (qui: dal basso verso l'alto, da destra a sinistra: attraversamento post-vendita ), e non aggiorna la struttura attuale ma colloca i valori intermedi nello stack, l'esempio sopra potrebbe anche essere stato scritto come un assemblatore più simile
Variable "variable"
Int 4
Addition
String "bar"
List 2
FunctionCall "foo"