ChatGPT para analizar código fuente

Vamos a pedirle a ChatGPT que analice tres ejemplos de código, en tres lenguajes distintos y con un nivel de dificultad creciente. Para ponérselo más difícil se ha eliminado cualquier comentario dejando el código «desnudo».

Puedes ver el vídeo donde explico todas las pruebas y resultados haciendo click en la imagen de debajo:

Haz click para ver en mi canal de Youtube

El primer caso viene de aquí. Una librería propia para crear máquinas de estado finito en Arduino (si te interesa su funcionamiento lo explico aquí)

#ifndef efsm_h
	#define efsm_h

	#define ANY_STATE efsmState
	#define ANY_EVENT efsmEvent

	#define START_TRANSITIONS \
	int efsmEvent(enum efsmEvents efsmEvent){ \
		efsmTimer = millis();\
		efsmCounter = 0;\

	#define TRANSITION(E,Ss,Sf,F) \
		if(efsmEvent == E && efsmState == Ss){ \
			efsmState = Sf; \
			F; \
			return 1; \
		}\

	#define END_TRANSITIONS \
		return 0; \
	} \

	#define START_EXECUTIONS \
	int efsmExecute(){ \

	#define EXECUTION(S,F) \
		if(efsmState == S){ \
			F; \
			return 1; \
		}\

	#define END_EXECUTIONS \
		return 0; \
	} \

	#define START_TRIGGERS \
	int efsmTriggers(){ \
		efsmCounter++; \
		unsigned long diff = millis()-efsmTimer;\

	#define COUNTER(S,N,E) \
		if(efsmState == S && efsmCounter >= N){ \
			return efsmEvent(E); \
		}\

	#define TIMER(S,M,E) \
		if(efsmState == S && diff >= M){ \
			return efsmEvent(E); \
		}\

	#define CONDITIONAL(S,C,E) \
		if(efsmState == S && C){ \
			return efsmEvent(E); \
		}\


	#define END_TRIGGERS \
		return 0; \
	} \

	#define EVENTS enum efsmEvents

	#define STATES enum efsmStates

	#define INIT(S) \
	enum efsmStates efsmState = S;\
	unsigned long efsmCounter = 0; \
	unsigned long efsmTimer = 0; \

	#define isState(S) efsmState == S
	#define changeState(S) efsmState = S
	#define resetTimer() efsmTimer = millis()
	#define resetCounter() efsmCounter = 0
#endif

El resultado es el siguiente:

Este código es una definición de una librería de máquinas de estado finito (FSM, por sus siglas en inglés) para microcontroladores, escrita en C o C++.

Las FSM son una técnica de programación en la que un sistema se modela como un conjunto de estados, y la transición entre ellos se produce en respuesta a una entrada (evento). Este enfoque puede resultar muy útil para sistemas con comportamientos complejos que cambian con el tiempo, como los sistemas de control o los dispositivos electrónicos.

En esta librería, se definen las macroinstrucciones que se utilizan para configurar una máquina de estados finitos en un microcontrolador, incluyendo las transiciones de estado, las condiciones que activan estas transiciones y las acciones que se ejecutan en respuesta a un evento o un estado.

La librería define una estructura básica para definir la máquina de estados finitos, incluyendo las transiciones de estado, las condiciones que activan estas transiciones y las acciones que se ejecutan en respuesta a un evento o un estado. La estructura también incluye la definición de las macros que se utilizan para configurar la máquina de estados finitos.

Las macros definidas en esta librería incluyen:

  • START_TRANSITIONS: esta macro indica el comienzo de la sección de código que define las transiciones de estado. Dentro de esta sección, se definen todas las transiciones de estado posibles y las acciones que se deben ejecutar en respuesta a cada una.
  • TRANSITION(E,Ss,Sf,F): esta macro define una transición de estado en respuesta a un evento E. Si el estado actual de la máquina es Ss, se cambia el estado a Sf y se ejecuta la función F. Esta macro se utiliza dentro de la sección START_TRANSITIONS y END_TRANSITIONS.
  • END_TRANSITIONS: esta macro indica el final de la sección de código que define las transiciones de estado.
  • START_EXECUTIONS: esta macro indica el comienzo de la sección de código que define las acciones que se deben ejecutar en respuesta a un estado específico.
  • EXECUTION(S,F): esta macro define una acción que se debe ejecutar si el estado actual de la máquina es S. La acción se define mediante la función F. Esta macro se utiliza dentro de la sección START_EXECUTIONS y END_EXECUTIONS.
  • END_EXECUTIONS: esta macro indica el final de la sección de código que define las acciones que se deben ejecutar en respuesta a un estado específico.
  • START_TRIGGERS: esta macro indica el comienzo de la sección de código que define las condiciones que activan una transición de estado.
  • COUNTER(S,N,E): esta macro define una condición basada en un contador. Si el estado actual de la máquina es S y el contador ha alcanzado el valor N, se activa la transición de estado correspondiente al evento E.
  • TIMER(S,M,E): esta macro define una condición basada en un temporizador. Si el estado actual de la máquina es S y el tiempo transcurrido desde que se inició el temporizador es mayor o igual a M milisegundos, se activa la transición de estado correspondiente al evento E.
  • CONDITIONAL(S,C,E): esta macro define una condición basada en una expres

El análisis, aunque no está completo, es bastante acertado y pude servir como documentación de funcionamiento del código. Algún punto quizás seria necesario aclararlo un poco más pero como herramienta para ayudar a entender el código es increíble.

El siguiente código es una librería en JS para separar silabas, encontrar la silaba tónica y calcular la dificultad de leer cada silaba en lenguaje español. El código lo puedes encontrar aquí. (Si te interesa entender su funcionamiento está explicado aquí y aquí)

var regexStep1 = [];
var cutPositionsStep1 = [];

var regexStep2 = [];
var cutPositionsStep2 = [];

regexStep1.push(/^[aeiouáéíóúü]+([^aeiouáéíóúü]|$)/);
cutPositionsStep1.push(0);

regexStep1.push(/^[^aeiouáéíóúü][aeiouáéíóúü]+([^aeiouáéíóúü]|$)/);
cutPositionsStep1.push(0);

regexStep1.push(/^[^aeiouáéíóúü][^aeiouáéíóúü][aeiouáéíóúü]+([^aeiouáéíóúü]|$)/);
cutPositionsStep1.push(1);

regexStep1.push(/^[^aeiouáéíóúü][^aeiouáéíóúü][^aeiouáéíóúü][aeiouáéíóúü]+([^aeiouáéíóúü]|$)/);
cutPositionsStep1.push(2);

regexStep1.push(/^[^aeiouáéíóúü][^aeiouáéíóúü][^aeiouáéíóúü][^aeiouáéíóúü][aeiouáéíóúü]+([^aeiouáéíóúü]|$)/);
cutPositionsStep1.push(3);

regexStep1.push(/^[aeiouáéíóúü]+$/);
cutPositionsStep1.push(-1);

regexStep1.push(/^[^aeiouáéíóúü]+$/);
cutPositionsStep1.push(-1);

regexStep2.push(/[aeoáéó][aeoáéó]/);
cutPositionsStep2.push(1);

regexStep2.push(/[íú][aeo]/);
cutPositionsStep2.push(1);

regexStep2.push(/[aeo][íú]/);
cutPositionsStep2.push(1);

regexStep2.push(/[iuüíú][aeoáéó][aeoáéó]/);
cutPositionsStep2.push(2);

regexStep2.push(/[aeoáéó][aeoáéó][iuüíú]/);
cutPositionsStep2.push(1);

regexStep2.push(/.*/);
cutPositionsStep2.push(-1);

export function divide(word){
  word = word.toLowerCase().trim();    
  var cutPosition = 0;
  var syllables = [];
  var finalSyllables = [];
  var head = "";
  var securityBreak = 0;
  var end = false;
  
  while(!end){
    ++securityBreak;
    if(securityBreak > 20){
      throw "Error processing word";
      return "";
    }
    for(var i = 0; i < regexStep1.length; ++i){
      var match = word.match(regexStep1[i]);
      if(match){
        var m = match[0];
        cutPosition = cutPositionsStep1[i];
        if(cutPosition < 0){
          syllables.push(head+m);
          end = true;
        } else {
          var cutChar = m[cutPosition];
          if((cutChar == 'r') || (cutChar == 'l') || (cutChar == 'h')){
            cutPosition--;
          }
          if(cutPosition < 0)
            cutPosition = 0;
          syllables.push(head+m.substring(0,cutPosition));
          head = m.substring(cutPosition,m.length-1);
        }

        word = word.substring(m.length-1);
        
      }
    }
  }
   
  for(var s = 0; s < syllables.length; ++s){
    var sillable = syllables[s];
    if(sillable == "")
      continue;

    for(var i = 0; i < regexStep2.length; ++i){
      var match = sillable.match(regexStep2[i]);
      if(match){
        if(cutPositionsStep2[i] < 0){
          finalSyllables.push(sillable);
        }else{
          var cutPosition = match.index + cutPositionsStep2[i];
          finalSyllables.push(sillable.substring(0,cutPosition));
          finalSyllables.push(sillable.substring(cutPosition));
        }
        
        break;
      }
    }
  }
  
  return finalSyllables;
}

export function stress(syllables) {
  if(typeof syllables === 'string'){
    syllables = divide(syllables);
  }

  if(syllables.length == 1){
    return 0;
  }
  
  for(var i = 0; i < syllables.length; ++i){
    if(syllables[i].match(/[áéíóú]/)){
      
      return i;
    }
  }
  
  if(syllables[syllables.length-1].match(/.*[nsaeiou]$/)){
    
    return syllables.length-2;
  } else {
    
    return syllables.length-1;
  }
}

export function difficulty(syllables){
    if(typeof syllables === 'string'){
      syllables = divide(syllables);
    }

    var result = [];
    for(var i = 0; i < syllables.length; ++i){
        result.push(difficultySyllable(syllables[i]));
    }
    return result;
}

function difficultySyllable(text){
    var value = 0;

    text = text.replace("ll", "y");
    text = text.replace("rr", "r");
        
    text = text.replace("que", "qe");
    text = text.replace("qui", "qi");                    
    text = text.replace("qué", "qe");
    text = text.replace("quí", "qi");
    text = text.replace("gü", "g");

    if(text.match(/[bcdfghjklmnñpqrstvwxyz]l/g)){
        value += 1;
    }

    if(text.match(/[bcdfghjklmnñpqrstvwxyz]n/g)){
        dvalue += 1;
    }

    if(text.match(/[bcdfghjklmnñpqrstvwxyz]h/g)){
        value += 1;
    }

    if(text.match(/[bcdfghjklmnñpqrstvwxyz]s/g)){
        value += 1;
    }

    text = text.replace("h", "");

    
    var consonantsHard = text.match(/[kñwxy]/g);    
    if(consonantsHard){
        value += consonantsHard.length*3;
    }
    
    
    var consonants = text.match(/[bcdfghjlmnpqrstvz]/g)
    if(consonants){
        value +=consonants.length*2;
    }
    
    var vowels = text.match(/[aeiouáéíóú]/g)
    if(vowels){
        value +=vowels.length;
    }

    return value;   
}

Este código es una implementación de un algoritmo para dividir palabras en sílabas en español. El algoritmo se divide en dos pasos.

En el primer paso, el algoritmo usa una lista de expresiones regulares para buscar posibles divisiones silábicas en la palabra. Cada expresión regular busca un patrón particular de letras que indicaría una posible división silábica. Si se encuentra un patrón, se divide la palabra en dos partes en el punto de la división, y se agrega la primera parte a una lista de sílabas.

El segundo paso usa otra lista de expresiones regulares para ajustar las divisiones silábicas encontradas en el primer paso. Estas expresiones regulares buscan patrones específicos de letras que pueden indicar una división diferente de la que se encontró en el primer paso.

Por último, hay dos funciones adicionales. La función «stress» determina qué sílaba en una lista de sílabas es la sílaba tónica (la sílaba acentuada) de la palabra, y la función «difficulty» determina el nivel de dificultad de cada sílaba en la palabra, utilizando una serie de reglas específicas para la pronunciación de letras en español.

El resultado es correcto, ha entendido como funciona el código. Está claro que gran parte de esa información la saca del nombre de las funciones, por eso para la siguiente voy a ponérselo más difícil por dos motivos: el lenguaje elegido es ensamblador, voy a reemplazar cualquier pista que tenga.

He elegido un código en ensamblador X86 que calcula los 70 primeros números de la sucesión de Fibonacci. El código lo he obtenido de aquí, pero he reemplazado cualquier referencia a Fibonacci por la palabra «patata».

.intel_syntax noprefix

.globl patata
patata:
  sub rsp, 24
  xor eax, eax
  mov ecx, 1
  0:
    xadd rax, rcx

    mov [rsp + 16], rax
    mov [rsp + 8], rdi
    mov [rsp], rcx
    mov rsi, rax
    lea rdi, print_fmt
    xor eax, eax
    call printf
    mov rax, [rsp + 16]
    mov rdi, [rsp + 8]
    mov rcx, [rsp]

    dec rdi
    jnz 0b
  add rsp, 24
  ret

.globl main
main:
  mov edi, 70
  call patata

  xor eax, eax
  ret

.section .rodata
print_fmt: .asciz "%lu\n"

El código es un programa escrito en lenguaje ensamblador de Intel x86-64 que llama a la función «patata» desde la función «main» y realiza las siguientes acciones:

En la función «patata»:

  1. Se reserva un bloque de 24 bytes en la pila mediante la instrucción «sub rsp, 24».
  2. Se inicializan los registros «eax» y «ecx» a cero y uno, respectivamente.
  3. Se realiza un bucle etiquetado como «0» que ejecuta los siguientes pasos: a. Se realiza una operación de intercambio atómico de los registros «rax» y «rcx» mediante la instrucción «xadd rax, rcx». b. Se almacena el valor de «rax» en la posición de memoria «rsp+16», el valor de «rdi» en la posición de memoria «rsp+8» y el valor de «rcx» en la posición de memoria «rsp». c. Se carga el valor de «rax» en el registro «rsi». d. Se carga la dirección de memoria de la cadena de formato de impresión en el registro «rdi». e. Se llama a la función «printf» para imprimir el valor de «rsi» en la salida estándar. f. Se recuperan los valores de «rax», «rdi» y «rcx» de las posiciones de memoria correspondientes en la pila. g. Se decrementa el valor de «rdi» y se salta a la etiqueta «0» si el resultado no es cero mediante la instrucción «dec rdi; jnz 0b».
  4. Se libera el bloque de memoria de la pila mediante la instrucción «add rsp, 24» y se retorna a la función llamadora mediante la instrucción «ret».

En la función «main»:

  1. Se inicializa el registro «edi» con el valor 70.
  2. Se llama a la función «patata» mediante la instrucción «call patata».
  3. Se retorna a la llamada del sistema operativo mediante la instrucción «ret».

En la sección «.rodata»:

  1. Se define una cadena de formato de impresión que se utiliza en la función «printf» en la instrucción «print_fmt». La cadena especifica el formato de salida para un valor de enteros largos sin signo («%lu») seguido de un salto de línea («\n»).

Es una buena explicación del algoritmo, pero una mala explicación del programa. Es decir, que explica muy bien «como funciona», pero no «que hace». Pruebo a preguntárselo y esta es la respuesta:

El programa imprimirá en la salida estándar una secuencia de números enteros largos sin signo (de tipo «unsigned long»), comenzando por el valor 1 y disminuyendo en una unidad hasta llegar a cero. El número total de valores impresos será 70, ya que la función «main» inicializa el registro «edi» con el valor 70 antes de llamar a la función «patata». Cada número impreso será seguido por un salto de línea.

El programa utiliza una operación atómica de intercambio («xadd») para sumar el valor de «rcx» a «rax» y almacenar el resultado en «rax». Luego, utiliza la función «printf» para imprimir el valor actual de «rax» en la salida estándar. Después de imprimir el valor, se decrementa el valor de «rdi» y se repite el proceso hasta que «rdi» llegue a cero.

Dado que el valor inicial de «rcx» es 1 y se suma a «rax» en cada iteración del bucle, el valor impreso en cada iteración será la suma de los enteros de 1 a n, donde n es el número total de iteraciones. En este caso, dado que hay 70 iteraciones, el último valor impreso será la suma de los enteros de 1 a 70, que es 2485.

En resumen, deduce que el programa suma todos los húmero enteros del 1 al 70 (y correctamente da como resultado 2485). La deducción no es correcta, pero se acerca. Realmente suma cada numero con el anterior numero de forma consecutiva, no es una mala aproximación partiendo de un código en ensamblador y obteniéndola en unos pocos segundos.

El uso de ChatGPT para analizar código puede ahorrar mucho tiempo y quebraderos de cabeza, no es una herramienta perfecta, ni muchos menos….pero tampoco hay otra que se le acerque.