Compylateur
Posté le 08/05/2020 14:00
Bonjour à tous !
Il y a quelques temps j'ai fait un 'compilateur' qui permet d'exécuter un algorithme en langage naturel en le 'traduisant' en Python. Le code est atroce et repose sur un remplacement entre les commandes en langage naturel et les commandes en Python (à coup de dictionnaires et de tests conditionnels
)… J'aimerais faire de ce projet un 'vrai' compilateur
(on reste sur du Python
). Et j'ai quelques questions :
- La phase d'analyse lexicale repose pour l'instant sur une recherche et un replacement, avec un dictionnaire qui contient en clés les commandes en langage naturel, et en items, les commandes correspondantes en Python… Je me doute que ce n'est pas pertinent…
En fait l'analyse lexicale est mélangée à la phase d'analyse syntaxique.
- Comment faire pour basculer du langage naturel au Python ? Faut-il forcément passer par un hard code, ou est-ce que d'autre technique plus esthétiques existent ?
- L'analyse syntaxique est un bête
replace basé sur un dico… Du coup ça revient à la question précédente : comment éviter le hard code ?
- La phase sémantique… Je ne suis pas sûr d'avoir bien compris toutes les subtilités…
Dans mon cas, après le remplacement bête et méchant, la syntaxe Python n'est pas bonne, du coup je passe à travers différents tests conditionnels pour avoir un 'vrai' script fonctionnel… Encore une fois le hard code à coup de
if me plaît moyen…
- En derniers je refait un passage sur mon code généré et j'ajoute les alinéas. Est-ce que je devrais les gérer plus tôt (je pense à la phase d'analyse syntaxique… mais le placement des alinéas dépend du contexte du code, et sur une ligne donnée je vois pas trop comment faire…
Merci d'avance !
Citer : Posté le 10/06/2020 14:16 | #
Et... n'oublie pas que les fonctions peuvent avoir plusieurs arguments !
Citer : Posté le 10/06/2020 14:33 | #
AH… Oui mais non… on va dire que y a pas le droit
J'ai un token SPTR qui rassemble tous les séparateurs : virgules, point-virgule, mots-clé de séparation…
Du coup ma grammaire devient :
Citer : Posté le 10/06/2020 14:50 | #
Hmm, presque. D'abord... comme pour les opérateurs, comme pour les parenthèses, faut que tu sépares tous ces symboles en tokens différents. Seule la virgule a le droit d'être utilisée pour séparer des fonctions.
Ici tu autorises 1 ou 2 arguments exactement, ce qui est dommage. Tu peux en autoriser un nombre variable de cette façon
où * est un raccourci pour "0 ou plus", et cache donc une boucle.
Citer : Posté le 10/06/2020 14:55 | #
Donc je dois avoir un token par symbole… Ça va être atroce… faut vraiment que le lexer soit repensé…
Citer : Posté le 10/06/2020 15:07 | #
Non non, c'est comme ça que les lexers marchent. Avoir un type de token ça ne coûte rien. Ça ne prend pas plus de mémoire, ça ne prend pas plus de temps. Tous les lexers des langages de programmation ont un token par opérateur, un par type de parenthèse, et un par symbole de ponctuation.
Maintenant ton lexer à dicos est un peu exotique, mais sur le principe avoir beaucoup de types de tokens est parfaitement normal.
Citer : Posté le 10/06/2020 15:27 | #
Hu… je suis partit sur des set mais c'est pas plus propre
j'ai un truc qui ressemble à ça :
# Lexer
# ==================================================
# --- Main function --- #
def lexer(prgm_src):
var_type = {"des réels", "un réel", "des entiers", "un entiers", "un entier naturel", "des entiers naturels", "un entier relatif", "des entiers relatifs", "une liste", "des listes", "un flottant", "des flottants", "une chaîne de caractères", "des chaînes de caractères"}
cmnd = {"fin", "finsi", "fin si", "fintantque", "fin tantque", "fin tant que", "finpour", "fin pour", "afficher", "si", "alors", "sinon", "tant que", "tantque", "pour"}
sptr = {"et", "\n", "à", "entre", "de", ";", "faire"}
comp = {"=", "<", "<=", ">", ">=", "est supérieur à", "est supérieur ou égal à", "est inférieur à", "est inférieur ou égal à", "est différent de", "est égal à"}
user = {"saisir", "saisir la valeur de", "saisir les valeurs de", "demander la valeur de", "demander à l'utilisateur la valeur de"}
logi = {"et que", "ou que"}
assi = {"prend la valeur", "sont", "est"}
rang = {"allant", "variant"}
lpar = {"("}
rpar = {")"}
plus = {"+"}
moins = {"-"}
fois = {"*"}
divis = {"/"}
exp = {"^"}
comma = {","}
for i in {"=", "<", "<=", ">", ">=", "+", "-", "/", "*", "^", "(", ")", "[", "]", "{", "}", '"', "\n", ",", ";"}:
prgm_src = prgm_src.replace(i, " " + i + " ")
word = [i for i in prgm_src.lower().split(" ") if i != ""]
l_token = TokenList()
index, undef = 0, bool()
token = (var_type, cmnd, comp, user, logi, assi, sptr, rang, lpar, rpar, plus, moins, fois, divis, exp, comma)
name = ("TYPE", "CMND", "COMP", "USER", "LOGI", "ASSI", "SPTR", "RANG", "LPAR", "RPAR", "PLUS", "MINUS", "MULTI", "DIVI", "EXP", "COMMA")
while True:
undef = True
for j in range(len(token)):
for k in token[j]:
target = k.split(" ")
if index >= len(word): return l_token
if word[index] in target and lexer_detect(word, index, target):
l_token.add(Token(name[j], k))
undef = False
index += len(target)
if undef and word[index] == "\"":
l_token, index = text_detecter(word, index, l_token)
elif undef:
if word[index].isdigit():
l_token.add(Token("NUM", eval(word[index])))
else:
l_token.add(Token("VAR", word[index]))
index += 1
# --- Secondary functions --- #
def lexer_detect(word, index, target):
try:
return not 0 in [target[i] == word[i + index] for i in range(len(target))]
except:
return 0
def text_detecter(word, index, l_token):
txt = word[index]
index += 1
while word[index] != '"':
txt = txt + " " + word[index]
index += 1
l_token.add(Token("TEXT", txt + ' "'))
return l_token, index + 1
Citer : Posté le 10/06/2020 17:21 | #
Pour ce que c'est ça se tient, si tu veux l'écrire à la main je vois pas de gros changement structurel possible. Sinon faudrait utiliser un générateur de lexer avec les regex qui vont bien.
Citer : Posté le 10/06/2020 17:23 | #
je pensais virer les grand ensemble et faire deux tuples : symboles détecté / nom du symbole avec une concordances des index ? Du coup le code serait un peu plus simple ?
Citer : Posté le 10/06/2020 17:25 | #
Tu ferais mieux d'organiser un tel tuple comme un dictionnaire. Un truc du genre
",": "COMMA",
"+": "PLUS",
...
}
voire mieux, pour ces cas-là, juste utiliser la chaîne de caractère exacte. Le nom du token pour "," peut être ",".
Citer : Posté le 10/06/2020 17:26 | #
Ouaip, je change tout ça !
Citer : Posté le 10/06/2020 17:26 | #
Un exemple à la con de ça se trouve d'ailleurs dans mon propre code.
Citer : Posté le 10/06/2020 17:51 | #
J'ai ça :
# Lexer
# ==================================================
# --- Main function --- #
def lexer(prgm_src):
token = {
"(":"LPAR",
")":"RPAR",
"+":"PLUS",
"-":"MINUS",
"*":"MULTI",
"/":"DIVI",
"^":"EXP",
",":"COMMA"}
for i in {"=", "<", "<=", ">", ">=", "+", "-", "/", "*", "^", "(", ")", "[", "]", "{", "}", '"', "\n", ",", ";"}:
prgm_src = prgm_src.replace(i, " " + i + " ")
word = [i for i in prgm_src.lower().split(" ") if i != ""]
l_token = TokenList()
index, undef = 0, bool()
while index < len(word):
undef = True
for target in token.keys():
name = token[target]
if word[index] in target and lexer_detect(word, index, target):
l_token.add(Token(name, target))
undef = False
index += len(target)
break
if undef and word[index] == "\"":
l_token, index = text_detecter(word, index, l_token)
elif undef:
if word[index].isdigit():
l_token.add(Token("NUM", eval(word[index])))
else:
l_token.add(Token("VAR", word[index]))
index += 1
return l_token
# --- Secondary functions --- #
def lexer_detect(word, index, target):
try:
return not 0 in [target[i] == word[i + index] for i in range(len(target))]
except:
return 0
def text_detecter(word, index, l_token):
txt = word[index]
index += 1
while word[index] != '"':
txt = txt + " " + word[index]
index += 1
l_token.add(Token("TEXT", txt + ' "'))
return l_token, index + 1
À défaut d'être vraiment plus optimisé, rajouter des tokens ne m'oblige plus à modifier 5 trucs x)
Ajouté le 18/06/2020 à 08:14 :
Après quelques jours de pause, j'ai fini de mettre les fonctions ! o/ Je pense que c'est terminé : je gère les opérations mathématiques passées en argument, les différents paramètres… en sortie j'ai ça :
--- Tokens ---
('VAR', 'f')
('LPAR', '(')
('VAR', 'a')
('RPAR', ')')
--- AST ---
Function : f
Parameter : #1
Variable : a
>>> compylateur("3+a / dew(temp, humi)")
--- Tokens ---
('NUM', 3)
('PLUS', '+')
('VAR', 'a')
('DIVI', '/')
('VAR', 'dew')
('LPAR', '(')
('VAR', 'temp')
('COMMA', ',')
('VAR', 'humi')
('RPAR', ')')
--- AST ---
Operation : +
Number : 3
Operation : *
Variable : a
Operation : 1/
Function : dew
Parameter : #1
Variable : temp
Parameter : #2
Variable : humi
Et niveau code :
atm = self.expect(["VAR", "NUM", "LPAR", "MINUS"])
if atm.type == "MINUS": return self.atome(not minus)
elif atm.type == "VAR":
if self.token_ahead.type == "LPAR":
self.expect()
return Node("Function", atm.value, *self.fct())
if minus: return Node("Operation", "--", Node("Variable", atm.value))
else: return Node("Variable", atm.value)
elif atm.type == "NUM":
return Node("Number", (atm.value, -atm.value)[minus])
else:
e = self.expr()
self.expect("RPAR")
if minus: return Node("Operation", "--", e)
else: return e
def fct(self):
param = list()
while self.token_ahead.type != "RPAR":
if self.token_ahead.type in ("VAR", "NUM", "MINUS"):
param.append(Node("Parameter", "#{}".format(len(param)+1), self.expr()))
else: self.expect(["COMMA", "RPAR"])
return param
Citer : Posté le 18/06/2020 09:15 | #
C'est pas mal ! Je modifierais juste un truc sur les fonctions si j'étais toi, c'est dans cette boucle :
if self.token_ahead.type in ("VAR", "NUM", "MINUS"):
param.append(Node("Parameter", "#{}".format(len(param)+1), self.expr()))
else: self.expect(["COMMA", "RPAR"])
D'abord je vois pas trop comment ça termine dans le sens où à la fin tu attrapes la parenthèse fermante dans le else et donc elle n'est plus dans token_ahead au tour de boucle suivant vu que tu l'as lue. Ça c'est louche.
Et sinon j'inverserais le if/else car ici tu as été obligé de lister tous les tokens par lesquels une expression peut commencer (VAR, NUM et MINUS). Cette liste est susceptible de grandir avec le temps (le non logique, d'autres constantes comme π, que sais-je encore), et c'est facile d'oublier des tokens (en fait tu en as déjà oublié, notamment la parenthèse ouvrante !). C'est beaucoup plus facile de dire que COMMA et RPAR finissent les arguments et de laisser tout le reste à self.expr() dans le else.
Citer : Posté le 18/06/2020 09:22 | #
La parenthèse ouvrante n'est pas oubliée. En fait atome fonctionne comme ça :
=> Token de type "VAR".
=> Si le Token suivant est "LPAR" on envoie self.expect() ce qui permet d'avoir token_ahead sur le premier paramètre de la fonction.
=> On appelle fct pour récupérer les paramètres de la fonction.
Pour le RPAR dans le expect, c'est un peu le même raisonnement que pour LPAR ça permet d'avoir le token_ahead directement sur la prochaine opération
Au niveau du if / else, je change ça.
Citer : Posté le 18/06/2020 09:22 | #
Pourquoi parser a/b par a*1/b :o
Il sort quoi ton parseur pour "a-b-c" ?
Ecrivez vos programmes basic sur PC avec BIDE
Citer : Posté le 18/06/2020 09:26 | #
L'opération 1/b est gérée comme un nœud avec un seul enfant ça permet de gérer les priorités de calculs en rendant prioritaire l'opération 1/b c'est surtout utile quand on des divisions en chaîne : a/b/c qui devient : a * (1/b) * (1/c).
--- Tokens ---
('VAR', 'a')
('MINUS', '-')
('VAR', 'b')
('MINUS', '-')
('VAR', 'c')
--- AST ---
Operation : +
Variable : a
Operation : -
Variable : b
Operation : -
Variable : c
Citer : Posté le 18/06/2020 09:34 | #
J'imagine que ton parseur te sort un "+" pour les mêmes raisons ?
Donc on a : a+(-b)+(-c), et ton AST peut avoir plus de 2 opérandes pour le "+" ?
Pour moi le 1/b c'est du hack, surtout que tu peux pas le faire avec le modulo (techniquement dans ton contexte le modulo sera une fonction et non un opérateur du coup t'es bon). Faudrait proprement gérer les priorités avec un expect(["*", "/"]) qui te retourne le 1er token parmi une liste de tokens (à voir ce que Lephé pense de ça).
Ecrivez vos programmes basic sur PC avec BIDE
Citer : Posté le 18/06/2020 09:37 | #
Modulo sera en effet une fonction, en pseudo-code c'est rarement un opérateur… Je ne sais pas trop comment ça va se goupiller, mais ça devrait ressembler à mod(a, b) ou modulo(a, b) ou même rem(a, b)… ?
Citer : Posté le 18/06/2020 09:38 | #
ou même "A mod B" comme en basic casio
Ecrivez vos programmes basic sur PC avec BIDE
Citer : Posté le 18/06/2020 09:41 | #
Aussi… je pense même que reste de la division euclidienne de a par b doit être valable… (dans les livres de maths y a des algo en pseudo-code avec ce genre de notation dégueulasse )
Citer : Posté le 18/06/2020 11:38 | #
Eh ben désolé de te décevoir mais c'est la représentation correcte et non-redondante.
Et f((a+b)*2) ça donne quoi ?