Forth: di nuovo

Overview

Per due volte nel 2024 mi è capitato di trovare articoli su Forth, un linguaggio del 1970 che ha influenzato molti altri linguaggi stack-based come RPL, e mi ha fatto pensare parecchio. Ho trattato per la prima volta forth nel 2023

Forth è interessante perché puó girate anche su sistemi a 8bit, dispone di tre sorgenti di input/output (2 stack+ input) ed è pseudo-compilabile; oltre a ciò ne esiste una implementazione super portabile chiamata pForth, che è quella di cui vorrei parlare diffusamente in questo articolo 1.

Forth: nato per necessità

[Forth nasce nel 1970 ad opera di Charles H. Moore](https://en.m.wikipedia.org/wiki/Forth_(programming_language) mentre Moore lavorava al National Radio Astronomy Observatory (NRAO).

Ricompilare tutte le volte i programmi che servivano per pilotare il radio telescopio era tedioso. Cosí Moore sviluppo un 'programma universale' che gli consentiva di velocizzare lo sviluppo. Forth è molto arcaico, ed è a metá strada tra un linguaggio compilato ed uno interpretato.

La sintassi di forth è ridotta all’osso, da sembrare quasi inesistente. È composto da una serie di primitive di manipolazione dei suoi elementi base e tutto il resto è costruito da lí.

Il parser di Forth legge l'input, separato da spazi. Questi input sono chiamate "word". Il sistema distingue tra word native (che sono immediatamente eseguibili, per es "+") e word definite ex-novo dall'utente.

Forth è composto da due stack (uno per memorizzare i dati ed uno per i valori di ritorno delle subrotudini): si noti come questa scelta consenta di usare lo stack come meccanismo universle di comunicazioni tra le varie WORD, che quindi non hanno bisogno di dichiarare di quanti input hanno bisogno (anche se spesso essi sono documentati con delle parentesi tonde, che funcono da meri commenti).

Il nocciolo duro di Forth è fatto dal seguente codice:

 1while(true) {
 2 switch(*ip) {
 3  //arithmetics (+,-,*...):
 4  case PLUS: ds.push(ds.pop() + ds.pop()); ++ip;
 5  //stack manipulation (drop,swap,rot...):
 6  case DROP: ds.pop(); ++ip;
 7  //literal numbers (1,2,3...):
 8  case LITERAL: ds.push(ip[1]); ip+=2;
 9  //control flow:
10  case COND_BRANCH: if(!ds.pop()) ip+=ip[1]; else ip+=2;
11  case RETURN: ip = rs.pop();
12  //user-defined words: save return address & jump
13  default: rs.push(ip+1); ip = *ip;
14 }
15}

dove rs è lo stack di ritorno, e ds è lo stack dei dati.

Forth non ha operatori "overloaded" per cui per operare su tipi differenti di dati, avete bisogno di parole chiave dal nome diverso.

Per definire una word, si usa una sintassi del tipo

1
2: nuovaParola
3
4 word...di..cui...è...composta
5;

Le parole inserite tra : (due punti) e ; (punto e virgola) sono "compilate" e non sono eseguite immediatamente. Se si desidera, si puó richiedere l'esecuzione immediata con la word "immediate".

Per esempio i commentim che sono racchiusi tra parentesi tonde, sono definiti nel seguente modo

1: (   41 word drop ; immediate

L'idea qui è che qualsiasi cosa che segua (word) la parentesi aperta fino al carattere ascii 41 (che è la parentesi chiusa) venga immediatamente ignorato (drop).

Si usano le parentesi quadre per passare in modalità immediata, per cui l'esempio di cui sopra si puó scrivere anche come

1: ( [ CHAR ) ] LITERAL word drop ; immediate 

in questo caso l'uso delle parentesi quadre con LITERAL consente di calcolare il carattere ascii della parentesi quadra (41)

Infine, anche ció che segue una parola puo' essere manipolato: in questo modo in forth ci sono sempre due input: lo stack e i parametri che seguono una word. Per esempio, si puo' creare una costante con qualcosa come

142 CONSTANT LaRisposta

La definizione di CONSTANT in pForth è

1: CONSTANT  ( n <name> -c- ) ( -x- n )
2        CREATE , ( n -- )
3        DOES> @ ( -- n )
4;

La cui semantica è: prendi il nome che segue CONSTANT (,) crea una parola e poni il suo indirizzo sullo stack (DOES>) e infine memorizza in tale indirizzo (@) il valore che c'era sullo stack prima di CONSTANT.

Per una trattazione di questi aspetti consiglio questo ottimo articolo 2 di tal Yossi Kreinin ma anche il tutorial di GNU Forth 3

"Lego" semantico

Come si nota, Forth nasce da una attenta fattorizzazione delle esigenze che aveva il suo inventore. Dispone di:

  • stack multiplo
  • dizionario dati
  • concetto di puntatore e di memoria allocata
  • concetto di 'compilazione' immediata o postposta degli input forniti, in modo da catturare concetti come la meta-programmazione e le macro
  • concetto di tipo, esplicito

Se dovete eseguire semplici "calcoli" Forth, puó insegnarci ancora qualcosa su come attuare il design di un "system language".

Molto compatto

Date le sue caratteristiche Forth non ha bisogno di un sistema operativo per funzionare: è sufficiente un canale di input ed uno di output, e difatti questi sono i requisiti minimali per pForth. Non è un caso che forth sia "apparso" dentro cose come i firmware di alcuni sistemi si pilotaggio sviluppati alla NASA 4 (fonte: Wikipedia).

Critictá

E'molto facile mandare in core dump pForth, proprio perchè non c'è alcun tipo di verifica sul fatto che gli operandi siano corretti. Forth nasce per risolvere problemi nuemrici, e quindi è piusttosto debole in aree come la manipolazione delle stringhe: anche la semplice concatenazione sembra richiedere complesse operazioni di low-level memory management.

Pensare "a stack" porta a creare programmi che richiedono uno sforzo cognitivo/attitudine specifica.

Note finali

Curiositá: il linguaggio doveva chiamarsi "quarto" (FOURTH) in onore dei linguyaggi di quarta generazione, ma siccome il sistema IBM su cui era sviluppato non consentiva di fare nomi di file piú lunghi di cinque, fu "accorciato" in ... Forth!

Un aspetto interessante di forth sono alpunto i suoi elementi base, che non sono immediatamente intuitivi. Per esempio…