Skip to main content

🚀 Trilha Intermediária

Bem-vindo(a) à Trilha de Desenvolvimento com DSLs com Langium! Esta trilha foi cuidadosamente estruturada para oferecer um caminho completo de aprendizado, desde os conceitos básicos de DSLs (Domain-Specific Languages).

Além do conhecimento técnico, você aprenderá a criar sua própria DSL de forma eficiente, garantindo sintaxe expressiva, validações robustas e ferramentas modernas para facilitar o desenvolvimento.

🔎 O que você vai aprender?
Criação de gramáticas e análise sintática
Geração de código e integração com ferramentas externas

Carga horária total: 10h
📌 Formato: Vídeos, artigos, leituras complementares e desafios práticos

🎯 Pré-requisitos
Para um melhor aproveitamento da trilha, é recomendado possuir conhecimentos básicos de:

Tutoriais em Vídeo

Para obter orientação prática sobre o desenvolvimento com Langium, confira esta playlist de tutoriais em vídeo da TypeFox, que aborda os fundamentos do Langium e exemplos úteis:

1. Antes de começar vamos abrender o básico de BNF

O que é Gramática Formal?

  • É um conjunto de regras que define como construir frases ou sentenças válidas em uma linguagem.
  • Muito usada em compiladores, parsers, DSLs (Domain Specific Languages), etc.
  • Ela especifica a sintaxe da linguagem, ou seja, como símbolos e palavras podem ser combinados.

Existem quatro tipos na Hierarquia de Chomsky:

Tipo de GramáticaNomeExemplo
Tipo 0Gramática irrestritaQualquer regra
Tipo 1Gramática sensível ao contextoRegras que dependem do contexto
Tipo 2Gramática livre de contexto (CFG)A mais usada em linguagens de programação
Tipo 3Gramática regularExpressões regulares

Na prática, para DSLs e compiladores, quase sempre usamos Tipo 2.

O que é BNF? (Backus-Naur Form)

BNF significa Backus-Naur Form. É uma forma padronizada de escrever gramáticas formais, especialmente gramáticas livres de contexto.

Exemplo de BNF:

<expression> ::= <term> | <term> "+" <expression>
<term> ::= <factor> | <factor> "*" <term>
<factor> ::= "(" <expression> ")" | <number>
<number> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

➡️ Lê-se: uma <expression> é um <term> ou um <term> seguido de "+" e uma <expression>.

Elementos:

  • Não terminais: <expression>, <term>, etc.
  • Terminais: símbolos ou palavras da linguagem ("+", "*", números).
  • Produções: regras que definem como formar expressões válidas.

Variações da BNF

Além da BNF clássica, temos algumas extensões:

  • EBNF (Extended BNF): permite usar *, +, ?, etc. para repetir ou opcionalizar elementos.
  • ABNF (Augmented BNF): usada em RFCs da IETF para definir protocolos de internet.

Exemplo de EBNF:

expression = term, { "+", term };
term = factor, { "*", factor };
factor = "(", expression, ")" | number;
number = digit, { digit };
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
  • 📚 Gramáticas descrevem como formar sentenças válidas em uma linguagem.
  • 🧩 BNF é uma forma padronizada para definir gramáticas, muito usada para especificação de linguagens.
  • 💡 Para criar DSLs ou compiladores, você vai definir uma gramática, geralmente usando uma ferramenta baseada em BNF ou EBNF.

🔗 Referências

  1. Documentação Teórica

  2. Livros

    • Compilers: Principles, Techniques, and Tools (Dragon Book), de Aho, Lam, Sethi e Ullman.
    • Programming Language Pragmatics, de Michael L. Scott.
  3. Ferramentas Práticas

2. Criando uma nova gramática

Agora vamos editar a gramática simples que permita criar uma classe com nome e atributos. Abra o arquivo hello.langium e coloque o conteúdo abaixo:

 1| grammar Hello
2|
3| entry Model:
4| (classes+=ClassDefinition)*;
5|
6| ClassDefinition:
7| 'class' name=ID '{'
8| (attributes+=AttributeDefinition)*
9| '}';
10|
11| AttributeDefinition:
12| type=DATATYPE name=ID ';';
13|
14| hidden terminal WS: /\s+/;
15| terminal ID: /[_a-zA-Z][\w_]*/;
16|
17| DATATYPE returns string:
18| ('string' | 'int' | 'long')
19| ;
20|
21| hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//;
22| hidden terminal SL_COMMENT: /\/\/[^\n\r]*/;

Perceba que a gramatica acima, permite:

  1. Criar mais de uma classe: isso ocorre pois na linha 4, temos o operador * entre (classes+=ClassDefinition), conforme as regras de gramatica de BNF.
  2. Cada classe é única: Isso é devido ao terminal ID na linha 7.
  3. Cada classe pode ter nenhuma ou mais atributos: veja isso na linha 8.

Exemplo de uso da gramática

class Person {
string name;
int age;
long idNumber;
}

class Product {
string description;
int quantity;
long barcode;
}

Ao executar os comandos de npm run build teremos o seguinte problema:

src/cli/generator.ts:14:28 - error TS2339: Property 'greetings' does not exist on type 'Model'.

14 ${joinToNode(model.greetings, greeting => `console.log('Hello, ${greeting.person.ref?.name}!');`, { appendNewLineIfNotEmpty: true })}
~~~~~~~~~

src/cli/generator.ts:14:74 - error TS18046: 'greeting' is of type 'unknown'.

14 ${joinToNode(model.greetings, greeting => `console.log('Hello, ${greeting.person.ref?.name}!');`, { appendNewLineIfNotEmpty: true })}
~~~~~~~~

src/language/hello-validator.ts:2:29 - error TS2305: Module '"./generated/ast.js"' has no exported member 'Person'.

2 import type { HelloAstType, Person } from './generated/ast.js';
~~~~~~

src/language/hello-validator.ts:12:9 - error TS2322: Type '{ Person: (person: Person, accept: ValidationAcceptor) => void; }' is not assignable to type 'ValidationChecks<HelloAstType>'.
Object literal may only specify known properties, and 'Person' does not exist in type 'ValidationChecks<HelloAstType>'.

12 Person: validator.checkPersonStartsWithCapital
~~~~~~


Found 4 errors.

Isso acontece, pois mudamos a gramática e não temos nenhum teste de validação dos elementos dela. Isso será cenas dos próximos capitulos. Para resolver esses problemas iremos modificar os seguinte arquivos da seguinte forma:

  • hello-world-validator.ts:
import type { HelloWorldServices } from './hello-world-module.js';

/**
* Register custom validation checks.
*/
export function registerValidationChecks(services: HelloWorldServices) {
const registry = services.validation.ValidationRegistry;
const validator = services.validation.HelloWorldValidator;

registry.register( validator);
}

/**
* Implementation of custom validations.
*/
export class HelloWorldValidator {


}
  • generator.ts
import type { Model } from '../language/generated/ast.js';
import * as path from 'node:path';
import { extractDestinationAndName } from './cli-util.js';

export function generateJavaScript(model: Model, filePath: string, destination: string | undefined): string {
const data = extractDestinationAndName(filePath, destination);
const generatedFilePath = `${path.join(data.destination, data.name)}.js`;

return generatedFilePath;
}

3. Gerando código

Agora iremos criar um código em Java baseado na nossa linguagem.

3.1. Criando o Gerador de código

Primeiro passo é criar um arquivo chamado javaGenerate.ts no pasta cli. O arquivo deve ter o seguinte conteúdo:

// Importa o módulo 'fs' para operações de sistema de arquivos (criação de pastas, escrita de arquivos, etc.)
import fs from "fs";

// Importa tipos e funções utilitárias do AST gerado pela sua gramática Langium
import { ClassDefinition, isClassDefinition, Model } from "../language/generated/ast.js";

// Importa utilitário para construir strings com quebras de linha automaticamente
import { expandToStringWithNL } from "langium/generate";


// Função principal que gera arquivos Java a partir do modelo parseado
export function generateJava(model: Model, target_folder: string): void {

// Cria a pasta de destino, se ela ainda não existir
fs.mkdirSync(target_folder, { recursive: true });

// Filtra todas as definições de classe do modelo
const classes = model.classes.filter(isClassDefinition);

// Itera sobre cada definição de classe encontrada
for (const classDefinition of classes) {
// Obtém o nome da classe
const className = classDefinition.name;

// Define o caminho e nome do arquivo de saída
const fileName = `${target_folder}/${className}.java`;

// Gera o código-fonte da classe Java como string
const classCode = createClass(classDefinition);

// Escreve o conteúdo no arquivo correspondente
fs.writeFileSync(fileName, classCode);
}
}


// Função auxiliar que gera o código-fonte Java para uma única classe
function createClass(classDefinition: ClassDefinition): string {

return expandToStringWithNL`
public class ${classDefinition.name} {

// Declaração dos atributos da classe
${classDefinition.attributes.map(attr => `private ${attr.type} ${attr.name};`).join('\n')}

// Métodos getters e setters para cada atributo
${classDefinition.attributes.map(attr => `

public ${attr.type} get${capitalize(attr.name)}() {
return this.${attr.name};
}

public void set${capitalize(attr.name)}(${attr.type} ${attr.name}) {
this.${attr.name} = ${attr.name};
}`).join('\n')}
}
`;
}

// Função auxiliar para capitalizar a primeira letra de uma string
function capitalize(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}

3.2. Alterando a função "generateJavaScript"

A generateJavaScript função é criada como padrão pelo langium ao criar o projeto. Por motivos de preguiça, vamos deixar com o mesmo nome. Essa funçao é chamada toda vez que queremos criar um código. Vamos modificá-la para chamar a nossa função responsável por criar a classe java

// Importa o tipo Model do AST gerado pela gramática (representa o modelo parseado)
import type { Model } from '../language/generated/ast.js';

// Importa utilitário do Node.js para trabalhar com caminhos de arquivos e pastas
import * as path from 'node:path';

// Importa uma função utilitária para extrair o destino e o nome base do arquivo a partir dos parâmetros do CLI
import { extractDestinationAndName } from './cli-util.js';

// Importa a função que gera os arquivos Java a partir do modelo
import { generateJava } from './java_generate.js';

// Função que coordena a geração do código Java a partir de um arquivo modelo
export function generateJavaScript(model: Model, filePath: string, destination: string | undefined): string {

// Extrai o caminho de destino e o nome base do arquivo usando função utilitária
const data = extractDestinationAndName(filePath, destination);

// Cria o caminho completo para onde o arquivo gerado será salvo
const generatedFilePath = `${path.join(data.destination, data.name)}`;

// Chama a função de geração de código Java, passando o modelo e o caminho de destino
generateJava(model, generatedFilePath);

// Retorna o caminho completo do arquivo gerado
return generatedFilePath;
}

3.3. Compilando e Testando

Após realizar as mudanças no arquivo, temos que compilar. Para isso execute o comando:

npm run build

Crie o um arquivo teste.hello com o conteúdo apresentado na seção 2. Salve o arquivo em alguma pasta. No nosso caso, salvamos o teste.hello na pasta example do projeto. (Criamos essa pasta. ok?)

Após isso execute o seguinte comando no terminal gerar o código em java:

node ./bin/cli generate ./example/test.hello

Abra a pasta generated/test/ */ que teremos dois arquivos: Person.Java e Product.java

Person.java

public class Person {

// Declaração dos atributos da classe
private string name;
private int age;
private long idNumber;

// Métodos getters e setters para cada atributo


public string getName() {
return this.name;
}

public void setName(string name) {
this.name = name;
}


public int getAge() {
return this.age;
}

public void setAge(int age) {
this.age = age;
}


public long getIdNumber() {
return this.idNumber;
}

public void setIdNumber(long idNumber) {
this.idNumber = idNumber;
}
}

Product.java

public class Product {

// Declaração dos atributos da classe
private string description;
private int quantity;
private long barcode;

// Métodos getters e setters para cada atributo


public string getDescription() {
return this.description;
}

public void setDescription(string description) {
this.description = description;
}


public int getQuantity() {
return this.quantity;
}

public void setQuantity(int quantity) {
this.quantity = quantity;
}


public long getBarcode() {
return this.barcode;
}

public void setBarcode(long barcode) {
this.barcode = barcode;
}
}

Parabéns. Você criou o seu primeiro gerador de código!

4. Desafio

Modifique o Gerador de código para gerar classes em C#, Pyhton ou outra linguagem do seu desejo.