Compilateur Befunge en Python

En préparation du cours "Fondamentaux des compilateurs" pour les étudiants de 4e année, j'ai étudié différents langages de programmation ésotériques. Voici un bon article sur ce sujet . Le langage Befunge (Chris Press, 1993) m'a paru le plus intéressant dans l'article, je note notamment trois de ses caractéristiques:

  1. Le champ du programme est un tore bidimensionnel, c'est-à-dire physiquement, il s'agit d'une matrice rectangulaire de commandes de symboles fermées le long de la limite supérieure (inférieure) et le long de la colonne de gauche (droite). Le pointeur de commande se déplace dans le champ (chaque commande est un certain caractère avec les coordonnées x, y), exécute la commande et continue. Le mouvement peut être dans les 4 directions (par défaut, à droite du point 0,0), et lorsque vous allez au-delà du "champ", le pointeur apparaît sur le côté opposé.
  2. Il y a deux commandes (p, g) dans la langue qui changent le champ lui-même, c'est-à-dire le programme est "auto-réécrit" pendant l'exécution. Le code du programme au début peut ne pas être égal au code à l'arrivée. Le programme «123pgpg ## @» a démarré et le programme «ABC @ 1 @ 2 @ 3.14» (ce n'est pas un exemple correct) a fini de fonctionner.
  3. Chris Pressy a indiqué qu'il voulait créer un langage aussi complexe que possible à compiler. De facto, c'est vrai, en créant un compilateur qui rend les fichiers exe terriblement difficiles pour le programme, j'ai trouvé des informations que quelqu'un pourrait le faire en C ... Il vaut mieux créer un traducteur du langage vers le code Python, que j'appelle toujours compilateur pour plus de simplicité.


Le champ du programme 1993 comprend 25 lignes de 80 caractères chacune. Il y a 36 équipes dans la langue, chacune étant un caractère ASCII. Pour plus d'informations, voir Wikipedia , je vais en donner une brève description:

Déplacer les commandes (9):

> Déplacer vers la droite
<Déplacer vers la gauche
^ Déplacer vers le haut
v Déplacer vers le bas
_ Déplacer vers la droite si le haut de la pile est 0, sinon vers la gauche.
| Déplacer vers le bas si au-dessus de la pile 0, sinon vers le haut.
? Déplacer dans une direction aléatoire
# Sauter la cellule suivante («tremplin»)
@ Fin du programme

Commandes de manipulation de la pile (3)

:: Placer une copie du sommet sur la pile
\ Permuter le sommet et le sommet
$ Supprimer le sommet

Modification des commandes du code programme (2):
p "PUT": les coordonnées des cellules et le code ASCII du caractère placé à ces coordonnées
sont extraits de la pile g "GET": les coordonnées des cellules sont extraites de la pile; Le code ASCII d'un symbole à ces coordonnées est poussé sur la pile.

Commandes constantes (2):

0-9 Placez un nombre sur la pile de
début / fin de mode symbole, dans lequel les codes ASCII de tous les caractères de programme en cours sont

poussés sur la pile . Opérations arithmétiques (5):

+ Addition d'un sommet et d'un sommet
- Soustraction d'un sommet et d'un sommet
* Multiplication d'un sommet et d'une
division sommet / entier
% Reste de la division

Commandes pour la pile et opérations logiques (2)

:! Négation: zéro sur le sommet est remplacé par 1, valeur non nulle est remplacée par 0
`Comparaison" supérieure à ": si le sommet est plus grand que le sommet, 1 est placé sur la pile, sinon 0

commandes d'E / S (4):

& Demandez à l'utilisateur un nombre et placez-le sur la pile
~ Demandez à l'utilisateur un caractère et mettez sur la pile son code ASCII
. Imprimer le haut de la pile sous forme d'entier
, Imprimer le caractère correspondant au code ASCII en haut de la pile

J'ai décidé d'écrire un compilateur Befunge (interprète) en Python selon les règles de 1993 avec quelques restrictions: 1) le champ n'est pas de 25x80 caractères, mais le minimum en largeur et en hauteur du bloc de texte, 2) le champ n'est pas bouclé sur le tore, c'est-à-dire dépasser les frontières en sautant du côté opposé n'est pas traité. Ce n'est pas de la paresse (cependant, à qui je plaisante?), Pour les petits exemples tout fonctionne bien, et finir le champ en un vrai tore est assez simple, il y aurait un désir.

Le code est sorti à certains endroits inutilement "sur le front", mais cela est dû au fait qu'il est destiné aux étudiants et que sa tâche doit être aussi claire que possible, et non pas raccourcie en quelques lignes en chinois.

Partie 1


Le code est fourni du début à la fin avec une exception (qui sera spécifiée), il peut être copié dans un fichier et exécuté. Le texte intégral est disponible sur le lien rentry.co/ivansedov-befunge , il est trop tôt pour moi de donner le meilleur sur GitHub. Soit dit en passant, il y a environ 20 implémentations du langage Befunge là-bas, mais le code est soit en C (pas mon langage) ou en Python, mais tellement compliqué que je n'ai pas osé plonger. Cependant, vous pouvez y prendre des exemples de programmes pour les tests, par exemple ici github.com/causal-agent/befungee

from sys import *
import time
import random

Importez les bibliothèques:

  1. La bibliothèque sys est nécessaire pour obtenir le nom du fichier programme. Mon compilateur s'appelait bbb.py, un exemple de test dans le même répertoire 1.bf, Python version 3.7 lui-même, et donc l'appel de programme dans la console ressemblait à ceci: python3 bbb.py 1.bf
  2. time , , 0,5-1,0 .
  3. random «?» ( ), . -, Befunge - , « » (1 , 2 , 3 , 4 ). « » .

class Pointer:
    def __init__(self, x=0, y=0, vector=2, value=None):
        self.x = x
        self.y = y
        self.vector = vector
        self.value = value
        self.stack = []
        self.stack_sf = 0

    def __str__(self):
        return 'Point ({},{}) vektor:{} value:{} stack_sf:{} stack:{}'.format(self.x, self.y, self.vector, self.value, self.stack_sf, self.stack)

    def step(self):
        if self.vector == 1:
            self.x -= 1
        elif self.vector == 2:
            self.y += 1
        elif self.vector == 3:
            self.x += 1
        elif self.vector == 4:
            self.y -= 1

    def action(self):
	#   ,   

La classe principale utilisée dans le programme: Pointer (pointeur), a 6 propriétés et 2 méthodes. Propriétés: 1) coordonnée x (initialement = 0), 2) coordonnée y (initialement = 0), 3) vecteur (direction du mouvement, initialement = 2 à droite), 4) valeur (valeur de champ, qui se trouve à la coordonnée x, y, initialement = Aucun), il s'agit en fait de la commande à exécuter, 5) pile (pile de programme, initialement = []) et 6) stack_st (indicateur de saisie de lignes, initialement = 0).

La classe a deux méthodes: 1) étape () l'étape du pointeur, sans aucun contrôle et test, modifie les coordonnées x, y en fonction de la direction dans le vecteur et 2) l'action () est le cœur du compilateur, exécutant la commande de programme en cours. Je vais donner le code action () dans la deuxième partie, afin qu'il ne détourne pas de la logique.

# ===============================
# =                      =
# ===============================

def open_file(name):
    data = open(name, 'r').read()
    return data

def field_print():
    for row in A:
        for elem in row:
            print(elem, end='')
        print()  

Deux fonctions auxiliaires: 1) open_file (nom) ouvre le fichier par le nom soumis, le lit et retourne le contenu, et 2) field_print () imprime le contenu du tableau A, dans lequel se trouvent les caractères du programme. La création du tableau A est illustrée ci-dessous.

# ===========================================
# =                       =
# ===========================================

numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
operators = ['+', '-', '*', '/', '%']
point = Pointer()                           #  
text = open_file(argv[1]).split("\n")       #  
n = len(text)                               # n =  
m = 0                                       # m =  

#    (   m)
for line in text:
    if len(line) > m:
        m = len(line)

#   ( n  m )
A = [' '] * n
for i in range(n):
    A[i] = [' '] * m

#    
for i in range(len(text)):
    for j in range(len(text[i])):
        A[i][j] = text[i][j]

Paramètres de base du programme. Dans la liste des nombres, nous mettons tous les nombres de 0 à 9, qui peuvent être écrits dans le programme (10 ne correspondra plus, deux caractères). Dans la liste des opérateurs, nous mettons les opérateurs arithmétiques de base. Créer un objet point = Pointer (paramètres par défaut), avec lequel nous travaillerons tout le temps ...

Dans le texte variable, nous mettons le texte lu du programme exécutable, rompu par le symbole «nouvelle ligne». Par conséquent, le texte se compose de plusieurs lignes de texte de longueurs différentes, qui doivent être placées dans un tableau rectangulaire d'espaces à leur place. Il est facile de trouver le nombre de lignes n = len (texte), mais le nombre de colonnes doit être calculé en fonction de la longueur maximale des lignes incluses dans le texte. Je n'ai pas trouvé d'autre moyen de faire ce front: parcourir toutes les lignes et trouver celle avec la longueur maximale. Ayant sous la main n et m (le nombre de lignes et le nombre de colonnes du futur champ), vous pouvez créer un tableau à deux dimensions, le remplir avec des espaces, puis parcourir le texte pour placer les caractères à leur place.

Le résultat est un rectangle de caractères entre lesquels des espaces et tout cela est une matrice bidimensionnelle n par m. Après ces paramètres, vous pouvez appeler la fonction field_print () et vous assurer que tout est beau, rien n'a flotté et n'a pas violé la situation.

# ==========================================
# =                       =
# ==========================================

# field_print()

while point.value != '@':
    try:
        point.value = A[point.x][point.y]       # 1)    point
        point.action()                          # 2)  
        # print(point)                            # 3) :  
        # time.sleep(0.5)                         # 4) :  0.5 c
    except IndexError:                          #     =  
        print('     ')
        break

# field_print()
print()

Tout se termine par le programme principal (cycle), avant et après lequel vous pouvez afficher le champ (parfois utile). Le cycle tourne jusqu'à ce que le pointeur pointe vers le symbole «@» (esperluette, chien, fin de programme). A l'intérieur du cycle, 4 actions sont effectuées à chaque fois:

  1. Dans la propriété point.value, le caractère est lu, qui se trouve dans le tableau A aux coordonnées point.x, point.y
  2. La méthode point.action () est appelée, dans laquelle la commande courante (juste lire) est exécutée
  3. Un pointeur s'affiche à l'écran (toutes les propriétés)
  4. Un retard est effectué avant la prochaine itération (0,1 s - 0,5 s)

Les éléments 3 et 4 sont complètement facultatifs (ils sont même commentés), mais je recommande de les utiliser pour les tests. Toutes les actions à l'intérieur de la boucle se produisent avec la capture d'une erreur IndexError (erreur dépassant les limites de l'index), cela vous permet d'intercepter deux erreurs principales du compilateur:

  1. Nous nous sommes tournés vers la pile, et il n'y a pas de valeurs
  2. Nous avons accidentellement dépassé le programme (tableau) en largeur ou en hauteur

La dernière impression vide () est nécessaire pour qu'après l'affichage de la sortie, la console fonctionne à partir d'une nouvelle ligne.

Partie 2


Il est maintenant temps pour le code le plus important - le contenu de la méthode point.action () de la classe Pointer. Tout ce qui suit doit être inséré là où il a été écrit:

    def action(self):
	#   ,   

Faites attention à l'indentation:

        if self.value == '"' and self.stack_sf == 0:                # ,  "
            self.stack_sf = 1
        elif self.value == '"' and self.stack_sf == 1:
            self.stack_sf = 0
        elif self.stack_sf == 1:                                    # "Hello"   
            self.stack.append(ord(self.value))
        elif self.value in numbers and self.stack_sf == 0:
            # 123   
            self.stack.append(int(self.value))

En fait, le code à l'intérieur de action () comporte beaucoup de conditions, chacune étant exécutée lorsque cette commande est sous le pointeur. Tout commence par la condition "si la commande courante = guillemet et l'indicateur du début de la ligne stack_sf = 0", dans ce cas l'indicateur monte à 1. Nous sommes entrés dans la ligne.

(Sinon) si la commande actuelle = guillemet et que l'indicateur est élevé à 1, cela signifie que le guillemet a été trouvé une deuxième fois et vous devez arrêter de saisir la chaîne (l'indicateur stack_sf est abaissé à 0). Nous sommes hors ligne.

(Sinon) si les deux premières conditions n'ont pas fonctionné et que le drapeau stack_sf = 1, alors nous sommes "situés à l'intérieur de la ligne" et nous devons ajouter le code du symbole actuel à la pile. Pas le caractère lui-même, mais son code ASCII.

(Sinon) si le caractère actuel est parmi les éléments de nombres et que le drapeau est stack_sf = 0, alors, premièrement, c'est un chiffre et, deuxièmement, nous ne sommes pas à l'intérieur de la ligne, nous devons ajouter le caractère = chiffre actuel à la pile. Ajoutez non pas le code de caractère, mais le caractère lui-même. N'oubliez pas qu'il y a un numéro 1 et qu'elle a un code = 49. Donc, si nous sommes à l'intérieur de la ligne, alors nous devons ajouter 49 à la pile, et si c'est juste dans le programme, alors la commande 1 devrait ajouter à la pile 1.

Ci-après, toutes les conditions seront elif (sinon, si ...), donc je les écrirai simplement "si". De plus, toutes les conditions sont doubles, vous devez vérifier l'égalité du caractère actuel avec la commande et le fait que nous ne sommes pas à l'intérieur de la chaîne (à l'intérieur de la chaîne, tous les caractères sont traités différemment). Vous pouvez écrire tout cela de manière plus optimisée, mais cette solution vous permet de concentrer votre attention sur ce front.

        elif self.value in operators and self.stack_sf == 0:
            b = self.stack.pop()
            a = self.stack.pop()
            if self.value == '+':
                res = a + b                                         # a+b  
            elif self.value == '-':
                res = a - b                                         # a-b  
            elif self.value == '*':
                res = a * b                                         # a*b  
            elif self.value == '/':
                if b == 0:
                    res = 0
                else:
                    res = a // b                                    # a//b  
            elif self.value == '%':
                res = a % b                                         # a%b  
            self.stack.append(res)

Si le caractère courant est parmi les opérateurs (et stack_sf = 0), cela signifie que nous sommes entrés dans une opération arithmétique. Tous sont exactement les mêmes: 1) le nombre b est supprimé (avec suppression), 2) le nombre a est supprimé (avec suppression), 3) res = la valeur de l'opération entre a et b, 4) res est poussé sur la pile. En divisant par 0, la réponse est 0, bien que l'auteur de la langue ait prévu le choix de 0 ou 1.

        elif self.value == '!' and self.stack_sf == 0:
            a = self.stack.pop()
            if a == 0:
                a = 1
            else:
                a = 0
            # 0->1, 1->0
            self.stack.append(a)

        elif self.value == '`' and self.stack_sf == 0:
            a = self.stack.pop()        # 
            b = self.stack.pop()        # 
            if b > a:
                res = 1
            else:
                res = 0
            # b>a -> 1|0
            self.stack.append(res)

Si le symbole actuel est «!», Alors vous devez remplacer la tête (sommet) de la pile: il était 0 - il deviendra 1, il y avait quelque chose de différent de 0 - il sera 1. Si le symbole actuel est «» »(apostrophe), alors vous devez vérifier le haut et la colonne vertébrale : 1) si le sommet est plus grand que le sommet, puis 1, 2) si le sommet est inférieur (ou égal) au sommet, alors 0 est placé sur la pile. Veuillez noter que lorsque vous supprimez des éléments à comparer, ils sont supprimés (supprimés), pas sont copiés.

        elif self.value == '?' and self.stack_sf == 0:
            # ? ( )
            a = random.randint(1, 4)
            self.vector = a

        elif self.value == ':' and self.stack_sf == 0:              #  
            last = self.stack.pop()
            self.stack.append(last)
            self.stack.append(last)

        elif self.value == '\\' and self.stack_sf == 0:             # ab => ba
            a = self.stack.pop()
            b = self.stack.pop()
            self.stack.append(a)
            self.stack.append(b)

Si le caractère actuel est "?", Vous devez alors choisir une direction aléatoire et la suivre. Nous utilisons la fonction random.randint (1, 4), qui génère les nombres 1,2,3,4 et place la nouvelle valeur dans point.vector

Si le caractère actuel est «:», alors nous mettons sur la pile une copie du haut de la pile, c'est-à-dire lisez-le, puis ajoutez-le deux fois à la pile.

Si le caractère actuel est "\\" (barre oblique inverse), vous devez permuter le sommet et le sous-sommet. Nous obtenons deux nombres, les mettons sur la pile dans l'ordre inverse.

        elif self.value == '#' and self.stack_sf == 0:              #  ""
            self.step()

        elif self.value == ',' and self.stack_sf == 0:              # =65=A
            value = self.stack.pop()
            print(chr(value), end='')

        elif self.value == '.' and self.stack_sf == 0:              # Print 
            a = self.stack.pop()
            print(a, end='')

Si le symbole actuel est "#" (livre), alors vous devez sauter par-dessus la commande suivante (dans la direction). Notez qu'à la fin de l'action () il y a un saut inconditionnel en avant self.step (), il vous permet de passer à la commande suivante. Après avoir écrit self.step () dans le traitement «#», nous faisons en fait deux sauts et «sautons» la commande suivante après le «#».

Si le caractère actuel est «,» (virgule), vous devez imprimer un caractère dont le code ASCII est au-dessus de la pile. Si le nombre 65 est là, alors «A» devrait être affiché.

Si le caractère actuel est "." (point), vous devez ensuite imprimer le nombre qui se trouve sur le dessus de la pile, tout comme un nombre. S'il y a 65, vous devez afficher "65". Dans les deux cas, le paramètre end = '' est défini lors de la sortie afin qu'il n'y ait pas de nouveau saut de ligne.

        elif self.value == '_' and self.stack_sf == 0:              #  "_"
            test = self.stack.pop()
            if test == 0:
                #  = 0, (2)
                self.vector = 2
            else:
                #  !=0, (4)
                self.vector = 4

        elif self.value == '|' and self.stack_sf == 0:              #  "|"
            test = self.stack.pop()
            if test == 0:
                self.vector = 3
            else:
                self.vector = 1

Si le caractère actuel est "_" (trait de soulignement), vérifiez horizontalement. On retire de la pile le nombre à vérifier (test), si c'est = 0, puis on se déplace vers la droite (vecteur = 2), si ça! = 0, puis on se déplace vers la gauche (vecteur = 4).

Si le caractère actuel = "|" (barre verticale), vous devez alors effectuer une vérification verticale. On retire le nombre (test) de la pile, si it = 0, alors on descend (vecteur = 3), sinon on monte (vecteur = 1).

        elif self.value == '$' and self.stack_sf == 0:              #  
            self.stack.pop()

        elif self.value == '~' and self.stack_sf == 0:              # Input: A => 65
            val = input(' : ')
            self.stack.append(ord(val[0]))

        elif self.value == '&' and self.stack_sf == 0:              # Input: 65 => 65
            val = int(input(' : '))
            self.stack.append((val))

        elif self.value == 'p' and self.stack_sf == 0:              # x, y, symcode
            x = self.stack.pop()                                    # A(x,y) = symcode
            y = self.stack.pop()
            symcode = self.stack.pop()
            A[x][y] = chr(symcode)

        # x, y, value=A(x,y)
        elif self.value == 'g' and self.stack_sf == 0:
            x = self.stack.pop()                                    # ord(value) => 
            y = self.stack.pop()
            value = A[x][y]
            self.stack.append(ord(value))

Si le caractère actuel = "$", vous devez supprimer le sommet. Faites simple pop ().

Si le caractère courant = "~" (tilde), alors nous demandons à l'utilisateur un caractère et mettons son code ASCII sur la pile. Utilisateur "A" (anglais) envoyé, nous devons mettre 65 sur la pile. Juste au cas où, nous mettrons val [0], sinon l'utilisateur peut saisir "Apple" et le traduire en code ne fonctionne pas.

Si le caractère actuel = "&" (esperluette), nous demandons un numéro à l'utilisateur et le mettons sur la pile. Vous avez entré 65, vous devez mettre 65 sur la pile.

Maintenant, les deux équipes les plus difficiles.

Si le caractère actuel = "p", vous devez extraire les coordonnées de la cellule et le code ASCII du caractère de la pile, puis mettre ce caractère dans ces coordonnées sur le champ A. Supposons que 1.2.65 soit sur la pile et que nous ayons (1.2) et 65, nous devrions mettre le symbole "A" dans la cellule (1.2). Encore une fois, je note: nous avons obtenu trois nombres et mis un symbole dans les coordonnées.

Si le caractère courant = "g", alors les coordonnées de la cellule sont récupérées de la pile, la cellule est recherchée sur le terrain, le caractère en est extrait et son code ASCII est poussé sur la pile. Supposons que le symbole "B" se trouve sur le terrain dans la cellule (2,3), l'équipe actuelle a obtenu "g" et nous avons obtenu 2,3 ​​de la pile. Dans ce cas, nous suivons les coordonnées (2,3), obtenons le symbole «B» à partir de là, le traduisons en nombre 66 (code symbole B) et mettons 66 sur la pile.

        elif self.value == '>' and self.stack_sf == 0:              # >
            self.vector = 2
        elif self.value == '<' and self.stack_sf == 0:              # <
            self.vector = 4
        elif self.value == '^' and self.stack_sf == 0:              # ^
            self.vector = 1
        elif self.value == 'v' and self.stack_sf == 0:              # v
            self.vector = 3
        self.step()                                                 #  

Eh bien, et les dernières lignes de code: l'organisation du déplacement du pointeur à travers le champ. Ici, tout est simple: on regarde le symbole et on change de direction (vecteur). À la toute fin de la fonction action () se trouve self.step (), qui fait un pas dans la direction actuelle. Ainsi, action () est à la fois l'exécution de l'action et l'étape du caractère suivant.

Conclusion


Écrire ce compilateur a été un plaisir. Et combien de joie a apporté les moments où vous jetez un certain code dans le programme, et il est exécuté (correctement!) Et vous l'observez! Le site befungius.aurlien.net a beaucoup aidé lors du travail , sur lequel l'auteur a posté l'interprète en ligne Befunge (en JavaScript). Voici sa vidéo d'une conférence www.youtube.com/watch?v=oCPT3L33848 , dans laquelle il parle de la langue.

Pour les tests, vous pouvez utiliser presque tous les programmes sur Befunge, à l'exception de ceux qui nécessitent un champ comme tore, c'est-à-dire avoir des transitions dans différentes directions du monde. Pour certains programmes, vous devez voir le champ lui-même (par exemple, le programme de compteur modifie la coordonnée A [0] [1]), donc insérez la sortie de cette coordonnée à l'écran ou affichez la matrice A entière après chaque étape.

En tout cas, ce n'est pas un programme léché pour briller, mais un code de formation qui peut être ajouté et édité. Les fautes de frappe sont possibles, même si je l'ai testé à plusieurs reprises. La critique n'est pas la bienvenue, mais pas interdite. Bonne chance à tous et bon code.

Bonjour le monde!

0"!dlrow olleH">:#,_@

Bazz

0> 1+:3%v
>^  v%5:_:5% v
,v.:_v     v0_0"zzub"v
"v         #
     >0"zzub"v
"   v"fizz"<         <
^<         $<>:#,_v
    >      #^^#   <

Fibonacci

62*1+v>01p001>+v>\:02p\:02gv
     0       ^             <
     .         :p
     "         .1
        v 0," "<0
     "  >1g12-+:|
     ,          @
     >^

Compter

>91+:9`v
p   v  _v
    >$0 v
^ 01+*68<

Mais ça ne marche pas, sortir du terrain

0>:00p58*`#@_0>:01p78vv$$<
@^+1g00,+55_v# !`\+*9<>4v$
@v30p20"?~^"< ^+1g10,+*8<$
@>p0\>\::*::882**02g*0v >^
`*:*" d":+*:-*"[Z"+g3 < |<
v-*"[Z"+g30*g20**288\--\<#
>2**5#>8*:*/00g"P"*58*:*v^
v*288 p20/**288:+*"[Z"+-<:
>*%03 p58*:*/01g"3"* v>::^
   \_^#!:-1\+-*2*:*85<^

Liens et documents supplémentaires:

  1. Code complet
  2. Version en ligne
  3. Elle est en JavaScript
  4. Documentation (anglais)
  5. Article de wiki (anglais)

All Articles