Translate

Mostrando las entradas con la etiqueta MateScript. Mostrar todas las entradas
Mostrando las entradas con la etiqueta MateScript. Mostrar todas las entradas

lunes, 6 de julio de 2026

Diseñando MateScript: Transforms, extendiendo el lenguaje sin modificar el compilador


A medida que avanzaba con el diseño de MateScript apareció una pregunta interesante.

¿Cómo agregamos azúcar sintáctico al lenguaje sin llenar el compilador de casos especiales?

Muchos lenguajes modernos terminan incorporando nuevas palabras reservadas, operadores o construcciones que solo existen porque el compilador las reconoce de forma explícita.


Quería explorar un enfoque diferente.

¿Y si gran parte del azúcar sintáctico pudiera implementarse fuera del compilador?

Así nacieron los Transforms.


¿Qué es un Transform?

Un Transform es una regla declarativa que toma una construcción sintáctica y la reemplaza por otra antes de que el compilador comience el análisis semántico.


La idea es extremadamente simple.


Cuando encuentres esto...

        ↓

Reemplázalo por esto.



No ejecuta código.

No modifica el compilador.

No realiza reemplazos de texto.

Simplemente transforma una representación sintáctica en otra.

En artículos anteriores decidimos que el desarrollador podrá escribir:


let name: String?;


Internamente eso es equivalente a:


let name: Optional<String>;


En lugar de implementar esta característica directamente dentro del compilador, podemos definir un Transform.


transform Nullable {

    T?

        =>

    Optional<T>

}


A partir de ese momento, cualquier aparición de:


String?


será transformada automáticamente en:


Optional<String>


El compilador nunca necesita conocer el operador ?.



Supongamos que MateScript decide no incorporar arrays como estructura del lenguaje.

Sin embargo, muchos desarrolladores están acostumbrados a escribir:


Int[10]


Podemos definir otro Transform.


transform Arrays {

    T[N]

        =>

    ArrayList<T>(N)

}


El resultado será equivalente a:

ArrayList<Int>(10)



Nuevamente, el compilador no necesita incorporar una característica nueva.

Simplemente aplica una transformación.


Una decisión importante es mantener estas reglas separadas del código fuente.

Los archivos de MateScript utilizarán la extensión: .mt

Mientras que los Transforms vivirán en archivos independientes: .tmt


Por ejemplo:

src/

    main/

        matescript/

            Person.mt

            Main.mt

        transforms/

            nullable.tmt

            arrays.tmt

            collections.tmt


De esta manera, el código de la aplicación permanece limpio y las transformaciones pueden evolucionar de forma independiente.

No todos los Transforms tendrán que ser escritos por el desarrollador.

El propio SDK de MateScript incluirá un conjunto de transformaciones oficiales.


Por ejemplo:

  • Nullable (T? -> Optional<T>)
  • Colecciones
  • Rangos
  • Literales
  • Otras construcciones de alto nivel


El desarrollador simplemente las utilizará sin preocuparse por su implementación.


Lo realmente interesante aparece cuando cualquier biblioteca puede distribuir sus propios Transforms.

Imaginemos una librería para programación reactiva.

Podría incluir nuevas construcciones sintácticas sin modificar el compilador.

Lo mismo podría ocurrir con librerías para SQL, testing, concurrencia o cualquier otro dominio.

En lugar de esperar una nueva versión del lenguaje, la comunidad podría extenderlo mediante nuevos Transforms.


¿Son macros? No exactamente.

Cuando escuchamos la palabra macro, solemos pensar en sistemas capaces de ejecutar código durante la compilación o incluso modificar el propio compilador.

Ese no es el objetivo de MateScript.

Los transforms son mucho más simples.

Son reglas declarativas de transformación sintáctica.

No realizan reemplazos de texto.

No tienen acceso al sistema de archivos.

No ejecutan código arbitrario.

No cambian el comportamiento del compilador.

Su única responsabilidad es transformar una construcción sintáctica en otra equivalente.


Gracias a este mecanismo, el compilador incorpora una nueva etapa.


Código fuente (.mt)

        │

        ▼

      Lexer

        │

        ▼

      Parser

        │

        ▼

 AST de sintaxis

        │

        ▼

 Carga de Transforms

 (SDK + proyecto)

        │

        ▼

 Motor de Transforms

        │

        ▼

 AST transformado

        │

        ▼

 Análisis semántico

        │

        ▼

 Generación de bytecode


El analizador semántico siempre trabaja sobre un único modelo del lenguaje.

No necesita conocer las distintas formas sintácticas que utilizaron los desarrolladores.


Quizás el aspecto más interesante de esta idea sea filosófico.

En lugar de incorporar cientos de características al compilador, MateScript puede mantener un núcleo pequeño y estable.

Las construcciones de alto nivel pueden implementarse mediante Transforms reutilizables.

Esto hace que el lenguaje sea más fácil de mantener, más sencillo de aprender y mucho más flexible para evolucionar con el tiempo.


Los Transforms representan una de las ideas más ambiciosas de MateScript.

Permiten agregar azúcar sintáctico sin modificar el compilador, mantienen el lenguaje pequeño y ofrecen una forma sencilla para que tanto la SDK como la comunidad puedan extender el lenguaje.

En cierto modo, los Transforms convierten al compilador en una plataforma sobre la cual pueden construirse nuevas formas de escribir código, sin perder coherencia ni simplicidad.


Diseñando MateScript: Diseñando Object, Null y Optional


Cuando pensamos en un lenguaje de programación, solemos imaginar su sintaxis, su compilador o su sistema de tipos.

Sin embargo, existe otro componente igual de importante: la biblioteca estándar.


De poco sirve tener una sintaxis elegante si los tipos fundamentales del lenguaje no forman un modelo coherente.

En este artículo comenzaremos a diseñar tres de los tipos más importantes de MateScript:

  • Object
  • Null
  • Optional


Los tres estarán profundamente relacionados.

En el artículo anterior llegamos a una conclusión interesante.


Queríamos que el desarrollador pudiera escribir:

let name: String?;


Pero no queríamos que el operador ? fuera una característica mágica del compilador.

Por eso decidimos definirlo como un alias de tipos.

T?

es equivalente a:

Optional<T>


Y Optional<T> se implementa utilizando un Union Type.


class Optional<T> {

    value: T | Null;

}


Ahora la pregunta cambia.


¿Cómo deben comportarse Optional y Null?


Como MateScript se ejecutará sobre la JVM, tiene sentido adoptar un modelo de objetos similar al de Java.


Todos los objetos heredarán de una clase base.


class Object {


    equals(other: Object): Boolean


    hashCode(): Int


    toString(): String


}


Esto significa que cualquier objeto del lenguaje compartirá el mismo comportamiento básico.


Por ejemplo:

person.toString()

list.hashCode()

optional.equals(other)


No existen excepciones.

Todo es un objeto.


MateScript intenta reducir la cantidad de código repetitivo.

Por eso no será necesario escribir modificadores como:


public


private


protected


La regla será mucho más simple.

Los atributos serán privados por defecto.

Los métodos serán públicos por defecto.


Por ejemplo:


class Person {


    name: String

    age: Int


    greet() {

        print("Hello!")

    }

}


Es una sintaxis muy cercana a TypeScript, pero manteniendo un modelo de objetos inspirado en Java.


En muchos lenguajes, null es un valor especial incorporado por el compilador.

En MateScript queremos seguir un enfoque diferente.


Null será un objeto singleton.


object Null {


    toString(): String


    equals(other: Object): Boolean


    hashCode(): Int


}


Existirá una única instancia.

Como cualquier otro objeto del lenguaje.

No será un valor mágico.

No requerirá reglas especiales.


Cuando empezamos a diseñar Optional, apareció una idea interesante.

Supongamos el siguiente código.


let name: String? = ...


let upper =

    name.map(x => x.toUpperCase());


¿Qué ocurre si name no contiene ningún valor?

Una posibilidad sería lanzar una excepción.

Otra sería devolver un nuevo Optional.

Pero existe una alternativa mucho más elegante.

Que el propio objeto Null implemente el mismo comportamiento.


Por ejemplo:

Null.map(...)


simplemente devolvería:

Null


Lo mismo ocurre con:

Null.flatMap(...)

Null.filter(...)


Todas estas operaciones simplemente devolverían nuevamente Null.


Esto permite que el código continúe fluyendo de forma natural sin necesidad de realizar comprobaciones constantes.


¿Entonces map pertenece a Object?

Podría parecer una buena idea.

Después de todo, todos los objetos podrían responder al método map().

Sin embargo, rápidamente aparecen situaciones extrañas.

5.map(...)

person.map(...)

car.map(...)


¿Qué significa transformar un número mediante map()?

¿O una persona?


La realidad es que map() no representa una operación común a todos los objetos.

Representa una operación sobre estructuras que contienen un valor.


En lugar de agregar map() a Object, resulta más razonable definir una interfaz específica.

Conceptualmente podría verse así:


interface Functor<T> {


    map<R>(

        mapper: (T) -> R

    ): Functor<R>;


}


Esta idea proviene de la programación funcional y aparece en lenguajes como Haskell, Scala y muchos otros.


No es necesario conocer teoría de categorías para utilizarla.

La idea es muy sencilla.


Un Functor es cualquier estructura capaz de transformar el valor que contiene sin perder su estructura.


Ahora Optional puede implementar esa interfaz.


class Optional<T>

    implements Functor<T> {


    value: T | Null;


}


Cuando existe un valor, map() aplica la función recibida.

Cuando el valor es Null, simplemente devuelve Null.

Desde el punto de vista del desarrollador, el código siempre se escribe de la misma manera.


name

    .map(...)

    .map(...)

    .map(...)


Sin preguntar constantemente si existe un valor.


Lo interesante es que nunca planeé incorporar conceptos de programación funcional.

Simplemente estaba buscando una forma elegante de implementar Optional.

Sin embargo, al avanzar en el diseño comenzaron a aparecer naturalmente ideas como:

  • objetos singleton;
  • Union Types;
  • Functors.


Y eso es una de las cosas más fascinantes de diseñar un lenguaje.

Las buenas abstracciones suelen aparecer como consecuencia de resolver problemas reales, no como un objetivo en sí mismo.


En este punto, MateScript comienza a tener una identidad propia.

  • Todos los objetos heredan de Object.
  • Null deja de ser un valor mágico para convertirse en un objeto singleton.
  • Optional<T> representa explícitamente la posibilidad de ausencia mediante T | Null.
  • Las operaciones de transformación se modelan mediante interfaces específicas, manteniendo la clase Object pequeña y coherente.



domingo, 5 de julio de 2026

Diseñando MateScript: ¿Cómo representar la ausencia de un valor?


Uno de los aspectos más interesantes de diseñar un lenguaje de programación es descubrir que muchas de las características que damos por sentadas están profundamente relacionadas entre sí.

Un buen ejemplo es la representación de los valores nulos.

A simple vista parece una decisión menor. Sin embargo, termina afectando el sistema de tipos, el compilador, la biblioteca estándar e incluso el runtime del lenguaje.

En lugar de intentar inventar una solución completamente nueva, MateScript adopta una filosofía diferente: tomar buenas ideas de otros lenguajes y combinarlas de una forma consistente.


En este artículo aparecen varias influencias claras:

  • El operador ? de Kotlin y C#.
  • Los Union Types de TypeScript.
  • Los objetos singleton de Scala.


Lo interesante es que ninguna de estas ideas fue incorporada de forma aislada. Todas terminan formando parte de un único diseño.

Desde el punto de vista del desarrollador, me gusta mucho la sintaxis introducida por Kotlin y posteriormente adoptada por C#.


let name: String?;


La intención es evidente.

La variable puede no contener un valor.

Sin embargo, no me gusta que el operador ? sea una característica mágica del compilador.


Siempre que sea posible prefiero que las características del lenguaje puedan explicarse utilizando otros conceptos ya existentes. 

En lugar de convertir ? en una regla especial del compilador, podemos definirlo como un alias de tipos.


Es decir:

T?

es simplemente equivalente a:

Optional<T>


Por ejemplo:

let name: String?;


sería exactamente lo mismo que escribir:

let name: Optional<String>;


Una vez realizado este reemplazo, el compilador deja de conocer la existencia del operador ?.

Para el resto del proceso de compilación solamente existe Optional<T>.


Ahora aparece una pregunta mucho más interesante. ¿Cómo implementamos Optional<T>?


Podríamos imaginar algo como esto:


class Optional<T> {

    private value: T;

}


Pero inmediatamente encontramos un problema. ¿Cómo representamos un value vacío?

En Java utilizaríamos null. Pero MateScript no tendrá un valor mágico llamado null.

Entonces necesitamos otra solución.


La idea tomada de Scala es tratar a Null como un objeto singleton del lenguaje.


Conceptualmente:

object Null


Esto significa que existe una única instancia de Null, exactamente igual que cualquier otro singleton.

No es una palabra reservada.

No es un valor especial incorporado por el compilador.

Es simplemente un objeto del lenguaje.


Hasta aquí parece que el problema está resuelto.

Pero todavía queda una pregunta.

Si value puede contener un objeto de tipo T o el objeto Null, entonces su tipo ya no puede ser simplemente T.

Necesitamos representar que un valor puede pertenecer a más de un tipo.


Es decir: T | Null


Y casi sin buscarlo llegamos a otro concepto conocido. Los Union Types.


La idea proviene de TypeScript.

Un Union Type representa un valor que puede pertenecer a uno de varios tipos.


Por ejemplo:

String | Null


indica que un valor puede ser:

un objeto String, o

el objeto Null.


Gracias a esto podemos redefinir Optional.

class Optional<T> {

    private value: T | Null;

}


Ahora sí el modelo es completamente consistente.


Un Optional<String> puede contener un String o el objeto Null.

No existe ningún valor mágico dentro del lenguaje.

Todo está expresado mediante el sistema de tipos.


Lo interesante es que los Union Types no aparecieron porque quisiera agregarlos al lenguaje.

Surgieron como una consecuencia natural del diseño de Optional.


Una vez que el compilador fue capaz de representar tipos como: T | Null apareció una pregunta inevitable.

¿Por qué limitar esa capacidad únicamente a Optional?


Si el compilador ya sabe trabajar con Union Types, también puede permitir que los desarrolladores los utilicen directamente.


Por ejemplo:

let value: String | Int;

let result: Success | Error;

let animal: Dog | Cat;


Lo que comenzó siendo una solución para representar la ausencia de un valor terminó convirtiéndose en una nueva característica del lenguaje.


Curiosamente, cada una de estas decisiones resuelve un problema distinto, pero juntas forman un diseño mucho más consistente.


El desarrollador escribe:

String?


El parser lo transforma en:

Optional<String>


Y Optional se implementa como:

Optional<T>

    └── value : T | Null


Todo utilizando conceptos generales del lenguaje.


No existen excepciones.

No existen reglas especiales.

No existen valores mágicos.


Diseñar un lenguaje muchas veces consiste en descubrir que una buena solución termina resolviendo varios problemas al mismo tiempo.


En MateScript decidimos combinar ideas provenientes de distintos lenguajes:

  • De Kotlin y C# tomamos la sintaxis ? para representar tipos opcionales.
  • De TypeScript adoptamos los Union Types para representar valores que pueden pertenecer a más de un tipo.
  • De Scala incorporamos la idea de los objetos singleton para representar Null como un objeto del lenguaje y no como un valor mágico del compilador.


Lo interesante es que ninguna de estas características fue agregada por separado.

Cada decisión fue llevando naturalmente a la siguiente, hasta formar un sistema de tipos más uniforme y coherente.



viernes, 3 de julio de 2026

Diseñando MateScript: un JavaScript de tipado estático para la JVM


Hace tiempo que me interesa el diseño de lenguajes de programación.

No solamente cómo construir compiladores, sino también cómo tomar decisiones sobre la sintaxis, el sistema de tipos y la experiencia de desarrollo.

Por eso decidí comenzar un proyecto experimental: MateScript.


¿Qué es MateScript?

MateScript es un lenguaje inspirado en JavaScript, pero con tipado estático y compilación a bytecode JVM.

JavaScript es uno de los lenguajes más populares del mundo, pero también arrastra varios problemas derivados de sus decisiones históricas.


Por ejemplo:

let age = 42;

age = "hola";


Esto es perfectamente válido.

MateScript buscará detectar estos problemas durante la compilación.


let age = 42;

age = "hola";


Resultado:

Type mismatch: expected Int found String


MateScript intentará mantener las características más atractivas de JavaScript:

  • Sintaxis sencilla
  • Funciones de primera clase
  • Lambdas
  • Inferencia de tipos
  • Programación funcional
  • Objetos


Pero agregando:

  • Tipado estático
  • Null Safety
  • Pattern Matching
  • Compilación a bytecode JVM
  • Interoperabilidad con Java


¿Por qué no usar TypeScript?

TypeScript es una excelente herramienta.

Sin embargo, sigue dependiendo del ecosistema JavaScript.


El objetivo de MateScript es diferente:

  • Compilar directamente a bytecode JVM
  • Ejecutarse sin Node.js
  • Integrarse con bibliotecas Java
  • Aprovechar el rendimiento y madurez de la plataforma JVM


Primeras decisiones de diseño

Variables inmutables por defecto

let name = "Emanuel";


Una variable declarada con let no podrá modificarse.


Para variables mutables utilizaremos:

var counter = 0;


Inferencia de tipos

No será necesario declarar tipos explícitamente.


let age = 42;


El compilador inferirá:

Int


Funciones

La sintaxis será muy similar a JavaScript.


function greet(name: String): String {

    return "Hola " + name;

}


Lambdas

let square = x => x * x;


Null Safety

Los tipos no aceptarán valores nulos por defecto.

let name: String = null;

Error de compilación.


Para permitir nulos:

let name: String?;


Y acá podemos hacer algo bastante elegante, podemos hacer que los tipos tengan alias por lo tanto : 

String? va a ser -> Optional<String> 


Ahora bien, como hago Optinal<T> , acá podemos utilizar el camino que usa Typescript union de tipos : T|Null y podemos hacer que Null sea un object como Scala. Y no vamos a tener nulos en nuestro lenguaje. 


¿Por qué la JVM?

La JVM es una de las plataformas más maduras de la industria.

  • Proporciona:
  • Garbage Collector
  • JIT Compiler
  • Ecosistema enorme
  • Bibliotecas maduras
  • Herramientas de monitoreo


En lugar de construir una máquina virtual propia, MateScript aprovechará toda esa infraestructura.


¿Qué sigue?

En los próximos artículos comenzaremos la implementación de MateScript.

El primer paso será construir una gramática utilizando ANTLR capaz de reconocer programas simples como:


let name = "Emanuel";

print(name);


A partir de allí iremos avanzando gradualmente hacia un compilador completo capaz de generar bytecode JVM.


La meta no es crear el próximo Kotlin. La meta es aprender cómo se construyen realmente los lenguajes de programación.