Site logo

Triceraprog
La programmation depuis le Crétacé

  • VG5000µ, les hooks d'appel ()

    Dans la série des hooks sur VG5000µ, voyons en cette fois la paire probablement la plus simple. Ça sera donc rapide.

    Le hook CALL

    Du nom de calhk et d'adresse $47D3, ce hook est utilisé en interne par la commande BASIC CALL.

    Le code de cette instruction est extrêmement simple :

    inst_call:   call     eval_num_ex
                 call     deint_impl
                 ld       a,$c3
                 ld       (calhk),a
                 ld       (calhk+1),de
                 jp       calhk
    

    En premier, l'argument passé à CALL est évalué, puis est ensuite transformé en entier sur 16 bits.

    Cette adresse, précédée par l'opcode pour JP à une adresse absolue 16 bits est placée dans le hook calhk. La routine saute enfin vers cette adresse, qui agit comme un tremplin vers l'adresse indiquée à l'instruction CALL.

    Comme c'est le RET de la routine appelée qui fera office de RET pour l'ensemble de l'instruction CALL, la préservation de l'environnement est à la charge de la routine appelée. Essentiellement, il vous faut préserver HL, qui pointe sur la fin de l'instruction. Si vous ne le faite pas, il y a de bonnes chances que vous obteniez un Erreur de Syntaxe en retour d'instruction.

    Pour un exemple d'utilisation, voyez les articles précédents, qui montent une routine assembleur en mémoire puis l’appellent.

    Récupérer des paramètres

    Que HL pointe juste après le CALL se révèle pratique pour récupérer des arguments potentiels. Dans l'article sur les hooks de périphériques, j'étais allé chercher un argument de type chaîne de caractères. Cette fois, je vais aller chercher trois arguments de type nombre. Le premier entier sur 8 bits, le second sur 16 bits, et le troisième un nombre quelconque (dans les limites du VG5000µ).

    Les deux premiers arguments seront obligatoires, le troisième optionnel.

    Grâce à ça, il est possible d'ajouter des commandes au BASIC, sans leur donner un nom cependant.

        defc out_number = $0726
        defc out_fp = $0731
        defc deint_impl = $2552
        defc eval_num_ex = $284d
        defc type_eq_num = $2850
        defc read_expr = $2861
        defc getbyt_impl = $2aa5
        defc out_str = $36aa
    
        org $7A00           ; Spécification de l'adresse mémoire d’implantation
    
    cmd_routine:
        rst  $08            ; Vérification obligatoire du caractère qui suit
        defb ','
    
        call getbyt_impl    ; Récupération d'un entier 8 bits dans A
        ld   C,A            ; Sauvegarde du premier paramètre dans C
        ld   B,$00          ; BC contient le premier paramètre
    
        rst  $08            ; Vérification obligatoire du caractère qui suit
        defb ','            ; La virgule de séparation
    
        push BC             ; Sauvegarde du premier paramètre dans la pile
    
        call eval_num_ex
        call deint_impl     ; Lecture d'un entier signé 16 bits dans DE
    
        push DE             ; Sauvegarde du second paramètre dans la pile
    
        ld   A,'('
        rst  $18            ; Affichage de la parenthèse ouvrante
    
        ex   (SP), HL       ; Récupération du deuxième paramètre, sauvegarde du pointeur
        call out_number     ; Affiche le contenu de HL
    
        ld   A,','
        rst  $18            ; Affichage de la virgule
    
        pop  HL
        ex   (SP), HL       ; Récupération du premier paramètre, sauvegarde du pointeur
        call out_number     ; Affiche le contenu de HL
    
        pop  HL             ; Récupération du pointeur d'exécution
    
        rst  $10            ; Lecture du caractère suivant
        jr   Z,no_third     ; Fin de la ligne, il n'y a pas de troisième paramètre
    
        dec  HL             ; Sinon, on revient en arrière pour vérifier le caractère suivant
        rst  $08
        defb ','            ; Qui doit être une virgule
    
        ld   A,','
        rst  $18            ; Affichage de la virgule
    
        call read_expr      ; Lecture du troisième paramètre comme une expression
    
        push HL             ; Sauvegarde du pointeur d’exécution
    
        call type_eq_num    ; Vérification du type de paramètre (numérique)
        call out_fp         ; Construction de la chaîne de caractères correspondant au nombre
        call out_str        ; Affichage de la chaîne
    
        pop  HL             ; Restauration du pointeur d’exécution
    
    no_third:
        ld   A,')'
        rst  $18            ; Affichage de la parenthèse fermante
    
        ret
    

    C'est assez long, il y a peut-être plus optimisé, le principal ici est que la suite d'instructions soit lisible et que les morceaux différents allant chercher les arguments puissent être pris indépendamment pour réutilisation.

    Le code est commenté, mais nécessite peut-être quelques autres éclaircissement :

    • rst $08 vérifie que le caractère codé juste après le RST est pointé par HL. Si ce n'est pas le cas, une erreur de syntaxe est levée. Le DEFB qui suit le RST est bien entendu sauté, le retour de la routine se fait juste après. La ROM se sert beaucoup de cette séquence pour vérifier la syntaxe de commandes et d'expressions.
    • rst $18 affiche le caractère présent dans A.
    • read_expr évalue une expression de n'importe quel type et met le résultat dans l'accumulateur flottant.
    • eval_num_ex lire une expression (via read_expr) et vérifie dans la foulée si elle est numérique.
    • getbyt_impl appelle eval_num_ex, la converti en entier, et vérifie que le résultat tient sur 8 bits. Le résultat est dans A.
    • deint_impl effectue une troncature entière sur 16 bits du nombre présent dans l'accumulateur flottant. Le résultat est dans DE.
    • out_number affiche le nombre (entier positif) présent dans HL.
    • out_fp écrit dans un buffer temporaire une chaîne représentant le nombre présent dans l'accumulateur flottant. Le pointeur vers la chaîne est renvoyé dans HL et la chaîne se termine par 0.
    • out_str affiche la chaîne pointée par HL se terminant par 0.

    On notera au passage que lors de l'affichage, les deux premiers paramètres sont inversés... c'était juste plus simple à écrire.

    Le hook RST

    Le second hook d'appel est rsthk, à l'adresse $47DC. Son fonctionnement n'est pas atteignable depuis le BASIC.

    La fonction RST du Z80 est une sorte de CALL sur 1 octet. Il permet de brancher à une série de 8 adresses prédéfinies : $0000, $0008, $0010, $0018, $0020, $0028, $0030, $0038. Suivant la syntaxe de l'assembleur, soit l'adresse, soit le numéro du restart (entre 0 et 7) est accepté.

    Toutes ces adresses sont allouées à des fonctions souvent appelées, cela permet de gagner de la place. Comme il n'y a pas beaucoup d'instructions possibles entre deux adresses de restart, la routine est souvent placée ailleurs, et certains emplacements sont remplis avec des données.

    Par exemple, entre RST $00, qui fait un démarrage complet de la machine et RST $08, qui fait un test de syntaxe, se situe la chaîne ".1.1" pour la ROM 1.1. C'est cette chaîne qui est affichée au démarrage de la machine.

    Les différents RST

    Puisqu'on y est, voici les différents RST sur VG5000µ, brièvement.

    • $00 : redémarrage complet de la machine
    • $08 : vérification de la présence d'un caractère pointé par HL, ou lancement d'une erreur de syntaxe (pour vérifier une virgule entre deux arguments par exemple)
    • $10 : acquisition d'un caractère depuis la chaîne pointée par HL (avec quelques flags concernant sa nature, et en sautant les espaces)
    • $18 : envoie d'un caractère sur le périphérique sélectionné (écran, imprimante, modem)
    • $20 : comparaison de HL et DE, qui a lieu très souvent.
    • $28 : renvoie -!, 0 ou 1 en fonction du signe de l'accumulateur flottant
    • $30 : libre
    • $38 : vecteur d'interruption (utilisé par l'affichage)

    Un RST libre

    Le restart $30 est donc libre, et son code est le suivant :

    usrrst:      jp       rsthk
    

    Net et précis. Il suffit donc d'ajouter en rsthk un JP suivi d'une adresse absolue, et vous pouvez alors utiliser RST $30 dans vos routines pour un appel très fréquent.

    Cependant, il y a une limite : il n'existe qu'un seul hook libre pour tout le monde. À garder en tête si vous mélanger des routines qui veulent chacune utiliser cette instruction.

    À noter aussi que si RST $30 ne prend qu'un octet dans votre routine, le saut utilise deux indirections pour un total de 31 T States contre 17 pour un CALL.

    Le résultat

    Voici le résultat de l'appel de la routine via CALL avec décodage de paramètre.

    Un CALL avec paramètres


  • VG5000µ, les hooks de périphériques ()

    L'article précédent présentait un accrochage sur un hook d'interruption. Dans cet article, je vais regarder du côté des hooks de commandes de périphériques.

    Ces hooks sont initialisés différements des 10 premiers, selon le code qui suit :

                 ld       a,$c3
                 ld       (inst_lpen),a
                 ld       (inst_disk),a
                 ld       (inst_modem),a
                 ld       hl,no_device
                 ld       ($47f2),hl
                 ld       ($47f5),hl
                 ld       ($47f8),hl
    

    L'instruction placée au début de chaque vecteur de redirection est un JP et l'adresse par défaut est celle d'une routine indiquant que le périphérique n'est pas géré.

    Il s'agit d'une extension de commandes mise à disposition par le VG5000µ, permettant de traiter les commandes LPEN, MODEM et DISK. Il est d'ailleurs amusant de voir que dans le manuel d'utilisation, ces trois commandes sont mentionnées dans le rappel des instructions reconnues, mais qu'elles ne sont pas décrites.

    Dans la table des instructions, le décodage de ces tokens envoie directement sur chacun des trois vecteurs, sans traitement particulier. C'est donc du code de décodage de paramètres qu'il faut écrire si l'on veut traiter ces instructions.

    Mise en place de la routine

    Le code est similaire à celui de la mise en place de la routine sur l'interruption. Seule l'adresse du hook change.

        defc dskhk = $47f4  ; Adresse du hook
    
        org $7A00           ; Spécification de l'adresse mémoire d'implémentation
    
        push AF             ; Sauvegarde des registres sur la pile
        push HL
    
        ld A,$C3            ; Mise en place de la routine sur le HOOK
        ld (dskhk),A        ; Il y a normalement déjà un 'JP' ici, mais on s'en assure
        ld HL,cmd_routine
        ld (dskhk+1),HL     ; l'adresse de la routine
    
    
        pop HL              ; Restauration des registres depuis la pile
        pop AF
    
        ret                 ; Retour au programme appelant
    
    cmd_routine:
        ret
    

    Passons donc rapidement sur la partie la plus intéressante : le traitement de l'instruction.

    Traitement de l'instruction

    Lorsque l'interpréteur appelle la routine associée à une instruction, plusieurs choses sont mises en place. Les principales sont :

    • HL pointe vers la chaîne en train d'être interprétée, sur le prochain caractère à lire,
    • Le flag "Z" est à 1 si l'on est à la fin de la chaîne,
    • L'adresse de retour sur la pile est positionnée pour traiter l'instruction suivante.

    Le contrat en sortie est :

    • HL doit pointer sur le caractère après le dernier consommé par l'instruction,
    • S'il s'agit d'une fonction, son résultat doit être dans l'accumulateur flottant (numérique, ou pointeur de chaîne).

    Pour s'amuser avec le paramètre, je vais écrire le décodage d'un paramètre de type chaîne, qui sera ensuite affiché à l'écran, suivi par un message.

        defc type_eq_str = $2851
        defc read_expr = $2861
        defc out_str = $36aa
        defc out_str1 = $36ad
    
    cmd_routine:
        push HL             ; Sauvegarde du pointeur d'exécution
    
        call read_expr      ; Lecture de l'expression suivant la commande
        ex (sp), hl         ; Remplacement de la valeur du pointeur d'exécution
    
        call type_eq_str    ; Vérification du type du paramètre
        call out_str1       ; Affichage de la chaîne
    
        ld hl, answer
        call out_str        ; Affichage du message de la commande
    
        pop HL              ; Récupération du pointeur d'exécution
    
        ret
    
    answer:
        defm " <-- PARAM", $00
    

    La structure est assez simple, car les routines nécessaires sont toutes disponibles dans la ROM.

    Tout d'abord, on sauve le pointeur vers la chaîne interprétée. HL est actuellement sur le début du paramètre de DISK (en tout cas, ce qui suit).

    L'appel à read_expr se charge de lire une expression valide (ou échouer avec une erreur). À la sortie de la routine, HL est positionné à la fin de ce qui a été consommé par l'expression.

    Ce pointeur est celui qu'il faudra conserver pour l’interpréteur. Du coup, on échange la valeur précédente de HL actuellement sur la pile avec la nouvelle valeur. Hop, le tour est joué.

    L'appel à type_eq_str vérifie si le type de l'expression est bien une chaîne, et affiche un message d'erreur sinon (et dans ce cas l'interprétation est arrêtée immédiatement).

    Si c'est bien une chaîne, l'appel à out_str1 affiche le résultat de l'expression de type chaîne de caractères qui vient d'être évaluée.

    L'appel à out_str qui suit affiche la chaîne terminée par un 0 définie dans le programme.

    Et voilà, voir le résultat à la fin de l'article.

    BASIC

    Voici un programme BASIC pour charger et lancer la routine.

    10 CLEAR 50,&"79FF"
    20 S=&"7A00"
    30 READ A$
    40 IF A$="FIN" THEN END
    50 A$="&"+CHR$(34)+A$+CHR$(34):A=VAL(A$)
    60 POKE S,A
    70 S=S+1
    80 GOTO 30
    300 DATA F5,E5,3E,C3,32,F4,47,21,10,7A,22,F5,47,E1,F1,C9,E5,CD,61,28
    310 DATA E3,CD,51,28,CD,AD,36,21,23,7A,CD,AA,36,E1,C9,20,3C,2D,2D,20
    320 DATA 50,41,52,41,4D,0
    1000 DATA FIN
    RUN
    CALL &"7A00"
    

    Pour éviter d'avoir à tout rentrer au clavier, voici le fichier .k7. À charger avec CLOAD, suivi d'un RUN et du CALL &"7A00".

    Le résultat

    La commande DISK


  • VG5000µ, les hooks ()

    Pour cet article, nous allons laisser de côté la partie BASIC-80 pour regarder du côté d'un fonctionnement spécifique au VG5000µ. Pas que le principe soit original, il est présent dans de nombreuses machines, mais que les capacités sont diverses suivant les différentes machines.

    Les « hooks » (la traduction de « crochet » me semble un peu hasardeuse, une « accroche » me semble un peu meilleur) est un moyen qu'offre le système pour intervenir lors de certaines opérations en augmentant le fonctionnement de la ROM. En y mettant son grain de sel en quelque sorte.

    Plus prosaïquement, les « hooks » sont des appels à des adresses précises, en RAM, à des routines qui peuvent être modifiées. Il peut s'agir aussi sur d'autres machines de récupérer dans une variable système une adresse d'indirection. Sur le VG5000µ, toutes les adresses de « hooks » sont fixes.

    Les « hooks » sont parfois aussi appelés vecteurs d'indirection. Ou bien tout simplement vecteurs.

    L'initialisation des « hooks »

    L'initialisation des « hooks » arrive très tôt dans l'initialisation de la machine, juste après la détection de la mémoire disponible.

                 ld       a,$c9
                 ld       hl,inthk
                 ld       b,$1e
    
    hk_ini_lop:  ld       (hl),a
                 inc      hl
                 djnz     hk_ini_lop
    
                 ld       a,$c3
                 ld       (inst_lpen),a
                 ld       (inst_disk),a
                 ld       (inst_modem),a
                 ld       hl,no_device
                 ld       ($47f2),hl
                 ld       ($47f5),hl
                 ld       ($47f8),hl
                 ld       hl,resetlang
                 ld       ($47ef),hl
                 ld       (nmihk),a
    

    A prend la valeur C9, qui est le code pour l'instruction RET sur Z80. HL est initialisé à inthk, qui est le premier hook d'une plage consécutive. Et B prend $1e, la taille de cette plage.

    La première étape et de remplir cette plage avec des RET. Cette plage contient les 10 premiers hooks, que sont les suivants.

    • $47D0, inthk : interruption masquable
    • $47D3, calhk : vecteur CALL
    • $47D6, sonhk : vecteur de générateur de son
    • $47D9, plyhk : début de commande PLAY
    • $47DC, rsthk : vecteur pour l'instruction RST utilisateur
    • $47DF, prthk : début de commande PRINT
    • $47E2, outhk : début d'impression de caractère
    • $47E5, crdhk : début de retour chariot
    • $47E8, inlhk : début de lecture d'une ligne
    • $47EB, inphk : début de commande INPUT

    Chaque hook fait 3 octets de long, nous verrons pourquoi plus loin. Et pour le moment tous remplis de RET. Un CALL à ces adresses ne fait donc rien d'autre que de revenir à l'appelant immédiatement.

    A prend ensuite la valeur C3, qui, suivi d'une adresse sur deux octets, est un JP absolu à cette adresse. Les hooks lpnhk, dskhk et modhk sont remplis avec ce jp no_device, qui est un branchement vers l'erreur indiquant que le périphérique n'est pas géré.

    Plus étonnant est la valeur que prend le hook nmihk, qui est appelé en cas d'interruption non masquable. Cette interruption est appelée lors de l'appui sur la touche Delta du VG5000µ. La routine resetlang met le système en anglais au niveau des messages et du clavier puis ressort de l'interruption. Et c'est tout.

    À vrai dire, cela ne dure qu'un instant. Juste après, le VG5000µ initialise sa partie graphique puis remplace le hook par une nouvelle valeur :

                 ld       hl,test_reset
                 ld       ($47ef),hl
    

    Cette nouvelle routine test_reset vérifie si la touche Ctrl est appuyée. Si ce n'est pas le cas, la routine sort immédiatement. Sinon, un reset à chaud a lieu.

    Mise en place d'une routine

    « Accrocher » une routine est assez facile, et demande juste quelques précautions. Afin de prendre un premier exemple, je vais accrocher une routine sur l'interruption qui provoque l'affichage sur le VG5000µ.

    Rapidement, dans le VG5000µ, le processeur graphique est la cause d'une interruption INT à chaque rafraîchissement. C'est la seule raison, de base, qui provoque cette interruption.

    Lors de l'interruption, le PC est branché en $0038 et la première instruction y est call inthk. On a donc une possibilité d'agir lors de l'interruption, avant que le rafraîchissement potentiel de l'écran n'ait lieu (il n'a pas lieu à chaque fois).

    Le code nécessaire pour une routine de « hook » est en deux parties. La première se charge de modifier le branchement du « hook » vers notre routine. La seconde est la routine elle-même.

    Commençons par la première partie. À noter que pour être propre, il faudrait effectuer un chaînage en faisant appeler à notre propre routine une routine éventuellement déjà installée. Je ne m'en occuperai pas ici.

        defc inthk = $47D0  ; Adresse du hook
    
        org $7A00           ; Spécification de l'adresse mémoire d'implémentation
    
        push AF             ; Sauvegarde des registres sur la pile
        push HL
    
        ld A,$C3            ; Mise en place de la routine sur le HOOK
        ld (inthk),A        ; le 'JP'
        ld HL,int_routine
        ld (inthk+1),HL     ; et l'adresse
    
    
        pop HL              ; Restauration des registres depuis la pile
        pop AF
    
        ret                 ; Retour au programme appelant
    
    int_routine:
        ret
    

    Le programme est directement commenté. Si depuis le BASIC, ce programme est injecté et appelé, alors... il ne se passera pas grand chose de visible, mais en réalité, le RET de int_routine sera appelé à chaque interruption.

    Une routine plus intéressante

    Pour rendre les choses plus intéressantes, voici une routine à installer qui affiche à l'écran une petite barre qui tourne.

        defc screen = $4000
    
    int_routine:
        push AF             ; Sauvegarde de AF
    
        ld  A,(IX+$00)
        dec A
        jp nz, no_display   ; Respect du timer de rafraîchissement
    
        push HL             ; Sauvegarde de HL
    
        ld A,(count)        ; Compteur du caractère à afficher
        inc A
        cp A, $4            ; S'il est à la dernière position, on boucle
        jp nz, display
        ld A, $0
    
    display:
        ld (count),A        ; Mise à jour du compteur
    
        ld HL,cursor        ; Récupération du caractère à afficher
        add A,L
        ld L,A
        ld A,(HL)
    
        ld HL,screen + 32   ; Affichage
        ld (HL), A
    
        ld (ix+$01),$01     ; Force le ré-affichage
    
        pop HL              ; Restauration des registres depuis la pile
    no_display:
        pop AF
    
        ret
    
    count:
        defb 0
    cursor:
        defb $2F, $60, $5C, $7C     ; Les 4 caractères qui forment l'animation
    

    Il faut veiller dans cette routine à bien préserver les registres utilisés, nous sommes ici en pleine interruption, nous n'avons aucune connaissance du contexte.

    La lecture de la variable système à travers le registre IX permet de savoir si le système va considérer un rafraîchissement de l'affichage. La commande DISPLAY du BASIC influe directement sur une valeur de compteur qui, lorsqu'il arrive à zéro, provoque éventuellement un affichage avant de revenir à sa valeur spécifiée.

    L'affichage n'est cependant pas systématique. Le bit 0 de la variable système qui suit le compteur doit être à 1 pour que l'affichage est vraiment lieu. Et l'on trouve parsemé dans la ROM des ld (ix+$01),$01 qui signifient qu'un rafraîchissement de l'écran est demandé. Ce que je fais à la fin de la routine.

    La partie après PUSH HL est un bête compteur cyclique de 0 à 3, qui est ensuite utilisé pour pointer dans un tableau de 4 caractères provoquant l'animation.

    L'adresse écran est calculé en dur et on y place directement le caractère. Puis le contexte est restauré.

    Une dernière note pour comprendre l'affichage du VG5000µ dans sa ROM BASIC. La ROM maintient en $4000 une image logique du contenu de l'écran, et un ensemble de variables systèmes, pointées en tout temps par IX. Lorsqu'un rafraîchissement à lieu, tout ce contenu est envoyé vers le processeur graphique pour une grande (et lente !) mise à jour.

    Il se peut donc que des modifications aient lieu en mémoire graphique côté processeur qui ne soient pas répercutées tout de suite vers le processeur graphique.

    Mais tout ceci est une autre histoire qui n'est pas le sujet ici.

    Le test

    Voici un programme BASIC qui va monter la routine en mémoire. Suivi d'un RUN pour l'exécuter puis du CALL pour lancer le hook.

    10 CLEAR 50,&"79FF"
    20 S=&"7A00"
    30 READ A$
    40 IF A$="FIN" THEN END
    50 A$="&"+CHR$(34)+A$+CHR$(34):A=VAL(A$)
    60 POKE S,A
    70 S=S+1
    80 GOTO 30
    300 DATA F5,C5,D5,E5,3E,C3,32,D0,47,21,14,7A,22,D1,47,E1,D1,C1,F1,C9
    310 DATA F5,DD,7E,0,3D,C2,3A,7A,E5,3A,3C,7A,3C,FE,4,C2,28,7A,3E,0
    320 DATA 32,3C,7A,21,3D,7A,85,6F,7E,21,20,40,77,DD,CB,1,C6,E1,F1,C9
    330 DATA 0,2F,60,5C,7C
    1000 DATA FIN
    RUN
    CALL &"7A00"
    

    Pour éviter d'avoir à tout rentrer au clavier, voici le fichier .k7. À charger avec CLOAD, suivi d'un RUN et du CALL &"7A00".

    La suite

    Nous avons vu un exemple de mise en place de routine sur un « hook » du VG5000µ. Dans les articles suivant, j'irai examiner les autres « hook » et dans quels contextes ils sont appelés.


  • VG5000µ, les chaînes de caractères ()

    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 HLqui 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.


  • VG5000µ, les variables en mémoire ()

    Pour terminer cette série sur la gestion de la mémoire par le BASIC sur VG5000µ, j'aborde la manière dont sont stockées les variables en mémoire.

    Les variables systèmes

    On l'a vu dans l'article sur la cartographie de la mémoire, il y a six variables systèmes intéressantes sur ce sujet :

    • (vartab) est la première adresse d'une variable. C'est ici que sont stockés les noms des variables numériques (avec leur valeur) ou chaînes (avec un pointeur vers leur contenu),
    • (arytab) est la première adresse de stockage du contenu des tableaux dimensionnés par DIM (ou bien les tableaux crées par défaut par le BASIC avec un DIM implicite), avec leur nom, leur taille, et leur contenu (ou pointeurs),
    • (strend) est la première adresse de la zone libre de stockage, tout ce qu'il y a à partir de cette adresse et « au-dessus » jusqu'à la pile (pointée par le registre SP) est la mémoire BASIC « libre » (ce qui n'est pas tout à fait vrai puisque la mémoire pour les chaînes de caractères est séparée).
    • (stktop) est le haut de la pile, l'adresse un octet au-dessus est la première adresse de la zone réservée pour les chaînes de caractères.
    • (fretop) est le pointeur de la zone libre pour les chaînes de caractères. L'adresse juste au-dessus contient le début des données de chaînes de caractères,
    • (memsiz) est l'adresse la plus haute adressable par le BASIC, et aussi le haut de l'espace réservée aux chaînes de caractères (inclus).

    Une première constatation est qu'il y a deux mémoires réservées au BASIC, disjointes. La première est celle qui contiendra tous les noms des variables et les contenus numériques. La place restante de cette zone est donnée par la fonction FRE(0) (la valeur du paramètre importe peu, seul son type importe).

    La seconde est celle qui contiendra tout le contenu des chaînes de caractères, dans des variables ou dans des tableaux. L'espace restant dans cette zone est donné par la fonction FRE(" ") (la encore, seul le type du paramètre compte). L'espace est fixe, de 50 octets au démarrage de la machine, et déterminé par le premier paramètre de la commande CLEAR. Faire un CLEAR 0 est tout à fait possible, mais alors vous ne pourrez plus stocker de chaîne de caractères.

    Création d'une variable

    Aparté sur la commande LET

    Dans le BASIC tel qu'il a été créé à l'université de Dartmouth, chaque ligne doit contenir une commande et une seule. La définition et l'assignation d'une variable se font avec la commande LET. Cette obligation de commande a un avantage sur l'analyse du programme BASIC par un compilateur ou un interpréteur : s'il n'y a pas de commande, c'est une erreur de syntaxe, et il n'y a pas d'exception.

    Le BASIC de Microsoft a relaxé cette obligation en rendant la commande LET optionnelle, et cette exception a été conservée par de nombreux BASIC par la suite. Mais pas partout, sur un ZX81 par exemple, chaque nouvelle ligne demande d'entrer une instruction via la touche du clavier correspondante, et le LET est obligatoire.

    Lorsque l'instruction LET est optionnelle, le décodage du BASIC lors de la tokénisation est plus complexe : il faut vérifier que ce qui est trouvé sur la ligne ne correspond à aucune instruction et dans ce cas-ci, faire comme si une instruction LET était présente. C'est du code en plus pris dans la ROM.

    Avant tout, que cherche-t-on ?

    Lorsque BASIC rencontre une variable, il lui faut toujours en premier lieu vérifier son existence. En effet, la première assignation de valeur à une variable vaut création avec une valeur par défaut.

    Et pour savoir si cette variable existe, il faut en connaître le nom. C'est la première partie de la routine qui se trouve en $38da.

    getvar:      xor      a,a
    
                 ld       (dimflg),a
                 ld       c,(hl)
    get_id:      call     a_to_z
                 jp       c,stx_err_prt
    
                 xor      a,a
                 ld       b,a
                 ld       (valtyp),a
    
                 rst      chget
                 jr       c,idnum_trail
                 call     a_to_z_2
                 jr       c,id_end
    
    idnum_trail: ld       b,a
    idtrail_skp: rst      chget
                 jr       c,idtrail_skp
    
                 call     a_to_z_2
                 jr       nc,idtrail_skp
    
    id_end:      sub      a,$24
                 jr       nz,num_vrble
    
                 inc      a
                 ld       (valtyp),a
                 rrca
                 add      a,b
                 ld       b,a
                 rst      chget
    num_vrble:   ld       a,(subflg)
    

    En premier lieu, (dimflg) est mis à zéro. Cette variable sert lorsque l'on manipule un tableau. Comme on n'a pas encore cette information, la routine part sur du non tableau.

    Note : il se peut que A soit différent de 0, lorsque l'on arrive par l'instruction DIM, qui saute par-dessus le XOR A,A, mais laissons ça de côté, le fonctionnement de DIM est de l'acrobatie en assembleur Z80...

    HL pointe, comme souvent lors de décodage d'une ligne, sur l'emplacement de la ligne en cours d'exécution. Si on est arrivé ici, c'est que l'on s'attend à trouver un nom de variable à cet endroit. Le premier caractère est donc lu dans C, puis il est vérifié que ce caractère est une lettre. En effet, chaque identifiant doit commencer par une lettre.

    Si ce n'est pas le cas, un saut vers le traitement d'une erreur de syntaxe est fait immédiatement.

    Donc le cas contraire, tout comme la routine s'initialise dans un mode non tableau, elle part du principe que la variable est numérique. On place donc 0 dans (valtyp), dans lequel est tenu à jour en tout temps le type de l'expression en cours. 0 dans (valtyp) signifie numérique.

    Par RST CHGET, un potentiel second caractère pour le nom de la variable est lu. Cette routine chget renvoie le caractère lu dans A et l'accompagne de quelques informations. Si le flag Carry est à 1, cela signifie que le caractère est un chiffre. La routine en profite pour traiter ce cas en sautant plus loin, un chiffre en deuxième caractère est valide.

    Si ce n'était pas un chiffre, on vérifie que c'est une lettre. Note au passage : la première fonction A_TO_Z lit un caractère depuis ce que pointe HL, A_TO_Z_2 fait la même vérification mais depuis le caractère déjà présent dans A.

    Si ce second caractère n'est pas une lettre, alors on doit avoir le nom de l'identifiant, on passe à la suite en id_end.

    Le LD B,A sauve le second caractère de la variable s'il existe. B avait été initialisé à 0 peut avant.

    Entre idtrail_skp et id_end (exclu), une boucle saute tous les caractères qui sont soit des chiffres, soit des lettres. En effet, il est tout à fait valide d'avoir des noms de variables plus long que deux caractères. Même si les caractères surnuméraires sont ignorés.

    Arrivé en id_end, on vérifie le dernier caractère lu. Est-ce un $, si non, on saute plus loin en num_vrble, la variable est bien numérique. Dans le cas contraire, 1 est mis dans (valtyp) pour désigner un type chaîne de caractères.

    Puis le second caractère de la variable, qui avait été sauvé dans B est augmenté de $80 (A égal à 1 après RCCA vaut $80). C'est comme cela que les variables de types chaînes de caractères sont identifiées : le second octet de leur nom a le bit 7 à 1.

    Lorsque l'on arrive dans num_vrble, on a dans C le premier caractère de la variable, dans B le second caractère de la variable, porteur de l'information de type chaîne, et (valtyp) qui contient aussi le type de la variable.

    Aparté sur les noms de variables longs

    Dans le BASIC original, la simplicité voulu par le langage avait amené les auteurs à ne permette des noms de variables qu'à une seule lettre, éventuellement suivie par un chiffre. Plus tard, les variables de type chaînes ont été ajoutées, et le nombre de caractères étendus.

    Dans le monde de la micro-informatique 8 bits, il n'y a pas beaucoup de place en RAM, et cette limite est soit conservée, soit relaxée par l'intermédiaire du système permissif des noms longs, dont seuls les premiers caractères sont significatifs.

    Sur VG5000µ, ce sont les deux premiers caractères qui comptent. D'ailleurs, tous les noms de variable en interne ont deux caractères, le second caractère étant éventuellement égal à $00.

    Je trouve cette idée de caractères significatifs désastreuse. Si elle part de l'idée que l'on peut utiliser des noms de variables plus expressifs, elle n'en n'offre pas les moyens, car absolument aucun test n'est fait sur ces caractères supplémentaires. D'expérience, il est facile, dans un programme un peu long, de ne plus faire attention au fait que deux noms longs possèdent les deux mêmes premiers caractères.

    Et si l'on y fait attention, il faut alors trouver un autre nom pour éviter la collision, et ce nom perd souvent en signification claire, et par la même perd l'intérêt des noms longs.

    Pour qui est-ce ?

    Il existe quelques contraintes sur les variables, et c'est dans l'aiguillage suivant qu'elles sont traitées.

    num_vrble:   ld       a,(subflg)
                 dec      a
                 jp       z,aryvar
    
                 jp       p,simplevar
    
    
                 ld       a,(hl)
                 sub      a,' ('
                 jp       z,subscript
    
    simplevar:   xor      a,a
    

    La variable système (subflg) sert à plusieurs choses. Ici, elle donne une indication sur une contrainte au niveau de la variable attendue. Si (subflg) est à 1, c'est que l'on s'attend à un tableau, le branchement est alors vers la recherche d'une variable tableau.

    Si (subflg) est supérieur à 1, alors c'est que les tableaux sont interdits, on saute donc vers simplevar. Les deux cas d'interdictions sont la variable d'index d'un FOR et la variable paramètre d'une fonction DEFFN.

    S'il n'y a pas de contrainte sur cette variable, mais que l'on trouve une parenthèse ouvrante à la suite de la variable, alors c'est qu'il y a un index, et l'on va vers cette routine.

    Et dans le cas contraire, il s'agit d'une variable simple. Ouf!

    Dans le cas simple

    Comme cet article va être assez long comme ça, on traitera les tableaux une autre fois. À présent que l'on sait que l'on a affaire à une variable simple (pas un tableau), que l'on a son nom et son type, il est grand temps de vérifier si elle existe !

    La première partie de cette recherche consiste à initialiser le domaine de recherche et... de vérifier si par hasard on ne serait pas en train d'évaluer une fonction DEFFN.

    simplevar:   xor      a,a
                 ld       (subflg),a
                 push     hl
    
                 ld       d,b
                 ld       e,c
    
                 ld       hl,(prmnam)
                 rst      de_compare
                 ld       de,prmval
                 jp       z,pop_hl_ret
    
                 ld       hl,(arytab)
                 ex       de,hl
                 ld       hl,(vartab)
    

    Tout d'abord, (subflg) est remis à 0. Le contexte a bien été traité et ne doit plus l'être. Puis le PUSH HL sert à sauver le pointeur vers la ligne tokenisée en court.

    Le nom de la variable, présente dans BC est placée dans DE pour utiliser la comparaison entre DE et HL.

    HL prend la valeur de la variable système (prmnam) (parameter name), qui contient, s'il y a lieu, le nom de la variable qui sert de paramètre à une fonction DEFFN en train d'être évaluée.

    Si la variable que l'on cherche est celle du paramètre d'une fonction que l'on est en train d'évaluer, il faut la traiter spécialement, car le nom de cette variable ne doit pas affecter une variable du même nom hors de la fonction.

    Le traitement spécial consiste à, si DE et HL sont égaux, sortir immédiatement de la routine de recherche, en faisant pointer DE sur la variable système prmval. Ce buffer de 4 octets contient la valeur actuelle du paramètre de la fonction, et DE est le pointeur que renvoie la routine de recherche de variable pour identifier la valeur cherchée.

    Dans le cas d'une recherche générique, DE est initialisé avec (arytab) et HL avec (vartab), les deux bornes de la mémoire contenant les variables simples.

    Cette fois on cherche !

    Ça y est, après tous ces préparatifs, on arrive au point de la recherche de la variable dans la mémoire ! Et pour cela, la routine va dérouler une boucle qui va cherche parmi tous les variables existantes une dont le nom correspond.

    next_var:    rst      de_compare
                 jp       z,no_var_yet
    
                 ld       a,c
                 sub      a,(hl)
    
                 inc      hl
                 jp       nz,var_diff
    
                 ld       a,b
                 sub      a,(hl)
    var_diff:    inc      hl
                 jp       z,var_found
    
                 inc      hl
                 inc      hl
                 inc      hl
                 inc      hl
                 jp       next_var
    

    En début de boucle, une comparaison entre HL et DE est faite. Si les deux sont égaux, c'est qu'on a fini la recherche. En effet, tout au long de la boucle, HL, parti de (vartab), va être incrémenté. DE représente la limite haute, qui ne bouge pas.

    Si la recherche est terminée sans avoir trouvé la variable, alors on saute en no_var_yet, il va falloir la créer. En effet, tout accès à une variable en BASIC induit sa création si elle n'existe pas encore.

    En se souvenant que BC contient le nom de la variable inversé, le premier caractère, dans C est soustrait de celui pointé par HL. S'ils sont différents, c'est qu'on n'a pas trouvé la variable pour le moment, on saute plus loin.

    Si le premier caractère correspond, on fait le même test avec le second caractère. Si ces deux tests passent, alors on a trouvé le nom, on saute en var_found.

    Sinon, on saute les quatre octets suivants, qui contiennent la valeur de la variable, et on boucle.

    Note : comme le nom de la variable en interne est modifié en fonction de son type, cette recherche montre bien que les variables A et A$ sont deux variables différentes. De même que les fonctions, que l'on n'a fait qu'effleurer. Dans la cas d'une fonction définition par DEF FN A(...), c'est le premier des deux octets qui porte un bit 7 à 1, et qui défini donc une troisième espace de nom.

    Le cas où les deux octets aurait un bit 7 à 1 pourrait définir une fonction sur des chaînes de caractères. Mais ce cas n'est pas permis par le BASIC sur VG5000µ.

    Création de la variable

    De la section précédente, on peut sortir soit en ayant trouvé la variable, soit en ne l'ayant pas trouvé. Si la variable n'est pas trouvé, il faut la créer et l'initialiser puis enchaîner sur la section où la variable est trouvée... si elle a pu être créée bien entendu.

    Comme c'est assez long, voyons ça en plusieurs partie. Tout d'abord, la création elle-même, avec un petit plot twist.

    no_var_yet:  pop      hl
                 ex       (sp),hl
    
                 push     de
                 ld       de,from_eval
                 rst      de_compare
                 pop      de
                 jp       z,ret_null
    
                 ex       (sp),hl
                 push     hl
    
                 push     bc
                 ld       bc,$0006
                 ld       hl,(strend)
                 push     hl
                 add      hl,bc
    
                 pop      bc
                 push     hl
                 call     mem_move_ckk
                 pop      hl
    
                 ld       (strend),hl
                 ld       h,b
                 ld       l,c
                 ld       (arytab),hl
    

    Tout d'abord, on récupère le pointeur sur la ligne évaluée qui était depuis la pile et on l'échange avec la valeur actuellement en haut de la pile. C'est un pas de danse assez classique en Z80 qui permet d'aller récupérer la valeur en deuxième position sur la pile.

    Puis on sauve la valeur de DE (qui contient (arytab)) avant d'y mettre une valeur spécifique : l'adresse d'une instruction de retour après un CALL particulier. Cette adresse est le chemin que prend l'appel à la récupération d'une variable lors de l'évaluation d'une expression.

    Si on vient de là, alors c'est un cas spécifique, et on saute à la section ret_null qui est un raccourci qui renvoie directement la valeur nulle à l'appelant. Et tout ceci sans créer de variable ! Après tout, pourquoi créer une variable qui a sa valeur par défaut ? Nous verrons ret_null plus loin.

    Dans le cas où l'on ne vient par d'une évaluation, alors l'adresse de retour est remise à sa place sur la pile et on y repousse le pointeur sur la ligne exécutée.

    Il s'agit maintenant de vérifier s'il y a de la place en mémoire pour créer la variable. Une variable a besoin de 6 octets en mémoire, et c'est donc avec 6 qu'est initialisé BC (après avoir été sauvé sur la pile, car BC contenait une information importante : le nom de la variable).

    Ici, il faut suivre... HL est initialisé avec (strend), c'est-à-dire la première adresse libre en mémoire principale et on lui ajoute 6 via BC. Au passage, sur la pile est poussé (strend), qui est récupéré dans BC, puis l'adresse (strend) + 6 est poussée sur la pile.

    On résume, par ordre croissant, on a :

    • Dans DE se trouve (arytab)
    • Dans BC se trouve (strend)
    • Dans HL se trouve (strend) + 6

    Sur la pile on a (strend) + 6 en première position de POP.

    Tout est prêt pour appeler mem_move_chk qui va déplacer la zone comprise entre DE et BC, c'est-à-dire toute la mémoire des tableaux, vers une zone dont l'adresse de fin sera HL. Autrement dit, les tableaux sont poussés de 6 octets pour faire de la place pour la nouvelle variable.

    Cette routine de déplacement commence aussi par une vérification que la place nécessaire est disponible. Dans le cas contraire, une erreur est levée et le processus est arrêté.

    Après le déplacement de la mémoire pour faire de la place, les variables (strend) et (arytab) sont ajustées à leur nouvelles valeur.

    clear_mem:   dec      hl
                 ld       (hl),$00
                 rst      de_compare
                 jr       nz,clear_mem
    
                 pop      de
                 ld       (hl),e
                 inc      hl
                 ld       (hl),d
                 inc      hl
    

    À présent qu'un emplacement est libre pour la variable, on parcourt son emplacement pour y placer des $00 avec cette boucle.

    Puis on récupère le nom de la variable dans DE et on enregistre ce nom dans les deux premiers octets des 6 octets tout neufs.

    La variable est à présent créée. Il ne reste plus qu'à retourner un pointeur avec l'emplacement de sa valeur à l'appelant.

    var_found:   ex       de,hl
                 pop      hl
                 ret
    

    DE prend la valeur de HL qui pointe juste après le nom de la variable, donc sur les 4 octets de sa valeur.

    Puis HL récupère sa valeur de pointeur d'exécution et la routine se termine.

    Retour de variable non définie

    On l'a vu juste avant, si, lors de l'évaluation d'une expression, une variable n'est pas trouvée, le BASIC ne va pas créer cette variable et se contentera de renvoyer la valeur nulle pour le type demandé.

    Nous pouvons le confirmer avec cette petite expérience :

    10 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
    20 PRINT FNPK(&"49D8"), FNPK(&"49DC")
    30 PRINT A
    40 PRINT A$
    50 PRINT NPK(&"49D8"), NPK(&"49DC")
    

    Donne :

    Aucune variable créée

    Le nombre à droite est (vartab) et ne bouge pas, puisque le listing ne bouge pas. La valeur de droite est la valeur de (strend) et reste constante entre le premier et le second affichage. Aucune variable n'a été créée par les accès à A et A$.

    La différence de 12 octets correspond à la variable créé par la fonction elle-même et à la variable paramètre de cette fonction. En effet, et c'est assez étrange, alors que la recherche de variable dans une fonction est court-circuitée, comme expliqué plus haut, le BASIC créé tout de même une variable vide, qui ne sera pas utilisée.

    Du coup, si vous voulez éviter de perdre 6 octets, prenez comme non de paramètre de vos fonction une variable utilisée ailleurs. Elle ne sera pas modifiée.

    Changeons un peu l'expérience et cette fois, donnons les valeurs nulles (0 pour un nombre, "" pour une chaîne) spécifiquement aux deux variables (le LET est optionnel, mais je le laisse pour être clair sur la signification de la création de la variable).

    Aucune variable créée

    La valeur de (vartab) est différente par rapport au premier test car le programme est un peu plus long. Néanmoins, sa valeur ne change pas avant et après la création des variables, ce qui donne une référence. Il y a toujours 12 octets pour les variables créées par la fonction.

    Par contre, (strend) augmente de 12 octets avant et après les assignations. Les variables ont bien été créées.

    Et cette valeur nulle ?

    ret_null:    ld       (fac),a
                 ld       hl,null_str
                 ld       (faclo),hl
                 pop      hl
                 ret
    

    Lors du branchement vers ret_null, A avait été mis à 0, c'est donc cette valeur que l'on met dans l'accumulateur flottant, qui contient la valeur de l'expression évaluée. Lorsqu'on lit une chaîne, ce sont dans les deux premiers octets de ce même accumulateur que se trouve un pointeur vers la chaîne évaluée. On y place un pointeur vers une chaîne nulle (null_str contient un $00).

    POP HL récupère le pointeur sur la ligne en exécution. Pas besoin de mettre en place DE, car cette branche de la routine n'est appelée qu'en cas d'évaluation d'expression, et comme l'adresse de retour a été enlevée de la pile pour comparaison et n'y a pas été remis, le retour ne se fait pas à l'appelant direct (qui irait chercher la valeur de la variable retournée) mais à son appelant précédent (l'évaluateur).

    Est-ce que cette optimisation acrobatique était bien nécessaire ? Je n'ai pas la réponse.

    Reset de la mémoire

    Une dernière chose avant de clore cet article. Le BASIC du VG5000µ efface toutes les variables lorsqu'un programme est lancé avec RUN. On est assuré que la mémoire est « effacée » (le contenu est toujours là, seul les pointeurs sont réinitialisés). Les valeurs spécifiées par un CLEAR sont par contre conservées.

    De même, les variables sont effacées au moindre changement dans le listing.

    Un peu de BASIC

    Et pour terminer, un petit listing BASIC qui va afficher les variables présentes en mémoire... et donc du programme en cours d'exécution.

    10 DEFFNPK(P)=PEEK(P+1)*256+PEEK(P)
    20 VT=FNPK(&"49D8")
    30 AT=FNPK(&"49DA")
    40 PRINT (AT-VT)/6;" VARIABLE(S)"
    50 FOR PT=VT TO AT-1 STEP 6
    60 PRINT CHR$(PEEK(PT) AND 127);
    70 PRINT CHR$(PEEK(PT+1) AND 127)
    80 NEXT PT
    

    Affiche

      4 VARIABLE(S)
     PK
     P
     VT
     AT
    

    Et PT ? Comme la variable est créés après la récupération de AT ((arytab)), cette variable n'est pas vue par le programme, ce qui est tout à fait conforme à ce qui était attendu.


« (précédent) Page 11 / 22 (suivant) »

Tous les tags

3d (14), 6809 (1), 8bits (1), Affichage (24), AgonLight (2), Altaïr (1), Amstrad CPC (1), Apple (1), Aquarius (2), ASM (30), Atari (1), Atari 800 (1), Atari ST (2), Automatisation (4), BASIC (30), BASIC-80 (4), C (3), Calculs (1), CDC (1), Clion (1), cmake (1), Commodore (1), Commodore PET (1), CPU (1), Debug (5), Dithering (2), Divers (1), EF9345 (1), Émulation (7), Forth (3), Game Jam (1), Hector (3), Histoire (1), Hooks (4), i8008 (1), Image (16), Jeu (14), Jeu Vidéo (4), Livre (1), Logo (2), Machine virtuelle (2), Magazine (1), MAME (1), Matra Alice (3), MDLC (7), Micral (2), Motorola (1), MSX (1), Musée (2), Nintendo Switch (1), Nombres (3), Optimisation (1), Outils (3), Pascaline (1), Peertube (1), Photo (2), Programmation (3), Python (1), ROM (15), RPUfOS (5), Salon (1), SC-3000 (1), Schéma (5), Synthèse (14), Tortue (1), Triceraprog (1), VG5000 (62), VIC-20 (1), Vidéo (1), Z80 (20), z88dk (1)

Les derniers articles

Instance Peertube pour Triceraprog
Environnement de développement pour Picthorix
Un jeu en Forth pour Hector HRX : Picthorix
Yeno SC-3000 et condensateurs
Suite de tests pour VG5000µ
Un peu d'Atari ST
Le Forth sur Hector HRX
J'MSX 24 et un micro jeu
Récréation 3D, Matra Alice
Tuiles des plus très-curieuses

Atom Feed

Réseaux