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.