Necessidade? Mais ou menos!? Mas por que não fazer?

Em paralelo ao Projeto Pseudo-Código Online (Que já está próximo de acabar), estou com um projeto que finalmente saiu da fase de planejamento, sempre tive como hobby desenvolvimento para plataformas antigas e/ou embarcadas simples (Ex: Arduino), e elas tem algumas coisas em comum, são 8 ou 16bits e tem suporte a linguagens de programação antigas ou burocráticas, principalmente limitadas na manipulação de Strings, e no caso a linguagem C não me atrái tanto pra usar neste tipo de hobby, já que não acho nada que eu goste, por que não criar a minha linguagem de programação?

Mas não quero criar algo único que se tenha que aprender todo uma base de conhecimento nova para se usar, queria criar algo familiar, pensei em várias linguagens para servir de base, Ruby, Python, C#, Java, F#, Rust, Go, e comparei prós e contras, fiz alguns testes, e cheguei a alguns pontos e requisitos que limitariam a implementação:

  • Cálculos devem ser feitos por padrão em 16 bits. (Cálculos em 32 e 64bits podem ser opcionais mas não como padrão, como nas linguagens modernas)

  • Tem que ser uma linguagem estritamente tipada, ou pelo menos que a parte dinâmica seja opcional. (Para economizar processamento da verificação da estrutura do objeto sempre que for interagir com ele)

  • Ser modular. (Assim podendo desabilitar partes da biblioteca padrão, para ambientes mais limitados)

  • Ser orientada à objetos. (Já tem dezenas de linguagens procedurais nesses ambientes)

  • Tem que funcionar bem como uma linguagem procedural, mesmo que pra isso se use classes estáticas. (Pois na implementação da runtime, a parte mais demorada é a orientação a objetos, então tem que ser possível ir construindo em camadas, adicionando complexidade ao passar do tempo)

  • Código feito para esta nova linguagem, deve ser compilável na linguagem “mãe”, mesmo que não seguindo 100% das melhores práticas desta. (Programe uma vez e compile (e chore) para várias plataformas)

  • O compilador ser pequeno o suficiente para ser executável em algumas das plataformas destino.

  • O compilador deve em algum momento ser auto-suficiente, mesmo que pra isso seja codificado como um software procedural inicialmente. (Sendo mais claro: Poder compilar a si mesmo)

  • Ser possível criar as ferramentas auxiliares na própria linguagem, por exemplo o Assembler e o Linker.

  • O conjunto de ferramentas deve ser independênte de plataforma.

  • O compilador deve ser fácil de ser programado, então uma linguagem “mais limitada” na parte de sintaxe, facilitaria o desenvolvimento.

Depois de muito pensar cheguei a alguns candidatos, onde infelizmente tive que tirar linguagens que realmente eu gostaria de portar, mas por serem muito dinâmicas e/ou com a runtime base muito complexa, por exemplo Ruby, que é extremamente dinâmica, o que gera inúmeros problemas em processadores limitados.

Acabei ficando com Java e C#, por terem suas sintaxes travadas, e não dependente de identação, o que agiliza validações necessárias, das duas, acabei excluindo o Java, por já ter existido ports no passado para ambientes mais limitados, que não ficaram muito bons na minha opinião, por exemplo o Java ME, e queria tentar algo que não tivesse sido tentado antes.

C# e agora?

C# é uma linguagem com sintaxe simples do ponto de vista de se montar uma árvore binária para armazenar os tokens (trechos de código da linguagem), assim facilitando a compilação, mas tem um detalhe…. C# é extremamente complexa na parte da biblioteca nativa, e agora?

Corta TUTOO!!

Vamos limitar a linguagem ao seu mínimo do mínimo para ser compilável, por exemplo o que ficaria neste primeiro esboço:

  • Strings (Afinal o motivador foram elas)

  • Comandos básicos (if, while, …)

  • Classes estáticas e funções estáticas (Inicialmente apenas elas)

  • Tipos simples da própria linguagem (byte, sbyte, ushort, short, bool)

  • Uma pseudo orientação a objetos, o compilador trataria de converter coisas como 123.ToString() para algo próximo do procedural.

  • Campos, Variáveis “Globais”, Variáveis locais, Ponteiros invisíveis para o que seriam os objetos.

  • Verificação de overflow de variáveis, afinal não queremos outra linguagem C, e sim algo mais próximo de um Rust ou da .NET.

Lembrando que isto seria, apenas um pontapé, quando concluir estes itens, será expandido, quem sabe um dia chegando a um mini C# mais completo.

Como compilar uma linguagem feita para uma sandbox bonitinha?

Agora que vem o problema, a maioria das linguagens modernas não são compiladas diretamente para a máquina, e quando são, é em tempo de execução para trazer mais velocidade, num processo complexo de conversão de bytecode para linguagem de máquina, que não seria viável para uma arquitetura 16bits, principalmente por dificilmente aproveitarmos o bytecode dessas linguagens por serem a maioria focados para cálculo em 32 ou 64 bits.

Uma das formas possíveis é fazer um jogo de fumaça e espelhos, para que o programador veja a orientação a objetos, mesmo que limitadamente, e inicialmente por baixo dos panos ela não existir.

Strings

Sempre elas, o motivo que nas linguagens clássicas são sempre um puxadinho da biblioteca padrão, é que é muito complexo manter uma boa estrutura de strings nativa, mas estamos aqui pra isso, iniciando por como armazená-las.

Temos que achar forma simples de se manter strings na memória com algum nível de segurança, lembrando que estamos falando de processadores limitados, onde normalmente não existem nem a separação de aneis de permissionamento, quem dirá limitação de acesso a memória.

Mas conseguiremos é no máximo impedir que o programador, por métodos normais, insira 500 bytes onde cabe 200, mas … existe um jeito, se antes de cada string, podemos guardar o tamanho máximo dela, para o controle do tamanho atual da string, se terminará com 0 após o último caractere válido dela (Binário 0, e não o número escrito “0”), ainda como extra pode-se guardar um byte a mais ao final da string onde sempre será 0, por exemplo segue abaixo um exemplo de uma string nesse formato, onde cada byte é representado por um bloco entre []:

[5] [o] [i] [e] [0] [0] [0]

Observe que no inicio tem o número 5, que guarda o tamanho máximo desta string, seguido de “oie” e zeros, antes de toda operação com strings, se leria primeiro o número 5, para verificar qual o tamanho máximo que cabe nela, e depois efetuaria o que precisa, sempre observando o limite, e preservando o ultimo 0 extra dela.

Este zero final da string é apenas para caso o código tenha que interagir com código externo onde normalmente as strings são terminadas com 0, então se mandar para o sistema operacional imprimir uma string de 5 bytes que contenha “oieee”, o sistema operacional irá percorrer todos os bytes até o último “e”, onde encontrará depois dele um 0 que encerrará a rotina.

Assim “protegemos”, mesmo que de forma arcaica, nosso próprio programa, que não aceitará concatenar strings maior que o destino, e o sistema operacional e/ou bibliotecas externas que dependem do 0 final da string.

Certo, mas por onde começar? De baixo, do naddaa.

Para que esses objetivos sejam alcançados, precisamos fatiar nossos objetivos em camadas, igual uma cebola, onde vamos abri-la a chorar… =), abaixo eu listei os o que nos guiará neste projeto:

  • Ter uma plataforma destino inicial, com um Assembler simples que possa ser utilizado pelo nosso compilador.

  • Quebrar o compilador em etapas e sub-etapas, para facilitar e economizar memória caso seja executádo na plataforma destino.

  • As primeiras etapas devem ser independente de plataforma, deixando apenas as ultimas etapas gerarem código nativo, facilitando o porte para outros processadores.

  • Implementar a partir do mais simples e ir adicionando complexidade.

Qual a plataforma inicial?

Como já tem um curso em andamento aqui no blog de Assembly para 8086, vou começar por ela, mas tenho como objetivo no mínimo cubrir 3 plataformas ao terminar o projeto, 8086 (IBM PC, Placas PC104 de uso industrial), Z80 (MSX, Dispositivos Embarcados) e o ATmega (Arduino UNO).

Etapas, Sub-etapas, Sub-coisos e Sub-trécos

Por que quebrar em pedacinhos? Por que os ambientes que tenho como objetivo rodar tem no máximo 1 Mega de RAM, e manter uma base de código enorme no compilador impediria de rodar nestas plataformas, inicialmente vou separar em 5 etapas, mas dentro de cada uma vou criar fases de processamento, onde se houver necessidade posso separa-las facilmente em etapas independentes, serão:

  • Pré-Processamento - Leitura e separação de trechos de códigos em um formato binário fácil de processar

  • Processamento - Compilação desses trechos em uma estrutura com a lógica final, por exemplo convertendo Expressões matemáticas em blocos pequenos e sequenciais

  • Compilação Final - Conversão deste binário independente de plataforma em código Assembly da plataforma destino (com o objetivo de simplificar essa etapa, não gerarei binário direto)

  • Montagem - Montágem do código de máquina, onde gerará o código binário e uma tabela de referências internas e externas

  • Vinculação - Vinculação de todos os arquivos binários (normalmente um projeto é feito de vários arquivos de código fonte), onde lê as tabelas de referências, vincula os binários em um único, corrigindo os apontamentos entre eles, e dependendo de como for a saída final, marcando quais as bibliotecas externas que devem ser carregadas pelo sistema operacional quando for executado.

Dessas apenas duas etapas são mais complexas, a segunda e a última, essas tomarão mais tempo de desenvolvimento para ficarem perfeitas, atualmente o projeto está no status abaixo e também incluo a ordem que serão feitas:

  • Pré-Processamento - 10% - Terceira
  • Processamento - 0% - Quarta
  • Compilação Final - 0% - Quinta
  • Montagem - 80% - Primeira
  • Vinculação - 10% - Segunda

Por que fazer de tras pra frente?

Fazendo o destino primerio, aprendemos como o código tem que ser gerado pelas etapas anteriores para que seja ao menos minimamente otimizado, e nos familiarizamos com a plataforma destino.

Quando ficará pronto?

Imagino que o projeto inicial leve 6 meses no ritmo atual, fazendo aos sabados, por mais que a parte de montagem tenha ficado praticamente pronta, muito rápididamente (1 dia), a etapa de processamento levará mais tempo para ficar perfeita, para que sirva para compilar o próprio projeto do compilador, por exemplo.

Conforme forem havendo Alphas e Betas, irei postar aqui os executáveis no de caso alguem tenha o interesse em testar, como é um Hobby que pretendo terminar bonitinho, vou manter durante o período de desenvolvimento o código fechado, mas provávelmente libere ao final o código fonte como opensource, só não o farei antes para não perder o foco em ter que manter um GitHub bonitinho.

Além disso, o código inicial, como explicado anteriormente, não seguirá nenhuma boa prática do C#, ou sequer será orientado a objeto, então quando estiver mais amadurecido, provavelmente já terá orientação a objetos, e o compilador também seja parcialmente convertido para padrões mais próximos ao do C# para ser disponibilizado publicamente.

Afinal será um C#?

Não tem nem como, por exemplo Reflection seria praticamente impossível, Dynamics bem dificil, List<>, Dictionary<>, só serão possíveis muito no futuro, então será uma nova linguagem, que será compilável pelo compilador do C#, mas não terá sequer o nome do C#, como não sou muito criativo =P, se chamará HCSustenido (Podendo ser escrito como HC#).

Protótipos?

Ainda está muito inicial para poder subir os executáveis, o Montador está concluído apenas a parte matemática, comparações e de ponteiros, faltando terminar os pulos.

Os protótipos iniciais do Vinculador gerarão executáveis para Windows 3.1 (Afinal por que não?), na teoria são executáveis com “conversores” tipo WineVDM em Windows 64bits, mas também quero gerar binários puros sem vinculação de sistema operacional para uso embarcado e de desenvolvimento de sistemas operacionais.

Vou poder fazer programas para Windows?

Só se for 16bits, o objetivo não é suportar plataformas novas, pois essas já tem o C#, no máximo seria viável gerar código compatível com Windows 9X, mas seria extrapolação do foco do projeto, estou focando no Windows 3.1 por ter dezenas de bibliotecas, podendo usa-las para itens mais básicos, como solicitar algo para o usuário, ou escrever algo na tela e não precisar implementar tudo do zero no início.

Poderá implementar [Insira seu sistema operacional clássico aqui]?

Nada impede de gerar binário para MINIX, MS-DOS, CP/M-86, ELKS, ou outro sistema operacional 16bits, mas o foco inicial é fazer o básico funcionar, esse básico funcionando poderá se expandir.

Como posso ajudar?

Caso queira ajudar o projeto entre em contato comigo, via twitter ou por aqui, que posso disponibilizar um puxadinho no Git para você acessar :P

E agora?

Esperar kkk, provavelmente até semana que vem tenha ao menos o Montador de Código 8086 pronto, gerando ““““Incríveis”””” aplicativos para Win3.1, onde começarei a fazer o compilador em si.

Detalhes extras, Cuidado! Extremamente NERD

  • O Compilador gerará código de chamada no padrão PASCAL FAR, o que torna compatível com a API Win16.

  • O código Assembly será próximo do padrão do NASM, estou usando Este Datasheet do Processador 8086 e Este de uma versão genérica como base para gerar os comandos Assembly, quero ter uma abrangência de 100% dos comandos mesmo que inicialmente não sejam extremamente otimizados, por exemplo não gerando comandos de 8bits quando o valor couber, ao invés de gerar comandos de 16bits.

  • Não suportarei macros complexas, mas suportarei o básico para implementar chamadas de forma simples (INVOKE, PROC, ENDPROC, CALLP)

  • Vou implementar a forma mais simples do executável padrão NE para Win16.

  • Como brincadeira estou implementando o mínimo da System.Windows.Forms para poder usar o MessageBox.

  • O código está sendo montado do zero, não estou vendo outros compiladores para não ser influenciado, principalmente por algumas metodologias mais modernas serem muito complexas para rodar em processadores muito limitados.

  • E o screenshot no topo é de parte do código fonte do Montador, é uma tabela binária, no estilo Assembler bem anos 80, mas é uma das formas mais rápidas de gerar um Montador.

E este foi o motivo que não teve postagem na noite desses ultimos dias, estava terminando o planejamento e iniciando este projeto.

Ficamos por enquanto até aqui,

Inté =)