En el contexto de la materia Lenguajes y Compiladores I (FIUBA), la cátedra provee una implementación funcional de un intérprete de Lox escrita en Python, llamada Plox. A lo largo de la cursada fuimos estudiando que en su estado final este intérprete entrega un lenguaje Turing-completo: en principio, capaz de expresar cualquier cómputo que una máquina pueda realizar.
Sin embargo, la completitud de Turing no garantiza utilidad práctica por sí sola. Para que un lenguaje pueda interactuar con el mundo real, leer archivos, abrir sockets de red, enviar y recibir datos, necesita algún mecanismo para cruzar la frontera entre el programa de usuario y el sistema operativo. Sin esa interfaz, Lox puede calcular el número π con mil decimales, pero no puede servir una página web ni escribir un archivo en disco.
Esa observación fue el punto de partida de nuestro trabajo práctico: extender Plox para que código Lox pueda hacer llamados al sistema operativo, dotándolo de la interfaz mínima indispensable para interactuar con el hardware a través del kernel, sin delegar silenciosamente todo ese trabajo a Python.
Nuestra propuesta se articula en dos capas:
- Builtins de bajo nivel en Python que exponen syscalls del SO, gestión de memoria dinámica y operaciones sobre buffers, registrados automáticamente en el entorno global del intérprete.
- Una librería estándar escrita enteramente en Lox (
stdlib/lox/) que envuelve esas syscalls con funciones de más alto nivel, al estilo de lo que hacelibcen C.
El objetivo final fue demostrar la viabilidad del enfoque implementando un servidor HTTP básico completamente escrito en Lox (examples/http-server.lox), capaz de responder peticiones GET con Hello, World!.
La implementación de la cátedra ya incluye lo esencial del lenguaje: aritmética, strings, funciones, clases, closures y resolución estática de scopes. Lo que falta, y que este TP incorpora, es todo lo que permite que Lox deje de ser un lenguaje aislado y pueda interactuar con el sistema operativo.
Las extensiones que agregamos son:
- Sentencia
import: permite ejecutar el contenido de otro archivo Lox en el entorno actual, habilitando el uso de la librería estándar. - Memoria dinámica (
malloc,free,buffer_write,buffer_read,buffer_write_int,buffer_read_int): builtins para alocar buffers reales en el heap del proceso y leer/escribir en ellos strings y enteros en little-endian. - Syscalls al SO (
syscall): builtin de aridad variable que permite invocar cualquier syscall de Linux x86-64 directamente desde código Lox. - Filesystem I/O (
open,close,read,write,unlink): funciones escritas en Lox sobresyscall, al estilo POSIX. Forman parte destdlib/lox/io.lox. - Sockets TCP (
socket,bind,listen,accept,connect,setsockopt,htons): funciones escritas en Lox que construyen las estructuras que el kernel espera y las pasan a las syscalls correspondientes. Forman parte destdlib/lox/socket.lox. - Servidor HTTP en Lox: usando todo lo anterior,
examples/http-server.loximplementa un servidor HTTP que respondeHello, World!a peticiones GET.
Una alternativa más sencilla habría sido exponer funciones Python de alto nivel directamente como builtins de Lox. Descartamos ese enfoque porque el objetivo era que Lox haga el trabajo, no que Python lo haga por debajo.
Al exponer syscalls crudas, Lox tiene que construir las estructuras que el kernel espera (como sockaddr_in) byte a byte, convertir el puerto a big-endian con htons, y manejar los file descriptors devueltos por el sistema. Es exactamente lo que haría un programa en C.
Se extendió el parser y el intérprete para soportar la sentencia:
import "ruta/al/archivo.lox";
Al ejecutarse, el intérprete lee el archivo indicado, lo escanea, parsea, resuelve y lo ejecuta en el entorno global actual. Esto hace posible el uso de la librería estándar desde cualquier programa Lox.
Implementación: plox/Interpreter.py
Se implementó un MemoryManager que gestiona buffers de C (ctypes.Array) que residen en memoria real del proceso. Sus direcciones efectivas se exponen a Lox como valores opacos de tipo ctypes.c_void_p.
Los builtins disponibles desde Lox son:
| Builtin | Descripción |
|---|---|
malloc(size) |
Aloca size bytes en el heap; retorna un puntero |
free(ptr) |
Libera el buffer asociado al puntero |
buffer_write(ptr, str) |
Escribe un string UTF-8 en el buffer |
buffer_read(ptr, n) |
Lee hasta n bytes del buffer como string |
buffer_write_int(ptr, offset, value, size) |
Escribe un entero en little-endian en el buffer |
buffer_read_int(ptr, offset, size) |
Lee un entero en little-endian del buffer |
Los punteros no soportan aritmética: sumarles o restarles un número produce comportamiento indefinido. Para acceder a distintas posiciones de un buffer se usa el parámetro offset de las funciones de lectura/escritura de enteros. Este recorte fue intencional.
Implementación: plox/MemoryManager.py, plox/builtins.py
Se registró el builtin syscall con aridad variable, que permite invocar cualquier syscall de Linux x86-64 directamente desde Lox:
// syscall(número_de_syscall, arg1, arg2, ...)
var fd = syscall(41, 2, 1, 0); // SYS_socket, AF_INET, SOCK_STREAM, 0
Internamente usa ctypes.CDLL(None).syscall(...). Los punteros se pasan como ctypes.c_void_p para evitar truncamiento en 64 bits; los demás argumentos se convierten a enteros. Solo está soportado en Linux x86-64.
Implementación: plox/builtins.py en la clase SyscallBuiltin.
Define constantes y funciones para manejar sockets TCP estilo POSIX, escrita completamente en Lox.
Las constantes cubren los números de syscall (SYS_socket, SYS_bind, etc.) y los valores usuales de configuración (AF_INET, SOCK_STREAM, SO_REUSEADDR, entre otros).
Las funciones disponibles son socket, setsockopt, htons, bind, listen, accept y connect. La función bind es representativa del enfoque: construye el struct sockaddr_in en el heap campo a campo, llama a la syscall con el puntero al struct, y libera la memoria antes de retornar.
fun bind(fd, port) {
var port_be = htons(port);
var sockaddr = malloc(16);
buffer_write_int(sockaddr, 0, AF_INET, 2);
buffer_write_int(sockaddr, 2, port_be, 2);
buffer_write_int(sockaddr, 4, INADDR_ANY, 4);
var result = syscall(SYS_bind, fd, sockaddr, 16);
free(sockaddr);
return result;
}
Implementación: stdlib/lox/socket.lox
Define constantes de flags (O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC) y modos de archivo (MODE_0644, MODE_0666), y expone las funciones open, close, read, write y unlink. Cada una aloca un buffer temporal en el heap para pasar strings al kernel, llama a la syscall correspondiente, y libera el buffer.
Implementación: stdlib/lox/io.lox
Usando las dos librerías anteriores, el servidor escucha en el puerto 8080 y responde Hello, World! a cualquier conexión entrante:
import "stdlib/lox/socket.lox";
import "stdlib/lox/io.lox";
var server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, 8080);
listen(server_fd, 5);
while (true) {
var client_fd = accept(server_fd);
var response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!";
write(client_fd, response);
close(client_fd);
}
El servidor requiere Linux x86-64 por sus syscalls de red. Para ejecutarlo en cualquier plataforma se puede usar el Dockerfile incluido:
docker build -t plox . && docker run --rm -p 8080:8080 ploxPara correr otros ejemplos o el REPL interactivo con Docker:
docker run --rm plox plox examples/hello.lox # un script cualquiera
docker run --rm -it plox plox # REPL interactivoSin Docker, en Linux x86-64 directamente:
# Instalar dependencias (requiere uv)
uv tool install --editable .
plox examples/http-server.loxPara cada una de las funcionalidades implementadas hay un ejemplo en la carpeta examples/.
-
Memoria dinámica:
examples/dynamic_memory.loxmuestra el uso directo de los builtins de memoria. -
Filesystem I/O:
examples/file_io.loxmuestra el uso de las funciones de manejo de archivos. -
Servidor de sockets:
examples/sockets.loximplementa un servidor TCP que verifica el valor de retorno de cada operación antes de continuar:
Para evaluar el costo de nuestra implementación comparamos el servidor HTTP de Lox contra implementaciones idiomáticas en C, Go, Python y Node.js. Las métricas relevantes fueron: transacciones por segundo (TPS), latencia promedio, throughput y consumo de memoria RSS.
Go es el lenguaje con mejor rendimiento en TPS y throughput, superando inclusive a C, lo cual se explica por su modelo de concurrencia con goroutines y un runtime optimizado para I/O de red. C actúa como baseline con bajo consumo de memoria. Node.js y Python quedan en un rango intermedio. Lox muestra el desempeño más bajo en todas las métricas de tiempo, lo cual era esperable dada la acumulación de capas de interpretación, el overhead de Python y el uso de ctypes para cada syscall.
Normalizando respecto a C (100%): Go alcanza aproximadamente 3.5×, Node.js y Python se ubican en torno al 75.9% y 61.8% respectivamente, y Lox llega al 6.7%.
El radar permite ver simultáneamente todas las métricas. Un dato algo sorprendente es que el consumo de memoria de Lox es comparable al de Python y Go, y menor al de Node.js. Tiene sentido: el perfil de Lox es muy similar al de Python con más deficiencias, ya que su implementación subyacente es Python más el overhead de ctypes. El bajo rendimiento en tiempo de ejecución es la consecuencia directa de ese stack.
- Solo Linux x86-64. Los números de syscall y el layout de
sockaddr_inson específicos de esa ABI. En macOS o Windows el intérprete lanza un error. El Dockerfile permite ejecutar el código en cualquier plataforma. - Sin aritmética de punteros. Los punteros devueltos por
mallocson valores opacos. Para acceder a distintas posiciones de un buffer hay que usar el parámetrooffsetdebuffer_write_int/buffer_read_int. - Sin manejo de errores avanzado en sockets. El único mecanismo disponible desde Lox es verificar si el retorno de una syscall es negativo. No hay equivalente a
errnoni astrerror. - Flags de
opencon OR manual. Lox no tiene operadores bitwise. Para combinar flags hay que calcular el OR previamente y pasar el valor decimal resultante (e.g.,65paraO_WRONLY | O_CREAT).
Este trabajo fue desarrollado como trabajo práctico de la materia Lenguajes y Compiladores I de la Facultad de Ingeniería de la Universidad de Buenos Aires (FIUBA), cuatrimestre 2026-1.
- Implementación original de Plox: provista por la cátedra de Lenguajes y Compiladores I, FIUBA, siguiendo el libro Crafting Interpreters de Robert Nystrom.
- Linux man-pages:
man 2 socket,man 2 bind,man 2 listen,man 2 accept,man 2 connect,man 2 read,man 2 write,man 2 openat,man 2 close,man 2 unlink,man 2 setsockopt. Fuente de números de syscall, layouts de structs y convenciones de retorno. - eldipa Hands-on Sockets in C++: https://github.com/eldipa/hands-on-sockets-in-cpp. Referencia para el manejo de bajo nivel de sockets en C, que guió la implementación de
socket.lox. - Python
ctypes: https://docs.python.org/3/library/ctypes.html. Base para elMemoryManagery para las llamadas a syscalls mediantectypes.CDLL(None).syscall.


