Introduction
Le Turbo Pascal 3 était très efficace pour l'époque et beaucoup de personne ont effectué du reverse engineering pour mieux comprendre comment il fonctionnait. Voici donc les analyses de certains personnes afin de mieux comprendre le Turbo Pascal 3 en interne et sa génération du code.
Structure du compilateur
Les compilateurs se composent généralement des groupes fonctionnels suivants :
- Analyse lexicale
- Analyse syntaxique (parser)
- Optimiseur
- Générateur de code
- Gestionnaire de table de symboles
- Gestionnaire d'erreurs
Le Turbo Pascal 3 n'est pas votre compilateur habituel ... L'analyseur est entrecoupé de parties du générateur de code, et il n'y a pas d'optimiseur. La plupart des compilateurs ont besoin de plusieurs passages (PASS) pour faire leur travail, mais le Turbo Pascal 3 est un compilateur à une seule passage (plus rapide). Heureusement, les langages de programmation de type Pascal sont conçus avec une compilation simple en un seul passage à l'esprit. Tous les symboles doivent être définis avant d'être utilisés. Le compilateur peut facilement déterminer le type d'une constante sans regarder vers l'avant :
- symbole
- 7FFF
- 12345
- 12345.5 : Ceci est une exception. Le Turbo Pascal 3 vérifie d'abord si une constante numérique est un entier ou une constante réelle. En fait, un compilateur Pascal standard n'a pas à faire cela - le Pascal standard exige que la section entière d'une constante réelle soit un nombre entier valide !
- 09CFH - Cette notation (utilisée par de nombreux assembleurs et langages) est un exemple négatif. Les compilateurs - comme les gens - lisent de gauche à droite. Pour lire un nombre dans cette notation, il doit être lu dans un tampon pour reconnaître son type, puis il peut être converti. Devinez pourquoi Turbo Pascal utilise le caractère «$» pour les nombres hexadécimaux... Ainsi, il est plus simple pour le Turbo Pascal de déterminer un nombre hexadécimal si le premier caractère peu lui indiquer.
Analyse lexicale
La tâche de l'analyse lexicale est de lire le code source à partir de la mémoire ou d'un fichier d'inclusion, d'éliminer les commentaires et de reconnaître les directives, symboles et mots-clefs du compilateur. Il est appelé par l'analyseur. A chaque appel un élément de langage (mot-clef, symbole, constante ...) est lu. La position de départ est enregistrée. Si une erreur est détectée, l'éditeur démarre et le curseur pointe vers cette position.
Analyse des structures de programme
La tâche de cette partie du compilateur est d'analyser la structure du programme à compiler et de vérifier la syntaxe. Comme la plupart des compilateurs Pascal, le Turbo Pascal 3 utilise un analyseur de descente récursif. La génération de code est incluse dans l'analyseur. La compilation des structures de programme est assez simple. Habituellement, la syntaxe est décrite sous forme de Backus-Naur ou par un «schéma ferroviaire». À titre d'exemple, l'instruction IF sera couverte.
IF cond THEN stat1 {ELSE stat2}
Après avoir lu un élément, l'analyseur prend la piste applicable. S'il n'y en a pas, la syntaxe est incorrecte et une erreur est signalée. Il est possible d'avoir un analyseur généré automatiquement par un soi-disant compiler-compiler, si la forme Backus-Naur de la syntaxe est donnée. Malheureusement, cela n'aide pas beaucoup: les parties vraiment difficiles d'un compilateur - la génération de code et l'optimisation - doivent encore être écrites manuellement. Comment la déclaration IF est-elle convertie ? (La section correspondante dans le compilateur est au déplacement 6C12h). La procédure d'instruction lit un IF et appelle la procédure IF. Tout d'abord, la condition - en fait une expression arithmétique de type booléen - est évaluée. Cela se fait en appelant la procédure d'expression. L'expression est lue jusqu'à ce qu'un symbole THEN soit trouvé. Cela met fin à l'expression, dont le type est booléen. La procédure IF insère ici un saut conditionnel à la fin de l'instruction 1. Le déplacement est inséré plus tard - il n'est pas encore connu. Si l'expression a été terminée par autre chose qu'un THEN, une erreur est signalée. Maintenant, la première instruction (stat1) est convertie. En fait, il s'agit d'un appel récursif de la procédure d'instruction (c'est pourquoi il s'agit d'un analyseur de descente récursif). Veuillez noter que la définition de la syntaxe est également récursive ! En raison de possibles instructions IF imbriquées, les variables de la procédure IF sont enregistrées sur la pile. Après cette déclaration, un ELSE peut suivre. Si c'est le cas, un saut à la fin de stat2 est émis et le saut depuis le début de stat1 est corrigé, puis la deuxième instruction est convertie et le deuxième saut corrigé. Le code assembleur 8086 produit ressemble à ceci (IF .. THEN) :
Tandis qu'un code code assembleur 8086 produit ressemble à ceci pour un IF .. THEN .. ELSE :
Le saut en longueur au début n'est pas toujours nécessaire. Malheureusement, le compilateur ne peut pas prédire la durée de l'instruction. Pour améliorer cela, le saut devrait être remplacé par un saut court et le code suivant déplacé, ce qui compliquerait un peu le compilateur. Toutes les autres structures de programme sont converties de la même manière.
Analyse des expressions arithmétiques
L'évaluation des expressions est un peu plus complexe, car la priorité des opérations doit être prise en compte. La solution dans Turbo Pascal 3 est cependant assez simple (code commençant à 7A70h). Les expressions sont généralement converties en notation polonaise inversée (soit la méthode Infix to Postfix). Il existe 5 groupes d'opérations :
- la négation (priorité la plus élevée)
- le NOT
- multiplication, division,...
- addition, soustraction, ...
- comparaisons, IN (priorité la plus basse)
On pourrait le convertie par la structure de programme suivante :
- Procedure atom;Begin
- Case operation of
- CONST:lecture_constant;
- VAR :lecture_variable; { indexé -> récursive }
- '(' :lecture_expression; { récursive }
- ')' doit_suivre;
- func :lectures_parametres; { récursive }
- appel_emettre_fonction;
- TYPE :'(' doit_suivre; { type conversion, exemmple. Integer(TRUE) }
- lecture_expression; { récursive }
- ')' doit_suivre;
- convertir_type -> type_rechercher
- Else syntax_error;
- End;
- End;
-
- Procedure neg; { négation - }
- Var negflag:Boolean;
- Begin
- negflag:=(operation=neg);
- atom;
- If negflag Then emettre_la_negation;
- End;
-
- Procedure NOT; { NOT }
- Var
- notflag:Boolean;
- Begin
- notflag:=(op=NOT);
- neg;
- IF notflag THEN emettre NOT;
- End;
-
- PROCEDURE mult_level; { multiplication ... }
- Var
- mult_op:type_operation;
- Begin
- NOT;
- While op in mult_ops do Begin
- sauve_le_resultat;
- mult_op:=op;
- NOT;
- emettre_operation(mult_op);
- End;
- End;
-
- Procedure add_level; { addition ... }
- Var
- add_op:type_operation;
- Begin
- mult_level;
- While operation In add_ops do Begin
- save the result;
- add_op:=operation;
- mult_level;
- emettre_operation(add_op);
- End;
- End;
-
- Procedure expression; { comparisons, IN }
- Var
- cmp_op:type_operation;
- Begin
- add_level;
- If operation in cmp_ops THEN BEGIN
- save the result;
- cmp_op:=op;
- add_level;
- emit_operation(cmp_op);
- END;
- End;
Le traitement des multiplication : Le contenu de a doit être empilé, car le registre AX est nécessaire pour la multiplication. Lequel est reconnu en définissant le drapeau push_ax. Si le code suivant utilise le registre AX (détruisant son contenu), il doit émettre PUSH AX. Enfin, si cela s'est produit, le registre doit être restauré par POP CX. Le code produit est plutôt simple. En convertissant l'expression en «b * c + a», le code pourrait être produit :
Pendant l'évaluation, la vérification de type et la conversion de type (Integer ← Real ...) sont également effectuées. L'ensemble d'instructions 8086/8088 est souvent mal utilisé. L'expression a := a + 1 donne ce code (l'instruction INC a serait une meilleure solution) :
Les expressions représentent généralement la majeure partie du code produit, leur conversion est donc très importante.
Optimisation
L'objectif de l'optimisation du code est de réduire la taille et/ou le temps d'exécution du code produit. Il est généralement impossible de trouver une solution optimale, car un compromis espace-temps doit être fait. Le Turbo Pascal 3 n'a pas d'optimiseur. Cependant, pour améliorer l'efficacité de vos programmes par des optimisations manuelles ou par des optimiseurs complémentaires, il est bon de savoir comment fonctionnent les optimisations courantes. Les optimisations peuvent être locales ou globales : elles peuvent couvrir une seule instruction ou un programme entier. L'optimisation globale est beaucoup plus difficile et peut causer des problèmes. Les appels GOTO et les appels de fonction ou de procédure peuvent empêcher l'optimiseur de fonctionner efficacement. Les effets secondaires peuvent provoquer des erreurs difficiles à trouver. Un exemple :
La séquence d'évaluation et donc le résultat dépendent du compilateur utilisé. Les variables ne restent pas nécessairement constantes entre les affectations. Considère ceci :
- wait_int:=FALSE;
- Repeat Until wait_int;
Ainsi, il peut attendre une instruction d'interruption pour définir un drapeau. Un compilateur optimisant convertirait cela en une boucle sans fin ... Les compilateurs C modernes utilisent le mot clef volatile pour éviter ce genre de situation.
Utilisation des variables de registre
De nombreuses opérations de chargement et d'entreposage peuvent être éliminées en utilisant des variables de registre. Sur le 8086/8088, c'est assez difficile, car il y a peu de registres, souvent avec des utilisations spéciales. Par exemple, dans les sous-expressions suivantes :
- c:=(a+b)*d;
- e:=g-(a+b);
La sous-expression (a + b) peut être utilisée deux fois. Les expressions de la forme a[i]:= a[i]+1 sont également une bonne cible pour les optimisations.
Indexation des tableaux
Les références avec des indices constants (a[5]) ou des indices avec un déplacement constant (a[i+1]) peuvent être optimisées. L'indexation des tableaux dans les boucles peut également être considérablement améliorée.
Pliage constant
Les programmes peuvent être plus lisibles si les expressions de constantes peuvent être écrites sous une forme symbolique. Le compilateur peut évaluer ces expressions au moment de la compilation. Les versions ultérieures du compilateur le font.
Réduction de la force
Cette expression signifie remplacer les opérations par des équivalents moins lourds en puissance, par exemple x * 0.2 au lieu de x/5 (les multiplications sont plus rapides que les divisions, prenant plus de cycle d'horloge de microprocesseur).
Optimisation des boucles
Prenons par exemple, la boucle suivante :
- FOR i:=1 TO 100 DO dest[i]:=a+b;
La sous-expression a+b peut être évaluée en dehors de la boucle, car a et b ne changent pas dans la boucle.
Élimination du code mort
Prenons par exemple, le code suivant n'est jamais exécuté :
L'instruction IF peut être omise - la condition n'est jamais remplie. La même chose peut être faite avec des procédures n'étant jamais utilisées. Il existe des optimiseurs éliminant toutes les procédures inutilisées de la bibliothèque d'exécution des programmes traduits par Turbo Pascal. Les versions ultérieures du compilateur le font.
Évaluation des expressions booléennes
Prenons par le code suivant :
Il peut être changé selon le format suivant :
La même chose peut être faite avec les opérateurs OR et NOT. Ne vous attendez jamais à ce que les expressions booléennes soient exécutées complètement ! Les versions ultérieures du compilateur le font.
Alignement variable
Les variables dans le segment de données et sur la pile doivent être alignées sur des déplacements réguliers pour améliorer les performances sur les IBM PC en 16 bits.
Génération de code
Le générateur de code a la tâche difficile de convertir les éléments reconnus par l'analyseur en code exécutable. S'il devient difficile de dire si le code a été généré par un programmeur humain ou par un compilateur, c'est en effet un bon code... Ne vous attendez pas trop à cela de la part de Turbo Pascal 3. Dans les sections suivantes, le code produit par Turbo Pascal 3 sera expliqué.
- Program
- ;biblioth+èque d'exécution, sinon fichier de chaîne
- CALL initmem ;définir des segments
- W mainflag ;voir le code source
- W turbocs,turbods
- W cssize,dssize
- w heapsize,maxhpsz
- w maxfile,stdinsz,stdoutsz
- MOV BP,SP ;cadre de pile
- CALL uncrunch ;étendre les recouvrements
- W link,*+2
- ; partie de définition
- ; partie de programme = programme principal
- XOR AX,AX ;Arr+?t
- CALL progend
Partie de définition
La partie définition peut contenir du code, elle doit donc être ignorée par :
- JMP l1
- l1:
Constantes structurées
Les constantes structurées sont entreposées dans le même format que les variables normales.
Recouvrements
L'espace nécessaire pour les recouvrements n'est pas entreposé dans le fichier .COM. Il est libéré par la procédure de décroissance. Cette situation signifie qu'il remonter au code suivant. Le code est exécuté au début de l'exécution du programme et après le chargement d'une procédure de recouvrement.
- CALL rdover ;lire le fichier de recouvrement
- W $ffff ;procédure de recouvrement maintenant en mémoire = invalide
- B 'over.001' ;nom du fichier de recouvrement
Voici le code dans la section lire à partir du fichier de recouvrement :
- CALL uncrunch ;développer le recouvrement
- W link,*+2 ;lien pour les procédures et les fonctions de recouvrement de décroissance
- W link,* ;pour décroiser
Définitions à l'avance
Pour les définitions à l'avance, un saut vers la définition finale est produit. Le déplacement est inséré lors de la définition réelle.
- JMP defined_proc
Procédures externes
Le code lu à partir d'un fichier externe n'est pas modifié.
Définitions de procédure
Les variables locales des procédures et des fonctions sont toujours entreposées sur la pile. Cette situation signifie que seules les procédures actives occupent de l'espace sur la pile. Cela permet également les appels récursifs. Le transfert des paramètres et l'allocation de l'espace de pile peuvent être assez compliqués, ralentissant ainsi les appels de procédure. Pour chaque procédure, une structure de données appelée cadre de pile ou enregistrement d'activation est construite sur la pile. Le pointeur vers le cadre de pile est toujours entreposé dans le registre BP (le 8086/8088 ne peut pas utiliser le pointeur de pile SP comme registre d'index). La structure du cadre de pile est la suivante :
Position | Description |
---|---|
BP+. | Résultat de la fonction (espace alloué par l'appelant) |
BP+. | Premier paramètre |
BP+4 | Dernier paramètre |
BP+2 | Adresse de retour |
BP+0 | Pointeur vers le cadre de pile de l'appelant |
BP-2 | Nouveau cadre de pile |
BP-4 | Variables locales |
BP-. | Haut de la pile |
Le code d'une entrée de procédure standard ressemble à ceci :
- PUSH BP ; enregistrer l'ancien pointeur
- MOV BP,SP ; définir un nouveau pointeur
- PUSH BP ; enregistrer un nouveau pointeur (pour l'affichage)
- definition_part ; constantes, procédures locales
- SUB SP,#size ; allouer de l'espace pour les variables locales
- ; 1..2 octets: DEC SP
- program part ; la procédure réelle
- MOV SP,BP ; oublier les variables locales
- POP BP ; restaurer l'ancien pointeur
- RET prmsize ; retourne, supprime les paramètres de la pile
- ; pas de paramètres: RET
La manière dont les résultats des fonctions sont transmis dépend de leur type. Les scalaires (entier ...) sont renvoyés dans le registre AX, pour les résultats booléens, les drapeaux sont définis avec OR AX, AX. Les vrais sont de toute façon sur la pile. Les chaînes de caractères doivent être déplacées de manière à n'occuper que leur longueur effective :
Malheureusement, les choses ne sont pas aussi simples. Considérez les définitions de procédure imbriquées :
La procédure interne level2 utilise une variable locale de level1, mais s'appelle également elle-même de manière récursive. Le déplacement de pile de i dépend de l'ordre d'appel. Le Turbo Pascal 3 utilise un soi-disant affichage pour résoudre ce problème. L'affichage contient des pointeurs vers les cadres de pile des procédures d'appel. Chaque procédure ajoute également son propre pointeur à l'affichage. L'affichage est une extension du cadre de la pile.
Position | Description |
---|---|
BP+0 | Ancien pointeur |
BP-2 | Afficher la procédure la plus externe |
BP-2 | Afficher la procédure la plus externe |
BP-. | Afficher |
BP-. | Afficher la procédure en cours |
BP-. | Variables locales |
BP-. | Haut de la pile |
Ceci est maintenu par le code suivant :
Les microprocesseurs plus récents (80186, 80286,...) ont des commandes spéciales pour ces opérations (ENTER, LEAVE). Veuillez noter que le référencement des variables via l'affichage est plus lent que les références normales. Si la vitesse est importante, n'imbriquez pas les définitions de procédure.
Structures de programme
Partie de programme
Les GOTO et EXIT ne sont pas vraiment aussi simples. Parfois, les variables de pile (FOR, WITH) doivent être supprimées, ce qui est fait à la fin de la procédure.
Déclaration
Si la directive d'interruption utilisateur est définie, un INT 3 est émis pour chaque instruction. Cela appelle une routine vérifiant les interruptions de l'utilisateur. Cette fonctionnalité peut être mal utilisée pour tracer un programme ou pour profiler son temps d'exécution. Si cela n'est utilisé nulle part dans le programme, vous pouvez également insérer des points d'arrêt sous forme d'instructions INLINE pour le débogage avec DEBUG.
WHILE
La boucle WHILE est résolue de la façon suivante :
REPEAT
- l1: statement
- condition ; évaluer la condition
- J.. l2 ; condition remplie: fin
- JMP l1 ; non satisfait: répéter
- l2:
FOR
Le compteur (entreposé sur pile) et la variable de contrôle sont indépendants : les affectations à la variable de contrôle ne modifient pas le nombre d'exécutions de boucle.
- ; valeur de départ -> AX
- PUSH AX
- ; valeur finale -> AX
- POP CX
- XCHG CX,AX
- SUB CX,AX ; calculer la différence
- JGE l1 ; (DOWNTO: JNG)
- JMP l3 ; ne pas exécuter
- l1: INC CX ; (DEC CX)
-
- l2: PUSH CX ; compteur de sauvegarde
-
- POP CX ; compteur de restauration
- DEC CX ;(INC CX)
- JZ l3 ;0: terminé
- INC loop_var ;(DEC) variable de contrôle de mise +à jour
- JMP l2 ;boucle
- l3: ;fin
CASE
Prenons pour exemple le code suivant :
Nous aurons un code générer ressemblant à ceci :
- ;évaluer la sélection
- CMP AX,#2 ;comparer
- JZ ok1 ;:oui
- CMP AX,#5
- JZ ok1 ;:oui
- JMP test2 ; essayez le cas suivant
- ok1: ;correcte - exécuter
- JMP endcase ;aller à la fin
- test2:
- CMP AX,#7 ;vérifier le sous-intervalle
- JL test2no ;:non
- CMP AX,#9
- JLE ok2 ;:oui
- test2no:
- JMP else ;non: exécute la partie ELSE
- ok2: ;correcte - exécuter
- JMP endcase
- else: ; partie ELSE
- endcase:
Les instructions CASE compliquées peuvent dépasser l'intervalle des sauts courts. Dans ce cas, ce qu'on appelle des sauts longs par hanches sont émis :
GOTO
Un saut est émis. Si le GOTO laisse un bloc WITH ou FOR, la pile doit être nettoyée. Ceci est reconnu et corrigé à la fin de la partie programme.
WITH
Le compilateur a une pile WITH interne. Les pointeurs pour les WITH indexés sont entreposés sur la pile :
Si l'adresse est connue au moment de la compilation, cela n'est pas nécessaire.
Appels de procédure
Si la directive de compilation $K+ est définie, un contrôle de pile est exécuté :
Ensuite, les paramètres sont évalués et transmis. Paramètre normal :
Pour les paramètres de chaîne de caractères :
Pour les paramètres d'ensemble (SET) :
Dans le cas des réels, le paramètre est déjà sur la pile.
Variable structurée
Paramètres VAR placer le pointeur sur la pile.
Pour les procédures de recouvrements, le code suivant doit être inséré :
Ensuite, la procédure est appelée par le code suivant :
- CALL procedure
Appel de fonction
L'espace de pile est alloué pour le résultat (SUB SP, #space_needed), tout le reste est identique. Au retour, le résultat est sur pile (réel, chaîne de caractères) ou en AX (entier, scalaire).
Appel de procédures et de fonctions standard
Les procédures standard comme la lecture et l'écriture peuvent avoir n'importe quel nombre de paramètres de n'importe quel type. Cela signifie qu'une grande flexibilité est nécessaire. Ce problème est résolu dans Turbo Pascal 3 de manière très efficace : la table de procédure standard ne contient que l'adresse de la routine de conversion correspondante. Cette routine émet le code pour lire les paramètres (certains d'entre eux passés dans les registres!) Et pour appeler la bibliothèque d'exécution. Certaines fonctions (Swap, Hi, Lo) n'appellent pas de procédure mais créent du code en ligne à la place.
Affectations et expressions
Scalaire/pointeur : les registres ES et DI sont enregistrés, si nécessaire. Cela n'est fait que si l'expression ne consiste pas en une constante ou une simple variable. Variable normale :
Dans le cas de variable structurée :
Les conversions de type son traiter de la manière suivante :
Expressions
Les opérations arithmétiques pour les scalaires sont émises par ecalc. Pour chaque opération, un bloc de paramètres (commençant à à l'adresse 973E) contrôle la génération du code.
Expression | Assembleur |
---|---|
expr1 - 5 | SUB AX,#5 |
expr1 - var | SUB AX,var |
expr1 - expr2 | XCHG CX, AX (premier résultat en CX, deuxième en AX) SUB AX, CX |
Les expressions du type a:=a+1 ne sont pas bien converties. a:=succ(a) est une meilleure solution, mais n'est pas optimal.
Définir des expressions
Les ensembles sont entreposés sous une forme compressée et doivent être développés à leur taille maximale (32 octets) pour effectuer des opérations sur les ensembles. En raison de cet ensemble, les opérations ont tendance à être lentes. Les constructeurs d'ensemble sont gérés de manière inefficace. [5, var1..var2] est convertie comme ceci :
Si les paramètres sont variables, tout va bien. Pour les ensembles constants, c'est désastreux.
- Dans le cas d'un test conditionnel :
- Dans le cas d'un ensemble passant par des constantes :
- Dans le cas d'une évaluation de cas :
La solution conventionnelle. Très lent, car l'ensemble est toujours construit lorsque cela est exécuté. Prend beaucoup de place pour les ensembles compliqués.
Cela prend un peu plus de place pour cet exemple (la constante définie prend 32 octets), mais est beaucoup plus rapide.
Pour les cas simples, cela donne le code le plus court et le plus rapide.
Références de variables
MEM / MEMW
Utilisez ABSOLUTE pour les variables avec une adresse constante.
Indexation avec WITH
Dans un bloc WITH, tous les noms de variables doivent être recherchés d'abord dans les portées des enregistrements actifs, puis dans la table de symboles standard. Cela peut prendre un certain temps. Si le déplacement de base n'est pas connu au moment de la compilation (WITH rec[var] DO), un pointeur WITH doit être calculé et entreposé sur la pile, sinon cela se fait au moment de la compilation.
Indexation de tableau, indexation de chaîne de caractères
Si nécessaire, ES et DI doivent être sauvegardés avant l'évaluation. Un code différent est produit en fonction de l'index. Index constant : L'index est vérifié au moment de la compilation, multiplié par la taille de l'élément et ajouté au déplacement de base, aucun code n'est émis.
Index de variable avec contrôle de l'intervalle :
Index de variable sans contrôle d'intervalle : La soustraction de la limite inférieure peut être omise, elle est multipliée par la taille de l'élément puis soustraite du déplacement de base. L'index est multiplié par la taille de l'élément. Ceci est optimisé pour certaines tailles d'élément importantes :
L'index est ensuite entreposé dans DI :
- XCHG DI,AX
ou ajouté à l'index existant :
- ADD DI,AX
Indexation des enregistrements
C'est très simple: le déplacement de la variable d'enregistrement est ajouté au déplacement de mémoire de la variable.
Indexation du pointeur
Les pointeurs sont chargés avec LES DI, pointer_var.
Utilisation des modes d'adressage
La procédure einstr émet une commande utilisant le mode d'adressage correct. Si nécessaire, un préfixe de segment (CS: ou ES:) est inséré. non indexé, pas sur la pile :
- MOV AX,var
Si indexé :
Dans le cas d'une pile et variables locales :
Dans le cas d'une pile et de variables locales des appelants :
Calculer le pointeur vers la variable
Le déplacement est lu dans le registre DI :
Ensuite, le segment est remis sur la pile :
- PUSH CS/DS/ES/SS
Lire une variable
Pour lire une variable scalaire :
Pour les variables d'un octet, l'instruction XOR AH, AH est inséré - le Turbo Pascal 3 utilise toujours des variables entières en interne. Pour lire des variables de pointeurs :
Pour lire des variables réels :
- ; définir le pointeur sur la variable
- CALL xldreal
Pour lire des variables de SET :
Pour lire des variables de chaîne de caractères STRING :
- ; Définir le pointeur sur la variable
- CALL strload
Entreposage la variable
Scalaire : si la directive {$R+} est défini, la vérification de l'intervalle est effectuée :
Si la variable n'est ni indexée ni sur la pile, il y a à nouveau une format d'entier courte :
Sinon, le MOV correct est émis par un einstr. Le pointeur est :
Pour un nombre réel :
- ; définir le pointeur sur la variable
- CALL xstoreal
Pour une chaîne de caractères :
Pour un ensemble (SET) :
Table des symboles
Le gestionnaire de table de symboles doit insérer et rechercher des symboles de tout type. La difficulté est que les définitions peuvent être de n'importe quelle complexité et que les noms peuvent avoir n'importe quelle longueur. La structure mise en oeuvre dans Turbo Pascal 3 représente étroitement la définition et la séquence de référence, ce qui facilite la navigation à travers les types complexes. Les symboles sont recherchés en commençant par la définition la plus récente. Pour les nouvelles définitions, le bloc actuel (limité par la borne définie au début d'une définition de procédure) et la table de mots-clefs sont recherchés pour les définitions en double. Ainsi, il est possible de remplacer les anciennes définitions. À la fin d'une procédure, les variables locales peuvent être supprimées de la table des symboles. Cela réduit l'utilisation de la mémoire et le temps de recherche.
Structure d'entrée de la table des symboles
La table de symboles symtab est entreposée dans le segment de pile (la pile réelle n'a pas besoin de beaucoup d'espace) et s'agrandit. Les entrées ont cette structure de base :
Position | Description |
---|---|
off | Entrée suivante |
... | ... |
off-2 | Mot de balise = type d'entrée |
off-4 | Longueur du nom |
off-5 | Nom |
off-. | Entrée |
0 | Déplacement vers l'entrée suivante |
La table des symboles est toujours recherchée en arrière, en regardant d'abord les entrées les plus récentes. Une recherche linéaire est utilisée. Des exemples d'entrées de table de symboles peuvent être trouvés dans les tables du compilateur (à partir de l'adresse 9277). Voici les mots de balises :
Valeur | Nom | Description |
---|---|---|
0100 | Label | 1: imbrication des procédures (pour éviter les sauts dans ou en dehors des procédures) 2: 0 = ok, FF = pas encore défini 4: déplacement |
0200 | Constant | 1: type de constante 3: constante - les chaînes de caractères sont entreposés à l'envers. |
0300 | Type | 2: pointeur vers la définition de type |
0400 | Variable | Pour les sous-variables d'un enregistrement, l'octet de poids faible du mot d'étiquette est le numéro de la définition d'enregistrement à laquelle appartient cette entrée. 2: pointeur vers la définition de type 4: décalage 5: 0 = normal, FF = indirect (VAR) 6: segment FF = DS FE = CS FD = ES (via le pointeur) .. = SS, le numéro correspond au niveau d'imbrication de la procédure Pour les variables ABSOLUTE de format $B800:0, un pointeur est entreposé dans le segment de code, l'entrée de la table d'identificateurs de symboles CS indirecte. |
0500 | Procedure | Procédure |
0600 | Function | - 2: fonction uniquement - pointeur vers le type de résultat - 4: fonction uniquement - déplacement sur la pile - 5: fonction uniquement - 0 = normal, FF = indirect - 6: fonction uniquement - segment: SS - 8: décalage de code de procédure / fonction - A: position dans le fichier de recouvrement / du saut vers l'avant - C: 0 = ok, FF = définition avant - E: durée de la procédure de superposition -10: nombre de listes de paramètres : - 2: type pointer | peut être répété - 3: 0 = normal, FF = paramètre VAR | - 4: nombre de paramètres de ce type | - 5: noms | Les paramètres sont également répertoriés en tant que variables locales. |
0000, 0800 | Subentries | Sous-entrée |
Structure des sous-entrées
Les entrées normales ne sont pas suffisantes pour la description de types complexes. Des sous-entrées sans nom sont utilisées pour cela. Pour les définitions complexes, cela donne une structure arborescente. ARRAY[1..15] OF INTEGER est un tableau ayant la sous-intervalle 1..15 comme type d'index et INTEGER comme type d'élément.
Recherche dans la table des symboles
Le Turbo Pascal 3 utilise une recherche linéaire pour trouver des symboles dans la table des symboles. Cela peut être lent. Un programme dans le pire des cas peut réduire la vitesse de compilation à 0,022 ligne par seconde...
Gestionnaire d'erreurs
De nombreux compilateurs essaient de continuer la compilation après qu'une erreur a été trouvée. La difficulté à ce sujet est que cela ne devrait pas déclencher une avalanche de messages d'erreur sans signification. Le Turbo Pascal 3 s'arrête toujours si une erreur est trouvée. Cela peut également être considéré comme un avantage : le programmeur est obligé de résoudre les problèmes un par un.
Bibliothèque d'exécution
Dans la bibliothèque d'exécution, toutes les procédures et fonctions standard nécessaires à l'exécution des programmes sont entreposées. Le Turbo Pascal 3 le fait d'une manière plutôt inutile (mais simple): il insère toujours toutes les procédures. Les versions ultérieures du compilateur incluent uniquement les procédures étant réellement utilisées.
Carte mémoire
Les segments sont répartis comme suit :
Tas
L'entreposage du tas est alloué en blocs dont la taille est un multiple de 8 octets. Les blocs libres sont conservés avec cette structure :
Position | Description |
---|---|
+0 | Ce champ contient un pointeur vers le bloc libre suivant → liste chaînée. |
+4 | Ce champ contient la longueur du bloc libre. |
+8 | Ce champ contient un bloc libre. |
Le Hpstrt pointe vers le premier bloc libre. Le dernier bloc de la liste - l'espace libre entre la pile et le tas - est marqué par un 0. S'il n'y a pas assez d'espace entre le tas et la pile, une erreur est signalée.
Arithmétique à virgule flottante
Les nombres à réels (à virgule flottante) sont divisés en deux parties : l'exposant donne l'ordre de grandeur, la mantisse donne la précision nécessaire. Voici la formule mathématique associé :
nombre réel = mantisse x 2exposant |
La mantisse est une fraction binaire avec une précision de 40 bits. La mantisse représente toujours un nombre compris entre 0,5 et 1, c'est-à-dire qu'elle est normalisée. Cette situation signifie que le bit le plus significatif est toujours 1. Ici, le signe est entreposé. Les exemples utilisent des nombres décimaux pour plus de simplicité.
Veuillez noter que les additions et soustractions en virgule flottante peuvent provoquer de grosses erreurs d'arrondi, si les exposants diffèrent trop. On prétend souvent que l'arithmétique BCD cause moins d'erreurs. Ce n'est pas correct - ils ne sont pas aussi visibles. Certaines calculatrices effectuent en fait des arrondis cosmétiques, si le résultat est proche d'un nombre entier. La meilleure façon d'éviter les erreurs d'arrondi est de calculer les montants en cents et non en dollars. Les multiplications et les divisions BCD sont lentes. Cependant, BCD a un avantage: les conversions vers et depuis ASCII sont beaucoup plus rapides. Comme les applications métier consistent généralement principalement en des ajouts et des soustractions, le BCD peut en fait être plus rapide.
Les racines carrées sont évaluées en utilisant l'approximation de Newton. C'est une bonne solution, mais pas la meilleure : le 8087 fait des racines carrées plus rapidement que les divisions ! Les fonctions transcendantales sont évaluées à l'aide des polynômes standard trouvés dans les livres de mathématiques. Ne vous attendez pas à beaucoup de vitesse et de précision des transcendantaux Turbo Pascal 3.
Bugs
Grâce à la relative simplicité des algorithmes utilisés, le Turbo Pascal 3 est quasiment exempt de bogues. Enfin presque.
Définir comme paramètre de procédure
Si le dernier élément est dans l'intervalle de 248..255 et le premier au-dessus de 7, cela ne fonctionne pas. Redéfinissez l'ensemble ou transmettez-le en tant que paramètre VAR.
SizeOf
Parfois, une charge redondante est émise.
UpCase
Le type de paramètre n'est pas vérifié. Essayez UpCase(15).
WHILE
Le mot réservé DO peut être omis.
Vitesse du compilateur
Le Turbo Pascal 3 est peut-être plus rapide que la plupart des autres compilateurs, mais il y a encore une large marge d'améliorations :
- Table de symboles : utiliser le hachage
- Inclure des fichiers : utiliser un tampon plus grand
- Ne copiez pas les lignes source dans un autre tampon
L'éditeur pourrait être beaucoup plus rapide. L'écran est ré-affiché de manière intelligente (en utilisant DelLine et InsLine). Sur un terminal, cela peut être plus rapide - avec la vidéo cartographiée en mémoire, c'est une nuisance. La recherche et le remplacement sont lents.
Écrire des programmes plus rapides avec Turbo Pascal 3
Évitez les fonctions de chaîne de caractères standard :
- Utilisez ORD(ST[0]) au lieu de LENGTH(ST).
- Utilisez MOVE au lieu d'attributions de chaînes de caractères :
MOVE(st1,st2,max_len+1);
Écrivez des constantes réelles avec un point décimal (10.0 au lieu de 10). Ce détail élimine les conversions.
Utiliser GOTO si nécessaire. Les GOTO de Turbo Pascal 3 sont meilleurs que les GOTO de
BASIC : les noms peuvent être utilisés à la place des nombres.
Les enmtrée/sorties peuvent être améliorées si des tampons plus grands sont utilisés (utilisez des multiples de 512 octets pour de meilleurs résultats). Utilisez BlockRead
et BlockWrite. Si de gros blocs sont lus ou écrits, la surcharge du DOS et Turbo Pascal 3 n'a pas autant
d'importance que pour les petits blocs.