Nova linguagem de programação Mash

Por vários anos, tentei desenvolver minha própria linguagem de programação. Eu queria criar, na minha opinião, a linguagem mais simples, totalmente funcional e conveniente possível.

Neste artigo quero destacar as principais etapas do meu trabalho e, para começar, descrever o conceito criado da linguagem e sua primeira implementação, na qual estou trabalhando atualmente.

Deixe-me dizer antecipadamente que escrevi todo o projeto em Free Pascal, porque... os programas nele podem ser montados para um grande número de plataformas, e o próprio compilador produz binários muito otimizados (eu coleciono todos os componentes do projeto com o sinalizador O2).

Tempo de execução da linguagem

Em primeiro lugar, vale a pena falar sobre a máquina virtual que tive que escrever para rodar futuras aplicações na minha linguagem. Decidi implementar uma arquitetura de pilha, talvez porque fosse a maneira mais fácil. Não encontrei um único artigo normal sobre como fazer isso em russo, então, depois de me familiarizar com o material em inglês, comecei a projetar e escrever minha própria bicicleta. A seguir apresentarei minhas ideias e desenvolvimentos “avançados” neste assunto.

Implementação de pilha

Obviamente, no topo da VM está a pilha. Na minha implementação funciona em blocos. Essencialmente, este é um array simples de ponteiros e uma variável para armazenar o índice do topo da pilha.
Quando é inicializado, um array de 256 elementos é criado. Se mais ponteiros forem colocados na pilha, seu tamanho aumentará nos próximos 256 elementos. Assim, ao remover elementos da pilha, seu tamanho é ajustado.

A VM usa várias pilhas:

  1. Pilha principal.
  2. Uma pilha para armazenar pontos de retorno.
  3. Pilha coletora de lixo.
  4. Try/catch/finalmente bloqueia a pilha do manipulador.

Constantes e Variáveis

Este é simples. As constantes são tratadas em um pequeno trecho de código separado e estão disponíveis em aplicações futuras por meio de endereços estáticos. Variáveis ​​​​são um array de ponteiros de um determinado tamanho, o acesso às suas células é feito por índice - ou seja, endereço estático. As variáveis ​​podem ser colocadas no topo da pilha ou lidas a partir daí. Na verdade, porque Embora nossas variáveis ​​armazenem essencialmente ponteiros para valores na memória da VM, a linguagem é dominada pelo trabalho com ponteiros implícitos.

Coletor de lixo

Na minha VM é semiautomático. Aqueles. o próprio desenvolvedor decide quando chamar o coletor de lixo. Não funciona usando um contador de ponteiro regular, como em Python, Perl, Ruby, Lua, etc. É implementado através de um sistema de marcadores. Aqueles. quando se pretende atribuir um valor temporário a uma variável, um ponteiro para esse valor é adicionado à pilha do coletor de lixo. No futuro, o coletor percorrerá rapidamente a lista de ponteiros já preparada.

Lidando com blocos try/catch/finally

Como em qualquer linguagem moderna, o tratamento de exceções é um componente importante. O núcleo da VM é encapsulado em um bloco try..catch, que pode retornar à execução do código após capturar uma exceção, colocando algumas informações sobre ela na pilha. No código do aplicativo, você pode definir blocos de código try/catch/finalmente, especificando pontos de entrada em catch (manipulador de exceção) e finalmente/end (fim do bloco).

Multithreading

É suportado no nível da VM. É simples e conveniente de usar. Funciona sem sistema de interrupção, portanto o código deve ser executado em vários threads várias vezes mais rápido, respectivamente.

Bibliotecas externas para VMs

Não há como ficar sem isso. VM oferece suporte a importações, semelhante à forma como é implementado em outras linguagens. Você pode escrever parte do código em Mash e parte do código em idiomas nativos e depois vinculá-los em um só.

Tradutor de linguagem Mash de alto nível para bytecode para VMs

Linguagem intermediária

Para escrever rapidamente um tradutor de uma linguagem complexa em código VM, primeiro desenvolvi uma linguagem intermediária. O resultado foi um espetáculo terrível, semelhante ao de uma montadora, que não faz sentido considerar aqui. Direi apenas que neste nível o tradutor processa a maioria das constantes e variáveis, calcula os seus endereços estáticos e os endereços dos pontos de entrada.

Arquitetura do tradutor

Não escolhi a melhor arquitetura para implementação. O tradutor não constrói uma árvore de código, como fazem outros tradutores. Ele olha para o início da estrutura. Aqueles. se o trecho de código que está sendo analisado se parece com “while <condição>:”, então é óbvio que esta é uma construção de loop while e precisa ser processada como uma construção de loop while. Algo como um switch-case complexo.

Graças a esta solução arquitetônica, o tradutor não foi muito rápido. No entanto, a facilidade de sua modificação aumentou significativamente. Adicionei as estruturas necessárias mais rápido do que meu café esfriava. O suporte OOP completo foi implementado em menos de uma semana.

Otimização de código

Aqui, é claro, poderia ter sido melhor implementado (e será implementado, mas mais tarde, assim que for possível). Até agora, o otimizador só sabe como cortar códigos não utilizados, constantes e importações do assembly. Além disso, várias constantes com o mesmo valor são substituídas por uma. Isso é tudo.

Linguagem mash

Conceito básico de linguagem

A ideia principal era desenvolver a linguagem mais funcional e simples possível. Acho que o desenvolvimento cumpre sua tarefa com força.

Blocos de código, procedimentos e funções

Todas as construções do idioma são abertas com dois pontos. : e são fechados pela operadora final.

Procedimentos e funções são declarados como proc e func, respectivamente. Os argumentos estão listados entre parênteses. Tudo é como a maioria das outras línguas.

Operador retorno você pode retornar um valor de uma função, operador quebrar permite sair do procedimento/função (se estiver fora dos loops).

Exemplo de código:

...

func summ(a, b):
  return a + b
end

proc main():
  println(summ(inputln(), inputln()))
end

Projetos Suportados

  • Loops: for..end, while..end, até..end
  • Condições: if..[else..]end, switch..[case..end..][else..]end
  • Métodos: proc <nome>():... fim, func <nome>():... fim
  • Rótulo e ir para: <nome>:, pular <nome>
  • Enumerações enum e matrizes constantes.

Variáveis

O tradutor pode determiná-los automaticamente ou se o desenvolvedor escrever var antes de defini-los.

Exemplos de código:

a ?= 10
b ?= a + 20

var a = 10, b = a + 20

Variáveis ​​globais e locais são suportadas.

OOP

Bem, chegamos ao tópico mais delicioso. Mash suporta todos os paradigmas de programação orientada a objetos. Aqueles. classes, herança, polimorfismo (incluindo dinâmico), reflexão automática dinâmica e introspecção (completa).

Sem mais delongas, é melhor apenas dar exemplos de código.

Uma classe simples e trabalhando com ela:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

proc main():
  x ?= new MyClass(10, 20)
  println(x->Summ())
  x->Free()
end

Produzirá: 30.

Herança e polimorfismo:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

class MyNewClass(MyClass):
  func Summ
end

func MyNewClass::Summ():
  return ($a + $b) * 2
end

proc main():
  x ?= new MyNewClass(10, 20)
  println(x->Summ())
  x->Free()
end

Produzirá: 60.

E quanto ao polimorfismo dinâmico? Sim, isso é reflexão!:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

class MyNewClass(MyClass):
  func Summ
end

func MyNewClass::Summ():
  return ($a + $b) * 2
end

proc main():
  x ?= new MyClass(10, 20)
  x->Summ ?= MyNewClass::Summ
  println(x->Summ())
  x->Free()
end

Produzirá: 60.

Agora vamos fazer uma introspecção de valores e classes simples:

uses <bf>
uses <crt>

class MyClass:
  var a, b
end

proc main():
  x ?= new MyClass
  println(BoolToStr(x->type == MyClass))
  x->rem()
  println(BoolToStr(typeof(3.14) == typeReal))
end

A saída será: verdadeiro, verdadeiro.

Sobre operadores de atribuição e ponteiros explícitos

O operador ?= é usado para atribuir a uma variável um ponteiro para um valor na memória.
O operador = altera um valor na memória usando um ponteiro de uma variável.
E agora um pouco sobre dicas explícitas. Eu os adicionei ao idioma para que existissem.
@<variable> — pega um ponteiro explícito para uma variável.
?<variável> — obtém uma variável por ponteiro.
@= — atribui um valor a uma variável por meio de um ponteiro explícito para ela.

Exemplo de código:

uses <bf>
uses <crt>

proc main():
  var a = 10, b
  b ?= @a
  PrintLn(b)
  b ?= ?b
  PrintLn(b)
  b++
  PrintLn(a)
  InputLn()
end

Produzirá: algum número, 10, 11.

Tente..[pegar..][finalmente..]fim

Exemplo de código:

uses <bf>
uses <crt>

proc main():
  println("Start")
  try:
    println("Trying to do something...")
    a ?= 10 / 0
  catch:
    println(getError())
  finally:
    println("Finally")
  end
  println("End")
  inputln()
end

Planos para o futuro

Continuo olhando e olhando para GraalVM & Truffle. Meu ambiente de execução não possui um compilador JIT, portanto, em termos de desempenho, atualmente só é competitivo com o Python. Espero ser capaz de implementar a compilação JIT baseada em GraalVM ou LLVM.

repositório

Você pode brincar com os desenvolvimentos e acompanhar o projeto sozinho.

site
Repositório no GitHub

Obrigado por ler até o fim, se você leu.

Fonte: habr.com

Adicionar um comentário