Hice un lenguaje de programación esotérico (esolang) Hice un lenguaje de programación esotérico (esolang)

Hice un lenguaje de programación esotérico (esolang)

PHD-INTERPRETER es un intento de esolang parodia de PHP no es Turing-complete por que solo:

  • Imprime texto por pantalla
  • Solo se pueden declarar variables de texto o números
  • Se puede poner comentarios
    Y poco más, si quieres realizar mejoras, hazlo sin problema developer!.

PHD: Proyecto Hola Developers!

Importante: Necesitas como mínimo el CLI de node

Hola Mundo

hdp!
holaDevelopers 'Hola Developers'!
!hdp

Comando para intepretar el .phd

node inteprete.js intepretado.phd 'Tu nombre'

Fichero de ejemplo de uso .phd

hdp!
@ Edad por defecto
hola edad = 18! 
holaDevelopers 'Hola Developers'!
holaDevelopers 'Tu nombre es: //edad y tienes //nombre años'!
!hdp

Esto muestra por consola:

Hola Developers
Tu nombre es: 18 y tienes Tu nombre años

Tutorial del interprete

Lectura de argumentos y preparación

const fs = require('fs');
const path = require('path');

// Leer ruta del fichero .phd y argumentos extra
const [, , phdPath, ...cliArgs] = process.argv;
  • require('fs') y require('path') importan módulos de Node:
    • fs sirve para leer archivos.
    • path ayuda a construir rutas absolutas.
  • process.argv es un array con lo que escribes en la línea de comandos:
    • posición 0: ruta a node
    • posición 1: ruta a phd.js
    • posición 2: ruta al .phd (primer argumento “real”)
    • resto: argumentos extras (por ejemplo el nombre).
  • La asignación por desestructuración [, , phdPath, ...cliArgs]:
    • ignora los dos primeros elementos,
    • guarda el tercero en phdPath,
    • y el resto en el array cliArgs.
if (!phdPath) {
    console.error('Uso: node phd.js archivo.phd [arg1 arg2 ...]');
    process.exit(1);
}
  • Si no pasas un archivo .phd, muestra un mensaje de uso y termina el programa con código de error 1.
// Variables del lenguaje PHD
const env = Object.create(null);
// Para ejemplo: primer argumento lo guardamos como variable 'nombre'
if (cliArgs[0]) {
    env['nombre'] = cliArgs[0];
}
  • env será “la memoria” del lenguaje: ahí se guardan las variables que declares con hola.
  • Object.create(null) crea un objeto sin prototipo, para que solo tenga las claves que tú guardes.
  • Si el usuario pasa algún argumento extra (por ejemplo Johnny), se mete en env.nombre.
  • Así //nombre en tu lenguaje puede usar ese valor sin declararlo en el .phd.
  • env es “la memoria” de tu lenguaje, donde guardas todas las variables que declares con hola nombre = ....
  • Object.create(null) crea un objeto vacío de verdad, sin heredar nada de Object.prototype.
  • Eso evita cosas raras como que env.toString exista por defecto; solo tendrá lo que tú metas.
  • cliArgs[0] es el primer argumento que escribes al llamar a Node, después del nombre del archivo .phd.
  • El if dice: si hay al menos un argumento, guarda ese valor en env['nombre']. Así, dentro de tu lenguaje, //nombre usará ese valor aunque no lo declares en el .phd.

Función principal que recorre el archivo

function runPhd(code) {  
const lines = code.split(/\r?\n/);

let insideProgram = false;  
let buffer = '';
  • runPhd recibe el contenido completo del archivo .phd como un string.
  • code.split(/\r?\n/) lo divide en un array de líneas (funciona tanto con \n como con \r\n).
  • insideProgram indica si ya estamos dentro del bloque entre hdp! y !hdp.
  • buffer sirve para ir acumulando texto hasta encontrar un ! (fin de instrucción).
  • Se recorre cada línea del archivo.
  • trim() elimina espacios al principio y al final.
if (!insideProgram) {  
    if (line.toLowerCase() === 'hdp!') {  
        insideProgram = true;  
    }  
    continue;  
}
  • Mientras no estemos dentro del programa:
    • si la línea es hdp! (ignorando mayúsculas/minúsculas), activamos insideProgram.
    • se hace continue para saltar a la siguiente línea y no procesar nada más.
if (line.toLowerCase() === '!hdp') {  
    insideProgram = false;  
    break;  
}
  • Si ya estamos dentro y encontramos !hdp, salimos del programa:
    • ponemos insideProgram a false
    • break para dejar de leer más líneas (ignoramos lo que venga después).
// Acumular hasta encontrar "!"  
buffer += (buffer ? ' ' : '') + line;  
while (buffer.includes('!')) {  
    const idx = buffer.indexOf('!');  
    const stmt = buffer.slice(0, idx).trim();  
    buffer = buffer.slice(idx + 1).trim();  
    
    if (stmt) executeStatement(stmt);  
}
  • Se añade la línea actual al buffer. Si el buffer no está vacío, se mete un espacio antes para separar.
  • while (buffer.includes('!')):
    • puede haber más de una instrucción en una misma línea, o una instrucción que continúa desde la línea anterior.
  • idx es la posición del primer !.
  • stmt es el texto desde el principio del buffer hasta el ! (sin incluirlo).
  • Luego se recorta el buffer para quitar esa parte + el ! y se hace trim().
  • Si stmt no está vacío, se llama a executeStatement(stmt) para ejecutar esa instrucción PHD.
  • buffer se vuelve: "hola nombre = 'Johnny'! holaDevelopers 'Hola'!".
  • Primera vuelta del while:
    • idx apunta al primer ! (después de 'Johnny').
    • stmt = "hola nombre = ``'Johnny'".
    • Se ejecuta executeStatement("hola nombre = 'Johnny'").
  • Luego se recorta buffer para que quede: "holaDevelopers 'Hola'!", y el while vuelve a hacer lo mismo para la siguiente instrucción.

Decidir qué tipo de instrucción es

function executeStatement(stmt) {  
// Declaración de variable: hola nombre = 'Johnny'  
    if (stmt.toLowerCase().startsWith('hola ')) {  
        handleVarDeclaration(stmt);  
    } else if (stmt.toLowerCase().startsWith('holadevelopers ')) {  
        handlePrint(stmt);  
    } else {  
        // Instrucción desconocida: ignorar  
    }  
}
  • executeStatement recibe una instrucción ya sin el !.
  • Si empieza por hola (con espacio) → se considera una declaración de variable y se llama a handleVarDeclaration.
  • Si empieza por holadevelopers → se considera un console.log especial y se llama a handlePrint.

❗ Cualquier otro comando se ignora silenciosamente (no lanza error, simplemente no hace nada).

Declaración de variables (hola)

function handleVarDeclaration(stmt) {  
    // quitar "hola "  
    const rest = stmt.slice(5).trim();  
    // formato esperado: nombre = valor  
    const match = /^([a-zA-Z_]\w\*)\s\*=\s\*(.+)$/.exec(rest);  
    if (!match) return;
  • stmt.slice(5) quita los primeros 5 caracteres (hola ).
  • rest debería quedar algo como: nombre = 'Johnny'.
  • El regex:
    • ^([a-zA-Z_]\w*) captura el nombre de la variable: empieza por letra o _, seguido de letras, números o _.
    • \s\*=\s* permite espacios alrededor del =.
    • (.+)$ captura el valor tal cual hasta el final de la línea.
  • Si no coincide con este patrón, se sale de la función (return) y no se hace nada.
  • name es el nombre de la variable (nombre en el ejemplo).
  • valueRaw es el texto del valor ('Johnny', 25, otraVariable, etc.), sin espacios alrededor.
  • Si el valor empieza y acaba con ' o ", se trata como string literal.
  • Se quitan las comillas (slice(1, -1)) y se guarda el texto dentro en env[name].
  • Si no tiene comillas, se intenta convertir a número.
  • Number(valueRaw) intenta parsear el string a número.
  • !Number.isNaN(...) comprueba que realmente es un número y no algo inválido.
  • Si es un número válido, se guarda como número en env[name].
  • Si no es string ni número, se asume que es el nombre de otra variable.
  • Se copia env[otraVariable] a env[name].
  • Si esa variable no existe, se guarda null.

Ejemplos:

hola edad = 30! → env.edad = 30.  
hola saludo = 'Hola'! → env.saludo = "Hola".  
hola x = y! → env.x = env.y (o null si y no existe).  

Imprimir con holadevelopers

function handlePrint(stmt) {  
    // quitar "holadevelopers "  
    const rest = stmt.slice('holadevelopers'.length).trim();

    let text = rest;
  • Se quita la palabra clave holadevelopers del principio de la instrucción.
  • rest es lo que viene después, normalmente un texto con comillas.
  • Si lo que queda empieza y termina con ' o ", se consideran comillas externas.
  • Se eliminan para quedarnos solo con el contenido.
// Reemplazar //var por su valor y si no pues imprime text
const result = text.replace(/\/\/([a-zA-Z_]\w*)/g, (\_, varName) => {  
const val = env[varName];  
return val !== undefined && val !== null ? String(val) : '';  
});

console.log(result);
  • El regex /\/\/([a-zA-Z_]\w*)/g busca patrones como //nombre, //edad, etc.:
    • // literal.
    • ([a-zA-Z\_]\\w\*) nombre de variable válido.
  • Cada vez que encuentra uno, ejecuta la función callback:
    • _ es el texto completo que coincidió (no lo usamos).
    • varName es el nombre de la variable capturado (por ejemplo nombre).
  • Busca env[varName]:
    • Si existe y no es null, lo convierte a texto con String(val) y lo inserta.
    • Si no existe o es null, inserta una cadena vacía.
  • Al final hace console.log(result); para mostrar en la consola el texto con las variables ya sustituidas.
  • En (_, varName) => { ... }:
    • _ es simplemente un nombre de variable. Equivale a escribir (matchCompleto, varName) => { ... }.

Leer el archivo .phd y arrancar

// Leer archivo .phd y ejecutar  
const fullPath = path.resolve(phdPath);  
fs.readFile(fullPath, 'utf8', (err, data) => {  
    if (err) {  
        console.error('Error al leer el archivo:', err.message);  
        process.exit(1);  
    }  
    runPhd(data);  
});
  • path.resolve(phdPath) convierte la ruta que le pasas en una ruta absoluta.
  • fs.readFile lee el archivo:
    • primer argumento: ruta absoluta.
    • segundo: codificación ('utf8' para obtener un string).
    • tercero: callback que recibe un error (err) y los datos (data).
  • Si hay error al leer (archivo no existe, permisos, etc.), se muestra el mensaje y se termina el programa.
  • Si todo va bien, se llama a runPhd(data), que procesa el contenido línea a línea y ejecuta tus comandos esotéricos.

Vídeo Tutorial

Para la investigación y para la documentación se ha usado IA generativa. Revisado y corregido por un humano.

← Volver a los proyectos