Executando verificação de segurança...
8

ANTLR: O jeito mais facil de criar linguagens de programação

Quem ja tentou fazer uma linguagem de programação, sabe a dor que é fazer um lexer e um parser, mas sabia que tem um jeito de gerar essa parte do código escrevendo em um arquivo definindo a gramática?

Conheça o ANTLR!

ANTLR 4

O ANTLR atualmente suporta java, C#, python 2 e 3, go, c++, swift, php e dart

Ele consegue gerar codigo pra todas essas linguagens, mas a oficial e a que tem melhor integração é o java com gradle/maven, então vai ser essa a linguagem que vou utilizar nesse post.

Alem disso ele tem extensões para arquivos .g4 do antlr para intellij, eclipse e vscode, que alem de dar syntax highlighting e um Language Server para checkar o codigo e mostrar os error no editor, ele ainda tem uma janela para testar a gramatica e mostrar a Parse Tree do seu codigo escrito na sua linguagem.

Começo

A linguagem desse exemplo vai ser uma linguagem stack-based como fortran:

function main in
    69 print
    35 34 + print
    69 96 swap print print
    70 dup print print
    2 2 + 4 = if
        "2 + 2 = 4, Cool" print
    end else 
        "2 + 2 = 22?" print
    end
end

Crie um projeto com o gradle e adicione o plugin do antlr e o runtime do antlr:

plugins {
    id 'java'
    id "antlr"
    id "application"
}

dependencies {
    antlr "org.antlr:antlr4:4.10.1"
    implementation "org.antlr:antlr4-runtime:4.10.1"
}
generateGrammarSource {
    arguments += ["-visitor", "-long-messages"]
}
application {
    mainClassName = "org.mylang.Main"
}
group 'org.mylang'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

Agora você precisa criar uma pasta em src/main/antlr e colocar criar um arquivo de gramatica:

_____________________________________
| src/main/antlr/MyLang.g4           |________________________________________________
--------------------------------------------------------------------------------------
grammar MyLang;

@header {
package org.mylang;
}
compilationUnit: function*;
function:
    FUNCTION_KEYWORD function_signature
    block;
function_signature: IDENTIFIER parameters ('--' return_types)? 'in';
parameters: type*;
return_types: type*;
type: IDENTIFIER;
END_KEYWORD : 'end';
fragment LOWERCASE : [a-z] ;
fragment UPPERCASE : [A-Z] ;
fragment DIGIT : [0-9] ;
FUNCTION_KEYWORD : 'function';
IDENTIFIER : (LOWERCASE | UPPERCASE | '_' ) (LOWERCASE | UPPERCASE | '_' | DIGIT)+ ;
STRING : '"' .*? '"';
if_block: 'if' block (else_block)?;
else_block: 'else' block;
block : statement* END_KEYWORD;
statement: if_block | push_value_statement | function_call | operation;
push_value_statement: expr;
function_call: IDENTIFIER;
operation:
   '+' #add
 | '-' #sub
 | '*' #mul
 | '/' #div
 | '%' #mod
 | '^' #pow
 | '=' #equals
 | 'dup' #duplicate
 | 'swap' #swap
 | 'pop' #pop
 | 'rot' #rotate;
NUMBER: '-'? DIGIT+;
expr: STRING | NUMBER;
N : ('\n' | '\r\n')+ -> skip;
WS : (' ' | '\t')+ -> skip;
____________________________________________________________________________________

Ao rodar a task generateGrammarSource do gradle ele vai gerar um parser e lexer que implementa a sua gramatica pra dentro da pasta build, e vai adicionar no classpath.
Na sua classe Main você precisa instanciar o lexer e o parser pra gerar a parse tree

package org.mylang;


import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.lumier.ir.IRChecker;
import org.lumier.ir.Program;
import org.lumier.ir.RunIR;

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        MyLangLexer lexer = new MyLangLexer(CharStreams.fromFileName("test.ml"));
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        MyLangParser parser = new MyLangParser(tokens);
        MyLangParser.CompilationUnitContext tree = parser.compilationUnit();
        // TODO: Create visitor
    }
}

Agora é preciso criar um visitor para visitar os nós da arvore e compilar em codigo de maquina ou executar na hora o codigo

Mas ai é por sua conta :) vou deixar um exemplo da minha linguagem:

package org.lumier;

import org.lumier.ir.*;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@SuppressWarnings("ALL")
public class LumierVisitorImpl extends LumierBaseVisitor<Object> {
    private Program programContext = new Program();

    @Override
    public Program visitCompilationUnit(LumierParser.CompilationUnitContext ctx) {
        for (LumierParser.FunctionContext functionContext : ctx.function()) {
            visitFunction(functionContext);
        }
        return programContext;
    }

    @Override
    public Void visitFunction(LumierParser.FunctionContext ctx) {
        String name = ctx.function_signature().IDENTIFIER().getText();
        List<Instruction> instructions = visitBlock(ctx.block());
        if (programContext.getFunctions().containsKey(name)) {
            throw new RuntimeException("Function " + name + " already exists");
        }
        List<Type> parameters = new ArrayList<>();
        for (LumierParser.TypeContext typeContext : ctx.function_signature().parameters().type()) {
            parameters.add(Type.fromKeyword(typeContext.getText()));
        }

        List<Type> returnTypes = new ArrayList<>();
        if (ctx.function_signature().return_types() != null)
            for (LumierParser.TypeContext typeContext : ctx.function_signature().return_types().type()) {
                returnTypes.add(Type.fromKeyword(typeContext.getText()));
            }
        Function function = new Function(name, instructions, parameters, returnTypes);
        programContext.getFunctions().put(name, function);
        return null;
    }

    @Override
    public Instruction visitIf_block(LumierParser.If_blockContext ctx) {
        return new Instruction(InstructionType.If, visitBlock(ctx.block()), ctx.else_block() != null ? visitBlock(ctx.else_block().block()) : null);
    }

    @Override
    public List<Instruction> visitBlock(LumierParser.BlockContext ctx) {
        List<Instruction> instructions = new ArrayList<>();
        for (LumierParser.StatementContext instructionContext : ctx.statement()) {
            instructions.addAll((Collection<Instruction>) visitStatement(instructionContext));
        }
        return instructions;
    }

    @Override
    public List<Instruction> visitStatement(LumierParser.StatementContext ctx) {
        List<Instruction> instructions = new ArrayList<>();
        // Check if it's a push value instruction
        if (ctx.push_value_statement() != null) {
            instructions.add(visitPush_value_statement(ctx.push_value_statement()));
        } else if (ctx.function_call() != null) {
            instructions.add(visitFunction_call(ctx.function_call()));
        } else if (ctx.operation() != null) {
            switch (ctx.operation().getText()) {
                case "=":
                    instructions.add(new Instruction(InstructionType.Equals));
                    break;
                case "+":
                    instructions.add(new Instruction(InstructionType.Add));
                    break;
                case "-":
                    instructions.add(new Instruction(InstructionType.Sub));
                    break;
                case "*":
                    instructions.add(new Instruction(InstructionType.Mul));
                    break;
                case "/":
                    instructions.add(new Instruction(InstructionType.Div));
                    break;
                case "%":
                    instructions.add(new Instruction(InstructionType.Mod));
                    break;
                case "^":
                    instructions.add(new Instruction(InstructionType.Pow));
                    break;
                case "rot":
                    instructions.add(new Instruction(InstructionType.Rot));
                    break;
                case "swap":
                    instructions.add(new Instruction(InstructionType.Swap));
                    break;
                case "dup":
                    instructions.add(new Instruction(InstructionType.Dup));
                    break;
                case "pop":
                    instructions.add(new Instruction(InstructionType.Pop));
                    break;
            }
        } else if (ctx.if_block() != null) {
            instructions.add(visitIf_block(ctx.if_block()));
        }
        return instructions;
    }

    @Override
    public Instruction visitPush_value_statement(LumierParser.Push_value_statementContext ctx) {
        if (ctx.expr().STRING() != null) {
            return new Instruction(InstructionType.Push, ctx.expr().getText().substring(1, ctx.expr().getText().length() - 1));
        } else if (ctx.expr().NUMBER() != null) {
            return new Instruction(InstructionType.Push, Integer.parseInt(ctx.expr().getText()));
        }
        throw new RuntimeException("Unreachable or unimplemented");
    }

    @Override
    public Instruction visitFunction_call(LumierParser.Function_callContext ctx) {
        String name = ctx.IDENTIFIER().getText();
        if (name.equals("print")) {
            return new Instruction(InstructionType.Print);
        }
        return new Instruction(InstructionType.CallFunction, name);
    }
}

No meu caso eu converto em estruturas de dados mais faceis de trabalhar tanto pra interpretar tanto pra compilar, alias isso é uma das coisas que programadores fazem muito, transformar informações em formatos mais faceis de trabalhar.

Agora na Main é preciso criar um novo visitor e chamar o metodo visit da classe pai passando a parse tree retornado pelo antlr

LumierVisitorImpl visitor = new LumierVisitorImpl();
Program context = (Program) visitor.visit(tree);

Eu decidi retornar um Program no visit que tem todos as funções e propriedades do programa, mas você pode retornar o que quiser, é so colocar no generic da classe pai o tipo que você quer retornar de todas os metodos de visit da sua classe

Conclusão

Tem muita coisa sobre desenvolvimento de linguagens de programação, até eu estou começando nisso, ainda tem varias coisas que faltam como a parte de optimização, a parte de compilar pra codigo de maquina ou interpretar dependendo da sua linguagem, tem varias coisas que você vai decidir colocar ou não colocar na sua linguagem. Vocẽ pode torcer dobrar ou fazer o que quiser com a informação nesse post, desenvolvimento de compiladores é uma coisa bem divertida e desafiante de fazer.

Carregando publicação patrocinada...
2

Coragem em!! É algo que futuramente vou querer aprender por brincadeira, deve ser muito bacana transformar o complexo em algo simples de se utilizar...

Sensacional, ótimo post

1