Comment les langages de programmation définissent-ils et sauvegardent-ils les fonctions / méthodes? Je crée un langage de programmation interprété dans Ruby et j'essaie de comprendre comment implémenter la déclaration de fonction.
Ma première idée est de sauvegarder le contenu de la déclaration dans une carte. Par exemple, si je faisais quelque chose comme
def a() {
callSomething();
x += 5;
}
Ensuite, j'ajouterais une entrée dans ma carte:
{
'a' => 'callSomething(); x += 5;'
}
Le problème avec cela est qu'il deviendrait récursif, car je devrais appeler ma parse
méthode sur la chaîne, qui appellerait ensuite à parse
nouveau lorsqu'elle se rencontrerait doSomething
, puis je manquerais finalement d'espace de pile.
Alors, comment les langages interprétés gèrent-ils cela?
programming-languages
language-design
functions
methods
Poignée de porte
la source
la source
Réponses:
Aurais-je raison de supposer que votre fonction "analyser" non seulement analyse le code mais l'exécute également en même temps? Si vous souhaitez le faire de cette façon, au lieu de stocker le contenu d'une fonction dans votre carte, stockez l' emplacement de la fonction.
Mais il y a une meilleure façon. Cela demande un peu plus d'efforts à l'avance, mais cela donne de bien meilleurs résultats à mesure que la complexité augmente: utilisez un arbre de syntaxe abstraite.
L'idée de base est que vous n'analysez le code qu'une seule fois. Ensuite, vous avez un ensemble de types de données représentant des opérations et des valeurs, et vous en créez un arbre, comme ceci:
devient:
(Il s'agit simplement d'une représentation textuelle de la structure d'un AST hypothétique. L'arbre réel ne serait probablement pas sous forme de texte.) Quoi qu'il en soit, vous analysez votre code dans un AST, puis vous exécutez directement votre interpréteur sur l'AST, ou utilisez un deuxième passage ("génération de code") pour transformer l'AST en une forme de sortie.
Dans le cas de votre langue, ce que vous feriez probablement, c'est d'avoir une mappe qui mappe les noms de fonction aux AST de fonction, au lieu des noms de fonction aux chaînes de fonction.
la source
(((((((((((((((( x )))))))))))))))))
. En réalité, les piles peuvent être beaucoup plus grandes et la complexité grammaticale du code réel est assez limitée. Certainement si ce code doit être lisible par l'homme.Vous ne devriez pas appeler l'analyse syntaxique en voyant
callSomething()
(je suppose que vous vouliez direcallSomething
plutôt quedoSomething
). La différence entrea
etcallSomething
est que l'un est une définition de méthode tandis que l'autre est un appel de méthode.Lorsque vous voyez une nouvelle définition, vous voudrez faire des vérifications pour vous assurer que vous pouvez ajouter cette définition, donc:
En supposant que ces vérifications réussissent, vous pouvez l'ajouter à votre carte et commencer à vérifier le contenu de cette méthode.
Lorsque vous trouvez un appel de méthode comme
callSomething()
, vous devez effectuer les vérifications suivantes:callSomething
Existe- t- il sur votre carte?Si cela vous
callSomething()
convient, à ce stade, ce que vous voulez faire dépend vraiment de la façon dont vous souhaitez l'aborder. À strictement parler, une fois que vous savez qu'un tel appel est correct à ce stade, vous ne pouvez enregistrer que le nom de la méthode et les arguments sans entrer dans les détails. Lorsque vous exécutez votre programme, vous invoquerez la méthode avec les arguments que vous devriez avoir au moment de l'exécution.Si vous voulez aller plus loin, vous pouvez enregistrer non seulement la chaîne mais un lien vers la méthode actuelle. Ce serait plus efficace, mais si vous devez gérer la mémoire, cela peut devenir déroutant. Je vous recommanderais simplement de conserver la chaîne au début. Plus tard, vous pouvez essayer d'optimiser.
Notez que tout cela suppose que vous avez lexxed votre programme, ce qui signifie que vous avez reconnu tous les jetons de votre programme et que vous savez ce qu'ils sont . Cela ne veut pas dire que vous savez s'ils ont encore du sens ensemble, ce qui est la phase d'analyse. Si vous ne savez pas encore ce que sont les jetons, je vous suggère de vous concentrer d'abord sur l'obtention de ces informations en premier.
J'espère que ça aide! Bienvenue chez Programmers SE!
la source
En lisant votre message, j'ai remarqué deux questions dans votre question. Le plus important est de savoir comment analyser. Il existe plusieurs types de parseurs (par exemple analyseur de descente récursive , LR Parsers , Packrat parseurs ) et générateurs d'analyseur (par exemple GNU bison , ANTLR ) vous pouvez utiliser pour parcourir un programme textuel « récursive » donné une grammaire (explicite ou implicite).
La deuxième question concerne le format de stockage des fonctions. Lorsque vous ne faites pas de traduction dirigée par la syntaxe , vous créez une représentation intermédiaire de votre programme, qui peut être une arborescence de syntaxe abstraite ou un langage intermédiaire personnalisé, afin de poursuivre le traitement avec lui (compiler, transformer, exécuter, écrire sur un fichier, etc.).
la source
D'un point de vue générique, la définition d'une fonction n'est guère plus qu'une étiquette, ou un signet, dans le code. La plupart des autres opérateurs de boucle, de portée et conditionnels sont similaires; ce sont des remplaçants pour une commande de base "jump" ou "goto" dans les niveaux d'abstraction inférieurs. Un appel de fonction se résume essentiellement aux commandes informatiques de bas niveau suivantes:
Une déclaration "retour" ou similaire fera alors ce qui suit:
Les fonctions ne sont donc que des abstractions dans une spécification de langage de niveau supérieur, qui permettent aux humains d'organiser le code de manière plus facile à gérer et intuitive. Lorsqu'elles sont compilées dans un langage assembleur ou intermédiaire (JIL, MSIL, ILX), et définitivement lorsqu'elles sont rendues sous forme de code machine, presque toutes ces abstractions disparaissent.
la source