Documentación generada a partir del compilador real altairc.
Basada en el funcionamiento interno real del compilador (
altairc, escrito en C: lexer → parser → sema → codegen → C → gcc → binario nativo) y del runtime (altair_rt.c). No incluyeasyncnisafe block, que todavía no están implementadas. El bloquedatasí está implementado (con una sintaxis distinta a la propuesta original) — ver §37.
altaircchar, file, p#Tipo y operadores a nivel de bitdata — contenedores de variables agrupadasAltair es un lenguaje compilado estáticamente y orientado a expresiones que
se traduce a C mediante altairc. Cada programa termina siendo un único
binario nativo, sin dependencias de runtime externas.
Pipeline del compilador:
código .at → lexer → parser → sema → codegen → código .c → gcc → binario
Propiedades clave:
- Cada variable tiene un nivel de almacenamiento declarado (ram/disk/cache/temp).
- Las variables pueden migrar entre niveles de almacenamiento en tiempo de
ejecución mediante orbit.
- Texto, listas, objetos y tokens son valores de primera clase.
- Toda la memoria la gestiona el runtime (no hay que hacer malloc/free
manual, salvo con los punteros crudos nuevos, ver §33).
- Los servidores HTTP se declaran con listen; las rutas con route.
Todo programa Altair tiene una cabecera opcional y un cuerpo. La cabecera da metadatos del programa; el cuerpo contiene todas las sentencias.
altair.doc;
name = MyApp
version = 1.0.0
author = "Jane Smith"
create altair.doc
/ Esto es un comentario (línea que empieza por /)
define text greeting = "Hello, world!"
log greeting
Campos de cabecera: name, version, author.
El parser lee la cabecera desde altair.doc; hasta create altair.doc. Todo
lo demás es el cuerpo del programa.
Comentarios: un / al inicio de una línea (o tras espacios en blanco)
seguido de un espacio o una letra empieza un comentario de una línea.
Hay dos formas equivalentes, ambas soportadas por el parser:
define <tipo> <nombre> [almacenamiento] [cualificadores] [= expresión]
<tipo> <nombre> [= expresión] [almacenamiento] [cualificadores]
La palabra define es opcional; el compilador reconoce igualmente una
declaración que empieza directamente por el tipo.
Ejemplos:
define numeric count = 0
numeric count2 = 0
define text name = "world"
define bool ready = true
define list items
define object user
| Palabra clave | Significado |
|---|---|
ram |
Memoria del proceso (por defecto). Rápido; se pierde al salir. |
disk |
Almacenamiento persistente en fichero. Sobrevive a reinicios. |
cache |
Persistente con TTL opcional. Se auto-expira. |
temp |
Se pone a cero al liberarse; para datos sensibles. |
auto |
El runtime elige el mejor nivel. |
define text username disk
define text session_token temp
define text api_response cache expire=30m
| Cualificador | Ejemplo | Efecto |
|---|---|---|
const |
define numeric pi const = 3.14159 |
No se puede reasignar |
expire=<dur> |
expire=5m |
Se auto-expira tras un tiempo |
weight=<n> |
weight=5 |
Pista de prioridad (0–100) |
Duraciones: 30s (segundos), 5m (minutos), 2h (horas).
name = "Alice"
count = count + 1
count += 10
count -= 3
count *= 2
count /= 4
count %= 7
release)Libera explícitamente una variable y su memoria:
define text buffer ram = "large data"
/ ... usar buffer ...
release buffer
release es seguro de llamar sobre variables que ya no existen (no hace nada).
numericUn número de coma flotante de 64 bits (double en C internamente).
define numeric x = 42
define numeric pi = 3.14159
textUna cadena UTF-8.
define text greeting = "Hello!"
define text multi = "Línea 1\nLínea 2"
La concatenación usa +:
define text full = "Hola, " + name + "!"
booldefine bool active = true
define bool done = false
listColección ordenada de tamaño dinámico.
define list scores = [10, 20, 30]
scores.append(40)
scores.remove(0) / elimina el índice 0
scores.clear() / vacía la lista
define numeric n = scores.length()
define numeric v = scores[1]
scores[0] = 99
.length funciona con o sin paréntesis.
objectVer §8 Clases y objetos.
tokenUn valor de un solo uso: se consume la primera vez que se lee, y una segunda
lectura lanza ALT0004.
define token invite_code = "XXXX-YYYY"
define text used = invite_code / consume el token
define text again = invite_code / ERROR: ALT0004, token ya consumido
char — texto de un carácter (añadido para self-hosting, §31)file — manejador de fichero (añadido para self-hosting, §31–32)p#Tipo — puntero crudo (añadido para self-hosting, §31, §33)| Op | Descripción | Ejemplo |
|---|---|---|
+ |
Suma / concatena texto | 1 + 2, "a" + "b" |
- |
Resta | 10 - 3 |
* |
Multiplica | 4 * 5 |
/ |
Divide | 10 / 3 |
% |
Módulo | 10 % 3 |
-x |
Negación | -count |
==, !=, <, >, <=, >= — funcionan sobre numeric y text
(lexicográfica para texto).
&& (y), || (o), ! (no). (En la fuente del propio compilador solo
existen estas formas simbólicas — no hay palabras "and"/"or"/"not"; esas
palabras sí las usa, por su cuenta, el lenguaje "Altair-Core" del compilador
auto-hospedado, ver §36).
if x > 0 && x < 100;
log "en rango"
break
& | ^ ~ << >> — solo válidos sobre numeric (internamente se convierten a
entero de 64 bits, se opera, y se vuelve a double).
!, -, ~*, /, %+, -<<, >>&^|<, >, <=, >===, !=&&||Casi todos los bloques en Altair terminan con la palabra break (no usan
llaves {}).
if score > 90;
log "Excelente"
break
elif score > 70;
log "Bien"
break
else;
log "Necesita mejorar"
break
define numeric n = 1
while n <= 10;
log n
n += 1
break
repeat 5 times;
log "hola"
break
/ o con una expresión:
repeat count;
log "tick"
break
forever;
log "corriendo..."
wait 1s
break
define list fruits = ["manzana", "plátano", "cereza"]
foreach fruit in fruits;
log fruit
break
Sale del bucle actual, o termina el programa si no hay bucle:
while true;
if done == true;
exit / sale del while
break
break
exit / termina el programa (fuera de un bucle)
wait 2s / espera 2 segundos
wait 500ms / (usar 0.5s para fracciones de segundo)
fun greet name text;
define text msg = "Hola, " + name + "!"
log msg
return msg
break
Con tipo de retorno:
fun add -> numeric numeric x, numeric y;
return x + y
break
Llamada:
greet("Alice")
define numeric result = add(10, 20)
Las funciones pueden leer y escribir variables globales, y sus parámetros se liberan correctamente al terminar (sin fugas de memoria).
Funciones recursivas:
fun factorial -> numeric numeric n;
if n <= 1;
return 1
break
return n * factorial(n - 1)
break
class Point;
define numeric x = 0
define numeric y = 0
fun distance;
define numeric dx = x * x
define numeric dy = y * y
return dx + dy
break
break
create object Point as p
p.x = 3
p.y = 4
log p.distance()
log p.x
Llamadas a métodos:
define text result = myObj.methodName(arg1, arg2)
Los objetos usan arrays dinámicos de campos/métodos internamente (muy poco overhead por instancia), así que crear muchos objetos es viable.
define list items = [1, 2, 3]
/ Añadir
items.append(4)
items.append("hola")
/ Acceso por índice (base 0)
define numeric first = items[0]
/ Modificar por índice
items[0] = 99
/ Eliminar por índice
items.remove(2)
/ Vaciar
items.clear()
/ Longitud (con o sin paréntesis)
define numeric n = items.length
define numeric m = items.length()
/ Iterar
foreach item in items;
log item
break
/ Concatenar dos listas
define list all = listA + listB
try;
/ código que puede fallar
define numeric result = 10 / 0
break
catch as err;
log err.code
log err.message
log err.line
break
El runtime lanza errores en: división por cero, índice fuera de rango, acceso
a objeto nulo, doble consumo de token, reasignación de const.
Anidar try/catch:
try;
try;
/ interno
break
catch as inner_err;
log inner_err.message
break
break
catch as outer_err;
log outer_err.message
break
Los snapshots guardan todas las variables registradas en disco de forma atómica (con checksum CRC32).
/ Guardar el estado actual
snapshot create "checkpoint1"
/ Restaurar un estado anterior
snapshot restore "checkpoint1"
/ Borrar un snapshot
snapshot delete "checkpoint1"
Se guardan en ~/.altair/<nombre_programa>/snap/.
choose outcome;
"win" 70
"draw" 20
"lose" 10
break
log outcome
Los pesos son proporcionales — no hace falta que sumen 100.
Consultas de metadatos del runtime y del compilador, mediante la sintaxis
namespace@clave:
/ Consultas de sistema
log system@time / timestamp Unix
log system@random / float 0.0–1.0 aleatorio
log system@pid / PID del proceso
log system@hostname / nombre de la máquina
log system@username / usuario actual
log system@os / "linux", "macos" o "windows"
log system@memory / RAM usada (bytes)
log system@diskfree / espacio libre en disco (bytes)
/ Introspección de variables
define disk numeric counter
log system@storage(counter) / "disk"
log system@weight(counter) / 0
log system@type(counter) / "numeric"
log system@size(counter) / tamaño serializado
/ Info del compilador
log compiler@version
log compiler@name / "altairc"
log compiler@build / fecha de compilación
log compiler@architecture / "x86_64" o "arm64"
/ Metadatos del programa (de la cabecera altair.doc)
log program@name
log program@version
log program@author
Un token solo se puede consumir una vez. Leerlo una segunda vez lanza
ALT0004.
define token api_key temp = "secret-api-key-abc123"
/ La primera lectura consume el token
define text key = api_key
log "Clave usada: " + key
/ La segunda lectura lanza error
define text key2 = api_key / ERROR: Token already consumed
Útil para contraseñas de un solo uso, códigos de invitación, tokens CSRF, etc.
orbit — migración entre varios nivelesDeclara una variable con estados de almacenamiento nombrados entre los que puede migrar:
define numeric data orbit 1 "hot" ram, 2 "warm" disk, 3 "cold" cache expire=1h = 0
/ Mover a otro estado
migrate data as "warm"
migrate data as 3
prefer — almacenamiento por orden de preferenciaPrueba niveles de almacenamiento en un orden de preferencia:
define text session prefer ram, cache expire=30m, disk = ""
El runtime usa el primer nivel disponible.
Arranca un servidor HTTP en un puerto. El bloque listen contiene
declaraciones de rutas, middleware, health, métricas y apagado.
listen 8080;
route "GET" "/hello";
respond.text("Hello, World!")
break
break
El servidor corre hasta que se termina. Las señales (SIGTERM, Ctrl+C)
disparan un apagado ordenado.
Cómo funciona: el servidor HTTP integrado de Altair usa sockets TCP POSIX (sin librerías externas). Es de un solo hilo pero usa un modelo de conexión por petición.
route "METHOD" "/path";
/ cuerpo del handler
break
Métodos soportados: GET, POST, PUT, DELETE, PATCH, * (cualquiera)
Sintaxis :param en las rutas:
route "GET" "/users/:id";
define text user_id = param("id")
respond.json("found user " + user_id)
break
Dentro de un handler:
define text body_data = body() / cuerpo crudo de la petición
define text auth = header("Authorization") / cabecera de la petición
define text id = param("id") / parámetro de ruta
respond.text("respuesta en texto plano")
respond.json("cadena o valor json")
respond.status(201)
respond.status(404)
respond.status(500)
Ejemplo:
route "POST" "/users";
define text payload = body()
if payload == "";
respond.status(400)
respond.json("missing body")
break
respond.status(201)
respond.json("user created")
break
El middleware se ejecuta antes de cada handler de ruta. Si el middleware
llama a stop, se detiene la cadena de la petición.
middleware auth;
define text token = header("Authorization")
if token == "";
respond.status(401)
respond.json("unauthorized")
stop
break
break
Varios middleware se encadenan en orden:
listen 8080;
middleware logger;
log "petición recibida"
break
middleware auth;
define text tok = header("X-API-Key")
if tok != "secret";
respond.status(401)
respond.json("bad key")
stop
break
break
route "GET" "/data";
respond.json("protected data")
break
break
Se puede añadir a cualquier ruta:
route "POST" "/login" rate_limit 10 per_minute;
respond.json("ok")
break
Al superar el límite, el runtime devuelve automáticamente HTTP 429 con
{"error":"rate limit exceeded"}.
Sintaxis:
route "METHOD" "/path" rate_limit N per_minute;
o
rate_limit N per_second
La declaración health registra una ruta GET que responde con estado JSON:
health "/health";
check "database" -> true
check "cache" -> true
break
Formato de respuesta:
{"status":"ok","checks":{"database":"ok","cache":"ok"}}
Si algún check devuelve false, el estado HTTP es 503 Service
Unavailable. El lado derecho de -> es una expresión (numérica o
booleana); cualquier valor distinto de cero se considera saludable.
Registra un endpoint de métricas compatible con Prometheus:
metrics "/metrics";
Expone un GET /metrics en formato Prometheus de texto plano.
El endpoint produce algo como:
# TYPE events_total counter
events_total 42
Registra lógica de limpieza que se ejecuta al recibir SIGTERM o al llamar a
altair_server_stop():
on_shutdown;
log "Servidor apagándose, guardando datos..."
snapshot create "shutdown_checkpoint"
break
Ejemplo con ciclo de vida completo:
listen 8080;
route "GET" "/";
respond.text("running")
break
on_shutdown;
log "adiós!"
break
break
Declara variables de sesión con TTL opcional:
session user_token expires 30m;
Las sesiones se guardan en memoria del proceso en un almacén clave-valor
indexado por un ID de sesión. Usa header("X-Session-ID") para recuperar el
ID de sesión del cliente.
Dentro de handlers de ruta:
route "GET" "/profile";
define text sid = header("X-Session-ID")
define text username = session_get(sid, "username")
if username == "";
respond.status(401)
respond.json("not logged in")
break
respond.json("Hello " + username)
break
route "POST" "/login";
define text user = body()
session_set("my-session-id", "username", user, 1800)
respond.json("logged in")
break
Usa el bloque config para declarar configuración respaldada por variables
de entorno:
config;
env("DATABASE_URL") default "postgres://localhost/mydb"
env("PORT") default "8080"
env("API_SECRET") required
break
default "valor" — valor de respaldo si la variable de entorno no está definidarequired — lanza ALT0015 al arrancar si la variable faltaAcceder a los valores:
La declaración env(...) crea variables en el ámbito, nombradas según la
clave:
config;
env("PORT") default "8080"
break
define numeric port = PORT
listen port;
route "GET" "/";
respond.text("ok")
break
break
Declara un pool de conexiones para una URL de base de datos:
db_pool db = connect("postgres://user:pass@localhost/mydb") max 20
Esto crea una variable db que guarda la cadena de conexión. Hay que
conectar tu propia librería cliente de base de datos en el C generado, o usar
el stub para prototipar. max fija el tamaño del pool (10 por defecto).
Altair da la sintaxis de declaración y el stub de pooling; el soporte completo de consultas SQL requiere enlazar tu propia librería cliente (PostgreSQL/MySQL/SQLite) en el C generado.
Registra tareas periódicas en segundo plano con job:
job cleanup every 5m;
log "ejecutando tarea de limpieza"
break
Sintaxis:
job <nombre> every <duración>;
/ cuerpo de la tarea
break
schedule es un alias de job:
schedule heartbeat every 30s;
log "heartbeat"
break
Cómo funciona: las tareas se comprueban en cada petición
(altair_jobs_tick() se llama por conexión). En escenarios de poco tráfico,
usa wait dentro de un forever para temporización precisa en su lugar.
Altair puede producir programas gráficos con ventana sobre raylib. Requiere
tener raylib disponible en tiempo de compilación (empaquetado en
libs/raylib/<os>/ o instalado en el sistema).
link graphics raylib
Debe aparecer una vez, antes de cualquier sentencia de ventana/loop/draw.
Indica al compilador que enlace -lraylib (más las librerías de ventaneo de
la plataforma) en el binario final.
window
title = "Mi Ventana"
width = 800
height = 600
fps = 60
create window
loop
clear skyblue
/ sentencias en cada frame
break
clear <color> limpia el frame. break cierra la definición del bloque
loop (no sale del programa — el while generado corre hasta que se cierra
la ventana).
draw <tipo>
x = 100
y = 100
...
create draw
| tipo | props requeridas | notas |
|---|---|---|
text |
content/text, x, y, size, color |
|
rect / rectangle |
x, y, width/w, height/h, color |
|
circle |
x, y, radius/r, color |
|
line |
x, y, x2, y2, color |
|
line_thick / thick_line |
añade thick/thickness |
|
triangle |
x,y,x2,y2,x3,y3,color |
|
pixel |
x, y, color |
|
image / texture |
image/src (variable de textura), x, y, color |
Los colores aceptan nombres de raylib (red, gold, skyblue, white,
...) o una declaración de bloque color.
link graphics raylib
window
title = "Paint Test"
width = 800
height = 600
fps = 60
create window
loop
clear skyblue
draw text
content = "Altair Graphics"
x = 10
y = 10
size = 24
color = white
create draw
draw rect
x = 100
y = 100
width = 200
height = 120
color = red
create draw
draw circle
x = 500
y = 200
radius = 60
color = gold
create draw
break
Compilar y ejecutar:
altairc test/paint.at -o paint
./paint
| Consulta | Devuelve |
|---|---|
system@time |
Timestamp Unix (numeric) |
system@random |
Aleatorio 0.0–1.0 (numeric) |
system@pid |
PID del proceso (numeric) |
system@hostname |
Nombre de la máquina (text) |
system@username |
Usuario del SO (text) |
system@os |
"linux" / "macos" / "windows" (text) |
system@memory |
RAM usada en bytes (numeric) |
system@diskfree |
Bytes libres en disco (numeric) |
compiler@version |
Versión del compilador |
compiler@name |
"altairc" |
compiler@build |
Fecha de compilación |
compiler@architecture |
"x86_64" / "arm64" |
program@name |
De la cabecera altair.doc |
program@version |
De la cabecera altair.doc |
program@author |
De la cabecera altair.doc |
altaircaltairc <source.at> [opciones] Compila un programa Altair
altairc guide Escribe ALTAIR_GUIDE.md en el directorio actual
altairc guide --stdout Imprime la guía por stdout
altairc --version Imprime la versión del compilador
altairc --help Muestra la ayuda
Opciones:
| Flag | Descripción |
|---|---|
-o <file> |
Nombre del binario de salida (por defecto: a.out) |
--emit-c |
Imprime el código C generado por stdout |
--emit-ast |
Imprime un resumen de nodos del AST |
--no-sema |
Salta el análisis semántico (compila más rápido) |
-v |
Número de versión |
Ejemplos:
# Compilar un programa
altairc hello.at -o hello
# Compilar un servidor
altairc server.at -o myserver
# Depurar: ver qué C se genera
altairc server.at --emit-c | head -200
# Generar la guía del lenguaje
altairc guide
| Código | Descripción |
|---|---|
ALT0001 |
Variable desconocida o acceso a objeto nulo |
ALT0002 |
Tipos incompatibles en aritmética o comparación |
ALT0003 |
Error de parseo / sintaxis |
ALT0004 |
Token ya consumido |
ALT0005 |
Error de snapshot (crear/restaurar/borrar) |
ALT0006 |
Bloque prefer sin entradas de almacenamiento |
ALT0007 |
Asignación a una variable const |
ALT0008 |
weight debe ser un entero no negativo |
ALT0009 |
orbit tiene números de estado duplicados |
ALT0010 |
División o módulo por cero |
ALT0011 |
Clave o namespace de introspección desconocido |
ALT0012 |
Estado de orbit no encontrado / variable sin orbit |
ALT0013 |
Índice de lista (o de texto) fuera de rango |
ALT0014 |
Campo de objeto no encontrado |
ALT0015 |
Variable de entorno requerida no definida |
char, file, p#Tipo y operadores a nivel de bitAñadidos para dar soporte a self-hosting (que un compilador de Altair pueda algún día escribirse en el propio Altair). Tocan las cuatro etapas del compilador:
lexer.c/h(tokens nuevos),parser.c(gramática nueva),ast.h(VTYPE_FILE,VTYPE_POINTER),codegen.c(nuevos casos de emisión) yaltair_rt.c/h(nuevas etiquetasALT_FILE/ALT_POINTERy nuevas funciones C integradas).
char — texto de un carácterchar es un alias de tipo, no un tipo de runtime nuevo. El parser mapea
el token char directamente a VTYPE_TEXT (tok_to_vtype() en
parser.c), así que una variable char es, a nivel de C/runtime, un valor
ALT_TEXT normal que contiene un solo carácter. Por eso toda operación de
texto (+, comparaciones, length()) ya funciona sobre char sin necesitar
codegen nuevo.
text code = "hola"
char c = code[0]
log c
text[índice]Antes de este cambio, el indexado [i] (ND_INDEX_ACCESS) solo funcionaba
sobre ALT_LIST. La función de runtime altair_list_get() ahora también
maneja ALT_TEXT: comprueba límites contra strlen() y devuelve una nueva
cadena de un carácter. Acceder fuera de rango lanza ALT0013, el mismo
código que usan las listas.
text s = "abc"
log s[0] / "a"
log length(s) / 3 (length() ahora también acepta text, no solo listas)
file — un tipo manejador de ficherofile es un tipo de valor genuinamente nuevo: VTYPE_FILE en el AST mapea
a ALT_FILE en runtime. AltairVal recibió un campo void *ptr (union)
compartido entre ALT_FILE (guarda un FILE* de C) y ALT_POINTER (ver
más abajo). Copiar un valor file (altair_val_copy) es una copia
superficial: el FILE* subyacente se comparte, igual que al asignar un
puntero en C.
file f = open("data.txt")
text contents = read(f)
close(f)
p#Tipo — punteros crudos (sintaxis actualizada)Esta sintaxis sustituye a la antigua
point@Tipo, que ya no existe. La palabra clave completapointse abrevió ap, y el namespace pasó de@a#(para diferenciarlo visualmente desystem@/compiler@, que son namespaces de solo lectura, mientras quep#opera sobre memoria mutable).pqueda reservada como palabra clave en todo el lenguaje: ya no se puede usar como nombre de variable o función.
Declarar y reservar memoria (en un solo paso):
p#Tipo nombre = alloc(n)
p#Tipo declara una variable de VTYPE_POINTER (ALT_POINTER en runtime).
El Tipo tras la # se guarda solo a efectos de documentación — el valor
subyacente es un bloque de memoria cruda de n bytes, así que Altair no
obliga a que el puntero apunte siempre a valores de Tipo.
Namespace p# — operaciones sobre un puntero ya creado:
| Operación | Descripción |
|---|---|
p#write(nombre, offset, valor) |
Escribe valor (numeric) en el slot offset |
numeric v = p#read(nombre, offset) |
Lee el slot offset |
p#bytes(nombre) |
Bytes totales reservados por alloc(n) |
p#null(nombre) |
true si el puntero es nulo o ya fue liberado |
p#free(nombre) |
Libera la memoria |
Cada offset es un slot de 8 bytes (un numeric/double), no un byte
crudo — así alloc(64) da 8 slots utilizables, del 0 al 7. Leer o
escribir fuera de esos límites falla de forma segura (p#write devuelve
false, p#read devuelve 0), sin volcar memoria ajena.
p#node n = alloc(64)
p#write(n, 0, 42)
p#write(n, 1, 100)
numeric a = p#read(n, 0)
numeric b = p#read(n, 1)
log a / 42
log b / 100
log p#bytes(n) / 64
log p#null(n) / false
p#free(n)
log p#null(n) / true
Por qué antes no servía para nada: en la primera versión (point@Tipo,
con ptr_alloc/ptr_free/ptr_is_null), el puntero solo reservaba y
liberaba memoria — no existía forma de leer ni escribir dentro de ese
bloque, así que era inútil para construir listas enlazadas o nodos de AST.
p#write/p#read son justo lo que faltaba para que sea funcional de
verdad.
Detalle interno importante: p#free y p#write reciben el puntero por
referencia (no una copia), para que la mutación (liberar memoria, poner el
puntero a NULL) se refleje también en la variable original. El resto de
llamadas a función en Altair sí reciben copias — este es un caso especial
en el codegen (ND_FUNC_CALL con fun_name igual a p_free/p_write
usa cg_expr_receiver en vez de cg_expr para su primer argumento).
Tokens nuevos & | ^ ~ << >>, distinguidos correctamente de &&/|| (el
lexer comprueba primero las formas de dos caracteres). Se insertaron nuevos
niveles de precedencia en la gramática de expresiones, del más flojo al más
fuerte, igual que en C:
comparación ( == != < > <= >= )
|
bitor ( | )
|
bitxor ( ^ )
|
bitand ( & )
|
shift ( << >> )
|
suma/resta ( + - )
Cada operador compila a un pequeño helper de runtime (altair_band,
altair_bor, altair_bxor, altair_bnot, altair_shl, altair_shr en
altair_rt.c) que exige que ambos operandos sean numeric — los números de
Altair son double internamente, así que las operaciones a nivel de bit
convierten a long long antes de aplicar el operador de C y vuelven a
double al devolver. Pasar un operando que no sea numérico lanza ALT0002.
numeric flags = 6 & 3 / 2
numeric merged = 6 | 3 / 7
numeric x = 1 << 4 / 16
numeric inv = ~0 / -1
Son llamadas a función normales (ND_FUNC_CALL) — no hizo falta gramática
nueva, porque el compilador ya tenía un mecanismo genérico: cualquier
llamada nombre(args) que no sea un método conocido de lista/texto compila
directo a una llamada en C _fn_nombre(args). Cada builtin de abajo es
simplemente una función de C llamada _fn_<nombre> implementada en
altair_rt.c.
| Función | Firma | Descripción |
|---|---|---|
open(path) |
file |
Abre para lectura (modo "r"). |
open_write(path) |
file |
Abre para escritura, truncando (modo "w"). |
open_append(path) |
file |
Abre para añadir (modo "a"). |
read(f) |
text |
Lee todo el contenido restante de f. |
read_line(f) |
text |
Lee una línea (sin el \n/\r final). |
write(f, texto) |
bool |
Escribe texto en f. |
close(f) |
bool |
Cierra f. Se puede llamar dos veces sin problema. |
create_file(path) |
bool |
Crea un fichero vacío si no existe. |
delete_file(path) |
bool |
Borra un fichero. |
mkdir(path) |
bool |
Crea un directorio (éxito si ya existe). |
file_exists(path) |
bool |
Comprueba existencia vía stat(). |
list_dir(path) |
list |
Lista los nombres de un directorio (excluye ./..). |
exec(cmd) |
numeric |
Ejecuta cmd con system(); devuelve el código de salida. |
exec_capture(cmd) |
text |
Ejecuta cmd con popen(); devuelve el stdout capturado. |
create_file("build/out.txt")
file f = open_write("build/out.txt")
write(f, "gcc invoked here")
close(f)
numeric rc = exec("gcc build/main.c -o programa.exe")
if rc == 0;
log "build ok"
break
Resolución de rutas: las rutas se pasan tal cual a la librería estándar de
C (fopen, stat, opendir...) relativas al directorio de trabajo del
proceso — no hay ninguna resolución automática hacia una carpeta de
proyecto para file/open/etc. (Esa carpeta de "carpeta propia por
proyecto" descrita para las variables persistentes sí se implementó, ver
§35, pero es un mecanismo aparte, no parte de file/open.)
p#| Sintaxis | Firma | Descripción |
|---|---|---|
p#Tipo nombre = alloc(n) |
declaración | Reserva n bytes y crea el puntero nombre |
p#write(nombre, offset, valor) |
bool |
Escribe valor en el slot offset (8 bytes) |
numeric v = p#read(nombre, offset) |
numeric |
Lee el slot offset |
p#bytes(nombre) |
numeric |
Bytes totales reservados |
p#null(nombre) |
bool |
true si es nulo o ya liberado |
p#free(nombre) |
bool |
Libera la memoria |
Internamente, alloc(n) reserva n bytes más una cabecera oculta de
sizeof(size_t) bytes justo antes de la dirección que se le entrega a
Altair — esa cabecera guarda n, y es lo que consulta p#bytes. p#free
libera desde el inicio real del bloque (puntero - sizeof(size_t)), no
desde donde apunta la variable Altair.
Los punteros no tienen recolector de basura y no se liberan
automáticamente al salir de ámbito — es gestión manual de memoria
intencionadamente cruda, al estilo de C. Usar un puntero después de
p#free no está protegido salvo que compruebes p#null tú mismo antes.
p#byte buf = alloc(256)
p#write(buf, 0, 1)
log p#read(buf, 0)
p#free(buf)
Antes de este cambio, un binario Altair compilado no tenía forma de leer su
propio argv. Como un compilador auto-hospedado necesita aceptar una ruta
de fichero fuente por línea de comandos, el main(int argc, char **argv)
generado ahora llama a altair_set_args(argc, argv) antes de ejecutar el
cuerpo del programa.
| Función | Firma | Descripción |
|---|---|---|
argc() |
numeric |
Número de argumentos de línea de comandos (incluye argv[0]). |
arg(i) |
text |
El argumento i-ésimo (base 0; arg(0) es la ruta del binario). |
numeric n = argc()
if n < 2;
log "uso: programa <archivo>"
else;
log "archivo: " + arg(1)
break
Sintaxis: tras la declaración normal de una variable, un identificador
punteado a modo de "nombre de fichero" (por ejemplo player.coins):
numeric coins = 120 player.coins
text playerName = "Nombre" player.name
Cómo funciona internamente:
algo.algo, se guarda como n->persist_file en el nodo AST (usa una
instantánea/restauración del lexer para poder "mirar hacia delante" sin
romper el resto del parseo si no hay patrón ident.ident).persist_file está presente, se emite una
llamada a altair_persist_load("player.coins"). Si el fichero ya existe,
su valor sobreescribe el de la expresión inicial (esto permite que el
valor sobreviva entre ejecuciones). Si no existe, se usa el valor inicial
y se escribe inmediatamente al fichero. La variable además se registra
en un listado global para volcarse también al apagar el programa.coins = coins + 10) dispara automáticamente un
altair_persist_save(...) con el nuevo valor.variables/ tiene dos líneas: una
etiqueta de tipo (N numeric, T text, B bool) y el valor serializado.
Esto permite auto-describir el tipo al recargar sin que el compilador
necesite conocerlo de antemano.variables/ (relativa al directorio de trabajo del binario
compilado). La carpeta se crea sola si no existe.altair.doc;
name = "PersistTest"
version = "1.0"
author = "a"
create altair.doc
numeric coins = 120 player.coins
text playerName = "Nombre" player.name
log "coins=" + coins
coins = coins + 10
log "coins tras sumar=" + coins
Primera ejecución → imprime coins=120, luego coins tras sumar=130, y dentro
de variables/ quedan player.coins (N / 130) y player.name
(T / Nombre). Segunda ejecución → el 120 inicial es ignorado porque ya
existe variables/player.coins; arranca directamente con coins=130.
Limitación conocida: de momento solo numeric, text y bool se
serializan; declarar una variable list con nombre punteado no falla, pero
su contenido no se persiste entre ejecuciones.
selfhost/altair.at es un compilador escrito enteramente en Altair (usa
file, indexado de texto, char, y el control de flujo / funciones que
Altair ya tenía) que compila un subconjunto deliberadamente más pequeño del
lenguaje, llamado Altair-Core, directamente a C.
(bootstrap, una sola vez)
altair.at ──────────────────────────▶ altairc-selfhost
(Altair real) altairc (en C) (binario nativo)
(repetible, sin necesitar inventar un compilador C)
programa.atc ─────────────────────────▶ programa.gen.c ──▶ gcc ──▶ binario
altairc-selfhost
Compilarlo:
./altairc selfhost/altair.at -o selfhost/altairc-selfhost
Usarlo:
./altairc-selfhost fib.atc fib.gen.c
gcc fib.gen.c -o fib_bin -lm # -lm hace falta: numeric % compila a fmod()
./fib_bin
Altair-Core conserva la forma if/elif/else...break, while...break y
fun nombre -> tipo ... break de Altair, pero no tiene análisis semántico
propio — los errores de tipos aparecen como errores de gcc sobre el C
generado, no como errores de Altair. Los ; tras las cabeceras son
opcionales: el lexer de Altair-Core los trata igual que un espacio en
blanco.
programa := stmt*
stmt := vardecl | assign | log | return | if | while | fun-call
vardecl := ("numeric"|"text"|"bool") IDENT "=" expr
assign := IDENT "=" expr
log := "log" expr
if := "if" expr stmt* ("elif" expr stmt*)* ("else" stmt*)? "break"
while := "while" expr stmt* "break"
fun := "fun" IDENT ("->" tipo)? (tipo IDENT ("," tipo IDENT)*)? stmt* "break"
return := "return" expr?
Y expresiones con precedencia clásica (or → and → not → comparación →
suma/resta → mult/div/mod → unario → primario), donde primario incluye
número, cadena, true/false, identificador, llamada nombre(args) y
(expr).
| Altair-Core | C generado |
|---|---|
numeric |
double |
text |
char* |
bool |
int |
% |
fmod(a, b) (el % nativo de C no aplica a double) |
+ |
+ nativo — solo aritmético, no concatena texto |
| Función | Propósito |
|---|---|
concat(a, b) |
Concatenación de texto (no hay + para texto). |
streq(a, b) |
Igualdad de texto (==/!= sobre char* compararía punteros, no contenido). |
numstr(n) |
numeric → text, para usar con log. |
fun fib -> numeric numeric n;
if n < 2;
return n
break
return fib(n - 1) + fib(n - 2)
break
numeric i = 0
while i < 10;
log numstr(fib(i))
i = i + 1
break
log concat("hola, ", "mundo")
altairc-selfhost compila programas Altair-Core, pero todavía no puede
compilar su propio código fuente (altair.at), porque altair.at usa
funcionalidades que Altair-Core no soporta: list, file/open/read/
close, indexado de texto, y declaraciones con más de un parámetro
tipado. Cerrar ese círculo — reescribir altair.at usando solo
Altair-Core, tras ampliar Altair-Core con listas y E/S de ficheros — es el
siguiente paso natural y no está hecho todavía.
Lo que sí está probado de punta a punta:
- altairc (en C) compila con éxito altair.at (Altair real) →
altairc-selfhost.
- altairc-selfhost compila con éxito programas Altair-Core (recursión,
if/elif/else, while, concatenación de texto, %, comparación de
texto) → C.
- El C generado compila limpio con gcc y produce el resultado correcto.
data — contenedores de variables agrupadasAgrupa varias variables bajo un nombre común y un "lugar" de almacenamiento
compartido. data, p y # son ahora también palabras clave reservadas.
data Nombre lugar;
tipo var1 = valor1
tipo var2 = valor2
data create
lugar puede ser uno de los cuatro niveles de almacenamiento normales
(ram, disk, cache, temp) o cualquier identificador propio (una
extensión personalizada, p. ej. data PlayerSave save; ... data create).data create (no con break).variables/<Nombre>.<var>.<lugar>. Por eso
cada campo tiene su propio fichero — necesario para que migrate pueda
mover todos los ficheros que compartan el prefijo <Nombre>. de una vez.data PlayerSave disk;
numeric coins = 120
text name = "Nombre"
data create
log coins
log name
coins = coins + 5
Esto crea variables/PlayerSave.coins.disk y variables/PlayerSave.name.disk,
y cada asignación posterior a coins o name los mantiene actualizados
(igual que en §35).
data Nombre migrate lugar
Busca todos los ficheros variables/Nombre.*. y los renombra al nuevo
lugar. Es una operación de fichero real (rename()), no un stub.
Limitación conocida — importante: el fichero al que cada variable lee y
escribe queda fijado en tiempo de compilación (el nombre se hornea en el
C generado). migrate renombra el fichero en disco, pero si el programa
sigue corriendo tras el migrate y esa variable vuelve a asignarse, o el
programa simplemente termina (el volcado final de cierre también usa el
nombre original), el fichero antiguo se vuelve a crear con el nombre de
antes. Es decir: migrate sirve como operación puntual de reetiquetado /
exportación, pero no convierte el programa en curso en un consumidor
dinámico de la nueva ubicación — no es un cambio de nivel de almacenamiento
en caliente que el propio programa recuerde y respete después.
data PlayerSave disk;
numeric coins = 120
data create
coins = coins + 5
data PlayerSave migrate cache
/ en este punto variables/PlayerSave.coins.cache existe con valor 125,
/ pero si el programa reasigna coins otra vez, o simplemente termina,
/ variables/PlayerSave.coins.disk se vuelve a crear también.
Guía generada a partir de la implementación real del compilador y el
runtime de Altair. Versión del compilador: 1.7. No cubre async ni
safe block: no están implementadas todavía. El bloque data (§37) sí
está implementado.
altairc guide - complete language reference for the Altair programming language. Generated by:
altairc guideDoes not coverasyncorsafe block, which are not implemented yet. Thedatablock is implemented (with syntax different from the original proposal) — see §37.
data Block — grouped variable containersAltair is a statically compiled, expression-oriented language that transpiles to C via altairc. Every program compiles to a single native binary with no runtime dependencies. v1.6.5vB adds a complete HTTP server layer, job scheduling, session management, and configuration management built directly into the language syntax.
Compiler pipeline:
.at source → lexer → parser → sema → codegen → .c → gcc → binary
Key properties:
- Every variable has a declared storage tier (ram/disk/cache/temp)
- Variables can migrate between storage tiers at runtime via orbit
- Strings, lists, objects, and tokens are first-class values
- All memory is managed by the runtime (no manual malloc/free)
- HTTP servers are declared with listen; routes are declared with route
Every Altair program has an optional header and a body. The header provides program metadata; the body contains all statements.
altair.doc;
name = MyApp
version = 1.0.0
author = "Jane Smith"
create altair.doc
/ This is a comment (line starting with /)
define text greeting = "Hello, world!"
log greeting
Header fields: name, version, author
The header is parsed from altair.doc; to create altair.doc. Everything else is the program body.
Comments: A / at the start of a line (or after whitespace) followed by a space or letter begins a single-line comment.
define <type> <name> [storage] [qualifiers] [= expression]
Examples:
define numeric count = 0
define text name = "world"
define bool ready = true
define list items
define object user
| Keyword | Meaning |
|---|---|
ram |
In-process memory (default). Fast; lost on exit. |
disk |
Persistent file storage. Survives restarts. |
cache |
Persistent with optional TTL. Auto-expired. |
temp |
Zeroed on release; for sensitive data. |
auto |
Runtime chooses best tier. |
define text username disk
define text session_token temp
define text api_response cache expire=30m
| Qualifier | Example | Effect |
|---|---|---|
const |
define numeric pi const = 3.14159 |
Cannot be reassigned |
expire=<dur> |
expire=5m |
Auto-expires after duration |
weight=<n> |
weight=5 |
Priority hint (0–100) |
Durations: 30s (seconds), 5m (minutes), 2h (hours).
name = "Alice"
count = count + 1
count += 10
count -= 3
count *= 2
count /= 4
count %= 7
Explicitly release a variable and free its memory:
define text buffer ram = "large data"
/ ... use buffer ...
release buffer
v1.6.5vB:
releaseis safe to call on variables that no longer exist - it is a no-op.
Stores a 64-bit floating-point number.
define numeric x = 42
define numeric pi = 3.14159
define numeric big = 1000000
A UTF-8 string.
define text greeting = "Hello!"
define text multi = "Line 1\nLine 2"
String concatenation uses +:
define text full = "Hello, " + name + "!"
define bool active = true
define bool done = false
A dynamically sized ordered collection.
define list scores = [10, 20, 30]
scores.append(40)
scores.remove(0) / remove index 0
scores.clear() / remove all
define numeric n = scores.length()
define numeric v = scores[1]
scores[0] = 99
v1.6.5vB Bug #7 fix:
list.length()with parentheses now works correctly.
See Section 8: Classes & Objects.
A single-use value - consumed once, then marked as consumed.
define token invite_code = "XXXX-YYYY"
define text used = invite_code / consumes the token
define text again = invite_code / throws ALT0004 "Token already consumed"
| Op | Description | Example |
|---|---|---|
+ |
Add / concatenate strings | 1 + 2, "a" + "b" |
- |
Subtract | 10 - 3 |
* |
Multiply | 4 * 5 |
/ |
Divide | 10 / 3 |
% |
Modulo | 10 % 3 |
-x |
Negate | -count |
v1.6.5vB Bug #3 fix: String concatenation no longer O(n²). Long repeated concat in loops no longer causes OOM.
==, !=, <, >, <=, >= - work on numerics and text (lexicographic for text).
and, or, !
if x > 0 and x < 100;
log "in range"
break
!, -*, /, %+, -<, >, <=, >===, !=andorif score > 90;
log "Excellent"
break
elif score > 70;
log "Good"
break
else;
log "Needs improvement"
break
define numeric n = 1
while n <= 10;
log n
n += 1
break
repeat 5 times;
log "hello"
break
/ Or with an expression:
repeat count;
log "tick"
break
forever;
log "running..."
wait 1s
break
define list fruits = ["apple", "banana", "cherry"]
foreach fruit in fruits;
log fruit
break
Exits the current loop or terminates the program:
while true;
if done == true;
exit / exits the while loop
break
break
exit / terminates the program (outside a loop)
wait 2s / wait 2 seconds
wait 500ms / (use 0.5s for sub-second waits)
fun greet name text;
define text msg = "Hello, " + name + "!"
log msg
return msg
break
Return type annotation:
fun add numeric x numeric, y numeric;
return x + y
break
Calling functions:
greet("Alice")
define numeric result = add(10, 20)
v1.6.5vB Bug #4 fix: Functions can now read and write global variables. Previously, globals were inaccessible inside function bodies.
v1.6.5vB Bug #5 fix: Function parameters are properly freed when the function returns, eliminating a per-call memory leak.
Recursive functions work correctly:
fun factorial numeric n numeric;
if n <= 1;
return 1
break
return n * factorial(n - 1)
break
class Point;
define numeric x = 0
define numeric y = 0
fun distance;
define numeric dx = x * x
define numeric dy = y * y
return dx + dy
break
break
create object Point as p
p.x = 3
p.y = 4
log p.distance()
log p.x
Method calls:
define text result = myObj.methodName(arg1, arg2)
v1.6.5vB Bug #6 fix: Objects now use dynamic field/method arrays (~50 bytes/instance vs ~10 KB previously). This makes creating large numbers of objects viable.
define list items = [1, 2, 3]
/ Append
items.append(4)
items.append("hello")
/ Access by index (0-based)
define numeric first = items[0]
/ Modify by index
items[0] = 99
/ Remove by index
items.remove(2)
/ Clear
items.clear()
/ Length (with or without parentheses)
define numeric n = items.length
define numeric m = items.length() / v1.6.5vB: both forms work
/ Iterate
foreach item in items;
log item
break
/ Concatenate two lists
define list all = listA + listB
try;
/ code that might fail
define numeric result = 10 / 0
break
catch as err;
log err.code
log err.message
log err.line
break
Throwing errors: (from user code, not directly, but via runtime operations)
The runtime throws on: division by zero, index out of bounds, null object access, token double-consume, const reassignment.
v1.6.5vB Bug #1 fix: Sequential
try/catchblocks no longer crash after ~34 uses. The try-depth counter is now always restored correctly - both on success and on error paths.
Nesting try/catch:
try;
try;
/ inner
break
catch as inner_err;
log inner_err.message
break
break
catch as outer_err;
log outer_err.message
break
Snapshots save all registered variables to disk atomically (with CRC32 checksum).
/ Save current state
snapshot create "checkpoint1"
/ Restore previous state
snapshot restore "checkpoint1"
/ Delete a snapshot
snapshot delete "checkpoint1"
Snapshots are stored in ~/.altair/<program_name>/snap/.
choose outcome;
"win" 70
"draw" 20
"lose" 10
break
log outcome
Weights are proportional - they don't need to add to 100.
Query runtime and compiler metadata:
/ System queries
log system@time / Unix timestamp
log system@random / 0.0–1.0 random float
log system@pid / Process ID
log system@hostname / Machine hostname
log system@username / Current user
log system@os / "linux", "macos", or "windows"
log system@memory / RAM used (bytes)
log system@diskfree / Free disk space (bytes)
/ Variable introspection
define disk numeric counter
log system@storage(counter) / "disk"
log system@weight(counter) / 0
log system@type(counter) / "numeric"
log system@size(counter) / serialized size
/ Compiler info
log compiler@version / "1.6.5vB"
log compiler@name / "altairc"
log compiler@build / build date
log compiler@architecture / "x86_64" or "arm64"
/ Program metadata (from altair.doc header)
log program@name
log program@version
log program@author
A token can only be consumed once. Attempting to read it a second time throws ALT0004.
define token api_key temp = "secret-api-key-abc123"
/ First read consumes the token
define text key = api_key
log "Key used: " + key
/ Second read throws
define text key2 = api_key / ERROR: Token already consumed
Use tokens for one-time passwords, invite codes, CSRF tokens, etc.
Declare a variable with named storage states it can migrate between:
define numeric data orbit 1 "hot" ram, 2 "warm" disk, 3 "cold" cache expire=1h = 0
/ Move to a different state
migrate data as "warm"
migrate data as 3
Try storage tiers in preference order:
define text session prefer ram, cache expire=30m, disk = ""
The runtime uses the first available tier.
Start an HTTP server on a port. The listen block contains route, middleware, health, metrics, and shutdown declarations.
listen 8080;
route "GET" "/hello";
respond.text("Hello, World!")
break
break
The server runs until terminated. Signals (SIGTERM, Ctrl+C) trigger graceful shutdown.
How it works: Altair's built-in HTTP server uses POSIX TCP sockets (no external libraries required). The server is single-threaded but uses a connection-per-request model.
route "METHOD" "/path";
/ handler body
break
Supported methods: GET, POST, PUT, DELETE, PATCH, * (any)
Use :param syntax in paths:
route "GET" "/users/:id";
define text user_id = param("id")
respond.json("found user " + user_id)
break
Inside a route handler:
define text body_data = body() / raw request body
define text auth = header("Authorization") / request header
define text id = param("id") / path parameter
respond.text("plain text response")
respond.json("json string or value")
respond.status(201) / set status code
respond.status(404)
respond.status(500)
Example:
route "POST" "/users";
define text payload = body()
if payload == "";
respond.status(400)
respond.json("missing body")
break
respond.status(201)
respond.json("user created")
break
Middleware runs before every route handler. If the middleware calls stop, the request chain is halted.
middleware auth;
define text token = header("Authorization")
if token == "";
respond.status(401)
respond.json("unauthorized")
stop
break
break
Multiple middleware declarations are chained in order:
listen 8080;
middleware logger;
log "request received"
break
middleware auth;
define text tok = header("X-API-Key")
if tok != "secret";
respond.status(401)
respond.json("bad key")
stop
break
break
route "GET" "/data";
respond.json("protected data")
break
break
Attach a rate limit to any route:
route "POST" "/login" rate_limit 10 per_minute;
/ handle login
respond.json("ok")
break
When the limit is exceeded the runtime automatically returns HTTP 429 with {"error":"rate limit exceeded"}.
Syntax:
route "METHOD" "/path" rate_limit N per_minute;
or
rate_limit N per_second
The health declaration registers a GET route that responds with JSON status:
health "/health";
check "database" -> true
check "cache" -> true
break
Response format:
{"status":"ok","checks":{"database":"ok","cache":"ok"}}
If any check returns false, the HTTP status is 503 Service Unavailable.
The right-hand side of -> is an expression (numeric or bool). Any non-zero value is considered healthy.
Register a Prometheus-compatible metrics endpoint:
metrics "/metrics";
This exposes a GET /metrics endpoint in plain-text Prometheus format.
Incrementing metrics from code:
Use the built-in metric functions (callable from route handlers via expressions):
route "POST" "/events";
/ metric_inc("events_total") - emitted in generated C
respond.json("recorded")
break
The metrics endpoint will output:
# TYPE events_total counter
events_total 42
Register cleanup logic to run when the server receives SIGTERM or calls altair_server_stop():
on_shutdown;
log "Server shutting down, flushing data..."
snapshot create "shutdown_checkpoint"
break
Example with full lifecycle:
listen 8080;
route "GET" "/";
respond.text("running")
break
on_shutdown;
log "bye!"
break
break
Declare session variables with optional TTL:
session user_token expires 30m;
This declares a session variable. Sessions are stored in-process in a key-value store keyed by a session ID. Use header("X-Session-ID") to retrieve the session ID from the client.
Inside route handlers:
route "GET" "/profile";
define text sid = header("X-Session-ID")
define text username = session_get(sid, "username")
if username == "";
respond.status(401)
respond.json("not logged in")
break
respond.json("Hello " + username)
break
route "POST" "/login";
define text user = body()
/ store username in session for 30 minutes
session_set("my-session-id", "username", user, 1800)
respond.json("logged in")
break
Use the config block to declare environment-variable-backed configuration:
config;
env("DATABASE_URL") default "postgres://localhost/mydb"
env("PORT") default "8080"
env("API_SECRET") required
break
default "value" - fallback when the env var is not setrequired - throws ALT0015 at startup if the env var is missingAccessing env values:
The env(...) declaration creates variables in scope named after the key:
config;
env("PORT") default "8080"
break
define numeric port = PORT
listen port;
route "GET" "/";
respond.text("ok")
break
break
Declare a connection pool for a database URL:
db_pool db = connect("postgres://user:pass@localhost/mydb") max 20
This creates a variable db that holds the connection string. Connect your own database client library in the generated C code, or use the stub for prototyping.
The max qualifier sets the pool size (default 10).
Note: Altair v1.6.5vB provides the declaration syntax and connection pooling stub. Full SQL query support requires linking your PostgreSQL/MySQL/SQLite client library in the generated C.
Register recurring background jobs using job:
job cleanup every 5m;
log "running cleanup job"
/ ... cleanup logic ...
break
Syntax:
job <name> every <duration>;
/ job body
break
schedule is an alias for job:
schedule heartbeat every 30s;
log "heartbeat"
break
How it works: Jobs are checked on every request (altair_jobs_tick() is called per connection). In low-traffic scenarios, use wait inside a forever loop for precise timing instead.
Altair can produce windowed graphical programs on top of raylib. This requires raylib to be available at compile time (vendored in libs/raylib/<os>/ or installed system-wide — see libs/raylib/README.md).
link graphics raylib
This must appear once, before any window/loop/draw statements. It tells the compiler to link -lraylib (plus the platform windowing libraries) into the final binary.
window
title = "My Window"
width = 800
height = 600
fps = 60
create window
loop
clear skyblue
/ statements each frame
break
clear <color> clears the frame. break ends the loop block (it does not exit the program — it closes the frame loop definition itself; the generated while runs until the window is closed).
draw <kind>
x = 100
y = 100
...
create draw
| kind | required props | notes |
|---|---|---|
text |
content/text, x, y, size, color |
|
rect / rectangle |
x, y, width/w, height/h, color |
|
circle |
x, y, radius/r, color |
|
line |
x, y, x2, y2, color |
|
line_thick / thick_line |
adds thick/thickness |
|
triangle |
x,y,x2,y2,x3,y3,color |
|
pixel |
x, y, color |
|
image / texture |
image/src (a texture variable), x, y, color |
Colors accept raylib color names (red, gold, skyblue, white, ...) or a color block declaration.
link graphics raylib
window
title = "Paint Test"
width = 800
height = 600
fps = 60
create window
loop
clear skyblue
draw text
content = "Altair Graphics"
x = 10
y = 10
size = 24
color = white
create draw
draw rect
x = 100
y = 100
width = 200
height = 120
color = red
create draw
draw circle
x = 500
y = 200
radius = 60
color = gold
create draw
break
Compile and run:
altairc test/paint.at -o paint
./paint
| Query | Returns |
|---|---|
system@time |
Unix timestamp (numeric) |
system@random |
Random 0.0–1.0 (numeric) |
system@pid |
Process ID (numeric) |
system@hostname |
Machine hostname (text) |
system@username |
OS username (text) |
system@os |
"linux" / "macos" / "windows" (text) |
system@memory |
RAM used in bytes (numeric) |
system@diskfree |
Free disk bytes (numeric) |
compiler@version |
"1.6.5vB" |
compiler@name |
"altairc" |
compiler@build |
Build date |
compiler@architecture |
"x86_64" / "arm64" |
program@name |
From altair.doc header |
program@version |
From altair.doc header |
program@author |
From altair.doc header |
altairc <source.at> [options] Compile an Altair program
altairc guide Write ALTAIR_GUIDE.md to the current directory
altairc guide --stdout Print guide to stdout
altairc --version Print compiler version
altairc --help Show usage
Options:
| Flag | Description |
|---|---|
-o <file> |
Output binary name (default: a.out) |
--emit-c |
Print the generated C source to stdout |
--emit-ast |
Print AST node summary |
--no-sema |
Skip semantic analysis (faster compile) |
-v |
Version number |
Examples:
# Compile a program
altairc hello.at -o hello
# Compile a server
altairc server.at -o myserver
# Debug: see what C is generated
altairc server.at --emit-c | head -200
# Generate the language guide
altairc guide
| Code | Description |
|---|---|
ALT0001 |
Unknown variable or null object access |
ALT0002 |
Type mismatch in arithmetic or comparison |
ALT0003 |
Parse / syntax error |
ALT0004 |
Token already consumed |
ALT0005 |
Snapshot error (create/restore/delete) |
ALT0006 |
Prefer block has no storage entries |
ALT0007 |
Assignment to a const variable |
ALT0008 |
Weight must be a non-negative integer |
ALT0009 |
Orbit has duplicate state numbers |
ALT0010 |
Division or modulo by zero |
ALT0011 |
Unknown introspection key or namespace |
ALT0012 |
Orbit state not found / variable has no orbit |
ALT0013 |
List index out of bounds |
ALT0014 |
Object field not found |
ALT0015 |
Required env var not set |
Added to support self-hosting — i.e. so a compiler for Altair can eventually be written in Altair itself. These are low-level, "systems" features layered on top of the existing type system. Internally they touch every stage of the pipeline:
lexer.c/h(new tokens),parser.c(new grammar),ast.h(VTYPE_FILE,VTYPE_POINTER),codegen.c(new emission cases), andaltair_rt.c/h(newAltVTypetagsALT_FILE/ALT_POINTERand new builtin C functions).
char — single-character textchar is a type alias, not a new runtime type. The parser maps the char
token straight onto VTYPE_TEXT (see tok_to_vtype() in parser.c), so a
char variable is, at the C/runtime level, an ordinary ALT_TEXT value that
happens to hold one character. This means every text operation (+,
comparisons, length()) already works on char for free — no new codegen was
needed.
text code = "hola"
char c = code[0]
log c
text[index]Before this change, [i] indexing (ND_INDEX_ACCESS) only worked on
ALT_LIST. The runtime function altair_list_get() now also handles
ALT_TEXT: it bounds-checks against strlen() and returns a freshly
allocated one-character string. Out-of-range access raises ALT0013, the same
error code used for list bounds.
text s = "abc"
log s[0] / "a"
log length(s) / 3 (length() now also accepts text, not just lists)
file — a file handle typefile is a genuinely new value type: VTYPE_FILE in the AST maps to
ALT_FILE at runtime. Internally, AltairVal gained a void *ptr union
member shared between ALT_FILE (holds a C FILE*) and ALT_POINTER (see
below). Copying a file value (altair_val_copy) is a shallow copy — the
underlying FILE* is shared, exactly like assigning a pointer in C.
file f = open("data.txt")
text contents = read(f)
close(f)
p#Type — raw pointers (updated syntax)This syntax replaces the old
point@Type, which no longer exists. The full keywordpointwas shortened top, and the namespace changed from@to#(to visually distinguish it fromsystem@/compiler@, which are read-only namespaces, whilep#operates on mutable memory).pis now a reserved keyword throughout the language: it can no longer be used as a variable or function name.
Declare and allocate memory (in one step):
p#Type name = alloc(n)
p#Type declares a variable of VTYPE_POINTER (ALT_POINTER at
runtime). The Type after # is stored only for documentation purposes —
the underlying value is a raw memory block of n bytes, so Altair does not
enforce that the pointer only ever points at values of Type.
Namespace p# — operations on an existing pointer:
| Operation | Description |
|---|---|
p#write(name, offset, value) |
Writes value (numeric) into slot offset |
numeric v = p#read(name, offset) |
Reads slot offset |
p#bytes(name) |
Total bytes allocated by alloc(n) |
p#null(name) |
true if the pointer is null or already freed |
p#free(name) |
Frees the memory |
Each offset is an 8-byte slot (a numeric/double), not a raw byte —
so alloc(64) gives 8 usable slots, 0 through 7. Reading or writing
outside those bounds fails safely (p#write returns false, p#read
returns 0), without touching foreign memory.
p#node n = alloc(64)
p#write(n, 0, 42)
p#write(n, 1, 100)
numeric a = p#read(n, 0)
numeric b = p#read(n, 1)
log a / 42
log b / 100
log p#bytes(n) / 64
log p#null(n) / false
p#free(n)
log p#null(n) / true
Why the old version was useless: in the first version (point@Type,
with ptr_alloc/ptr_free/ptr_is_null), the pointer could only allocate
and free memory — there was no way to read or write inside that block, so
it was useless for building linked lists or AST nodes. p#write/p#read
are exactly what was missing to make it actually functional.
Important internal detail: p#free and p#write receive the pointer
by reference (not a copy), so the mutation (freeing memory, setting the
pointer to NULL) is also reflected in the original variable. Every other
function call in Altair receives copies — this is a special case in codegen
(ND_FUNC_CALL with fun_name equal to p_free/p_write uses
cg_expr_receiver instead of cg_expr for its first argument).
New tokens & | ^ ~ << >>, lexed distinctly from &&/|| (the lexer checks
the two-character forms first). New precedence levels were inserted into the
expression grammar, from loosest to tightest, matching C's convention:
comparison ( == != < > <= >= )
|
bitor ( | )
|
bitxor ( ^ )
|
bitand ( & )
|
shift ( << >> )
|
add/sub ( + - )
Each operator compiles to a small runtime helper (altair_band, altair_bor,
altair_bxor, altair_bnot, altair_shl, altair_shr in altair_rt.c)
that requires both operands to be numeric — Altair numbers are double
internally, so bitwise ops cast to long long before applying the C operator
and cast back to double on return. Passing a non-numeric operand raises
ALT0002 (type mismatch).
numeric flags = 6 & 3 / 2
numeric merged = 6 | 3 / 7
numeric x = 1 << 4 / 16
numeric inv = ~0 / -1
These are ordinary function calls (ND_FUNC_CALL) — no new grammar was
needed, because the compiler already had a generic fallback: any call
name(args) that isn't a known list/string method compiles straight to a C
call _fn_name(args). Each builtin below is simply a C function named
_fn_<name> implemented in altair_rt.c.
| Function | Signature | Description |
|---|---|---|
open(path) |
file |
Open for reading ("r" mode). |
open_write(path) |
file |
Open for writing, truncating ("w" mode). |
open_append(path) |
file |
Open for appending ("a" mode). |
read(f) |
text |
Read the entire remaining contents of f. |
read_line(f) |
text |
Read one line (strips the trailing \n/\r). |
write(f, text) |
bool |
Write text to f. Always returns true unless f is invalid. |
close(f) |
bool |
Close f. Safe to call twice. |
create_file(path) |
bool |
Create an empty file if it doesn't already exist. |
delete_file(path) |
bool |
Delete a file. |
mkdir(path) |
bool |
Create a directory (succeeds if it already exists). |
file_exists(path) |
bool |
Check for existence via stat(). |
list_dir(path) |
list |
List entry names in a directory (excludes ./..). |
exec(cmd) |
numeric |
Run cmd via system(); returns the exit code. |
exec_capture(cmd) |
text |
Run cmd via popen(); returns captured stdout. |
create_file("build/out.txt")
file f = open_write("build/out.txt")
write(f, "gcc invoked here")
close(f)
numeric rc = exec("gcc build/main.c -o programa.exe")
if rc == 0;
log "build ok"
break
Path resolution: paths are passed straight to the C standard library
(fopen, stat, opendir, ...) relative to the process's current working
directory — there is no automatic "resolve into a project folder" behavior
for file/open/etc.
Not yet implemented: the
variables/auto-persistence folder,data ... save datablocks,async/safe block, and persistent variables bound to a file (e.g.numeric coins = 120 player.coins) described in the original self-hosting proposal are not part of this build. Everything in sections 31–35 covers only what was actually implemented: file I/O,char, raw pointers, bitwise ops,exec, CLI args, and the bootstrap self-hosted compiler.
p# reference| Syntax | Signature | Description |
|---|---|---|
p#Type name = alloc(n) |
declaration | Allocates n bytes and creates the pointer name |
p#write(name, offset, value) |
bool |
Writes value into slot offset (8 bytes) |
numeric v = p#read(name, offset) |
numeric |
Reads slot offset |
p#bytes(name) |
numeric |
Total bytes allocated |
p#null(name) |
bool |
true if null or already freed |
p#free(name) |
bool |
Frees the memory |
Internally, alloc(n) reserves n bytes plus a hidden header of
sizeof(size_t) bytes right before the address handed to Altair — that
header stores n, which is what p#bytes looks up. p#free frees from
the real start of the block (pointer - sizeof(size_t)), not from where
the Altair variable points.
Pointers are not garbage collected and are not automatically freed
on scope exit — this is intentionally raw, C-style manual memory
management. Using a pointer after p#free is not protected unless you
check p#null yourself first.
p#byte buf = alloc(256)
p#write(buf, 0, 1)
log p#read(buf, 0)
p#free(buf)
Previously, a compiled Altair binary had no way to read its own argv. Since
a self-hosted compiler needs to accept a source file path on the command
line, the generated main(int argc, char **argv) now calls
altair_set_args(argc, argv) before running the program body.
| Function | Signature | Description |
|---|---|---|
argc() |
numeric |
Number of command-line arguments (argv[0] included). |
arg(i) |
text |
The i-th argument (0-indexed; arg(0) is the binary path). |
numeric n = argc()
if n < 2;
log "uso: programa <archivo>"
else;
log "archivo: " + arg(1)
break
selfhost/altair.at is a compiler written entirely in Altair (using the
features from §31–34: file, text indexing, char, and the ordinary control
flow / functions Altair already had). It compiles a deliberately smaller
subset of the language — called Altair-Core — directly to C.
(bootstrap, one-time)
altair.at ──────────────────────────▶ altairc-selfhost
(real Altair) altairc (C) (native binary)
(repeatable, no C compiler needed to invent)
program.atc ─────────────────────────▶ program.gen.c ──▶ gcc ──▶ binary
altairc-selfhost
Build it:
./altairc selfhost/altair.at -o selfhost/altairc-selfhost
Use it:
./altairc-selfhost fib.atc fib.gen.c
gcc fib.gen.c -o fib_bin -lm # -lm needed: numeric % compiles to fmod()
./fib_bin
Altair-Core keeps Altair's if/elif/else...break, while...break, and
fun name -> type ... break shape, but is untyped at the compiler level (no
semantic analysis pass — type errors surface as gcc errors on the
generated C, not as Altair errors). Semicolons after headers are optional:
the Altair-Core lexer treats ; exactly like whitespace.
program := stmt*
stmt := vardecl | assign | log | return | if | while | fun-call
vardecl := ("numeric"|"text"|"bool") IDENT "=" expr
assign := IDENT "=" expr
log := "log" expr
if := "if" expr stmt* ("elif" expr stmt*)* ("else" stmt*)? "break"
while := "while" expr stmt* "break"
fun := "fun" IDENT ("->" type)? (type IDENT ("," type IDENT)*)? stmt* "break"
return := "return" expr?
expr := or ; or := and ("or" and)* ; and := not ("and" not)*
not := "not" not | cmp
cmp := add (("=="|"!="|"<"|">"|"<="|">=") add)?
add := mul (("+"|"-") mul)*
mul := unary (("*"|"/"|"%") unary)*
unary := "-" unary | primary
primary := NUMBER | STRING | "true" | "false" | IDENT | IDENT"("args")" | "("expr")"
| Altair-Core | Emitted C |
|---|---|
numeric |
double |
text |
char* |
bool |
int |
% |
fmod(a, b) (native C % doesn't apply to double) |
+ |
native + — arithmetic only, does not concatenate text |
| Function | Purpose |
|---|---|
concat(a, b) |
Text concatenation (there is no + overload for text). |
streq(a, b) |
Text equality (==/!= on char* would compare pointers, not content). |
numstr(n) |
numeric → text, for use with log. |
fun fib -> numeric numeric n;
if n < 2;
return n
break
return fib(n - 1) + fib(n - 2)
break
numeric i = 0
while i < 10;
log numstr(fib(i))
i = i + 1
break
log concat("hola, ", "mundo")
altairc-selfhost can compile Altair-Core programs, but cannot yet compile
its own source (altair.at), because altair.at itself uses features
Altair-Core doesn't support: list, file/open/read/close, text
indexing, and multi-parameter-type declarations beyond simple scalars. Closing
that loop — rewriting altair.at using only Altair-Core, after extending
Altair-Core with lists and file I/O — is the natural next step and is not
done in this build.
What is proven to work end-to-end, and was tested:
- altairc (C) successfully compiles altair.at (real Altair) → altairc-selfhost.
- altairc-selfhost successfully compiles Altair-Core programs (recursion,
if/elif/else, while, string concatenation, %, text comparison) → C.
- The generated C compiles cleanly with gcc and produces correct output.
altair.doc;
name = TodoAPI
version = 1.6.5vB
author = "Altair Team"
create altair.doc
/ Configuration
config;
env("PORT") default "8080"
env("API_SECRET") required
break
/ In-memory todo list
define list todos disk = []
define numeric next_id = 1
/ Auth middleware
middleware auth;
define text key = header("X-API-Key")
define text secret = API_SECRET
if key != secret;
respond.status(401)
respond.json("{\"error\":\"unauthorized\"}")
stop
break
break
/ Jobs
job persist every 5m;
snapshot create "todos_checkpoint"
log "todos saved"
break
/ Health
health "/health";
check "storage" -> 1
break
/ Metrics
metrics "/metrics";
/ Graceful shutdown
on_shutdown;
snapshot create "shutdown"
log "shutting down, saved state"
break
/ Routes
listen PORT;
route "GET" "/todos";
respond.json(todos)
break
route "POST" "/todos" rate_limit 60 per_minute;
define text title = body()
if title == "";
respond.status(400)
respond.json("{\"error\":\"title required\"}")
break
todos.append(title)
respond.status(201)
respond.json("{\"created\":true}")
break
route "DELETE" "/todos/:idx";
define text idx_str = param("idx")
todos.remove(0)
respond.json("{\"deleted\":true}")
break
route "GET" "/";
respond.text("TodoAPI v1.6.5vB running")
break
break
Run:
altairc todo.at -o todo
API_SECRET=mysecret PORT=3000 ./todo
Test:
curl http://localhost:3000/
curl -H "X-API-Key: mysecret" http://localhost:3000/todos
curl -H "X-API-Key: mysecret" -X POST -d "Buy milk" http://localhost:3000/todos
curl http://localhost:3000/health
curl http://localhost:3000/metrics
data Block — grouped variable containersGroups several variables under a common name and shared storage "place".
data, p, and # are now also reserved keywords.
data Name place;
type var1 = value1
type var2 = value2
data create
place can be one of the four normal storage tiers (ram, disk,
cache, temp) or any custom identifier (a user-defined extension,
e.g. data PlayerSave save; ... data create).data create (not break).variables/<Name>.<var>.<place>. That's why each field
gets its own file — necessary so that migrate can move all files
sharing the <Name>. prefix at once.data PlayerSave disk;
numeric coins = 120
text name = "Nombre"
data create
log coins
log name
coins = coins + 5
This creates variables/PlayerSave.coins.disk and
variables/PlayerSave.name.disk, and every subsequent assignment to
coins or name keeps them up to date (same as in §35).
data Name migrate place
Finds all variables/Name.*. files and renames them to the new place.
This is a real file operation (rename()), not a stub.
Known limitation — important: the file each variable reads from and
writes to is fixed at compile time (the name is baked into the
generated C). migrate renames the file on disk, but if the program keeps
running after the migrate and that variable gets reassigned again, or the
program simply exits (the final shutdown dump also uses the original
name), the old file gets recreated under the previous name. In other
words: migrate works as a one-off relabeling/export operation, but it
does not turn the running program into a dynamic consumer of the new
location — it's not a hot storage-tier switch that the program itself
remembers and honors afterward.
data PlayerSave disk;
numeric coins = 120
data create
coins = coins + 5
data PlayerSave migrate cache
/ at this point variables/PlayerSave.coins.cache exists with value 125,
/ but if the program reassigns coins again, or simply exits,
/ variables/PlayerSave.coins.disk gets recreated too.
Altair Language Reference - v1.7 - Generated by altairc guide
Compiler: altairc 1.7 | License: MIT