🚀 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:
- TypeScript: Documentação Oficial
- Node.js: Guia de Introdução
- Langium Básico: Trilha Básica
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ática | Nome | Exemplo |
---|---|---|
Tipo 0 | Gramática irrestrita | Qualquer regra |
Tipo 1 | Gramática sensível ao contexto | Regras que dependem do contexto |
Tipo 2 | Gramática livre de contexto (CFG) ⭐ | A mais usada em linguagens de programação |
Tipo 3 | Gramática regular | Expressõ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
-
Documentação Teórica
-
Livros
- Compilers: Principles, Techniques, and Tools (Dragon Book), de Aho, Lam, Sethi e Ullman.
- Programming Language Pragmatics, de Michael L. Scott.
-
Ferramentas Práticas
- ANTLR (Another Tool for Language Recognition)
- Langium (TypeScript Language Toolkit)
- BNF Playground — testador interativo de BNF
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:
- Criar mais de uma classe: isso ocorre pois na linha 4, temos o operador * entre (classes+=ClassDefinition), conforme as regras de gramatica de BNF.
- Cada classe é única: Isso é devido ao terminal ID na linha 7.
- 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.