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.