Dans l'article précédent, on avait vu la création d'une variable dans la zone principale de la mémoire. Cette variable a par défaut un contenu nul, et ne s'occupe pas de savoir si ce contenu est un nombre ou une chaîne de caractères. Les quatre octets de contenus qui suivent les deux octets du nom sont donc tous les quatre à $00
.
Pour qu'une valeur soit associée à une variable, il faut une instruction d'assignation, directement via LET
(éventuellement de manière implicite), plus indirectement avec une instruction FOR
, ou encore plus indirectement par un couple READ
/DATA
.
Dans tous les cas, la valeur à assigner à la variable est le résultat de l'évaluation d'une expression, c'est-à-dire le résultat d'un calcul numérique ou d'une opération à partir de chaînes.
Afin de comprendre comment sont créées et stockées les chaînes de caractères, c'est donc du côté de l'évaluation d'expression qu'il faut commencer.
Évaluation d'expression
L'évaluation d'une expression commence en $2861
et nous n'allons pas nous y attarder. Nous suivons la piste immédiatement vers la routine de lecture d'une valeur depuis le buffer d'entrée. Cette routine se situe en $28d8
et commence comme suit :
parse_value: xor a,a
ld (valtyp),a
rst chget
jp z,missing_op
jp c,str_to_num
cp a,'&'
jp z,str_hex_dec
call a_to_z_2
jr nc,str_to_var
cp a,'+'
jr z,parse_value
cp a,'.'
jp z,str_to_num
cp a,'-'
jr z,str_to_min
cp a,'"'
jp z,str_to_str
cp a,$b7 ; 'NOT'
jp z,str_to_not
cp a,$b4 ; 'FN'
jp z,str_to_fn
sub a,$c3 ; 'SGN'
jr nc,str_to_func
Voici toute une série de tests pour déterminer ce que contient l'opérande pointée actuellement par HL
.
On remarque au tout début que la valeur par défaut de l'expression en cours est mis à 0
(c'est-à-dire : valeur numérique).
Puis le premier caractère est lu et la suite de tests ressemble à ceci :
- Est-ce qu'on est à la fin de la ligne ? Alors il manque quelque chose...
- Est-ce que c'est un chiffre ? Alors on commence à convertir l'entrée en nombre
- Est-ce que ça commence par
&
? Alors on commence à décoder un nombre hexa - Est-ce que c'est une lettre ? Alors on va lire une variable
- Est-ce que c'est un '+' ? On l'ignore et on boucle un caractère plus loin
- Est-ce que c'est un '.' ? Alors on commence à convertir l'entrée en nombre
- Est-ce que c'est un '-' ? Alors on démarre une sous-expression qui sera inversée
- Est-ce que c'est un '"' ? Alors on décode une chaîne !
- Etc... (les trois derniers cas sont pour
NOT
, une fonction utilisateur, ou une fonction prédéfinie, puis on continue avec le traitement des parenthèses)
D'après cette liste, on part donc vers str_to_str
.
Les chaînes à la chaîne
Arrivée dans str_to_str
, on a HL
qui pointe vers une chaîne qui commence avec des guillemets. La première étape va être de chercher la fin de la chaîne et de compter le nombre de caractères.
str_to_str: ld b,'"'
ld d,b
direct_str: push hl
ld c,$ff
loop_str: inc hl
ld a,(hl)
inc c
or a,a
jr z,create_str
cp a,d
jr z,create_str
cp a,b
jr nz,loop_str
create_str: cp a,'"'
call z,skipch
ex (sp),hl
inc hl
ex de,hl
ld a,c
Cette routine commence par placer le caractère guillemets dans les registres B
et D
. Les caractères présents dans B
et D
sont des terminateurs potentiels. Cette routine est en effet appelée par un autre chemin directement en direct_str
avec d'autres terminateurs possibles.
Note : ces autres terminateurs possibles sont :
et ',' dans le cas où la chaîne est lue par une instruction READ
depuis une séquence de DATA
.
La suite du préambule de la routine se fait en poussant sur la pile le pointeur sur la ligne en exécution et en initialisant C
avec -1
. C
est le compteur de caractères. Au passage, on peut en déduire que les chaînes de caractères auront donc comme longueur maximale 255.
Puis débute la boucle loop_str
, qui commence par avancer le pointeur HL
sur le caractère suivant, récupère la valeur de ce caractère dans A
et incrémente le nombre de caractères.
Le premier test vérifie si A
est nul. Si c'est le cas, on a atteint la fin de la chaîne et il est temps de la créer. De même que si le caractère est égal à l'un des deux terminateurs. Dans le cas contraire, la boucle est bouclée et le caractère suivant traité.
Note : mais et s'il y a plus de 255 caractères avant de trouver un terminateur ? Ça ne se passe pas très bien... Il n'y a pas de tests et vous pouvez vérifier (c'est un peu long) qu'il peut se passer des choses étranges.
Avant de créer la chaîne, il faut mettre les choses en place. Si le dernier caractère sont des guillemets, une routine va les consommer et ignorer tout ce qui est inintéressant, pour recaler HL
sur la prochaine valeur ou instruction.
Ce pointeur est échangé avec le haut de la pile, qui contenait le début de la chaîne. Ce début de chaîne est avancé de 1 pour ignorer les premier guillemets (le chemin READ
s'arrange pour mettre HL
au bon endroit en sachant qu'il sera incrémenté ici).
Puis le pointeur de début de chaîne est transféré dans DE
et le nombre de caractères lus dans A
.
Création de la chaîne temporaire
À présent que l'on sait où est la chaîne (pointée par DE
) et combien de caractères elle contient, l'étape suivant consiste à l'extraire dans un endroit où l'évaluation de l'expression ou l'assignation pourra la trouver.
call crt_tmp_str
cpy_to_pool: ld de,dsctmp
ld hl,(temppt)
ld (faclo),hl
ld a,$01
ld (valtyp),a
call cpy_detohl_4
rst de_compare
ld (temppt),hl
pop hl
ld a,(hl)
ret nz
ld de,$001e
jp error_out
Tout commence par un appel à crt_str_dsc
qui créé un descripteur temporaire de chaîne à l'adresse dsctmp
($499b
). Dans ce buffer qui sert aux opérations sur les chaînes, la routine placera en premier octet la taille de la chaîne, puis rien de spécial, puis la valeur de 'DE' sur les deux derniers octets.
Les descripteurs de chaînes font donc 4 octets, dont le deuxième est inutilisé.
Puis, DE
prend la valeur de dsctmp
, le buffer temporaire qui vient d'être initialisé, et HL
la valeur contenue dans la variable système temppt
. Ce pointeur est initialisé par le BASIC vers le buffer tempst
, qui est un buffer de 120 octets réservé.
Ce pointeur est placé dans l'accumulateur flottant, qui maintient en fait toute valeur courante d'une expression, qu'elle soit numérique (lorsque (valtyp)
vaut 0
) ou chaîne (lorsque (valtyp)
vaut 1
).
Et d'ailleurs, (valtyp)
passe à 1
pour indiquer la nature du contenu de l'accumulateur flottant.
L'appel suivant est une routine qui copie 4 octets pointés par DE
vers ce qui est pointé par HL
. Autrement dit, le descripteur de chaîne qui vient d'être créé est copié vers le buffer temporaire pointé par HL
.
Comme dsctmp
est placé astucieusement après le buffer tempst
, si jamais, après copie, HL
est égal DE
, alors c'est qu'on a atteint la fin de l'espace de travail, une erreur est latente, traitée un peu plus loin. Comme 120 (la taille du buffer) est divisible par 4 (la taille des descripteurs) on est assuré de tomber juste, et que HL
ne dépasse jamais DE
.
En attendant de traiter l'erreur, il s'agit de mettre les choses en ordre. La variable système (temppt)
est mise à jour avec la nouvelle valeur de HL
, puis on récupère le pointeur sur la ligne depuis la pile, et le caractère pointé par HL
est placé dans A
, tout est prêt pour continuer le décodage.
Enfin, on sort de la routine si la dernière comparaison n'était pas nulle (il reste de la place dans le buffer temporaire) ou bien on saute vers une erreur indiquant à l'utilisateur que l'opération sur les chaînes de caractères était trop complexe.
Note : il existe 30 emplacements de descripteurs de chaîne dans le buffer temporaire avant que la routine ne laisse tomber avec un message d'erreur. En sachant qu'une expression comme PRINT "ABC" + "CDE" + "DEF"
en consomme 2, ça laisse de la marge...
Association de la variable
Pour l'association de la variable avec sa valeur, voyons le cas de l'instruction LET
(qui est de toute façon appelée par FOR
et READ
).
Passons rapidement sur le début de l'instruction LET
qui récupère l'adresse de la variable à gauche du signe égal selon la méthode décrite dans l'article précédent, appel l'évaluation de ce qui est à droite du signe égal, et vérifie que les types sont cohérents des deux côtés (soit numérique, soit chaîne de caractères).
Une fois tout ceci en place, l'association de la chaîne elle-même a lieu :
let_string: push hl
ld hl,(faclo)
push hl
inc hl
inc hl
ld e,(hl)
inc hl
ld d,(hl)
ld hl,(txttab)
rst de_compare
jr nc,crtstrentry
ld hl,(strend)
rst de_compare
pop de
jr nc,pop_string
ld hl,dsctmp
rst de_compare
jr nc,pop_string
defb $3e
crtstrentry: pop de
call bc_from_tmp
ex de,hl
call save_str
pop_string: call bc_from_tmp
pop hl
call cpy_detohl_4
pop hl
ret
En début de routine, HL
pointe vers la variable à gauche du signe =
, on sauve cette adresse sur la pile pour plus tard.
Puis on récupère dans HL
la valeur de la dernière expression évaluée, qui est dans l'accumulateur flottant. On pousse aussi cette valeur sur la pile et on va chercher deux octets plus loin le pointeur vers la chaîne de caractère elle-même, qui est placée dans DE
.
À présent, il s'agit de savoir où sont situés ces octets de chaînes. Le premier cas est une comparaison avec (txttab)
. Si les caractères sont avant, c'est qu'ils sont dans les variables systèmes, et donc dans un endroit volatile, il va donc falloir les copier ailleurs et c'est ce que va faire le saut en crtstrentry
.
Note : si vous vous amusez avec les pointeurs de zones mémoire du BASIC pour déplacer le contenu du code, gardez en tête que pour le BASIC, une chaîne située avant le code est volatile.
Le second test vérifie si la chaîne se situe avant (strend)
. Si c'est le cas, c'est que la chaîne se trouve dans le programme.
Note : en toute rigueur, la comparaison aurait du être faite avec (vartab)
, car il n'y a pas de contenu de chaînes entre (vartab)
et (strend)
. En regardant d'autres dérivés du BASIC-80, je pense qu'il s'agit d'une adaptation un peu hâtive, car d'autres BASIC-80 semblent placer leurs chaînes différemment. Même si le pointeur de comparaison n'est pas exactement le bon, le test fonctionne néanmoins, et ce n'est pas plus lent.
Si la chaîne se trouve dans le programme, on va pouvoir conserver ce pointeur sans dupliquer les octets ailleurs. En effet, un programme n'est pas volatile et le moindre changement dans le listing efface toutes les variables. On est donc assuré que les chaînes de caractères présentes dans le programme lorsque celui-ci tourne restent en place.
Note : cela donne quelques contraintes si vous vous amusez à modifier le listing en cours de route depuis le programme qui tourne...
Le troisième test, enfin, vérifie si le contenu de la chaîne ne serait pas par hasard dans un autre buffer temporaire, celui où l'on met le descripteur temporaire (et qui se situe juste avant dsctmp
)
Note : ce troisième test est étrange. Ce buffer est situé dans les variables système et donc est déjà avant le code BASIC. Je ne vois donc pas comment on peut arriver ici. Je pense que c'est un reliquat d’adaptation du BASIC-80 où le buffer temporaire se situe après le code BASIC.
Le defb $3e
est un instruction morte permettant d'éviter l'exécution du POP DE
qui suit. En effet, ce POP DE
pour récupérer le pointeur sur le descripteur de chaîne a déjà été fait lorsqu'on arrive par là.
crtstrentry
replace le pointeur HL sur les informations de la chaîne temporaire la plus récente (la plus en haut du buffer temporaire), puis cette adresse et échangée avec celle tenue dans DE
qui est aussi le pointeur vers ce même descripteur.
Note : ici, je ne sais pas dans quel cas HL
et DE
peuvent être différent. L'idée est d'enlever le descripteur de chaîne du buffer temporaire en ajustant le pointeur sur ce buffer (temppt)
, le buffer temporaire étant manipulé comme une pile, la routine bc_from_tmp
est en quelque sorte le pop
de cette pile, dont la valeur part dans BC
, mais avec une sécurité. Si le pointeur HL
n'est pas celui qui était attendu DE
alors le pop
n'a pas lieu, c'est juste une récupération de la valeur en haut de la pile.
Avec les informations récupérées, un appel à save_str
est effectué, et nous verrons ça juste après.
Dans tous les cas, la description de chaîne la plus récente du buffer temporaire est à nouveau récupérée, l'adresse de la variable popée de la pile dans HL
et le descripteur temporaire copié vers la valeur de cette variable.
Après une remise en ordre de la pile, on rend la main, la variable est maintenant associée à la valeur de la chaîne.
Et la création ?
Dans le cas où la chaîne de caractères doit être sauvée quelque part, alors un appel à save_str
est fait.
save_str
se situe en $3646
et est comme suit :
save_str: ld a,(hl)
inc hl
inc hl
push hl
call alloc_str_mem
pop hl
ld c,(hl)
inc hl
ld b,(hl)
call crt_str_dsc
push hl
ld l,a
call copy_str
pop de
ret
En entrée, HL
pointe vers un descripteur de variable de type chaîne. Le premier des 4 octets contient donc le nombre de caractères, qui est récupéré dans A
. Puis HL
est positionné sur le premier octet de l'adresse du contenu et cette adresse est poussée sur la pile.
L'appel à alloc_str_mem
vérifie ensuite s'il reste assez de place dans la mémoire dédiée aux chaînes pour ajouter A
octets.
Si la routine de vérification ressort, c'est qu'il y a de la place (une erreur aurait été immédiatement émise sinon) et une chaîne de A
caractères a été allouée, (fretop)
ajusté, et DE
pointe vers cette nouvelle allocation.
On récupère alors HL
pour obtenir dans BC
l'adresse actuelle du contenu de la chaîne.
Un appel à crt_str_dsc
crée une nouveau descripteur dans le buffer temporaire avec DE
comme pointeur de contenu.
Puis copy_str
est appelé après avoir sauvé le pointeur vers le nouveau descripteur dans la pile et mis la taille de la chaîne dans L
.
Je ne copie pas le code de copy_str
ici. Il est extrêmement simple et copie L
caractères de la zone pointée par BC
vers la zone pointée par DE
. Autrement dit, de la chaîne source vers l'emplacement nouvellement alloué.
Au retour, DE
prend la valeur du nouveau descripteur de chaîne, qui est actuellement dans le buffer temporaire et sera récupéré par la fin de la routine de l'instruction LET
.
Ouf!
Ramasse miettes
En fait... ce n'est pas tout à fait complet. Lors de la tentative d'allocation de chaîne alloc_str_mem
, s'il n'y a plus de place dans la mémoire dédiée, un ramasse miettes est lancé (garbage collection). Cette routine va compacter la mémoire des chaînes de caractères en comblant les trous des données qui ne sont plus valides, d'anciennes valeurs de chaînes qui ne sont plus pointées par aucune variable.
C'est un gros morceau qui doit parcourir les variables mais aussi les tableaux, je laisse ça de côté (pour le moment ?).
À la fin de cette routine, l'allocation est tentée à nouveau. Si lors de cette nouvelle tentative, il n'y a toujours pas assez de mémoire, alors l'erreur est vraiment lancée.
Un peu de BASIC
C'est un peu la tradition de ces articles, voyons maintenant un programme en BASIC qui affiche la valeur des variables. Puisque toutes les variables sont effacées au démarrage d'un programme, il est nécessaire d'en initialiser dans le programme.
10 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
20 PRINT"1":A$="ABC"
30 GOSUB 1000
40 PRINT"2":B$="DEF"
50 GOSUB 1000
60 PRINT"3":A$=""
70 GOSUB 1000
100 END
1000 VT=FNPK(&"49D8")
1010 AT=FNPK(&"49DA")
1020 FOR PT=VT TO AT-1 STEP 6
1030 T1=PEEK(PT)
1040 T2=PEEK(PT+1)
1050 IF (T2 AND 128)=0 THEN 1100
1060 PRINT CHR$(T1 AND 127);
1070 PRINT CHR$(t2 AND 127);
1080 PRINT "$="+CHR$(34);
1090 GOSUB 2000:PRINT CHR$(34)
1100 NEXT PT
1200 RETURN
2000 V=FNPK(PT+4)
2000 C=PEEK(PT+2)
2010 IF C=0 THEN RETURN
2020 V=FNPK(PT+4)
2030 FOR I=1 to C
2040 PRINT CHR$(PEEK(V+I-1));
2050 NEXT I
2060 RETURN
Va afficher
1
A$="ABC"
2
A$="ABC"
B$="DEF"
3
B$="DEF"
La partie du programme entre 10 et 100 s'occupe de manipuler des variables et d'appeler l'affichage de leur contenu.
La partie entre 1000 et 1200 regarde la liste des variables comme dans l'article précédent, mais ne sélectionne que celles de types chaînes de caractères.
La partie à partir de 2000 va chercher le nombre de caractères et le pointeur vers les données pour afficher le tout, caractère par caractère.