4

Pitch: Pop Language: a linguagem brasileira que quer unir a simplicidade de Lua ao poder de linguagens nativas

Estou criando a Pop porque não acho que uma linguagem precise escolher entre ser simples de usar e ser capaz de construir software sério.

Hoje, normalmente a escolha parece ser esta:

  • usar uma linguagem simples e dinâmica;
  • usar uma linguagem nativa, mas aceitar mais complexidade;
  • usar async por meio de um runtime externo;
  • usar actors por meio de uma biblioteca;
  • administrar memória manualmente ou aceitar um runtime pesado.

A Pop tenta juntar essas coisas de outra forma.

Ela é uma linguagem brasileira, estaticamente tipada, compilada para código nativo e inspirada principalmente na simplicidade de Lua e Luau.

O slogan oficial resume a ideia:

Simple by nature. Powerful by design.

Como é o código

Um programa simples em Pop:

namespace Application

function main()
    local name: String = "TabNews"
    local points: Int = 10

    print(`Olá, {name}! Você possui {points} pontos.`)
end

Também é possível deixar o compilador inferir os tipos:

namespace Application

function main()
    local name = "TabNews"
    local points = 10

    print(`Olá, {name}! Você possui {points} pontos.`)
end

A diferença é que inferência não significa tipagem dinâmica.

name continua sendo uma String.
points continua sendo um Int.

O compilador sabe o tipo exato dos valores, mesmo quando não é necessário escrevê-lo manualmente.

Um loop também continua simples:

namespace Progression

function main()
    local firstTerm: Int = 2
    local difference: Int = 3
    local amount: Int = 10

    for position = 1, amount do
        local term = firstTerm + (position - 1) * difference
        print(`a{position} = {term}`)
    end
end

Não é necessário transformar uma progressão aritmética em uma disputa com o compilador.

Ao mesmo tempo, operações numéricas continuam sendo verificadas e possuem comportamento definido.

Não é apenas Lua compilada

A aparência lembra Lua, mas o modelo da linguagem é diferente.

Em Lua, uma tabela pode representar praticamente qualquer coisa:

local user = {
    name = "Paulo",
    age = 47
}

Essa mesma estrutura pode ser usada como objeto, array, mapa, módulo ou namespace.

Na Pop, estruturas diferentes possuem significados diferentes.

Um array de nomes:

local names: {String} = {
    "Ana",
    "Bruno",
    "Carla",
}

Uma tabela relacionando nomes e pontuações:

local scores: {[String]: Int} = {
    ana = 10,
    bruno = 20,
    carla = 30,
}

Um record representa dados com formato conhecido:

public record User
    name: String
    score: Int
end

Uma classe representa uma entidade com identidade e comportamento:

public class Counter
    private value: Int = 0

    public function Counter:increment()
        self.value += 1
    end

    public function Counter:current(): Int
        return self.value
    end
end

A Pop não coloca tudo dentro de uma estrutura universal e deixa o runtime descobrir o que aquilo significa.

O código continua pequeno, mas os conceitos permanecem separados.

Tipagem estática sem transformar tudo em cerimônia

A Pop não possui um tipo dinâmico universal usado como saída de emergência.

Quando uma função recebe um User, ela recebe um User.

function showUser(user: User)
    print(`${user.name}: ${user.score}`)
end

Não é necessário verificar durante a execução se o valor possui um campo chamado name.

O compilador já sabe.

Isso melhora:

  • mensagens de erro;
  • autocomplete;
  • navegação pelo código;
  • refatoração;
  • otimização;
  • documentação;
  • integração entre bibliotecas.

Mas a intenção não é obrigar o programador a declarar tudo manualmente.

A Pop tenta pedir apenas as informações que realmente não podem ser descobertas.

Opcionais fazem parte do sistema de tipos

Ausência de valor precisa aparecer no tipo.

function findUser(id: Int): User?
    if id == 1 then
        return User {
            name = "Julia",
            score = 100,
        }
    end

    return nil
end

Quem chama essa função precisa lidar com a possibilidade de não existir um usuário.

Isso é diferente de receber um valor qualquer e descobrir depois que ele era nil.

A mesma ideia vale para erros tipados.

Uma operação pode declarar quais erros produz, e quem chama decide se deseja propagar, tratar ou transformar esse erro.

A linguagem não precisa esconder falhas atrás de exceções que podem surgir de qualquer lugar.

Generics sem apagar os tipos

Também é possível criar estruturas e funções genéricas:

public record Box<T>
    value: T
end

function wrap<T>(value: T): Box<T>
    return Box<T> {
        value = value,
    }
end

Ao usar:

local number = wrap(10)
local name = wrap("Pop")

O compilador conhece os tipos concretos:

Box<Int>
Box<String>

Não é necessário converter tudo para um objeto genérico ou realizar buscas dinâmicas para descobrir o que existe dentro da estrutura.

Iteração não depende de uma estrutura especial

A Pop possui protocolos nominais de iteração.

Isso permite usar for com arrays, listas, tabelas, ranges e tipos criados pelo próprio usuário, desde que implementem o contrato correto.

for name in names do
    print(name)
end

Também existem sequências para operações como transformação e filtragem:

local result = numbers
    .map(function(value)
        return value * 2
    end)
    .filter(function(value)
        return value > 10
    end)
    .collect()

A ideia é oferecer APIs de alto nível sem obrigar cada operação a virar uma caixa dinâmica no runtime.

Memória automática em um programa nativo

A Pop usa coleta de lixo.

Isso significa que o programador pode criar objetos, strings, listas, closures e estruturas complexas sem precisar liberar cada uma manualmente.

function createUsers(): {User}
    return {
        User { name = "Ana", score = 10 },
        User { name = "Bruno", score = 20 },
        User { name = "Carla", score = 30 },
    }
end

Não existe free, e também não é necessário expressar ownership e lifetimes no código comum.

Mas a Pop continua sendo compilada para código nativo.

O objetivo não é colocar uma linguagem dinâmica dentro de um executável. O compilador conhece os tipos, os formatos dos objetos e quais partes da memória contêm referências.

Para quem escreve o programa, memória é automática.

Para a implementação da linguagem, ela continua sendo precisa.

Metaprogramação sem transformar o programa em magia

A Pop também foi pensada para possuir attributes, UDAs, reflexão e metaprogramação.

Isso pode ser usado para coisas como:

  • serialização;
  • bindings;
  • validação;
  • geração de adapters;
  • documentação;
  • integração com bibliotecas;
  • geração de código repetitivo.

Um tipo poderia, por exemplo, declarar metadata:

@Serializable
public record User
    @JsonName("user_name")
    name: String

    score: Int
end

A diferença é que essa metadata é entendida durante a compilação.

O resultado ainda precisa ser código tipado e válido.

A metaprogramação não cria uma área em que os tipos deixam de existir ou em que o runtime começa a procurar métodos e campos por strings.

A intenção é permitir automação sem perder previsibilidade.

Async, coroutines e actors fazem parte da mesma fundação

A Pop não trata concorrência apenas como uma biblioteca instalada depois.

A linguagem está sendo construída para oferecer:

  • tasks;
  • funções async;
  • await;
  • coroutines;
  • channels;
  • cancelamento;
  • actors;
  • supervisão;
  • timers;
  • eventos externos.

Uma função async deverá continuar parecendo uma função normal:

async function loadUser(id: Int): User
    local response = await Http.get(`/users/{id}`)
    return decode<User>(response.body)
end

Uma coroutine pode suspender sua execução e continuar posteriormente.

coroutine function count(limit: Int): Int
    for value = 1, limit do
        yield value
    end
end

Channels permitem coordenar tarefas:

local channel = Channel<Int>.create(32)

spawn async
    for value = 1, 10 do
        await channel.send(value)
    end
end

while local value = await channel.receive() do
    print(value)
end

Actors resolvem outro problema.

Uma coroutine ajuda a suspender e retomar uma execução.

Async ajuda a esperar sem bloquear uma thread inteira.

Um actor organiza estado, mensagens e falhas dentro de uma unidade isolada.

actor Counter
    private value: Int = 0

    message Increment(amount: Int)
    message Current(): Int

    receive Increment(amount)
        self.value += amount
    end

    receive Current()
        return self.value
    end
end

A sintaxe ainda pode mudar durante o desenvolvimento, mas o modelo é esse: o estado pertence ao actor e outros componentes interagem com ele por mensagens.

Uma falha não precisa derrubar todo o sistema

Esse é um dos pontos em que a Pop se aproxima mais de Erlang e Elixir do que de simplesmente possuir goroutines ou fibers.

Uma task possui um estado próprio:

Ready
Running
Suspended
Completed
Cancelled
Panicked

Se uma task entra em panic, ela termina.

O worker que estava executando essa task continua funcionando.

Outras tasks continuam funcionando.

Um actor também pode falhar sem necessariamente destruir actors que não possuem relação com aquela falha.

supervisor ApplicationSupervisor
    child DatabaseActor
    child HttpServerActor
    child MetricsActor
end

Se DatabaseActor falhar, o supervisor pode decidir:

  • reiniciar o actor;
  • interromper outros actors relacionados;
  • registrar a falha;
  • degradar apenas aquela funcionalidade;
  • encerrar um grupo específico.

Isso é diferente de apenas executar várias funções ao mesmo tempo.

A linguagem precisa saber onde uma falha termina e quem é responsável por ela.

Por que não apenas usar Go?

Go tornou concorrência muito mais acessível com goroutines e channels.

Mas uma goroutine não é automaticamente uma fronteira de isolamento.

Ela pode compartilhar memória com qualquer outra parte do programa. Um panic não recuperado também pode encerrar o processo inteiro.

É possível criar actors em Go, mas a garantia vem da arquitetura criada pelo programador ou por uma biblioteca.

Na Pop, actors, tasks, cancelamento e supervisão são pensados como partes do mesmo runtime.

A diferença não é apenas possuir uma API chamada Actor.

É o scheduler entender que uma task possui:

  • identidade;
  • estado;
  • resultado;
  • owner;
  • cancelamento;
  • falha;
  • ciclo de vida.

Por que não apenas usar Rust?

Rust resolve segurança de memória e concorrência de uma maneira muito forte.

Mas essa segurança aparece diretamente no modelo de programação por meio de ownership, borrowing, lifetimes, Send, Sync e outras regras.

Além disso, Rust não define um runtime async oficial.

async e Future fazem parte da linguagem, mas scheduling, timers, I/O, blocking e tasks normalmente pertencem a bibliotecas como Tokio.

Actors também são implementados acima disso.

A Pop faz outra escolha.

Ela usa memória gerenciada e possui um scheduler integrado à plataforma.

A intenção é permitir que o programador use concorrência segura sem precisar escolher e combinar várias implementações diferentes para formar o runtime básico da aplicação.

Por que não apenas usar Crystal?

Crystal também possui uma sintaxe inspirada em Ruby, tipagem estática, macros e compilação nativa.

A diferença está principalmente no modelo semântico.

Crystal preserva várias características estruturais de Ruby, como duck typing estático e macros que trabalham diretamente com a AST.

A Pop trabalha com contratos nominais e tenta fechar completamente o significado das abstrações antes de gerar o programa.

Ela também está sendo construída desde o início com um runtime unificado para memória gerenciada, tasks, async, coroutines, actors e isolamento de falhas.

Não é uma tentativa de substituir Crystal. São escolhas diferentes.

O que a Pop está tentando oferecer

A combinação é esta:

sintaxe simples
+ tipagem estática
+ executáveis nativos
+ memória automática
+ generics
+ metaprogramação
+ async e coroutines
+ channels
+ actors
+ isolamento de falhas

Normalmente essas características aparecem divididas entre várias linguagens.

Lua oferece simplicidade.

Rust oferece controle e segurança.

Go oferece um runtime concorrente integrado.

Erlang e Elixir oferecem actors, supervisão e isolamento de falhas.

A Pop tenta aprender com todas elas sem simplesmente copiar o modelo completo de nenhuma.

A ideia não é criar uma linguagem em que tudo seja implícito.

É criar uma linguagem em que o programador não precise escrever a complexidade que o compilador e o runtime conseguem administrar corretamente.

Simple by nature. Powerful by design.

Carregando publicação patrocinada...