DSL universal. Isso é possível?


O idioma da área de assunto. Não sobrecarregado com construções de linguagem de uso geral. Ao mesmo tempo, permite implementar uma lógica muito complexa com apenas algumas linhas. Tudo isso é DSL.

No entanto, a criação de uma DSL exige que o desenvolvedor seja qualificado. O uso regular dessa abordagem se transforma em uma rotina de desenvolvimento de outro idioma. A solução pode ser criar uma ferramenta universal - um mecanismo que será aplicável a tarefas completamente diferentes e fáceis de modificar. Neste artigo, desenvolveremos em C # o mais simples do ponto de vista da implementação, mas ao mesmo tempo um mecanismo de linguagem bastante poderoso, com o qual você pode resolver uma ampla gama de problemas.

1. Introdução


Há duas maneiras de desenvolver um projeto de aplicativo: simplificá-lo de modo que seja óbvio que ele não tem deficiências ou torná-lo tão complexo que não possui deficiências óbvias. C.E. R. Hoar (CAR Hoare)
Neste artigo, quero compartilhar uma das técnicas de desenvolvimento que ajuda eu e minha equipe, por um lado, a lidar com a complexidade dos projetos. E, por outro lado, permite que você desenvolva rapidamente aplicativos de protótipo. À primeira vista, o desenvolvimento de uma linguagem de programação parece muito complicado. Assim é, se estamos falando de uma ferramenta universal. Se o objetivo é cobrir uma área de assunto estreita, o desenvolvimento de um idioma específico geralmente se justifica.

Uma vez, me deparei com a tarefa de desenvolver uma implementação de uma linguagem industrial (IEC 61131-3) para incorporar no software do cliente. No decorrer deste trabalho, fiquei interessado no tópico estrutura de intérpretes e, desde então, escrevo intérpretes de línguas esotéricas e não muito linguísticas como hobby. No futuro, surgiu um entendimento de como usar intérpretes auto-escritos para simplificar a vida cotidiana.


O principal objetivo das linguagens de programação sãs é simplificar o processo de programação e leitura de um programa. Escrever em asm é mais fácil do que em códigos de máquina, escrever em C é mais fácil do que em asm, em C # é ainda mais simples e assim por diante.

Isso é alcançado principalmente devido ao método mais popular de reducionismo - dividindo uma tarefa complexa em componentes simples e reconhecíveis - padronizando sua interação e uma certa sintaxe.

A linguagem de programação consiste em um conjunto de operadores, que em essência é a base da linguagem, blocos de construção elementares e sintaxe que define a maneira de escrever combinações de operadores, bem como a biblioteca padrão. Sequências de ações elementares de acordo com regras sintáticas são agrupadas em funções, funções são agrupadas em classes (se houver OOP), classes são combinadas em bibliotecas e essas, por sua vez, em pacotes. É assim que uma linguagem tradicional típica se parece. Em princípio, essas técnicas são suficientes para resolver a maioria das tarefas diárias. No entanto, esse não é o limite, porque você pode dar um passo adiante - para um nível mais alto de abstração e precisará ir além dos limites da linguagem usada se ela não suportar a metaprogramação na forma de macros.


Atualmente, a maioria dos projetos se resume a uma combinação de componentes prontos e uma parte samopisnogo insignificante de baixo nível. A combinação de componentes geralmente é feita por meio de uma linguagem de programação universal - C #, Java, Python e outras. Embora essas linguagens sejam de alto nível, elas também são universais e, portanto, contêm necessariamente construções sintáticas para operações de baixo nível, criação de funções, classes, descrição de tipos generalizados, programação assíncrona e muito mais. Por esse motivo, a tarefa “faça uma vez, faça duas, faça três” cresce demais com uma grande quantidade de construções sintáticas e pode aumentar até centenas de linhas de código e muito mais.

Você pode simplificar a reutilização de componentes se repetir a técnica do reducionismo, mas já para esses mesmos componentes. Isso é alcançado através do desenvolvimento de uma linguagem especializada que possui uma sintaxe simplificada e serve apenas para descrever a interação desses componentes. Essa abordagem é chamada YaOP (programação orientada a linguagem) e as linguagens são chamadas DSL (Linguagem Específica de Domínio - uma linguagem específica de domínio).

Devido à falta de construções redundantes, apenas algumas linhas no DSL podem implementar funcionalidades bastante complexas, o que leva a consequências positivas: a velocidade de desenvolvimento aumenta, o número de erros diminui e o teste do sistema é simplificado.

Se aplicada com sucesso, essa abordagem pode aumentar significativamente a flexibilidade do produto que está sendo desenvolvido devido à possibilidade de escrever scripts compactos que definam e estendem o comportamento do sistema. Pode haver muitas aplicações para essa abordagem, como evidenciado pela prevalência dessa abordagem, porque o DSL está em todo lugar. HTML comum é uma linguagem de descrição de documentos, SQL é uma linguagem de consulta estruturada, JSON é uma linguagem de descrição de dados estruturada, XAML, PostScript, Emacs Lisp, nnCron e muitos outros.


Com todas as vantagens, o DSL tem uma desvantagem significativa - altos requisitos para o desenvolvedor do sistema.

Nem todo desenvolvedor tem conhecimento e experiência no desenvolvimento de uma linguagem primitiva. Mesmo um número menor de especialistas pode desenvolver uma linguagem suficientemente flexível e produtiva. Existem outros problemas. Por exemplo, em um determinado momento no desenvolvimento da funcionalidade originalmente estabelecida, pode não ser suficiente e será necessário criar funções ou OOP. E onde existem funções, a otimização da recursão da cauda pode ser necessária sem loops, e assim por diante. Ao mesmo tempo, a compatibilidade com versões anteriores deve ser levada em consideração para que os scripts escritos anteriormente continuem funcionando com a nova versão.

Outro problema é que uma linguagem projetada para resolver um problema é completamente inadequada para outros. Portanto, você deve desenvolver uma nova DSL do zero, para que o desenvolvimento de novas linguagens se torne uma rotina. Isso novamente complica a manutenção e reduz a reutilização de código difícil de compartilhar entre diferentes implementações de DSL e projetos que os utilizam.


A saída é criar uma DSL para criar uma DSL. Aqui não quero dizer RBNF, mas uma linguagem que pode ser alterada por meios internos para a linguagem da área de assunto. O principal obstáculo na criação de uma linguagem flexível e transformável é a presença de um sistema de sintaxe e tipo rigidamente definido. Durante todo o período de desenvolvimento da indústria de computadores, várias linguagens flexíveis sem sintaxe foram propostas, mas elas sobreviveram até hoje e as linguagens Forth e Lisp continuam a se desenvolver ativamente. A principal característica dessas linguagens é que, devido à sua estrutura e homo-iconicidade, elas podem, devido aos meios embutidos, alterar o comportamento do intérprete e, se necessário, analisar construções sintáticas que não foram estabelecidas originalmente.

Existem soluções para a Forth estendendo sua sintaxe para C ou para Scheme. "Fort" é frequentemente criticado pela seqüência incomum de argumentos e operações pós-fixada, que é ditada pelo uso da pilha para passar argumentos. No entanto, “Fort” tem acesso a um intérprete de texto, isso permite ocultar o registro reverso do usuário, se necessário. E, finalmente, isso é uma questão de hábito, e é desenvolvido rapidamente.

A família de idiomas Lisp conta com macros que permitem inserir DSL, se necessário. E o acesso ao intérprete e ao leitor contribui para a implementação de intérpretes metacíclicos com recursos de interpretação especificados. Por exemplo, a implementação do Scheme lisp Racket é posicionada como um ambiente para o desenvolvimento de linguagens e possui linguagens prontas para a criação de servidores Web, construção de interfaces GUI, linguagem de inferência e outras.

Essa flexibilidade torna esses idiomas bons candidatos ao papel do mecanismo DSL universal.

Como resultado, “Fort” e Lisp se desenvolvem principalmente como linguagens de uso geral, embora de nicho - elas recorrem a funcionalidades que podem ser redundantes para uma linguagem DSL. Mas, ao mesmo tempo, são simples de implementar, o que significa que você pode desenvolver uma versão limitada com a possibilidade de sua expansão. Isso permitirá que você reutilize o núcleo dessa linguagem com pequenas modificações (idealmente - sem) para uma tarefa específica.

Também quero observar que essas linguagens são ótimas não apenas para escrever scripts, mas também para interação interativa com o sistema via REPL. Que, por um lado, pode ser conveniente para depuração e, por outro lado, atua como uma interface acessível ao usuário com o sistema. Acredita-se que a interface de texto com o sistema em alguns casos possa ser mais eficaz que a gráfica, uma vez que é muito mais simples de implementar, mais flexível, permite ao usuário generalizar operações típicas em funções, e assim por diante. Um exemplo impressionante de uma interface de texto pode ser o Bash. E se a linguagem é homo-icônica, sua construção pode ser relativamente fácil de gerar e analisar e, com o mínimo de esforço, implementar uma linguagem gráfica sobre o intérprete - isso pode ser útil quando o usuário-alvo estiver longe de programar.

Atualmente, as linguagens de descrição de dados XML e JSON são amplamente usadas como DSL para configuração. Obviamente, essa é uma ótima prática, mas em alguns casos os dados por si só não são suficientes e é necessário, por exemplo, descrever as operações neles.


Neste post, proponho criar um intérprete simples da linguagem Fort e mostrar como adaptá-la para resolver problemas específicos.

A linguagem Fort foi escolhida como a mais fácil de implementar e usar, embora poderosa o suficiente para usá-la como DSL para várias tarefas. De fato, o coração da linguagem é o intérprete de endereço, que mesmo no assembler ocupa apenas algumas linhas, e a maior parte da implementação recai sobre as primitivas, que são mais, mais universal, rápida e flexível a implementação deve ser. Outra parte importante do idioma é o intérprete de texto, que permite interagir com o intérprete de endereço.


Intérprete de endereço


O elemento básico da linguagem Fort é uma palavra que é separada de outras palavras e átomos (números) por espaços, extremidades de linhas e tabulações.

Uma palavra tem o mesmo significado e propriedades que uma função de outras linguagens, por exemplo C. Palavras conectadas na implementação, ou seja, implementadas da mesma maneira que o intérprete, são semelhantes aos operadores de outras linguagens. De fato, um programa em qualquer linguagem de programação nada mais é do que uma combinação de operadores de linguagem e dados. Portanto, a criação de uma linguagem de programação pode ser considerada como a definição de operadores e como combiná-los. Além disso, idiomas como C determinam uma maneira diferente de escrever operadores, que determina a sintaxe do idioma. Na maioria dos idiomas, a modificação de instruções geralmente não é possível - por exemplo, você não pode alterar a sintaxe ou o comportamento de uma instrução if.

Na linguagem Fort, todos os operadores e suas combinações (palavras do usuário) têm o mesmo método de escrita. Fort palavras são divididas em primitivo e personalizado. Você pode definir uma palavra que sobrecarregará o primitivo, alterando assim o comportamento dos primitivos. Embora, na realidade, a palavra redefinida seja implementada através das primitivas definidas inicialmente. Em nossa implementação, a função em C # será a primitiva. Uma palavra definida pelo usuário consiste em uma lista de endereços de palavras a serem executadas. Como existem dois tipos de palavras, o intérprete deve distinguir entre elas. A separação das primitivas e das palavras do usuário é realizada pelas mesmas primitivas, cada palavra do usuário começa com uma operação DoList e termina com uma operação Exit.

É possível descrever por um longo tempo como ocorre essa separação, mas é mais fácil entender isso estudando a ordem de execução do programa de intérpretes. Para fazer isso, implementamos um intérprete mínimo, definimos um programa simples e vemos como ele será executado passo a passo.


Nossa máquina forte consiste em memória linear, pilha de dados, pilha de retorno, ponteiro de instruções, ponteiro de palavras. Também teremos um local separado para armazenar primitivos.

public object[] Mem; //   public Stack<int> RS; //   public Stack<object> DS; //   public int IP; //   public int WP; //   public delegate void CoreCall(); public List<CoreCall> Core; //   

A essência da interpretação é navegar para o endereço na memória e executar a instrução que é indicada lá. O intérprete de endereço inteiro - o coração do idioma - no nosso caso será definido em uma função Next ().

 public void Next() { while (true) { if (IP == 0) return; WP = (int)Mem[IP++]; Core[(int)Mem[WP]](); } } 

Cada palavra de usuário começa com um comando DoList, cuja tarefa é salvar o endereço de interpretação atual na pilha e definir o endereço de interpretação da próxima palavra.

 public void DoList() { RS.Push(IP); IP = WP + 1; } 

Para sair da palavra, use o comando Exit, que restaura o endereço da pilha de retorno.

 public void Exit() { IP = RS.Pop(); } 

Para uma demonstração visual do princípio do intérprete, apresentamos um comando, que simula trabalhos úteis. Vamos chamá-lo de Olá ().

 public void Hello() { Console.WriteLine("Hello"); } 

Primeiro, você precisa inicializar a máquina e especificar as primitivas para o intérprete funcionar corretamente. Você também deve especificar os endereços das primitivas na memória do programa.

 Mem = new Object[1024]; RS = new Stack<int>(); DS = new Stack<object>(); Core = new List<CoreCall>(); Core.Add(Next); Core.Add(DoList); Core.Add(Exit); Core.Add(Hello); const int opNext = 0; const int opDoList = 1; const int opExit = 2; const int opHello = 3; // core pointers Mem[opNext] = opNext; Mem[opDoList] = opDoList; Mem[opExit] = opExit; Mem[opHello] = opHello; 

Agora podemos criar um programa simples; no nosso caso, o código do usuário começará no endereço 4 e consistirá em dois subprogramas. A primeira rotina começa no endereço 7 e chama a segunda, que começa no endereço 4 e exibe a palavra Olá.

 // program Mem[4] = opDoList; // 3)    IP = 9   ,   IP = WP + 1 = 5 Mem[5] = opHello; // 4)     Mem[6] = opExit; // 5)   ,  IP = 9    Mem[7] = opDoList; // 1)     Mem[8] = 4; // 2)     4,  WP = 4 Mem[9] = opExit; // 6)   ,  IP = 0    

Para executar o programa, primeiro salve o valor 0 na pilha de retorno, pela qual o intérprete de endereço interromperá o ciclo de interpretação, defina o ponto de entrada e, em seguida, inicie o intérprete.

 var entryPoint = 7; //    IP = 0; //  IP = 0,        WP = entryPoint; //  WP = 7      DoList(); //     ,  IP = 0    Next(); //    

Conforme descrito, neste intérprete, as primitivas serão armazenadas em memória separada. Obviamente, poderia ter sido implementado de maneira diferente: por exemplo, na memória do programa, um delegado para a função de operador foi armazenado. Por um lado, esse intérprete não seria mais fácil, mas, por outro, seria claramente mais lento, pois cada etapa da interpretação exigiria verificação, conversão e execução de tipos, mais operações são obtidas.

Cada palavra de usuário do nosso intérprete começa com a primitiva DoList, cuja tarefa é salvar o endereço atual da interpretação e ir para o próximo endereço. A saída da sub-rotina é executada pela operação Exit, que restaura o endereço da pilha de retorno para interpretação adicional. De fato, descrevemos todo o intérprete de endereço. Para executar programas arbitrários, basta expandi-lo com primitivas. Mas primeiro você precisa lidar com um intérprete de texto, que fornece uma interface para o intérprete de endereço.


Intérprete de texto


O idioma Fort não tem sintaxe; os programas nele escritos são palavras separadas por espaços, tabulações ou extremidades da linha. Portanto, a tarefa do intérprete de texto é dividir o fluxo de entrada em palavras (tokens), encontrar um ponto de entrada para eles, executar ou gravar na memória. Mas nem todos os tokens estão sujeitos à execução. Se o intérprete não encontrar a palavra, ele tenta interpretá-la como uma constante numérica. Além disso, o intérprete de texto possui dois modos: modo de interpretação e modo de programação. No modo de programação, os endereços das palavras não são executados, mas são gravados na memória, portanto, novas palavras são determinadas.

As implementações canônicas do “Fort” geralmente combinam um dicionário (entrada de dicionário) e memória de programa, definindo um único arquivo de código na forma de uma lista simplesmente conectada. Em nossa implementação, apenas o código executável estará na memória e os pontos de entrada das palavras serão armazenados em uma estrutura separada - um dicionário.

 public Dictionary<string, List<WordHeader>> Entries; 

Este dicionário define a correspondência de uma palavra para vários títulos, para que você possa definir um número arbitrário de rotinas com o mesmo nome e, em seguida, excluir essa definição e começar a usar a antiga. Além disso, o endereço antigo salvo permite encontrar o nome de uma palavra no dicionário, mesmo que tenha sido redefinida, o que é especialmente útil para gerar um rastreamento de pilha ou depurar para estudar a memória. WordHeader é uma classe que armazena um endereço de entrada de sub-rotina e um sinalizador de interpretação imediata.

 public class WordHeader { public int Address; public bool Immediate; } 

O sinalizador Imediato instrui o intérprete que esta palavra deve ser executada no modo de programação e não gravada na memória. Esquematicamente, a lógica do intérprete pode ser representada da seguinte forma: a mão direita é SIM, a esquerda é NÃO.


Usaremos o TextReader para ler o fluxo de entrada e o TextWriter para produzi-lo.

 public TextReader Input; public TextWriter Output; 

A implementação do intérprete de acordo com o esquema acima terá uma função: Interpreter ().

 void Interpreter() { while (true) { var word = ReadWord(Input); if (string.IsNullOrWhiteSpace(word)) return; // EOF var lookup = LookUp(word); if (IsEvalMode) { if (lookup != null) { Execute(lookup.Address); } else if (IsConstant(word)) { DS.Push(ParseNumber(word)); } else { DS.Clear(); Output.WriteLine($"The word {word} is undefined"); } } else { // program mode if (lookup != null) { if (lookup.Immediate) { Execute(lookup.Address); } else { AddOp(lookup.Address); } } else if (IsConstant(word)) { AddOp(LookUp("doLit").Address); AddOp(ParseNumber(word)); } else { IsEvalMode = true; DS.Clear(); Output.WriteLine($"The word {word} is undefined"); } } } } 

A interpretação é realizada em um loop, cuja saída é realizada ao atingir o final do fluxo de entrada (por exemplo, o final do arquivo), enquanto a função ReadWord retornará uma string vazia. A tarefa do ReadWord é retornar a próxima palavra a cada chamada.

 static string ReadWord(TextReader sr) { var sb = new StringBuilder(); var code = sr.Read(); while (IsWhite((char)code) && code > 0) { code = sr.Read(); } while (!IsWhite((char)code) && code > 0) { sb.Append((char)code); code = sr.Read(); } return sb.ToString(); } static bool IsWhite(char c) { return " \n\r\t".Any(ch => ch == c); } 

Após a leitura da palavra, é feita uma tentativa de encontrá-la no dicionário. Se for bem-sucedido, o título da palavra será retornado, caso contrário, nulo.

 public WordHeader LookUp(string word) { if (Entries.ContainsKey(word)) { return Entries[word].Last(); } return null; } 

Você pode verificar se o valor digitado é um número pelos dois primeiros caracteres. Se o primeiro caractere é um número, assumimos que é um número. Se o primeiro caractere é um sinal de "+" ou "-" e o segundo é um dígito, provavelmente este também é um número.

 static bool IsConstant(string word) { return IsDigit(word[0]) || (word.Length >= 2 && (word[0] == '+' || word[0] == '-') && IsDigit(word[1])); } 

Para converter uma seqüência de caracteres em um número, você pode usar os métodos padrão Int32.TryParse e Double.TryParse. Mas eles não diferem na velocidade por vários motivos, então eu uso uma solução personalizada.

 static object ParseNumber(string str) { var factor = 1.0; var sign = 1; if (str[0] == '-') { sign = -1; str = str.Remove(0, 1); } else if (str[0] == '+') { str = str.Remove(0, 1); } for (var i = str.Length - 1; i >= 0; i--) { if (str[i] == '.') { str = str.Remove(i, 1); return IntParseFast(str) * factor * sign; } factor *= 0.1; } return IntParseFast(str) * sign; } static int IntParseFast(string value) { // An optimized int parse method. var result = 0; foreach (var c in value) { if (!(c >= '0' && c <= '9')) return result; // error result = 10 * result + (c - 48); } return result; } 

O método ParseNumber pode converter valores inteiros e números de ponto flutuante, por exemplo, "1.618".

A execução da palavra ocorre da mesma maneira que usamos para executar o interpretador de endereço. No caso de uma exceção, um rastreamento de pilha do interpretador de endereço será impresso.

 public void Execute(int address) { try { if (address < Core.Count) { // eval core Core[address](); // invoke core function } else { // eval word IP = 0; // set return address WP = address; // set eval address DoList(); // fake doList Next(); // run evaluator } } catch (Exception e) { Output.WriteLine(e.Message); var wpEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == WP)); var ipEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == SearchKnowAddress(IP))); Output.WriteLine($"WP = {WP:00000} - '{wpEntry.Key}', IP = {IP:00000} - '{ipEntry.Key}'"); if (RS.Any()) { Output.WriteLine("Stack trace..."); foreach (var a in RS) { var ka = SearchKnowAddress(a); var sEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == ka)); Output.WriteLine($"...{a:00000} -- {sEntry.Key}"); } RS.Clear(); DS.Clear(); } else if (address < Core.Count) { var entry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == address)); Output.WriteLine($"Core word is {entry.Key}"); } IP = WP = 0; } } 

Quando o intérprete está no modo de compilação e a palavra não é marcada para execução imediata, seu endereço deve ser gravado na memória.

 public void AddOp(object op) { Mem[Here++] = op; } 

A variável here armazena o endereço da próxima célula livre. Como essa variável deve estar acessível no ambiente de tempo de execução como uma variável da linguagem Fort, o valor aqui é armazenado na memória do programa em um determinado deslocamento.

 public int _hereShift; public int Here { get => (int)Mem[_hereShift]; set => Mem[_hereShift] = value; } 

Para distinguir entre uma constante numérica e um endereço de palavra durante a interpretação, uma compilação da palavra doLit é compilada antes de cada constante durante a compilação, que lê o próximo valor na memória e o coloca na pilha de dados.

 public void DoLit() { DS.Push(Mem[IP++]); } 

Descrevemos intérpretes de endereço e texto; o desenvolvimento posterior consiste em preencher o núcleo com átomos. Versões diferentes de "Fort" têm um conjunto diferente de palavras básicas; a implementação mais minimalista será, talvez, eForth, que contém apenas 31 primitivas. Como o primitivo é executado mais rapidamente que as palavras de usuário compostas, as implementações Fort mínimas geralmente são mais lentas que as implementações detalhadas. Uma comparação do conjunto de palavras de várias versões de intérpretes pode ser encontrada aqui .

No intérprete descrito aqui, também tentei não inflar desnecessariamente o dicionário de palavras básicas. Mas, para facilitar a integração com a plataforma .net, decidi implementar matemática, operações booleanas e, é claro, reflexão através de um conjunto de primitivas. Ao mesmo tempo, algumas das palavras que geralmente são primitivas nas implementações do Fort estão ausentes aqui, implicando a implementação por meio do intérprete.

No momento da redação, o conjunto básico é de 68 palavras.
 // Core SetCoreWord("nop", Nop); SetCoreWord("next", Next); SetCoreWord("doList", DoList); SetCoreWord("exit", Exit); SetCoreWord("execute", Execute); SetCoreWord("doLit", DoLit); SetCoreWord(":", BeginDefWord); SetCoreWord(";", EndDefWord, true); SetCoreWord("branch", Branch); SetCoreWord("0branch", ZBranch); SetCoreWord("here", GetHereAddr); SetCoreWord("quit", Quit); SetCoreWord("dump", Dump); SetCoreWord("words", Words); SetCoreWord("'", Tick); SetCoreWord(",", Comma); SetCoreWord("[", Lbrac, true); SetCoreWord("]", Rbrac); SetCoreWord("immediate", Immediate, true); // Mem SetCoreWord("!", WriteMem); SetCoreWord("@", ReadMem); SetCoreWord("variable", Variable); SetCoreWord("constant", Constant); // RW SetCoreWord(".", Dot); SetCoreWord(".s", DotS); SetCoreWord("cr", Cr); SetCoreWord("bl", Bl); SetCoreWord("word", ReadWord, true); SetCoreWord("s\"", ReadString, true); SetCoreWord("key", Key); // Comment SetCoreWord("(", Comment, true); SetCoreWord("\\", CommentLine, true); // .net mem SetCoreWord("null", Null); SetCoreWord("new", New); SetCoreWord("type", GetType); SetCoreWord("m!", SetMember); SetCoreWord("m@", GetMember); SetCoreWord("ms@", GetStaticMember); SetCoreWord("ms!", SetStaticMember); SetCoreWord("load-assembly", LoadAssembly); SetCoreWord("invk", invk); // Boolean SetCoreWord("true", True); SetCoreWord("false", False); SetCoreWord("and", And); SetCoreWord("or", Or); SetCoreWord("xor", Xor); SetCoreWord("not", Not); SetCoreWord("invert", Invert); SetCoreWord("=", Eql); SetCoreWord("<>", NotEql); SetCoreWord("<", Less); SetCoreWord(">", Greater); SetCoreWord("<=", LessEql); SetCoreWord(">=", GreaterEql); // Math SetCoreWord("-", Minus); SetCoreWord("+", Plus); SetCoreWord("*", Multiply); SetCoreWord("/", Devide); SetCoreWord("mod", Mod); SetCoreWord("1+", Inc); SetCoreWord("1-", Dec); // Stack SetCoreWord("drop", Drop); SetCoreWord("swap", Swap); SetCoreWord("dup", Dup); SetCoreWord("over", Over); SetCoreWord("rot", Rot); SetCoreWord("nrot", Nrot); 


Para definir novas palavras de usuário, duas palavras do kernel são usadas: “:” e “;”. A palavra “:” lê o nome de uma nova palavra do fluxo de entrada, cria um cabeçalho com essa tecla, o endereço da palavra base doList é adicionado à memória do programa e o intérprete é colocado no modo de compilação. Todas as palavras subsequentes serão compiladas, com exceção daquelas marcadas como imediatas.

 public void BeginDefWord() { AddHeader(ReadWord(Input)); AddOp(LookUp("doList").Address); IsEvalMode = false; } 

A compilação termina com a palavra ";", que grava o endereço da palavra "exit" na memória do programa e o coloca no modo de interpretação. Agora você pode definir palavras personalizadas - por exemplo, loops, uma declaração condicional e outras.

 Eval(": ? @ . ;"); Eval(": allot here @ + here ! ;"); Eval(": if immediate doLit [ ' 0branch , ] , here @ 0 , ;"); Eval(": then immediate dup here @ swap - swap ! ;"); Eval(": else immediate [ ' branch , ] , here @ 0 , swap dup here @ swap - swap ! ;"); Eval(": begin immediate here @ ;"); Eval(": until immediate doLit [ ' 0branch , ] , here @ - , ;"); Eval(": again immediate doLit [ ' branch , ] , here @ - , ;"); Eval(": while immediate doLit [ ' 0branch , ] , here @ 0 , ;"); Eval(": repeat immediate doLit [ ' branch , ] , swap here @ - , dup here @ swap - swap ! ;"); Eval(": // immediate [ ' \\ , ] ;"); // C like comment 

Não descreverei o restante das palavras padrão aqui - há informações suficientes sobre elas na rede sobre os recursos temáticos correspondentes. Para interagir com a plataforma, defini 9 palavras:

  • "Nulo" - empurra nulo para a pilha;
  • "Type" - envia o tipo de classe para a pilha de "word TrueForth.MyClass type";
  • “Novo” - pega o tipo da pilha, cria uma instância da classe e a coloca na pilha; os argumentos do construtor, se houver, também devem estar na pilha “word TrueForth.MyClass type new”;
  • "M!" - pega uma instância de um objeto, nome do campo, valor da pilha e atribui um valor ao campo especificado;
  • "M @" - seleciona uma instância de um objeto da pilha, o nome do campo e retorna o valor do campo para a pilha;
  • "Ms!" E "ms @" - semelhantes aos anteriores, mas para campos estáticos, em vez de uma instância, deve haver um tipo na pilha;
  • "Load-assembly" - pega da pilha, deixa-a na montagem e carrega na memória;
  • “Invk” - pega o delegado, os argumentos da pilha e o chama “1133 word SomeMethod word TrueForth.MyClass type new m @ invk”.

Descrevi os principais pontos da implementação da linguagem Fort; essa implementação não busca oferecer suporte aos padrões ANSI para a linguagem, pois sua tarefa é implementar um mecanismo para a construção de DSL e não implementar uma linguagem de uso geral. Na maioria dos casos, o intérprete desenvolvido é suficiente para criar uma linguagem simples da área de assunto.

Existem várias maneiras de usar o intérprete acima. Por exemplo, você pode criar uma instância do intérprete e enviar um script de inicialização para a entrada, na qual as palavras necessárias são determinadas. Este último, através da reflexão, interage com o sistema.

 public static bool Init4Th() { Interpreter = new OForth(); if (File.Exists(InitFile)) { Interpreter.Eval(File.ReadAllText(InitFile)); return true; } else { Console.WriteLine($"  {InitFile}  !"); return false; } } 

Exemplo de configuração do sistema de distribuição de relatórios

 ( *****   ***** ) word GetFReporter word ReportProvider.FlexReports.FReporterEntry type new m@ invk constant fr //       :  word ReportProvider.FlexReports.FDailyReport type new ; //       :  word AddReport fr m@ invk ; //          :  [ ' word , ] ; //   :  [ ' word , ] ; //   :  [ ' s" , ] ; //  ,      " :  ; //  :  dup [ ' word , ] swap word MailSql swap m! ; :  dup [ ' word , ] swap word XlsSql swap m! ; ( *****    ***** ) cr s"   " . cr cr    "  08:00  mail@tinkoff.ru   seizure.sql    ,    "  08:00  mail@tinkoff.ru   fixed-errors-top.sql   fixed-errors.sql         WO"  08:00  mail@tinkoff.ru   wo-wait-complect-dates.sql       "  07:30  mail@tinkoff.ru   top-previous-input-errors.sql   previous-input-errors.sql        "  10:00  mail@tinkoff.ru   collection-report.sql       BPM   "  08:00  mail@tinkoff.ru   bpm-inbox-report.sql       ScanDoc3   7 "  07:50  mail@tinkoff.ru   new-sd3-complects-prevew.sql   new-sd3-complects.sql  ( ******************************** ) cr s"  " . cr 

Você pode fazer o contrário: passar objetos prontos para a entrada do intérprete pela pilha de dados e interagir com eles através do intérprete. Como por exemplo, eu fiz para restaurar as configurações do dispositivo para receber digitalizações de documentos, um scanner, uma webcam ou um dispositivo virtual (para depuração ou treinamento). Nesse caso, o conjunto de parâmetros, configurações, a ordem de inicialização de diferentes dispositivos é muito diferente e é resolvido trivialmente pelo intérprete forte.

 var interpreter = new OForth(); interpreter.DS.Push(this); // Push current instance on DataStack interpreter.Eval("constant arctium"); // Define constant with the instance if (File.Exists(ConfigName)) { interpreter.Eval(File.ReadAllText(ConfigName)); } 

A configuração é gerada programaticamente, acontece algo como isto:

 s" @device:pnp:\\?\usb#vid_2b16&pid_6689&mi_00#6&1ef84f63&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global" s" Doccamera" word Scanning.Devices.PhotoScanner.PhotoScannerDevice type new dup s" 3264x2448, FPS:20, BIT:24" swap word SetSnapshotMode swap m@ invk dup s" 1280x720, FPS:30, BIT:24" swap word SetPreviewMode swap m@ invk word SetActiveDevice arctium m@ invk 

A propósito, os scripts * .ps e * .pdf são gerados de maneira semelhante, porque PostScript e Pdf são essencialmente um subconjunto do "Fort", mas são usados ​​exclusivamente para renderizar documentos na tela ou na impressora.

É igualmente fácil implementar o modo interativo para console e não apenas aplicativos. Para fazer isso, você deve primeiro inicializar o sistema através do script preparado e, em seguida, iniciar a interpretação definindo o intérprete na entrada padrão STDIN.

 var interpreter = new OForth(); const string InitFile = "Init.4th"; if (File.Exists(InitFile)) { interpreter.Eval(File.ReadAllText(InitFile)); } else { Console.WriteLine($"  {InitFile}  !"); } interpreter.Eval(Console.In); // Start interactive console 

O script de inicialização pode ser assim:

 ( *****   ***** ) word ComplectBuilder.Program type constant main //     : mode! [ ' word , ] word Mode main ms! ; //    : init word Init main ms@ invk ; //  : load [ ' word , ] word LoadFile main ms@ invk ; //   : start word StartProcess main ms@ invk ; //   : count word Count main ms@ invk ; //   : all count ; //  ( *****  ***** ) init cr cr s"    ,     help" . cr cr ( *****  ***** ) : help s"         :" . cr s" load scandoc_test.csv 0 all start" . cr bl bl s" load scandoc_test.csv --    " . cr bl bl s" 0 all start --  ,  0      all " . cr cr s"     DEV TEST PROD:" . cr s" mode! DEV init" . cr s"     :" . cr s" word Mode main ms@ . cr" . cr ; 

Como entrada, pode haver não apenas um console ou texto de um aplicativo TextBox com uma interface do usuário, mas também uma rede. Nesse caso, você pode implementar um controle interativo simples, por exemplo, um serviço, para depurar, iniciar, parar componentes. As possibilidades de tal uso são limitadas pela imaginação do desenvolvedor e pela tarefa em questão. , UI - .

. , , .

, :

 public void Callback(string word, MulticastDelegate action) { if (string.IsNullOrWhiteSpace(word) || word.Any(c => " \n\r\t".Any(cw => cw == c))) { throw new Exception("invalid format of word"); } DS.Push(action); Eval($": {word} [ ' doLit , , ] invk ;"); } 

DS.Push(action), . , , [ ], , . ' Tick , doLit, , . Comma «,» doLit, .

, . , :

 public class WoConfItem { public string ComplectType; public string Route; public string Deal; public bool IsStampQuery; } 

— , :

 public class WoConfig { private OForth VM; private List<WoConfItem> _conf; public WoConfig(string confFile) { _conf = new List<WoConfItem>(); VM = new OForth(); //      VM.Callback("new-conf", new Action(ClearConf)); VM.Callback("{", new Func<WoConfItem>(NewConf)); VM.Callback("}", new Action<WoConfItem>(AddConf)); VM.Callback("complect-type", new Func<WoConfItem,string,WoConfItem>(ConfComplectType)); VM.Callback("route", new Func<WoConfItem,string,WoConfItem>(ConfRoute)); VM.Callback("deal", new Func<WoConfItem,string,WoConfItem>(ConfDeal)); VM.Callback("is-stamp-query", new Func<WoConfItem,bool,WoConfItem>(ConfIsStampQuery)); //  ,   ,       var initScript = new StringBuilder(); initScript.AppendLine(": complect-type [ ' word , ] swap complect-type ;"); initScript.AppendLine(": route [ ' word , ] swap route ;"); initScript.AppendLine(": deal [ ' word , ] swap deal ;"); initScript.AppendLine(": is-stamp-query ' execute swap is-stamp-query ;"); VM.Eval(initScript.ToString()); //   WatchConfig(confFile); } private void ReadConfig(string path) { using (var reader = new StreamReader(File.OpenRead(path), Encoding.Default)) { VM.Eval(reader); } } readonly Func<string, bool> _any = s => s == "*"; public WoConfItem GetConf(string complectType, string routeId) { return _conf?.FirstOrDefault(cr => (cr.ComplectType == complectType || _any(cr.ComplectType)) && (cr.Route == routeId || _any(cr.Route)) ); } public bool IsAllow(string complectType, string routeId) { return GetConf(complectType, routeId) != null; } void WatchConfig(string path) { var directory = Path.GetDirectoryName(path); var fileName = Path.GetFileName(path); //   ,     if (!File.Exists(path)) { if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var sb = new StringBuilder(); sb.AppendLine("\\ WO passport configuration"); sb.AppendLine("new-conf"); sb.AppendLine(""); sb.AppendLine("\\ Config rules"); sb.AppendLine("\\ { -- begin config item, } -- end config item, * -- match any values"); sb.AppendLine("\\ Example:"); sb.AppendLine("\\ { complect-type * route offer deal 100500 is-stamp-query true }"); sb.AppendLine(""); File.WriteAllText(path, sb.ToString(), Encoding.Default); } //   ReadConfig(path); //     var fsWatcher = new FileSystemWatcher(directory, fileName); fsWatcher.Changed += (sender, args) => { try { fsWatcher.EnableRaisingEvents = false; //        , //     ,   //     Thread.Sleep(1000); ReadConfig(path); } catch (Exception e) { Console.WriteLine(e); } finally { fsWatcher.EnableRaisingEvents = true; } }; fsWatcher.EnableRaisingEvents = true; } //  ,    void ClearConf() { _conf.Clear(); } void AddConf(WoConfItem conf) { _conf.Add(conf); } static WoConfItem NewConf() { return new WoConfItem(); } static WoConfItem ConfComplectType(WoConfItem conf, string complectType) { conf.ComplectType = complectType; return conf; } static WoConfItem ConfRoute(WoConfItem conf, string route) { conf.Route = route; return conf; } static WoConfItem ConfDeal(WoConfItem conf, string deal) { conf.Deal = deal; return conf; } static WoConfItem ConfIsStampQuery(WoConfItem conf, bool isStampQuery) { conf.IsStampQuery = isStampQuery; return conf; } } 


:

 \ WO passport configuration new-conf \ Config rules \ { -- begin config item, } -- end config item, * -- match any values \ Example: \ { complect-type * route offer deal 100500 is-stamp-query true } \ ***** offer ***** { complect-type offer route offer is-stamp-query false deal 5c18e87bfeed2b0b883fd4df } { complect-type KVK route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-cred route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-dep route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type quick-meeting route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type exica route offer is-stamp-query true deal 5d03a894e2f5850001435492 } { complect-type reissue route offer is-stamp-query true deal 5d03a894e2f5850001435492 } \ ***** offer-flow ***** { complect-type KVK route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-cred route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-dep route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type reissue route offer-flow is-stamp-query true deal 5d03a894e2f5850001435492 } 

, , DSL — .

, «». DSL.

, , — , , , , — . , .

— , . — , — !

, .

- .

Boa sorte

Source: https://habr.com/ru/post/pt477902/


All Articles