DSL usando M

Como todas as outras coisas da minha lista de “TODOs”, a linguagem M é uma das que mais tinha destaque e finalmente consegui um tempo para enfrentar o processo de aprendizado a fim de tirar o melhor proveito do que ela se propõe a resolver.

Conceitualmente, a linguagem M se baseia em quatro pilares: Linguagem, dados, constraints e transformação. Vamos a eles.

Linguagem M

Por definição, escrever nesta linguagem consiste em unir uma ou mais regra sintática tokenizada, cada qual contendo fragmentos ou o todo (no caso do padrão Main) da forma possível de escrita do texto (código). Abaixo o que poderia ser a definição para uma linguagem chamada HelloWorld:

mudule Simples
{
   language HelloLanguage {
     syntax Main = "Hello, World";
   }
}

Basicamente, na grande maioria dos casos, a construção de uma estrutura de linguagem utilizando M considera a necessidade de Tokens (equivalente as palavras conhecidas)  e syntaxes (equivalente a estrutura fraseal).

Na realidade, ao contrário do que eu escrevi poucas linhas acima, o nome real (full name) da linguagem que criamos a pouco seria Simples.HelloLanguage, uma estrutura que remete ao sistema de Namespaces que estamos acostumados nas linguagens formais da plataforma .Net.

Nas linhas escritas acima existe uma declaração de sintaxe bem simples onde permitimos (e reconhecemos) a utilização da forma “Hello, World” como sendo uma declaração válida e coesa. As informações definidas em forma de sintaxe podem ser entendidas como sendo as regras da linguagem, logo, com a regra que definimos até o momento apenas um texto exatamente igual a “Hello, World” seria interpretada como uma sintaxe válida. Qualquer outro texto/padrão será definido pelo processador como inválido.

Como você pode imaginar, uma linguagem que contém apenas uma regra é tão útil quanto um saco de areia no deserto. Geralmente teremos formas sintáticas contendo múltiplos padrões, como pode exemplo:

mudule Simples
{
   language CoresPrimarias {
      syntax Main = "Vermelho" | "Amarelo" | "Azul";
   }
}

Facilmente identificamos que em termos de cores primárias, de acordo com nossa definição acima, tanto “Vermelho”, quanto “Amarelo” ou “Azul” são informações que fazem sentido neste contexto, porém, “Roxo” não se aplica.

Intellipad

Este foi talvez um dos artigos mais difíceis de escrever dada a enorme falta de informação e diria até de suporte da Microsoft para a comunidade que quer começar a fazer alguma coisa com a ferramenta. Dentre outras dificuldades, não conseguia de forma alguma entender como fazer a compilação da gramática por meio do release mais recente do pacote SQL Server Modeling CTP Nov R3, mas depois de abrir um tópico (aparentemente no lugar errado) perguntando sobre o assunto, pude dar continuidade ao artigo.

Como sairemos da parte conceitual, é interessante que você tenha instalado em sua máquina as ferramentas para escrever e testar suas sintaxes localmente. Segue o link para o pacote necessário para o resto do artigo:

SQL Server Modeling CTP – Nov 2009 Release 3 (formerly “Oslo”)

Após a instalação você verá no menu iniciar, dentro da opção “Microsoft Oslo“, um atalho para o Intellipad.

A título de exemplo, vamos trabalhar com a necessidade de definir uma linguagem para especificar formas de pagamento de maneira inteligível. Digamos que:

  • Existem diversas formas de pagamento, cada qual com suas características e peculiaridades; Para cada forma de pagamento:
    • Poderão ocorrer  ações (Pagar, Agendar, etc);
    • Poderemos definir a fonte dos recursos;

Com estes dois casos simples, trabalharemos a seguir de forma a normatizar uma DSL usando M e permitir a escrita de instruções simples e diretas porém, que englobem o todo do negócio em questão. Para começar, vamos definir uma história simples:

Pagamento “Boleto”
Banco “Itau”
Moeda “Real”
Valor “R$300,00”
Pagar!
Fim Pagamento
Vamos começar a escrever nossa linguagem, inicialmente, validando a história em questão e transformando as informações e palavras chaves existentes nela em estruturas diretamente relacionadas ao que pretendemos ter como produto final.

Como todo bom inicio de desenvolvimento, teremos um código visivelmente estranho e “melhorável”, porém, até o momento ele atende nossas necessidades e cria abertura para aprendermos mais algumas coisas. Veja o código a seguir:

module Felipe
{
    language Financas
    {
       /// Sintaxe base
        syntax Main ="Pagamento"
            valorLiteral
            Banco
            Moeda
            Valor
            Pagar
            "Fim Pagamento";

        /// Declaração das variáveis globais conhecidas
        syntax Banco = "Banco" valorLiteral;
        syntax Moeda = "Moeda" valorLiteral;
        syntax Valor = "Valor" valorLiteral;

        /// Declaração das ações possíveis
        syntax Pagar = "Pagar!";

        /// Interleaves permitidos
        interleave whitespace = (" " | "\r" | "\n" | "\t")+;

        /// Tokens
        token valorLiteral = '"' !('\r' | '\n' | '"')+ '"';
   }
}

Para acompanhar os próximos passos e se familiarizar com a interface do Intellipad, faça o seguinte:

  • Abra o Intellipad e cole o código acima (e os demais que serão disponibilizados ao longo do artigo) salvando o arquivo com extensão .mg (recomendo salvar como “MinhaLinguagem.mg” para facilitar o acompanhamento do artigo). Isto dirá ao Intellipad que ele deve tratar esta janela em modo “DSL Grammar Mode”, ajudando e facilitando o desenvolvimento;
  • No caso da história, salve-a em um arquivo separado (preferencialmente com a extensão .dsl, melhor ainda se com o nome “Historia.dsl”, também para facilitar o acompanhamento)e carregue este arquivo por meio do caminho: “DSL” > “Open File as Input and Show Output…”. Desta forma você terá a visão completa em tempo real da aderência de sua DSL frente a história.
  •  

    Nota-se no código acima a adição de mais dois padrões sendo: interleave e token. Abaixo, a explicação para cada um:

    • Interleave: Quando criamos um padrão deste tipo estamos dizendo ao processador M que estes fragmentos de texto não devem ser considerados como parte do fluxo primário, ou seja, devem ser desconsiderados; Durante o processamento do texto (input), o processador automaticamente injeta estes padrões entre os termos de padrões do tipo syntax.
    • Token: Por meio deste padrão, dizemos ao processador quais são as formas de acessar informações literais em nossa linguagem criando uma espécie de filtro. No caso acima, estamos considerando qualquer coisa que comece e termine com aspas duplas, exceto por novas linhas, saltos de linha e aspas duplas entrelaçadas no escopo principal;

    O código acima juntamente com a história que definimos inicialmente permite  que o Intellipad consiga identificar alguns padrões:

    M Graph Mode

    M Graph Mode

    Legal, certo?! Que tal melhorar um pouco este código?

    Primeiro, vamos tentar diminuir um pouco a redundância existente nas declarações de algumas estruturas; Para tanto, vamos realizar as seguintes alterações:

    • Utilizar uma forma de declaração de syntax que permite um comportamento similar ao das funções (em linguagens de programação tradicional) para maximizar a reutilização dos formatos;
      • Chamarei esta “função” de variavel, uma vez que para o negócio, ela representa as variáveis com as quais trabalharemos. O funcionamento dela é bem auto-explicativo, você ao definir uma estrutura sintática composta só precisa seguir a sintaxe definida para a linguagem M e, consumir esta sintaxe não é mais difícil do que realizar uma chamada a um método qualquer.
    • Melhorar a sintaxe principal para permitir o uso opcional de algumas variáveis, de acordo com a necessidade;
    module Felipe
    {
        language Financas
        {
            /// Regra para variáveis globais
            syntax variavel(nomeToken) = nomeToken valorLiteral;
    
            /// Sintaxe base
            syntax Main ="Pagamento"
                valorLiteral
                Banco
                "Fim Pagamento";
    
            /// Declaração das variáveis globais conhecidas
            syntax Banco = "Banco" valorLiteral (Moeda | Valor)* Executar;
            syntax Moeda = variavel("Moeda");
            syntax Valor = variavel("Valor");
            token Pagar = "Pagar!";
            token Agendar = "Agendar!";
    
            /// Declaração das ações possíveis
            syntax Executar = (Pagar | Agendar);
    
            /// Interleaves permitidos
            interleave whitespace = (" " | "\r" | "\n" | "\t")+;
    
            /// Tokens
            token valorLiteral = '"' !('\r' | '\n' | '"')+ '"';
        }
    }
    

    Bom, neste ponto, nossa sintaxe está poderosa o suficiente para validar diversas formas diferentes de se contar a história inicial porém, sem perder a aderência ao escopo desejado.

    Syntax + Token = OK:
    Um detalhe muito importante de se ressaltar sobre o código acima é o uso de padrões syntax consumindo padrões de token, neste caso o interpretador permite a utilização, porém, no caso oposto (token consumindo syntax) um erro ocorre e (pelo menos na minha máquina) o Intellipad trava e fecha com a mensagem abaixo!

    BUM!!

    Intellipad error – BUM!!

    Uma outra funcionalidade interessante é a possibilidade de decorar nossos padrões do tipo token com atributos para permitir uma espécie de syntax highlight. Veja o código abaixo:

    module Felipe
    {
        language Financas
        {
            /// Regra para variáveis globais
            syntax variavel(nomeToken) = nomeToken tValorLiteral;
    
            /// Sintaxe base
            syntax Main =tPagamento
                tValorLiteral
                sBanco
                tFimPagamento;
    
            /// Tokens conhecidos
            @{Classification["Literal"]}
            token tValorLiteral = '"' !('\r' | '\n' | '"')+ '"';
    
            @{Classification["Keyword"]}
            token tPagar = "Pagar!";
    
            @{Classification["Keyword"]}
            token tAgendar = "Agendar!";
    
            @{Classification["Keyword"]}
            token tPagamento = "Pagamento";
    
            @{Classification["Keyword"]}
            token tFimPagamento = "Fim Pagamento";
    
            @{Classification["Keyword"]}
            token tBanco = "Banco";
    
            @{Classification["Keyword"]}
            token tMoeda = "Moeda";
    
            @{Classification["Keyword"]}
            token tValor = "Valor";
    
            /// Declaração das ações possíveis
            /// Sintaxes permitidas
            syntax sExecutar = (tPagar | tAgendar);
            syntax sBanco = tBanco tValorLiteral (sMoeda | sValor)* sExecutar;
            syntax sMoeda = variavel(tMoeda);
            syntax sValor = variavel(tValor);
    
            /// Interleaves permitidos
            interleave whitespace = (" " | "\r" | "\n" | "\t")+;
        }
    }
    

    O resultado é interessante:

    Borat said - Nice!

    Borat said: Nice!

    Productions

    Por fim, vamos ao conceito de Productions que, a grosso modo, podemos definir como a forma em que o resultado da interpretação de um dado texto frente à linguagem deverá ser apresentado. Para facilitar, vamos alterar um pouco o nosso código para deixá-lo mais amigável:

    Pagamento “Boleto”

    Moeda: Dólar

    Banco: Itau

    Fim Pagamento

    Esta alteração não tem influência direta no resultado quando utilizando Productions, alterei apenas para deixar a explicação mais simples.

    Obviamente que neste ponto a janela “M Graph Mode” já não exibe nada pois o interpretador já não consegue fazer o match entre a história e a definição da linguagem. Para resolver isto, vamos alterar a definição da seguinte maneira:

    module Felipe
    {
        language Financas
        {
            /// Sintaxe base
            syntax Main = tPagamento
                tValorLiteral
                (sMoeda | sBanco)*
                tFimPagamento;
    
            /// Tokens conhecidos
            @{Classification["Literal"]}
            token tValorLiteral = '"' !('\r' | '\n' | '"')+ '"';
    
            @{Classification["Keyword"]}
            token tPagamento = "Pagamento";
    
            @{Classification["Keyword"]}
            token tFimPagamento = "Fim Pagamento";
    
            @{Classification["Identifier"]}
            token tTipoMoeda = "Real" | "Dólar";
    
            @{Classification["Keyword"]}
            token tMoeda = "Moeda";
    
            @{Classification["Keyword"]}
            token tBanco = "Banco";
    
            @{Classification["Identifier"]}
            token tTipoBanco = "Itau" | "Real";
    
            /// Sintaxes auxiliares
            syntax sMoeda = tMoeda ':' tTipoMoeda;
            /// Production
            syntax sBanco = bk:tBanco ':' nome:tTipoBanco => id("O banco de destino é:"){nome};
    
            /// Interleaves permitidos
            interleave whitespace = (" " | "\r" | "\n" | "\t")+;
        }
    }
    

    Em suma, a idéia por traz dos padrões Production é o de você ser capaz de manipular a forma com que o resultado da análise do código é produzido.

    No código acima pode-se observar que no momento da definição da sintaxe sBanco, existe um identificador para o token tTipoBanco que permite a utilização desta informação em uma customização do resultado exibido na janela “M Graph Mode”. Complementando a análise da linha, é possível notar que existe uma chamada à expressão id( ), esta expressão é responsável por definir o nome do nó pai em que aquele resultado será exibido.

    Podemos adicionar mais alguma perfumaria ao código anterior, permitindo coisas como comentários in-line e em bloco, porém não vou me ater muito a estes detalhes visto que o conhecimento necessário para tanto já foi exposto. Vamos então para o próximo e derradeiro tópico: Output.

    CommandLine.exe

    Junto com o pacote Oslo PDC e SDK você encontra algumas outras ferramentas para dar suporte ao desenvolvimento em M, das quais duas se destacam:

    • m.exe: Ferramenta para compilar sua gramática de forma a permitir a validação da DSL; Você deve fornecer a ela o arquivo em que você descreveu sua gramática (nosso arquivo .mg) e esperar que um arquivo compilado seja gerado (arquivo .mx);
      m.exe MinhaLinguagem.mg
    • mx.exe: Por meio do seu arquivo gramatical compilado (.mx), juntamente com a história (a DSL) ele consegue gerar uma saída em formato “M” ou XAML. A sintaxe de execução é a seguinte:
      mx.exe Historia.dsl /r:MinhaLinguagem.mx
    • mg.exe: Apenas a título de menção honrosa, esta era a ferramenta para realizar a compilação da gramática até o CTP Nov R3.

    Após a geração do nosso arquivo .M, teremos algo muito parecido com isto:

    module Historia {
        Main {
            @[0] => "Pagamento",
            @[1] => "\"Boleto\"",
            @[2] =>  {
                @[0] {
                     {
                        @[0] {
                            sMoeda {
                                @[0] => "Moeda",
                                @[1] => ":",
                                @[2] => "Real"
                            }
                        }
                    }
                },
                @[1] {
                     {
                        @[0] {
                            @[O banco de destino é:] => "Itau"
                        }
                    }
                }
            },
            @[3] => "Fim Pagamento"
        }
    }
    

    Agora, finalmente podemos chegar a um modelo mais prático de como ler e parsear nossa DSL de dentro de um código C#, por exemplo:

    public static void Compilar()
            {
                /// Compilamos a gramática em tempo de execução de modo a permitir que eventuais alterações
                /// em sua estrutura sejam imediatamente replicadas por aqui. Isto também pode ser feito manualmente
                /// por meio da ferramenta "m.exe" que acompanha o Oslo
                CompilationResults results = Compiler.Compile(
                  new CompilerOptions
                  {
                      Sources =
                      {
                            new TextItem
                            {
                                /// Endereço para o arquivo contendo as definições da linguagem
                                Reader = new StreamReader(@"C:\Financas.mg"),
                                Name="MeuItem",
                                ContentType = TextItemType.MGrammar
                            }
                        }
                });
    
                /// Na linha abaixo temos efetivamente o resultado do parse de nossa DSL contra as regras
                /// que definimos em nossa linguagem. Daqui em diante você pode iterar entre os nodes oriundos
                /// da operação e realizar qualquer que seja a tarefa em mente para o processo, seja: criar e
                /// popular um DTO, instruir o código em algum Path específico, etc...
                var resultados = results.ParserFactories["Felipe.Financas"].Create().Parse(@"c:\Historia.dsl", null);
            }
    

    Conforme explicado no snippet acima, a manipulação do resultado do parse é bem intuitiva e, varia de acordo com a necessidade para qual sua DSL foi desenhada. Você pode também utilizar seu aquivo .mx compilado manualmente seguindo os passos descritos aqui.

    Flow

    Flow

    Importante: Se quando estiver compilando seu projeto você receber a mensagem “The type or namespace name ‘M’ does not exist in the namespace ‘Microsoft’ (are you missing an assembly reference?)“, a solução no meu caso foi ir até as propriedades do projeto e alterar o Target Framework de “.NET Framework 4-Client Profile” para “.NET Framework 4” (Mais info aqui).

    Outras leituras:

    Deixe um comentário

    Preencha os seus dados abaixo ou clique em um ícone para log in:

    Logotipo do WordPress.com

    Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

    Imagem do Twitter

    Você está comentando utilizando sua conta Twitter. Sair / Alterar )

    Foto do Facebook

    Você está comentando utilizando sua conta Facebook. Sair / Alterar )

    Foto do Google+

    Você está comentando utilizando sua conta Google+. Sair / Alterar )

    Conectando a %s

    %d blogueiros gostam disto: