
1. Introdução
Saudações a todos que apareceram para ler meu próximo artigo.
Repito, descrevo a criação de uma linguagem de linguagem de programação com base em trabalhos anteriores, cujos resultados são
descritos neste post .
Na primeira parte (link:
habr.com/post/435202 ), descrevi os estágios do design e da escrita de uma VM de linguagem que executará nossos aplicativos futuros em nosso idioma futuro.
Neste artigo, pretendo descrever os principais estágios da criação de uma linguagem de programação intermediária que será montada em um bytecode abstrato para execução direta em nossa VM.
Eu acho que não vai doer fornecer imediatamente links para o site do projeto e seu repositório.
SiteRepositórioDevo dizer imediatamente que todo o código está escrito no FPC e darei exemplos.
Então, começamos nossa iluminação.
Por que entregamos a linguagem intermediária?
Vale a pena entender que a conversão de um programa de uma linguagem de alto nível diretamente para um bytecode executável, que consiste em um conjunto limitado de instruções, é tão trivial que é melhor simplificá-lo por uma ordem de magnitude, adicionando uma linguagem intermediária ao projeto. É muito melhor simplificar o código gradualmente do que apresentar imediatamente expressões, estruturas e classes matemáticas com um conjunto de códigos de operação. A propósito, é assim que a maioria dos tradutores e compiladores de terceiros trabalha.
No meu artigo anterior, escrevi sobre como implementar uma VM de idioma. Agora precisamos implementar uma linguagem do tipo assembler e funcionalidade para escrever o tradutor posteriormente. Nessas etapas, lançamos as bases para o projeto futuro. Vale a pena entender que, quanto melhor a fundação, mais íngreme o edifício.
Damos o primeiro passo para realizar esse milagre
Para começar, vale a pena definir uma meta. O que vamos realmente escrever? Quais características o código final deve ter e o que deve fazer?
Posso criar uma lista das principais partes funcionais das quais essa parte do projeto deve consistir:
- Montador simples. Converte instruções simples em um conjunto de códigos de operação para VMs.
- A implementação básica do funcional para implementar variáveis.
- A implementação básica do funcional para trabalhar com constantes.
- Funcionalidade para dar suporte aos pontos de entrada para métodos e calcular seus endereços no estágio de tradução.
- Talvez mais alguns pães funcionais.
A ilustração acima mostra um fragmento de código em um idioma intermediário que é convertido em código para uma VM por um tradutor primitivo, que será discutido.
Assim, os objetivos são definidos, vamos prosseguir para a implementação.
Escrevendo um assembler simples
Nós nos perguntamos o que é montador?
De fato, este é um programa que realiza a substituição de códigos de operação em vez de suas descrições textuais.
Considere este código:
push 0 push 1 add peek 2 pop
Depois de processar o código do assembler, obtemos o código executável da VM.
Vemos que as instruções podem ser monossilábicas e bisilábicas. Não há instruções mais complicadas para a VM empilhada.
Precisamos de um código que possa extrair tokens de uma string (levamos em consideração que pode haver strings entre eles).
Nós escrevemos:
function Tk(s: string; w: word): string; begin Result := ''; while (length(s) > 0) and (w > 0) do begin if s[1] = '"' then begin Delete(s, 1, 1); Result := copy(s, 1, pos('"', s) - 1); Delete(s, 1, pos('"', s)); s := trim(s); end else if Pos(' ', s) > 0 then begin Result := copy(s, 1, pos(' ', s) - 1); Delete(s, 1, pos(' ', s)); s := trim(s); end else begin Result := s; s := ''; end; Dec(w); end; end;
Ok, agora precisamos implementar algo como uma construção de caso de switch para cada instrução, e nosso assembler simples está pronto.
Variáveis
Lembre-se de que nossa VM possui uma matriz de ponteiros para suportar variáveis e, consequentemente, endereçamento estático. Isso significa que o funcional para trabalhar com variáveis pode ser representado como um TStringList, no qual strings são os nomes de variáveis e seus índices são seus endereços estáticos. Deve-se entender que a duplicação de nomes de variáveis nesta lista é inaceitável. Eu acho que você pode imaginar o código necessário e / ou até mesmo escrever você mesmo.
Se você quiser examinar a implementação concluída, seja bem-vindo: /lang/u_variables.pas
Constantes
O princípio aqui é o mesmo que com variáveis, mas há uma coisa. Para otimizar, é melhor vincular não aos nomes das constantes, mas aos seus valores. I.e. cada valor constante pode ter um TStringList, que servirá para armazenar os nomes das constantes com esse valor.
Para constantes, você deve especificar o tipo de dados e, consequentemente, para adicioná-los ao idioma, você precisará escrever um pequeno analisador.
Implementação: /lang/u_consts.pas
Pontos de entrada do método
Para implementar o bloqueio de código, suporte para diferentes designs, etc. o suporte a essa funcionalidade deve ser implementado no nível do assembler.
Considere um exemplo de código:
Summ: peek 0 pop peek 1 pop push 0 new peek 2 mov push 2 push 0 add jr
A descrição acima é um exemplo de tradução do método Summ:
func Summ(a, b): return a + b end
Deve-se entender que não há códigos de operação para pontos de entrada. O que é um ponto de entrada para o método Summ? Esse número primo é o deslocamento do próximo ponto de entrada do código de operação. (o deslocamento do opcode é o número do opcode relativo ao início do bytecode abstrato executável). Agora temos uma tarefa - precisamos calcular esse deslocamento no estágio de compilação e, como opção, declarar a constante Summ como esse número.
Escrevemos para isso um certo contador de pesos para cada operador. Temos operadores monossilábicos simples, por exemplo, "pop". Eles ocupam 1 byte. Existem outros mais complexos, por exemplo, "push 123" - eles ocupam 5 bytes, 1 para o código de operação e 4 para o tipo int não assinado.
A essência do código para adicionar suporte ao assembler de pontos de entrada:
- Temos um contador, digamos i = 0.
- Percorremos o código, se tivermos uma construção do tipo "push 123" e adicionamos 5 a ele, se o código de operação simples for 1. Se tivermos um ponto de entrada, remova-o do código e declare a constante correspondente com o valor do contador e o nome do ponto de entrada.
Outra funcionalidade
Por exemplo, é uma simples conversão de código antes do processamento.
Sumário
Implementamos nosso pequeno montador. Vamos precisar implementar um tradutor mais complexo baseado nele. Agora podemos escrever pequenos programas para nossa VM. Assim, em outros artigos o processo de escrever um tradutor mais complexo será descrito.
Obrigado por ler até o fim, se você fez.
Se algo não estiver claro para você, estou aguardando seus comentários.