Como as linguagens de programação definem e salvam funções / métodos? Estou criando uma linguagem de programação interpretada em Ruby e estou tentando descobrir como implementar a declaração de função.
Minha primeira idéia é salvar o conteúdo da declaração em um mapa. Por exemplo, se eu fiz algo como
def a() {
callSomething();
x += 5;
}
Então eu adicionaria uma entrada no meu mapa:
{
'a' => 'callSomething(); x += 5;'
}
O problema disso é que ele se tornaria recursivo, porque eu teria que chamar meu parse
método na string, que chamaria parse
novamente quando encontrada doSomething
e, então, ficaria sem espaço de pilha eventualmente.
Então, como as linguagens interpretadas lidam com isso?
Respostas:
Eu estaria correto ao supor que sua função "analisar" não apenas analisa o código, mas também o executa ao mesmo tempo? Se você quiser fazer dessa maneira, em vez de armazenar o conteúdo de uma função no seu mapa, armazene o local da função.
Mas há uma maneira melhor. É preciso um pouco mais de esforço, mas produz resultados muito melhores à medida que a complexidade aumenta: use uma Árvore de sintaxe abstrata.
A idéia básica é que você analise o código apenas uma vez, sempre. Então você tem um conjunto de tipos de dados representando operações e valores e cria uma árvore deles, assim:
torna-se:
(Esta é apenas uma representação de texto da estrutura de um AST hipotético. A árvore real provavelmente não estaria na forma de texto.) De qualquer forma, você analisa seu código em um AST e executa o seu intérprete diretamente no AST, ou use um segundo passe ("geração de código") para transformar o AST em algum formato de saída.
No caso do seu idioma, o que você provavelmente faria é ter um mapa que mapeie nomes de funções para ASTs, em vez de nomes de funções para cadeias de funções.
fonte
(((((((((((((((( x )))))))))))))))))
. Na realidade, as pilhas podem ser muito maiores e a complexidade gramatical do código real é bastante limitada. Certamente, se esse código tiver que ser legível por humanos.Você não deveria estar chamando de análise ao ver
callSomething()
(presumo que você quis dizercallSomething
antesdoSomething
). A diferença entrea
ecallSomething
é que um é uma definição de método enquanto o outro é uma chamada de método.Quando você vir uma nova definição, deseje fazer verificações relacionadas à garantia de poder adicionar essa definição, portanto:
Supondo que essas verificações passem, você pode adicioná-lo ao seu mapa e começar a verificar o conteúdo desse método.
Ao encontrar uma chamada de método como
callSomething()
, você deve executar as seguintes verificações:callSomething
no seu mapa?Se você achar que
callSomething()
está tudo bem, então, neste momento, o que você gostaria de fazer realmente depende de como deseja abordá-lo. A rigor, quando você souber que essa chamada está correta nesse momento, você poderá salvar apenas o nome do método e os argumentos sem entrar em mais detalhes. Ao executar seu programa, você invocará o método com os argumentos que você deve ter no tempo de execução.Se você quiser ir além, poderá salvar não apenas a string, mas um link para o método real. Isso seria mais eficiente, mas se você precisar gerenciar a memória, pode ficar confuso. Eu recomendaria que você simplesmente segurasse a corda primeiro. Mais tarde, você pode tentar otimizar.
Observe que tudo isso pressupõe que você tenha lexxado seu programa, o que significa que você reconheceu todos os tokens em seu programa e sabe o que são . Isso não quer dizer que você saiba se eles ainda fazem sentido juntos, que é a fase de análise. Se você ainda não sabe quais são os tokens, sugiro que você se concentre em obter essas informações primeiro.
Espero que ajude! Bem-vindo ao programadores SE!
fonte
Ao ler sua postagem, notei duas perguntas na sua pergunta. O mais importante é como analisar. Existem muitos tipos de analisadores (por exemplo , analisador de descida recursiva , LR Parsers , Packrat Parsers ) e geradores de analisador (por exemplo, GNU bison , ANTLR ) que você pode usar para percorrer um programa textual "recursivamente", dada uma gramática (explícita ou implícita).
A segunda pergunta é sobre o formato de armazenamento para funções. Quando você não está fazendo tradução direcionada à sintaxe , cria uma representação intermediária do seu programa, que pode ser uma árvore de sintaxe abstrata ou alguma linguagem intermediária personalizada, a fim de continuar o processamento com ela (compilar, transformar, executar, escrever em um arquivo etc).
fonte
Do ponto de vista genérico, a definição de uma função é pouco mais que um rótulo ou marcador no código. A maioria dos outros operadores de loop, escopo e condicionais são semelhantes; eles são substitutos de um comando básico "pular" ou "ir para" nos níveis mais baixos de abstração. Uma chamada de função basicamente se resume aos seguintes comandos de computador de baixo nível:
Uma declaração "return" ou similar fará o seguinte:
As funções, portanto, são simplesmente abstrações em uma especificação de linguagem de nível superior, que permite aos humanos organizar o código de uma maneira mais sustentável e intuitiva. Quando compiladas em uma linguagem assembly ou intermediária (JIL, MSIL, ILX) e, definitivamente, quando renderizadas como código de máquina, quase todas essas abstrações desaparecem.
fonte