LFCInter - A C interpreter |
Voir la documentation qui est en Français. Les distributions sont disponibles à la fin de cette page. |
2017 Update : as using pre-standard C++ libraries, this code won't compile anymore using modern compiler. If you're courageous enough to fight against outdated functions and obsoleted includes, please share modifications made.
This is release 1.0 of a C interpreter I have developped as final learning project of my master.
It has the following features :
?:
),
goto'
),
int
, char
, pointer
) and arrays but no float
,
typedef
, unions
or structs supported
,
if...else
, while
, do...while
, for
). Missing
switch-case
,
//
) - Why GCC doesn't allow this in C code ? -
Unfortunatly, it is quite slow because it tokenizes the source code on the fly. A future version (???) may include a first pass tokenizer ...
All files are copyrighted by me but are fully PUBLIC DOMAIN. So you can use it as you want. But PLEASE, keep my name somewhere and FULLY documents all changes you have made. |
Ce programme est un Domaine Public: Il peut être diffusé librement temps que tous les fichiers de cette distribution sont inclus, sans que mon copyright n'ait été modifié. Toutes les modifications faites pour supprimer un bug ou pour porter ce code vers une autre plateforme DOIVENT ETRE CLAIREMENT IDENTIFIEES DANS LE CODE. De plus, je vous serais reconnaissant de m'avertir de telles modifications... |
Voici donc le résultat de 6 mois de programmation, pendant mes heures de loisir...
Le développement :
Le langage C++ a été choisi. "L'approche objet" a été utilisée lorsqu'elle simplifiait la programmation comme pour la gestion des mots clefs ou des
erreurs. Une programmation plus classique a été utilisée lors de certaines
tâches afin de diminuer le code: Par exemple, les opérateurs des calculs,
comme '+', '-', ... auraient pu être des opérateurs surchargés pour chaque
classe de représentation des valeurs mais le code nécessaire était trop
important !
Afin de garantir la portabilité sur plusieurs plate-formes, GCC a été
utilisé (version 2.7.0 disponible sur le CD Fresh Fish 10 pour Amiga). Ce
compilateur du 'Domaine Public', donc gratuit, est disponible sur toutes les
machines, du simple PC jusqu'aux plus puissantes mainframes. En plus de suivre
correctement la norme, le code généré comporte peu de bugs...
Le développement a été fait sur :
LFCInter a principalement été écrit pour les systèmes où les entiers et
les pointeurs ont la même taille (indispensable lors de convertions entier <->
pointeur mais aussi nécessaire aux opérateurs de comparaison)... C'est le cas
de tous les systèmes 32 bits, certains 64 bits. Comme d'habitude, le problème
se pose sur PC où certains modes de compilation autorisent des entiers sur 16
bits et des pointeurs sur 32 bits.
L'utilisation de l'option '-fatal
' permet un test en temps réel de tels
débordements et provoque une erreur en cas de perte de précision.
MICRO-SUCKER est encore passé par là !!
La compilation sur le MicroVaxII m'a permis de tester la portabilité vers
les plate-formes UNIX (aucun problème supplémentaire sur le code par rapport
à la compilation sur Amiga, sauf pour quelques fonctions comme expliqué plus
loin). Les environnements ciblés lors du développement ont toujours été
l'AmigaDOS et UNIX.
La compilation sous MS-DOS par le BORLAND C++ 4.52 n'a
été nécéssaire que pour la démonstration sur un portable PC. Les modifications
nécessaires ont été faites uniquement lorsqu'elles n'entraînaient pas des
incompatibilités avec les autres environnements, sinon les portions de codes
ont été supprimés (#ifndef __BCPLUSPLUS__
). De plus, n'ayant pas de machine
sous cet OS insipide et anachronique chez moi, il est évident que cette
version a été moins testée et comporte, sans doute, plus de bugs que les
autres.
Sur les systèmes plus ou moins apparentés à UNIX, en utilisant GCC,
le seul problème potentiel est l'inexistence de certaines fonctions (par exemple
isiso()
sur le VAX) ou encore des noms différents pour une même fonction
(stricmp()
qui devient strcasecmp()
pour les compilateurs 'POSIX compliant').
|
Cette classe, dont les instances peuvent être utilisées comme on le
ferait avec des pointeurs, implémente en elle-même le 'parser'. Son champ
principal
const char *ptr;
pointe sur le premier caractère de l'objet en cours d'évaluation. Il est
'protected' et, en aucun cas, il ne doit être modifié directement car lui sont
associés les champs
size_t len;
qui contient la longueur de l'objet, et
short int val;
qui contient la 'valeur symbolique' correspondante. Cette valeur entière,
définie sous forme d'énumération dans le fichier "Token.h
", accélère les tests
de syntaxe par la suite.
On distingue les valeurs 'smbl_id' qui indique que
l'objet est un identificateur et 'smbl_icn' pour signifier la fin du fichier
source ou que l'instance n'est pas initialisée. A chaque modification de 'ptr'
ces champs sont mis-à jour par la méthode construit()
.
Dans cette classe, se
trouve aussi
*
qui renvoit la valeur symbolique associée à l'objet courant,
obj()
qui retourne une 'string' contenant l'objet pointé,
idéale pour afficher les messages d'erreurs,
++
qui 'incrémentent' ptr pour qu'il pointe sur l'objet
significatif suivant; par significatif, s'entend que les espaces et les
commentaires sont sautés,
saute()
qui permet de ... sauter, une instruction, un bloc
d'instructions, à la fin d'un ()
ou d'un []
.
definition()
et interne()
indiquent si l'objet pointé est un
mot clef de définition de type ('int
', 'void
', ...), ou s'il s'agit d'une
fonction interne (printf()
, malloc()
, free()
).
|
Cette classe de "représentation de valeurs", contient, en plus des
valeurs en elles-mêmes, son type. Le 'type de base', c'est à dire celui qui
caractérise comment la valeur est mémorisée, est accessible par la méthode
type()
et peut prendre les valeurs suivantes
*
' : c'est un pointeur,
I
' : c'est un entier,
C
' : c'est un caractère,
V
' : c'est une valeur 'void',
L
' : c'est une valeur littérale, c'est à dire le type permettant de
stocker n'importe quelle autre valeur. Il est défini comme un 'long int'.
Pour un pointeur, le champ
string info;
contient le type de ce vers quoi il pointe. Par exemple, pour un pointeur
sur des caractères (une chaîne ?), typebase vaut '*
' et info vaut "C
".
|
struct _var
et ses dérivées _var_int
, _var_char
, _var_fonc
, _var_ptr
permettent de mémoriser les variables. La structure de base contient les
champs permettant d'identifier la variable (nom, type,...) ainsi que la
définition des méthodes utilisée pour obtenir la valeur mémorisée ou une
référence sur celle-ci. Chaque classe dérivée contient un champ 'val
' qui est
l'espace de stockage pour le contenu de la variable. Le champ 'succ
' sert à
lier les symboles ensemble (voir la structure _tablesmb) et 'h
' contient le
hash code du nom pour accélérer les recherches.
|
Cette classe mémorise les différents blocs de mémoire associés à un
tableau. Ainsi, ils seront libérés lors de sa destruction.
Cette classe ne
comporte que 2 champs :
void *data;
pointeur sur le bloc de données, alloué avec malloc(),
_resallouee *succ;
pointeur sur la ressource suivante. En effet, pour un tableau
char x[5][10];
6 ressources ont été allouées, 5 contenant 10 caractères pour stocker les
données de la seconde dimension, plus 1 contenant 5 pointeurs pour stocker la
premiere (des tableaux de tableaux sont en fait des tableaux de pointeurs sur
les premiers éléments des dimentions inférieures !). Grâce à ce système, des
tableaux de dimension quelconque peuvent être alloués... dans la limite de la
mémoire disponible.
|
Plutôt que d'utiliser des tables de symboles statiques comme le font la
plupart des compilateurs ou des interpréteurs, j'utilise des tables
dynamiques constituées d'une liste simplement chaînée, gérée par cette
structure. La fonction trouve_symbole()
permet de trouver un symbole dans la
table courante, dans celle des blocs parents ou enfin dans la table des
symboles globaux.
|
Cette classe implémente une pile de données homogènes. Au lieu d'allouer
les données une par une comme il faudrait le faire avec une liste doublement
chaînée, ce qui a tendance à fragmenter la mémoire (et à faire ramer les PC !),
elles sont regroupées en blocs. Le nombre d'allocation/libération étant
diminué, l'exécution des fonctions utilisant des piles, comme lors de calcul,
est accélérée...
Les blocs sont chaînés entre eux (structure LFDSData). Le champ 'cidx
'
mémorise quelle est la dernière donnée accédée et 'courant
' dans quel bloc
elle se trouve. Comme la majorité des accès à ce genre de pile est séquenciel,
ces champs évitent de nombreuses recherches de blocs.
Note : Lorsque l'on tente d'accéder à une donnée qui n'existe pas (index hors
limite), une donnée créée par le constructeur par défaut est renvoyée.
Cette classe dispose des méthodes et des opérateurs suivants:
Push()
: Ajoute une donnée au sommet de la pile, en créant s'il le faut un
nouveau bloc,
Pop()
: Retourne la dernière donnée poussée et la supprime de la pile,
operator[]
: Retourne une référence sur la donnée de la pile dont l'index
est passé en argument,
current()
et length()
: Renvoient respectivement l'index de l'élément
courant et celui du dernier.
Voici les différentes étapes effectuées lors du lancement d'LFCInter :
L'interprétation est elle-même assurée par les fonctions :
execfonc()
a pour tâche principale de décoder les arguments d'une fonction
et de tester si sa valeur de retour est correcte (par exemple, pas de valeur de
retour s'il s'agit d'une fonction déclarée comme 'void
'). Elle lance ensuite
execbloc()
sur le bloc principale de la fonction,
execbloc()
interprète un bloc. Si le premier élément lu est un '{
',
l'interprétation se terminera au '}
' correspondant, dans le cas contraire une
seule instruction est interprétée en lançant la fonction execinst()
,
execinst()
exécute une seule instruction. Pour celles qui sont répétitives,
une sorte de cache est utilisé afin de ne tester leur syntaxe qu'une seule
fois.
eval()
est appellée (voir le § des calculs). Si un '{
' est rencontré, execbloc()
est
appelée,
lancefonc()
est appelée: Elle lira les arguments et lancera execfonc()
,
interne()
est appelée: Elle contient
l'interface entre le programme source interprété et les fonctions du
compilateur. Dans la majorité des cas, il ne s'agit que de tester la validité
des arguments mais pour les fonctions plus complexes comme le printf()
,le code
nécessaire est plus important.
Cette B.N.F. est une adaptation de celle fournie dans la documentation du BORLAND C++ 4.0 (entre parenthèses se trouve la priorité de chaque 'couche'.)
|
L'entête du fichier LFCI_Cal.cxx contient plus d'informations sur les
différences entre cette BNF et celle du BORLAND (généralement des erreurs dans
la BNF incluse dans la documentation de ce compilateur !).
Si cette BNF permet de décrire formellement le langage, la transposer
directement en code C est totalement inadapté à un interpréteur. Par exemple,
prenons le cas de la couche de niveau 0:
|
+
' ou '-
' pour séparer 'exp_add
' et 'exp_mul
'.2+5*-3
", il trouvera d'abors le '-
' et devra déterminer s'il
s'agit bien du '-
' binaire ou du '-
' unaire. Pour ce faire, il doit avoir
'parsé' tout le début de l'expression ! Dans ce cas c'est non, donc le programme
recherche le symbole précédent et trouve le '+
', et à nouveau, il doit à
nouveau chercher si cet opérateur est binaire. En plus, s'il n'y a aucune
optimisation, il reparsera une nouvelle fois l'expression depuis le début, et
la même chose se passera pour toutes les couches... Quelle perte de temps !
|
Il est évident qu'un tel projet est complexe et demande beaucoup de temps. C'est pourquoi, cet interpréteur comporte certaines limitations par rapport au C standard :
va_list
' qui soient
compatibles avec les autres compilateurs). La seule exception concerne les
fonctions internes comme les '?printf()
'.
register
", "volatile
", "short
",
"long
", "const
", "extern
", "signed
", "unsigned
" et "auto
".
float
", "struct
", "union
", "double
" et
"static
" (voir le fichier "Token.cxx" qui contient la liste des mots clefs
reconnus).
T
'
(pointeur constant) car un code du genre
|
T
', l'opérateur sizeof()
fonctionnerait correctement sur des tableaux.
char ok[]={'O','u','i',0};
?:
,
int truc( void )
int truc()
char *x = "Salut \
tout le monde.";
est interdit. Il fait écrire
char *x = "Salut "
"tout le monde.";
Le résultat étant le même.
Break
. Je ne pense pas que ceci provienne d'LFCInter par lui-même
mais d'un bug de la 'ixemul.library' version 41.2 lors gestion des signaux
avec un processeur 16/32 bits car GCC lui-même plante dans cette configuration
lors d'un break. Ce problème n'existe pas sur un Amiga 1200/020 ou l'Amiga
4000/040, pleinement 32 bits...
time()
qui demandent un pointeur
sur un entier long comme argument car sur les systèmes ayant des entiers de 16
bits (PC par exemple), passer l'adresse d'un entier peut planter le système...
Il est évident qu'un tel projet ne peut être parfait en si peu de temps de développement, et il doit rester encore quelques bugs. Même si LFCInter 1.0 n'est pas un produit terminé, beaucoup des objectifs sont atteints :
La réalisation de ce projet m'a aussi apporté des connaissances nouvelles :
Je tiens particulièrement à remercier BABETH et le grand LAURENT sans qui ce texte et mes sources ne seraient qu'un ramassis ignoble de fautes d'orthographe (même s'il y en reste quelques unes!). Merci pour leur patience...
structure _amsg
qui mémorise les
options passées en arguments et certains paramètres internes qui y sont
associés...
_rep
), des variables
(_var
et ses dérivées) et le stockage des symboles (_tablesmb
).
printf()
, malloc()
, free()
...
Note: Certaines fonctions ne sont pas disponnibles sur certaines plate-formes (si l'OS hôte ne les possèdent pas...).
printf()
, sprintf()
, scanf()
, putchar()
, puts()
, gets()
, getchar()
,
flushstdout()
,
isdigit()
, islower()
, isspace()
, ispunct()
, isupper()
, isalpha()
, isxdigit()
,
isalnum()
, isprint()
, isgraph()
, iscntrl()
, isascii()
, isiso()
, toupper()
,
tolower()
, toiso()
,
strcat()
, strchr()
, strcmp()
, strcpy()
, strerror()
, strlen()
, strncat()
,
strncmp()
, strncpy()
, strrchr()
, strdup()
, stricmp()
, strnicmp()
, strcasecmp()
,
strncasecmp()
, strpbrk()
, strstr()
, strcoll()
, strcspn()
, strspn()
, strtok()
,
strtol()
,
realloc()
, malloc()
, free()
, swab()
, memchr()
, memcmp()
, memcpy()
, memmove()
,
memset()
, memccpy()
,
atexit()
, atoi()
, time()
, ctime()
, clock()
, sleep()
, system()
,
Attention, ce sont des archives : si votre navigateur affiche des caractères bizards, demandez lui de sauvegarder le contenu de ce qui est pointé (généralement, il suffit d'appuiller sur [SHIFT] en même temps que vous cliquez sur un lien).
Warning, following links point to archives files : if your browser display stranges characters, save the content of the link (Hold [SHIFT] key when clicking on a link).
Code source (tgz, 34 Kb)
Les commentaires du code sont en Français.
Code's comments are only in French, but I think the code itself is easy to understand.
Documentation & exemples (tgz, 12kb)
La documentation est uniquement en Français.
The documentation is only in French, but examples may be usefull.
exécutable Amiga (lha, 83kb)
Exécutable pour Amiga 68k : testé sur mon 1000 et mon 4000. L'ixemul.library v41.2+ est requise ainsi que sans doute un Workbench 2.0+ (il me semble que c'est nécessaire à l'ixemul.library mais je n'ai jamais testé sous 1.x).
Executable for Amiga 68k computers : tested on my 1000 & 4000. ixemul.library v41.2+ is need (and I think a 2.0+ system too).
exécutable pour Sparc/Solaris7 (tgz, 119 kb).
Exécutable pour station Sparc sous Solaris 7(ben, si ca ne fonctionne pas sur votre station (OS différent par exemple), recompilez les sources)
Executable for SparcStation under Solaris7 (if it doesn't work on your station, you should recompile source code).