Comment les langages de programmation définissent-ils et économisent des fonctions/méthodes? Je crée un langage de programmation interprété à Ruby et j'essaie de déterminer comment mettre en œuvre la déclaration de fonction.
Ma première idée est de sauvegarder le contenu de la déclaration sur une carte. Par exemple, si j'ai fait 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 c'est que cela deviendrait récursif, car je devrais appeler ma méthode parse
sur la chaîne, qui appellerait alors parse
à nouveau quand il a rencontré doSomething
, Et puis je manquerais d'un espace de pile finalement.
Alors, comment les langues interprétées gèrent-elles cela?
Serais-je correct en supposant que votre fonction "parse" non seulement analyse le code mais l'exécute également en même temps? Si vous vouliez le faire de cette façon, au lieu de stocker le contenu d'une fonction sur votre carte, stockez le emplacement de la fonction.
Mais il y a une meilleure façon. Il faut un peu plus d'efforts en avant, mais cela donne beaucoup de meilleurs résultats car la complexité augmente: utilisez un arbre de syntaxe abstraite.
L'idée de base est que vous n'étilisez que le code une fois, jamais. Ensuite, vous avez un ensemble de types de données représentant des opérations et des valeurs, et vous en faites un arbre, comme vous le souhaitez:
def a() {
callSomething();
x += 5;
}
devient:
Function Definition: [
Name: a
ParamList: []
Code:[
Call Operation: [
Routine: callSomething
ParamList: []
]
Increment Operation: [
Operand: x
Value: 5
]
]
]
(Il s'agit simplement d'une représentation de texte de la structure d'une ast hypothétique. L'arbre actuel ne serait probablement pas sous forme de texte.) Quoi qu'il en soit, vous analyser votre code dans une AST, puis vous exécutez votre interprète sur AST directement, ou utilisez une seconde ("génération de code") passe pour activer AST en un formulaire de sortie.
Dans le cas de votre langue, ce que vous feriez probablement est d'avoir une carte qui correspond à des noms de fonction pour fonctionner des asts, au lieu de noms de fonction aux chaînes de fonction.
Vous ne devriez pas appeler d'analyser lors de la vue callSomething()
(je présume que vous vouliez dire callSomething
plutôt que doSomething
). La différence entre a
et callSomething
est celui-là 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 chèques liés à vous assurer que vous pouvez ajouter cette définition, donc:
En supposant que ces chèques passez, vous pouvez l'ajouter à votre carte et commencer à vérifier le contenu de cette méthode.
Lorsque vous trouvez une méthode appel comme callSomething()
, vous devez effectuer les chèques suivants:
callSomething
existe dans votre carte?Si vous trouvez que callSomething()
est correct, alors à ce stade, ce que vous voudriez faire dépend vraiment de la manière dont vous souhaitez approcher cela. Strictement parlant, une fois que vous savez qu'un tel appel est correct à ce stade, vous ne pouviez enregistrer que le nom de la méthode et les arguments sans entrer plus de 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 économiser non seulement la chaîne, mais un lien vers la méthode réelle. Cela serait plus efficace, mais si vous devez gérer la mémoire, cela peut être déroutant. Je vous recommanderais de vous tenir simplement sur la chaîne au début. Plus tard, vous pouvez essayer d'optimiser.
Notez que cela suppose que vous supposez que vous avez lexexed votre programme, ce qui signifie que vous avez reconnu toutes les jetons de votre programme et savoir ce qu'ils sont. Cela ne veut pas dire que vous savez s'ils ressentent un sens encore, ce qui est la phase d'analyse. Si vous ne savez pas encore ce que sont les jetons, je vous suggère d'abord de vous concentrer sur cette information d'abord.
J'espère que cela aide! Bienvenue aux programmeurs SE!
Lire votre message, j'ai remarqué deux questions dans votre question. Le plus important est comment analyser. Il existe de nombreux types d'analyseurs (par exemple analyseur de descente récursives , Parsers LR , Packrat analgésique ) et des générateurs d'analyseurs (par exemple gnu bison , [~ # ~ ~] antlr [~ # ~] ) Vous pouvez utiliser pour traverser un programme textuel "récursivement" donné (explicite ou implicite ) grammaire.
La deuxième question concerne le format de stockage pour les fonctions. Lorsque vous ne faites pas Traduction dirigée par la syntaxe , vous créez une représentation intermédiaire de votre programme, qui peut être un Abstrait syntaxe ou une langue intermédiaire personnalisée , afin de traiter d'autres traitement avec celui-ci (compiler, transformer, exécuter, écrire sur un fichier, etc.).
D'un point de vue générique, la définition d'une fonction est un peu plus qu'une étiquette ou un signet, dans le code. La plupart des autres boucles, les opérateurs de la portée et des opérateurs conditionnels sont similaires; Ils sont stand-ins pour une commande "saut" de base ou "goto" dans les niveaux inférieurs d'abstraction. Un appel de fonction se résume essentiellement aux commandes informatiques de bas niveau suivantes:
Une déclaration "retour" ou similaire fera ensuite ce qui suit:
Les fonctions sont donc simplement des abstractions dans une spécification de langage de niveau supérieur, qui permettent aux humains d'organiser du code de manière plus maintenue et intuitive. Lorsqu'il est compilé dans un assemblage ou une langue intermédiaire (Jil, MSIL, ILX), et définitivement lorsqu'il est rendu en tant que code machine, presque toutes ces abstractions disparaissent.