Programação metamórfica
— Ensaios hipotéticos sobre a Tecnologia Ybymarense —
Um código metamórfico é aquele que tem a característica peculiar de alterar a si próprio independente do programador. Para isso, será introduzido um novo tipo de variável que pode ser chamado de variável logicial. Grosso modo, a ideia é que as linhas de código do programa estejam armazenadas em suas próprias variáveis. Desse modo, a alteração das variáveis do código alteram o comportamento do mesmo.
1. Conceitos iniciais da programação metamórfica
Uma variável logicial pode conter um comando, função ou valor. Para identificar uma variável logicial, esta poderá vir seguida de um símbolo. Neste trabalho será usado o caractere “@” para isso. Vejamos como exemplo a variável logicial de nome VAR.
VAR@ = #PRINT → VAR contém comando “PRINT”VAR@ = #SIN → VAR contém a função “SIN”VAR@ = “H” → VAR contém o caractere “H”VAR@ = 10 → VAR contém o valor numérico 10
Uma matriz, ou vetor, destas variáveis contém uma sequência de comandos e valores. Observe que o sinalizador “#” indica uma função ou comando. Constantes numéricas ou alfanuméricas não precisam ser precedidas por símbolos.
Vejamos um exemplo de vetor ou matriz de uma dimensão de variáveis logiciais:
VAR@[0] = #PRINT
VAR@[1] = #SIN
VAR@[2] = 10
Esta sequência forma uma linha de comando:
VAR@[0]
↓
PRINT
VAR@[1]
↓
SIN
VAR@[2]
↓
10
A linha de comando resultante imprimirá na saída padrão o valor do seno de 10. Podemos usar um comando para chamar uma variável logicial, como por exemplo:
CALL VAR@
Este comando fará com que o conteúdo de VAR@ seja executado. No caso, será impresso no dispositivo padrão (tela, por exemplo) o valor do seno de 10. Esta característica, por si só, em nada acrescentaria ao que as linguagens de programação atuais podem fazer pois se trata apenas de um comando tipo “eval()” desmembrado em seus fatores em vez de uma string completa. Os programas continuam rígidos.
O objetivo da programação metamórfica é possibilitar que os comandos contidos nas variáveis logiciais possam ser criados, deletados e alterados pelos dados e informações que são inseridas no sistema. Isso é relativamente fácil. A questão é como fazer com que a sequência de comandos resultante tenha sentido e evitar que o sistema trave ou entre em loop infinito, o que pode ocorrer, por exemplo, em caso de comandos aninhados ou recorrentes. Um exemplo:
VAR@[0] = #PRINT
VAR@[1] = #SIN
VAR@[2] = 10
VAR@[3] = #CALL
VAR@[4] = VAR@
Esta sequência de comandos dentro de VAR@ fará com que as instruções contidas em VAR@ sejam executadas indefinidamente caso a variável seja chamada. Para evitar isso, uma das regras poderá ser o impedimento de uma variável logical chamar a si própria. Essa é a solução fácil e tosca.
Foquemos no objetivo da programação metamórfica, que é a inteligência artificial, o que quer dizer que comandos usuais em linguagens de programação convencionais podem não ser adequados. Mesmo assim, será mantido aqui o esquema “entrada → processamento → saída”, onde o resultado (saída) será utilizado por outro comando em uma sequência de tamanho indefinido e variável.
Digamos que temos um comando que adiciona dois valores de 16 bits e entrega o resultado limitado também em 16 bits. Vejamos a sequência abaixo (observe que foi usado o sinalizador “&” para indicar entrada ou saída de dados).
Soma@[0] = #ADC
Soma@[1] = &Entrada1
Soma@[2] = &Entrada2
Soma@[3] = #OUT
Soma@[4] = &Saída1
Esta sequência de comandos faz com que o sistema some os valores da “Entrada1” com os da “Entrada2” (função #ADC) e os direcione (#OUT) para a “Saída1”. A estrutura é basicamente a seguinte:
1. Operação a ser feita
2. Definição das portas de entrada
3. Comando de saída
4. Definição das portas de saída
Se esta estrutura for mantida, o sistema, muito provavelmente, não travará por qualquer erro de comando. Um código de programação com esta filosofia poderá ser formado por um número variável de blocos com esta estrutura. Entretanto, entradas e saídas devem ser definidas antes da sequência de blocos. Veja a sugestão abaixo:
DEF &Entrada1 as input = Keyboard;
DEF &Entrada2 as input = Mouse;
DEF &Saída1 as output = Video;
DEF Soma@ as logicial_variable;
Soma@[0] = #ADC
Soma@[1] = &Entrada1
Soma@[2] = &Entrada2
Soma@[3] = #OUT
Soma@[4] = &Saída1
CALL Soma@;
END ROUTINE;
Da forma como está, a rotina pega um valor do teclado, outro do mouse, soma os dois e retorna o resultado no monitor de vídeo. A estrutura continua rígida. Observemos também que entradas e saídas dependem do hardware, somente alterando com a alteração deste. Devido a isso, por ora, consideraremos as definições de entrada e saída como valores invariantes.
Em continuação ao raciocínio, vamos associar um valor numérico a cada comando. Assim, teremos:
1 = #ADC
2 = #SUB
3 = #OUT
4 = &Entrada1
5 = &Entrada2
6 = &Saída1
7 = CALL
Desta forma, os valores numéricos contidos na sequência seriam os seguintes:
Soma@[0] = #ADC ;valor 1
Soma@[1] = &Entrada1 ;valor 4
Soma@[2] = &Entrada2 ;valor 5
Soma@[3] = #OUT ;valor 3
Soma@[4] = &Saída1 ;valor 6
Suponhamos que as entradas sejam numéricas. Assim, por exemplo, se tivermos 2 em &Entrada1 e 3 em &Entrada2, será enviado o valor 5 para &Saída1. Incluamos mais uma linha no código:
Soma@[0] = #ADC ;1
Soma@[1] = &Entrada1 ;4
Soma@[2] = &Entrada2 ;5
Soma@[3] = #OUT ;3
Soma@[4] = &Saída1 ;6
Soma@[0] = (&Entrada1)
Observemos que o valor contido na posição Soma@[0] irá variar conforme o valor de &Entrada1. Nesta posição está o comando #ADC, cujo número associado é 1. Se entrarmos os mesmos valores, teremos:
Soma@[0] = #ADC ;Comando de adição
Soma@[1] = &Entrada1 ;Valor 1ª entrada (2)
Soma@[2] = &Entrada2 ;Valor 2ª entrada (3)
Soma@[3] = #OUT ;Comando de saída
Soma@[4] = &Saída1 ;Valor de saída (5)
Soma@[0] = (&Entrada1) ;Coloca o valor de "&Entrada1" em Soma@[0] (comando SUB)
CALL Soma@ ;Executa
* Código após a primeira execução:
Soma@[0] = #SUB ;Comando de adição
Soma@[1] = &Entrada1 ;Valor da 1ª entrada
Soma@[2] = &Entrada2 ;Valor da 2ª entrada
Soma@[3] = #OUT ;Comando de saída
Soma@[4] = &Saída1 ;Valor de saída
Soma@[0] = &Entrada1 ;Coloca o valor de "&Entrada1" em Soma@[0]
Observemos que um valor de entrada alterou o código do programa. Este é o conceito primário da programação metamórfica.
2. Estrutura de uma linguagem metamórfica
Um código metamórfico é formado basicamente por vetores ou cadeias de tamanho variável onde cada posição poderá conter um valor numérico (quantitativo), um valor alfanumérico ou caractere (qualitativo) ou um valor de execução (comando ou função). A quantidade de vetores também é variável e pode mudar em tempo de execução. Podemos, por exemplo, criar o vetor do código ilustrado acima (já com os definidores de variáveis):
DEF&Entrada1 as input = Keyboard;
DEF&Entrada2 as input = Mouse;
DEF&Saída1 as output = Video;
VECTOR[1] = Soma@ {
0 = #ADC
1 = &Entrada1
2 = &Entrada2
3 = #OUT
4 = &Saída1
0 = &Entrada1
}
Observe que os definidores estão fora do vetor, o que significa que não podem ser alterados. Pode haver comandos fora dos vetores; neste caso, sua estrutura será rígida como os códigos estruturados conhecidos. De qualquer forma, podemos perceber que códigos metamórficos serão bem diferentes de outros tipos de código, inclusive seus tipos de comandos e variáveis.
Uma questão básica a resolver é como o sistema trabalhará se um valor entrado não corresponder a nenhum comando ou definidor de variável. Digamos que seja o valor 50. No exemplo, não há comando associado a este número. Vejamos o que acontece.
Código original:
DEF&Entrada1 as input = Keyboard;
DEF&Entrada2 as input = Mouse;
DEF&Saída1 as output = Video;
VECTOR[1] = Soma@ {
0 = #ADC
1 = &Entrada1
2 = &Entrada2
3 = #OUT
4 = &Saída1
0 = &Entrada1
}
&Entrada1 = 50
&Entrada2 = 40
Após a primeira execução (sem os definidores de variáveis):
VECTOR[1] = Soma@ {
0 = %50
1 = %50
2 = %40
3 = #OUT
4 = %90
0 = %50
}
(&Entrada1) = 50
(&Entrada2) = 40
No exemplo, foi usado o identificador “%” para indicar valor numérico. Observemos que não há mais comando de adição, restando apenas o comando de saída. Isso acontecerá sempre que o código estiver nos moldes ilustrados. Uma maneira melhor de tratar as variáveis contidas nos vetores é mantê-la do mesmo tipo após criada. Assim, os índices 0 e 3 sempre serão comandos e os índices 1, 2 e 4 serão valores de entrada ou saída. Uma lista de comandos deve ser associada a cada valor numérico e uma regra de substituição será usada quando não houver comando correspondente ao número inserido.
Assim, após a execução com essas regras, o código ficará assim:
VECTOR[1] = Soma@ {
0 = #NOVO COMANDO
1 = &Entrada1
2 = &Entrada2
3 = #OUT
4 = &Saída1
0 = &Entrada1
}
(&Entrada1) = 50
(&Entrada2) = 40
(&Saída1) = 90
Só que as variáveis numéricas inseridas não têm uma posição específica dentro do vetor, o que não é desejável. Então vamos criar uma posição para cada uma:
VECTOR[1] = Soma@ {
0 = #NOVO COMANDO
1 = &Entrada1
2 = &Entrada2
3 = #OUT
4 = &Saída1
5 = VAL (&Entrada1)
6 = VAL (&Entrada2)
7 = VAL (&Saída1)
0 = VAL (&Entrada1)
}
Assim, os valores de &Entrada1, &Entrada2 e &Saída1 serão armazenados nas posições 5, 6 e 7, respetivamente, e as posições 1, 2 e 4 continuam com sua função de entrada e saída. Observe que a última linha, que reatribui a posição 0, o valor (&Entrada1) está entre parênteses para indicar que ali não é uma posição de entrada, mas uma simples reatribuição de valor para a posição, o que resultará na alteração do comando. Da mesma forma, nas posições 5, 6 e 7 os descritores estão entre parênteses pelo mesmo motivo.
Temos, até aqui, definidos quatro tipos de dados que podem ser inseridos nas posições do vetor:
Identificador # → comando ou função.
Identificador & → entrada ou saída de dados.
Identificador % → valor numérico ou quantitativo.
Identificador $ → valor descritivo ou qualitativo.
Os tipos não podem ser alterados depois de definidos sob pena de tornar o código inútil. A alteração do conteúdo dos índices do vetor altera também o comportamento do código; entretanto o mesmo valor significará coisas diferentes conforme o identificador. Por exemplo, o valor 5 em um índice poderá significar:
2.1. Comandos exclusivos de uma linguagem metamórfica
#5 → comando AND (operador lógico AND)
&5 → entrada de dados (digamos, &Entrada1)
%5 → valor numérico inteiro 5
$5 → caractere “A”
Entre os comandos possíveis há dois que são especiais, e, pode-se dizer, exclusivos dos códigos metamórficos. São os comandos #INC e #EXC, que significam “Incluir” e “Excluir”. Observemos o exemplo abaixo para ver o que estes comandos fazem.
VECTOR[1] = Soma@ {
0 = &Entrada1
1 = #OUT
2 = &Saída1
3 = #INC
4 = 0
5 = "#ADC" }
Após a primeira execução:
VECTOR[1] = Soma@ {
0 = #ADC
1 = &Entrada1
2 = #OUT
3 = &Saída1
4 = #INC
5 = 0
6 = "#ADC"
}
E para o comando #EXC:
VECTOR[1] = Soma@ {
0 = &Entrada1
1 = #OUT
2 = &Saída1
3 = #EXC
4 = 0
}
Após a primeira execução:
VECTOR[1] = Soma@ {
0 = #OUT
1 = &Saída1
2 = #EXC
3 = 0
}
2.2. Exemplos de comandos
Comandos de alteração
INC – Inclui uma célula no vetor.
EXC – Exclui uma célula do vetor.
SUBS – Substitui uma célula no vetor.
Comandos de operação
ADC – Adiciona as duas células seguintes.
SBC – Subtrai a célula seguinte pela próxima.
MULT – Multiplica as duas células seguintes.
DIV – Divide a célula seguinte pela próxima.
REST – Retorna o resto da divisão da célula seguinte pela próxima.
AND – Efetua operação lógica AND entre as duas células seguintes.
OR – Efetua operação lógica OR entre as duas células seguintes.
XOR – Efetua operação lógica XOR entre as duas células seguintes.
NOT – Inverte a condição lógica da célula seguinte.
Comandos de Entrada/Saída:
DEF &xxx AS INPUT = DISP → Atribui o <DISP> a xxx como entrada
DEF &yyy AS OUTPUT = DISP → Atribui o <DISP> a yyy como saída
Comandos de criação e execução
VECTOR [num] = zzz@ → Cria o vetor número <num> com o nome <zzz>
CALL zzz@ → Executa o vetor <zzz>
3. Estrutura de um vetor metamórfico
Cada índice do vetor deverá ter um tamanho fixo, digamos, 32 bits. Alguns desses deverão ser reservados para definir qual tipo de variável logicial está contido nele. Reservando 4 bits teremos a definição de até 16 tipos, restando 28 bits para um valor numérico inteiro de 0 a 268.436.455 (ou –134.217.728 a 134.217.727, ou qualquer outro valor representável).
Em cada uma das posições do vetor poderá ser armazenado um comando, uma função, um valor numérico, um caractere alfanumérico, um descritor de variável, etc. Para cada um deles há um ID específico, que não poderá ser alterado depois que for criado. Um exemplo de ID’s:
00 – Marca início do vetor
01 – Comando
02 – Função
03 – Marcador de saída de dados
04 – Marcador de entrada de dados
05 – Valor inteiro sem sinal (0 a 268.436.455)
06 – Valor inteiro com sinal (–134.217.728 a +134.217.727)
07 – Expoente
08 – Primeira metade da mantissa
09 – Segunda metade da mantissa
10 – Expoente imaginário
11 – Primeira metade da mantissa imaginária
12 – Segunda metade da mantissa imaginária
13 – Caractere alfanumérico
14 – Caractere ideográfico
15 – Marca fim do vetor
Conforme convencionado, os quatro bits ID não podem ser modificados depois de criados; apenas os 28 bits restantes são variáveis.
Em alguns casos, algumas células podem ser agrupadas, como os ID’s 7, 8 e 9, a fim de compor um número de ponto flutuante, ou no caso do comando ADC que pode ser agrupado com os dois valores seguintes. Tais grupos podem formar um bloco (cuja estrutura não pode ser modificada) ou uma sequência de estrutura flexível.
