qillqa: el awk propio — 3531 líneas, sólo libc, y CERO divergencias contra dos oráculos
«awk soberano» (usuario, 2026-09-22), que era la tercera opción del ADR pendiente
y la única que no adoptaba la semántica de otro: gawk es C con extensiones GNU,
goawk es Go y rompe la premisa de cadena de la Etapa C, frawk es otro dialecto.
Banco diferencial contra DOS oráculos a la vez —el awk de busybox 1.36.1 (el
applet que hoy viaja) y gawk 5.3.2 --posix— con 109 casos: campos y separadores,
el modelo de valores, aritmética, expresiones regulares, las funciones de cadena,
printf, control de flujo, arrays, funciones de usuario, E/S y los rincones del
parser. Resultado: 104 iguales a los dos, 0 divergencias, y 5 casos donde los
ORÁCULOS discrepan entre sí — que es justo para lo que sirve tener dos.
De esos 5, cuatro los tenemos del lado correcto por POSIX: `2^3^2` es 512 (la
potencia asocia a la derecha; busybox da 64), `index(s,"")` es 1, el `%*d` de
printf existe, y `length(array)` funciona. El quinto era nuestro y se arregló:
una coincidencia VACÍA pegada al final de la anterior no sustituye, así que
`gsub(/a*/,"-")` sobre "xax" da 3 y `-x-x-`, no 4 y `-x--x-`.
El motor de expresiones regulares es propio y esa es la razón de fondo para no
adoptar un crate: POSIX es leftmost-LONGEST y la familia Perl —el crate `regex`
en su modo por defecto— es leftmost-first. Para /a|ab/ sobre "ab", POSIX devuelve
`ab` y Perl devuelve `a`. En un awk eso decide qué borra un sub(), dónde parte un
split() y cuánto valen RSTART y RLENGTH. Construcción de Thompson con conjunto de
hilos: sin retroceso, así que `(a*)*b` contra sesenta `a` no explota.
Las cinco cosas que el banco destapó, todas dando resultados plausibles y ninguna
reventando —quedan como pruebas de regresión en tests/divergencias_medidas.rs:
· leer NF no forzaba el partido en campos ⇒ `{print NF}` daba 0 hasta que
alguien tocara un campo. El partido es perezoso a propósito (es lo que hace
barato asignar a $0) y el precio es acordarse de forzarlo.
· `length` sin paréntesis se leía como una VARIABLE llamada length, que vale
vacío. Es el único builtin al que POSIX permite omitirlos.
· los arrays se pasan por referencia, y eso se decide mirando si la función usa
el parámetro como array — `a["k"]=1` tiene el nombre en el LUGAR de la
asignación, y sólo se miraba el valor.
· substr con inicio ≤ 0 recorta a 1 SIN ajustar el largo, y los no enteros se
truncan. La lectura estricta de POSIX daría otra cosa; ni busybox ni gawk la
hacen, y lo que hay que ejecutar son los guiones escritos contra ellos.
· la regla del vacío en gsub, arriba.
Verificado además contra este repo: de los 61 programas awk únicos que usan sus
guiones y recetas, qillqa acepta los 59 reales (los 2 restantes son f-strings de
Python que mi extractor tomó por awk). 29 pruebas propias en verde.
El nombre es quechua («escritura») y sigue el precedente de simi: lo registra el
comentario de cabecera y lo DECIDE un ADR. El comando instalado será `awk` — eso
es contrato y lo llaman los configure de medio corpus.
⚠ Lo medido es el build NATIVO del hub. Falta la receta, el estático musl con zig
cc, y la prueba que de verdad decide: un `configure` de autotools con qillqa como
/usr/bin/awk. Es la lección de brush — pasó 56/57 casos POSIX y no construía una
sola receta.
Va también recipes/ncurses-tools.toml (clear, reset, tput, tset, infocmp, tic),
declarada en `base`: la canónica instala sólo lib y cabeceras, así que clear y
reset en una imagen eran el applet de busybox. Instala los binarios y NO la base
terminfo: resuelven con los --with-fallbacks compilados en la librería, que
cubren la consola del metal, QEMU y cualquier ssh moderno.
⚠ Y ncurses-tools selló DINÁMICO dos veces antes de salir bien: ncurses usa
`-static`/`-dynamic` como MARCADORES de tramo alrededor de las librerías —su
forma portable de -Wl,-Bstatic … -Wl,-Bdynamic—, no como decisión global. El
`-dynamic` llega al final de la línea de enlace y gana por orden. Se quita
sobreescribiendo LIBS_TIC y LIBS_TINFO en la línea de make. Tercer sabor del
agujero que documenta scripts/static-audit.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -881,6 +881,13 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qillqa"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
|
||||
@@ -14,6 +14,7 @@ members = [
|
||||
"crates/hammerd",
|
||||
"crates/netup",
|
||||
"crates/simi",
|
||||
"crates/qillqa",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "qillqa"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "qillqa — el awk del producto: POSIX, sin deps, con su propio motor de expresiones regulares."
|
||||
|
||||
[[bin]]
|
||||
name = "qillqa"
|
||||
path = "src/main.rs"
|
||||
|
||||
# Cero crates de terceros, igual que simi. Un awk necesita leer ficheros, escribir a tuberías y
|
||||
# lanzar procesos (`system`, `print | "cmd"`, `getline < f`), y eso es libc. Lo demás —el motor ERE,
|
||||
# la conversión número/cadena, el printf— es propio: son justamente las piezas donde un awk se
|
||||
# diferencia de otro, y adoptarlas de un crate sería adoptar su semántica en vez de la de POSIX.
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,133 @@
|
||||
//! El árbol del programa.
|
||||
|
||||
use std::rc::Rc;
|
||||
use crate::regex::Regex;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum BinOp { Suma, Resta, Mult, Div, Modulo, Potencia }
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CmpOp { Lt, Le, Gt, Ge, Eq, Ne }
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Expr {
|
||||
Num(f64),
|
||||
Cad(String),
|
||||
/// Una ERE suelta: como expresión vale `$0 ~ /re/`.
|
||||
Ere(Rc<Regex>),
|
||||
Var(String),
|
||||
Campo(Box<Expr>),
|
||||
/// `a[i]`, `a[i,j]` (los índices se unen con SUBSEP).
|
||||
Indice(String, Vec<Expr>),
|
||||
Asigna(Box<Lugar>, Box<Expr>),
|
||||
/// `+=`, `-=`, … con su operador.
|
||||
AsignaOp(Box<Lugar>, BinOp, Box<Expr>),
|
||||
Bin(BinOp, Box<Expr>, Box<Expr>),
|
||||
Cmp(CmpOp, Box<Expr>, Box<Expr>),
|
||||
/// `~` y `!~`. El segundo `bool` dice si está negado.
|
||||
Coincide(Box<Expr>, Box<Expr>, bool),
|
||||
Y(Box<Expr>, Box<Expr>),
|
||||
O(Box<Expr>, Box<Expr>),
|
||||
No(Box<Expr>),
|
||||
Neg(Box<Expr>),
|
||||
Pos(Box<Expr>),
|
||||
Ternario(Box<Expr>, Box<Expr>, Box<Expr>),
|
||||
/// Concatenación por yuxtaposición: `a b`.
|
||||
Concat(Box<Expr>, Box<Expr>),
|
||||
/// `++x` / `x++` (y `--`). `previo` = es prefijo.
|
||||
Incr(Box<Lugar>, bool, f64),
|
||||
/// `(i, j) in a` y `i in a`.
|
||||
En(Vec<Expr>, String),
|
||||
Llamada(String, Vec<Expr>),
|
||||
Builtin(String, Vec<Expr>),
|
||||
/// `getline` en sus cuatro formas.
|
||||
Getline(Getline),
|
||||
/// `(expr)` agrupada: hay que recordarlo para distinguir `(a,b) in arr` de una lista.
|
||||
Grupo(Box<Expr>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Getline {
|
||||
/// `getline [lugar]` — de la entrada normal.
|
||||
Simple(Option<Box<Lugar>>),
|
||||
/// `getline [lugar] < "fichero"`.
|
||||
DeFichero(Option<Box<Lugar>>, Box<Expr>),
|
||||
/// `"cmd" | getline [lugar]`.
|
||||
DeTuberia(Box<Expr>, Option<Box<Lugar>>),
|
||||
}
|
||||
|
||||
/// Algo a lo que se puede asignar.
|
||||
#[derive(Clone)]
|
||||
pub enum Lugar {
|
||||
Var(String),
|
||||
Campo(Box<Expr>),
|
||||
Indice(String, Vec<Expr>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Redir {
|
||||
/// `> f`
|
||||
Fichero(Box<Expr>),
|
||||
/// `>> f`
|
||||
Anexa(Box<Expr>),
|
||||
/// `| "cmd"`
|
||||
Tuberia(Box<Expr>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Sent {
|
||||
Expr(Expr),
|
||||
Print(Vec<Expr>, Option<Redir>),
|
||||
Printf(Vec<Expr>, Option<Redir>),
|
||||
Bloque(Vec<Sent>),
|
||||
Si(Expr, Box<Sent>, Option<Box<Sent>>),
|
||||
Mientras(Expr, Box<Sent>),
|
||||
HazMientras(Box<Sent>, Expr),
|
||||
/// `for (init; cond; paso) cuerpo`
|
||||
Para(Option<Box<Sent>>, Option<Expr>, Option<Box<Sent>>, Box<Sent>),
|
||||
/// `for (k in a) cuerpo`
|
||||
ParaEn(String, String, Box<Sent>),
|
||||
Corta,
|
||||
Sigue,
|
||||
Siguiente,
|
||||
SiguienteFichero,
|
||||
Sale(Option<Expr>),
|
||||
Retorna(Option<Expr>),
|
||||
Borra(String, Vec<Expr>),
|
||||
/// `delete a` entero.
|
||||
BorraTodo(String),
|
||||
Nada,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Patron {
|
||||
Begin,
|
||||
End,
|
||||
/// Sin patrón: la acción corre para cada registro.
|
||||
Siempre,
|
||||
Expr(Expr),
|
||||
/// `expr1, expr2` — rango. El `bool` es el estado (dentro/fuera), que vive en el intérprete.
|
||||
Rango(Expr, Expr),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Regla {
|
||||
pub patron: Patron,
|
||||
pub accion: Option<Vec<Sent>>,
|
||||
/// Índice del estado de rango, para las reglas `expr1, expr2`.
|
||||
pub rango_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Funcion {
|
||||
pub nombre: String,
|
||||
pub params: Vec<String>,
|
||||
pub cuerpo: Vec<Sent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Programa {
|
||||
pub reglas: Vec<Regla>,
|
||||
pub funciones: Vec<Funcion>,
|
||||
pub rangos: usize,
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
//! `printf`/`sprintf` de awk, y la conversión número→cadena que usan CONVFMT y OFMT.
|
||||
//!
|
||||
//! Se escribe a mano porque el `format!` de Rust no habla el mismo idioma: no tiene `%c`, no tiene
|
||||
//! el `*` que toma la anchura de un argumento, y su `{:e}` no produce lo que produce `%e` de C
|
||||
//! (`1e2` contra `1.000000e+02`). Y porque awk manda cosas que C no: `%c` con un número es el
|
||||
//! carácter de ese código, pero `%c` con una cadena es su PRIMER carácter.
|
||||
//!
|
||||
//! Los formatos admitidos son los de POSIX: `%d %i %o %u %x %X %c %s %e %E %f %F %g %G %a %A %%`,
|
||||
//! con banderas `-+ #0`, anchura, precisión y `*` en las dos.
|
||||
|
||||
pub struct Campo {
|
||||
pub izquierda: bool,
|
||||
pub signo: bool,
|
||||
pub espacio: bool,
|
||||
pub alterno: bool,
|
||||
pub cero: bool,
|
||||
pub ancho: Option<usize>,
|
||||
pub precision: Option<usize>,
|
||||
pub conv: char,
|
||||
}
|
||||
|
||||
/// Aplica un formato de UNA conversión a un número. Lo usa `numero_a_cadena` para CONVFMT/OFMT,
|
||||
/// donde el formato viene de una variable del programa y puede ser cualquier cosa.
|
||||
pub fn una_conversion(fmt: &str, n: f64) -> String {
|
||||
let mut salida = String::new();
|
||||
let cs: Vec<char> = fmt.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < cs.len() {
|
||||
if cs[i] != '%' { salida.push(cs[i]); i += 1; continue; }
|
||||
if i + 1 < cs.len() && cs[i + 1] == '%' { salida.push('%'); i += 2; continue; }
|
||||
let (campo, siguiente) = match analizar(&cs, i + 1, &mut Vec::new(), &mut 0) {
|
||||
Some(x) => x,
|
||||
None => { salida.push('%'); i += 1; continue; }
|
||||
};
|
||||
salida.push_str(&aplicar_numero(&campo, n));
|
||||
i = siguiente;
|
||||
// Sólo la PRIMERA conversión consume el número; el resto del formato se copia tal cual.
|
||||
while i < cs.len() { salida.push(cs[i]); i += 1; }
|
||||
}
|
||||
salida
|
||||
}
|
||||
|
||||
/// Lee un especificador tras el `%`. `args` y `pos` sirven para los `*`, que consumen argumentos.
|
||||
fn analizar(cs: &[char], mut i: usize, anchos: &mut Vec<i64>, _pos: &mut usize) -> Option<(Campo, usize)> {
|
||||
let mut c = Campo {
|
||||
izquierda: false, signo: false, espacio: false, alterno: false, cero: false,
|
||||
ancho: None, precision: None, conv: 's',
|
||||
};
|
||||
loop {
|
||||
match cs.get(i) {
|
||||
Some('-') => c.izquierda = true,
|
||||
Some('+') => c.signo = true,
|
||||
Some(' ') => c.espacio = true,
|
||||
Some('#') => c.alterno = true,
|
||||
Some('0') => c.cero = true,
|
||||
_ => break,
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if cs.get(i) == Some(&'*') {
|
||||
i += 1;
|
||||
let v = if anchos.is_empty() { 0 } else { anchos.remove(0) };
|
||||
if v < 0 { c.izquierda = true; c.ancho = Some((-v) as usize); } else { c.ancho = Some(v as usize); }
|
||||
} else {
|
||||
let mut n = String::new();
|
||||
while matches!(cs.get(i), Some(d) if d.is_ascii_digit()) { n.push(cs[i]); i += 1; }
|
||||
if !n.is_empty() { c.ancho = n.parse().ok(); }
|
||||
}
|
||||
if cs.get(i) == Some(&'.') {
|
||||
i += 1;
|
||||
if cs.get(i) == Some(&'*') {
|
||||
i += 1;
|
||||
let v = if anchos.is_empty() { 0 } else { anchos.remove(0) };
|
||||
c.precision = Some(v.max(0) as usize);
|
||||
} else {
|
||||
let mut n = String::new();
|
||||
while matches!(cs.get(i), Some(d) if d.is_ascii_digit()) { n.push(cs[i]); i += 1; }
|
||||
c.precision = Some(n.parse().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
// Modificadores de longitud de C: se aceptan y se ignoran, que es lo que hacen los awk.
|
||||
while matches!(cs.get(i), Some('h') | Some('l') | Some('L') | Some('q') | Some('j') | Some('z') | Some('t')) { i += 1; }
|
||||
let conv = *cs.get(i)?;
|
||||
c.conv = conv;
|
||||
Some((c, i + 1))
|
||||
}
|
||||
|
||||
fn rellenar(s: String, c: &Campo, numerico: bool) -> String {
|
||||
let ancho = match c.ancho { None => return s, Some(a) => a };
|
||||
let largo = s.chars().count();
|
||||
if largo >= ancho { return s; }
|
||||
let faltan = ancho - largo;
|
||||
if c.izquierda { return s + &" ".repeat(faltan); }
|
||||
if c.cero && numerico && c.precision.is_none() {
|
||||
// El cero va DESPUÉS del signo: `%05d` de -1 es `-0001`, no `000-1`.
|
||||
let (signo, resto) = match s.chars().next() {
|
||||
Some(x) if x == '-' || x == '+' || x == ' ' => (s[..1].to_string(), s[1..].to_string()),
|
||||
_ => (String::new(), s),
|
||||
};
|
||||
return signo + &"0".repeat(faltan) + &resto;
|
||||
}
|
||||
" ".repeat(faltan) + &s
|
||||
}
|
||||
|
||||
fn con_signo(s: String, negativo: bool, c: &Campo) -> String {
|
||||
if negativo { return s; }
|
||||
if c.signo { return format!("+{s}"); }
|
||||
if c.espacio { return format!(" {s}"); }
|
||||
s
|
||||
}
|
||||
|
||||
fn aplicar_numero(c: &Campo, n: f64) -> String {
|
||||
let cuerpo = match c.conv {
|
||||
'd' | 'i' => {
|
||||
// awk trunca hacia cero, como C.
|
||||
let v = if n.is_nan() { 0 } else { n.trunc() as i64 };
|
||||
let mut s = v.abs().to_string();
|
||||
if let Some(p) = c.precision { if s.len() < p { s = "0".repeat(p - s.len()) + &s; } }
|
||||
let s = if v < 0 { format!("-{s}") } else { s };
|
||||
con_signo(s, v < 0, c)
|
||||
}
|
||||
'o' | 'x' | 'X' | 'u' => {
|
||||
// Los negativos se ven como unsigned de 64 bits, igual que C.
|
||||
let v = if n.is_nan() { 0i64 } else { n.trunc() as i64 };
|
||||
let u = v as u64;
|
||||
let mut s = match c.conv {
|
||||
'o' => format!("{u:o}"),
|
||||
'x' => format!("{u:x}"),
|
||||
'X' => format!("{u:X}"),
|
||||
_ => format!("{u}"),
|
||||
};
|
||||
if let Some(p) = c.precision { if s.len() < p { s = "0".repeat(p - s.len()) + &s; } }
|
||||
if c.alterno {
|
||||
match c.conv {
|
||||
'o' if !s.starts_with('0') => s = format!("0{s}"),
|
||||
'x' if u != 0 => s = format!("0x{s}"),
|
||||
'X' if u != 0 => s = format!("0X{s}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
'c' => {
|
||||
let cod = n.trunc() as u32;
|
||||
char::from_u32(cod).map(|x| x.to_string()).unwrap_or_default()
|
||||
}
|
||||
'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'a' | 'A' => {
|
||||
let p = c.precision.unwrap_or(6);
|
||||
let neg = n.is_sign_negative() && n != 0.0;
|
||||
let a = n.abs();
|
||||
let s = match c.conv {
|
||||
'f' | 'F' => format!("{a:.p$}", p = p),
|
||||
'e' | 'E' => exponencial(a, p, c.conv == 'E'),
|
||||
'a' | 'A' => format!("{a}"), // hexadecimal de coma flotante: rarísimo en awk
|
||||
_ => general(a, p, c.conv == 'G', c.alterno),
|
||||
};
|
||||
let s = if neg { format!("-{s}") } else { s };
|
||||
con_signo(s, neg, c)
|
||||
}
|
||||
's' => String::new(),
|
||||
otro => format!("%{otro}"),
|
||||
};
|
||||
rellenar(cuerpo, c, c.conv != 's' && c.conv != 'c')
|
||||
}
|
||||
|
||||
fn exponencial(a: f64, p: usize, mayus: bool) -> String {
|
||||
let s = format!("{:.*e}", p, a);
|
||||
// Rust escribe `1.5e2`; C quiere `1.5e+02`, con al menos dos dígitos y signo siempre.
|
||||
let (mant, exp) = match s.split_once('e') { Some(x) => x, None => return s };
|
||||
let n: i32 = exp.parse().unwrap_or(0);
|
||||
let e = if mayus { 'E' } else { 'e' };
|
||||
format!("{mant}{e}{}{:02}", if n < 0 { '-' } else { '+' }, n.abs())
|
||||
}
|
||||
|
||||
/// `%g`: el más corto entre `%e` y `%f`, y sin ceros a la derecha salvo con `#`.
|
||||
fn general(a: f64, p: usize, mayus: bool, alterno: bool) -> String {
|
||||
let p = if p == 0 { 1 } else { p };
|
||||
let exp = if a == 0.0 { 0 } else { a.abs().log10().floor() as i32 };
|
||||
let usa_e = exp < -4 || exp >= p as i32;
|
||||
let mut s = if usa_e {
|
||||
exponencial(a, p - 1, mayus)
|
||||
} else {
|
||||
let dec = (p as i32 - 1 - exp).max(0) as usize;
|
||||
format!("{a:.dec$}")
|
||||
};
|
||||
if !alterno && s.contains('.') {
|
||||
// Quitar ceros de cola, y el punto si queda solo. Con exponente hay que respetar la parte
|
||||
// de después de la `e`.
|
||||
if let Some(pos_e) = s.find(|c| c == 'e' || c == 'E') {
|
||||
let (m, e) = s.split_at(pos_e);
|
||||
let m = m.trim_end_matches('0').trim_end_matches('.');
|
||||
s = format!("{m}{e}");
|
||||
} else {
|
||||
s = s.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// El `printf` completo: recorre el formato consumiendo argumentos.
|
||||
pub fn printf(fmt: &str, args: &[Arg], convfmt: &str) -> String {
|
||||
let cs: Vec<char> = fmt.chars().collect();
|
||||
let mut salida = String::new();
|
||||
let mut i = 0;
|
||||
let mut n_arg = 0;
|
||||
// Los `*` consumen argumentos por delante; se resuelven al vuelo.
|
||||
while i < cs.len() {
|
||||
if cs[i] != '%' { salida.push(cs[i]); i += 1; continue; }
|
||||
if cs.get(i + 1) == Some(&'%') { salida.push('%'); i += 2; continue; }
|
||||
// Contar cuántos `*` trae este especificador para sacarlos de los argumentos.
|
||||
let mut anchos: Vec<i64> = Vec::new();
|
||||
{
|
||||
let mut j = i + 1;
|
||||
while matches!(cs.get(j), Some('-') | Some('+') | Some(' ') | Some('#') | Some('0')) { j += 1; }
|
||||
if cs.get(j) == Some(&'*') {
|
||||
anchos.push(args.get(n_arg).map(|a| a.numero() as i64).unwrap_or(0));
|
||||
n_arg += 1;
|
||||
j += 1;
|
||||
} else { while matches!(cs.get(j), Some(d) if d.is_ascii_digit()) { j += 1; } }
|
||||
if cs.get(j) == Some(&'.') {
|
||||
j += 1;
|
||||
if cs.get(j) == Some(&'*') {
|
||||
anchos.push(args.get(n_arg).map(|a| a.numero() as i64).unwrap_or(0));
|
||||
n_arg += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let (campo, sig) = match analizar(&cs, i + 1, &mut anchos, &mut n_arg) {
|
||||
Some(x) => x,
|
||||
None => { salida.push('%'); i += 1; continue; }
|
||||
};
|
||||
i = sig;
|
||||
let arg = args.get(n_arg);
|
||||
n_arg += 1;
|
||||
match campo.conv {
|
||||
's' => {
|
||||
let mut s = arg.map(|a| a.cadena(convfmt)).unwrap_or_default();
|
||||
if let Some(p) = campo.precision { s = s.chars().take(p).collect(); }
|
||||
salida.push_str(&rellenar(s, &campo, false));
|
||||
}
|
||||
'c' => {
|
||||
// ⚠ `%c` mira el TIPO: con un número es el carácter de ese código; con una cadena,
|
||||
// su primer carácter. Un `printf "%c", 65` da `A`, y `printf "%c", "65"` da `6`.
|
||||
let s = match arg {
|
||||
None => String::new(),
|
||||
Some(a) => match a.caracter(convfmt) { Some(c) => c.to_string(), None => String::new() },
|
||||
};
|
||||
salida.push_str(&rellenar(s, &campo, false));
|
||||
}
|
||||
_ => {
|
||||
let n = arg.map(|a| a.numero()).unwrap_or(0.0);
|
||||
salida.push_str(&aplicar_numero(&campo, n));
|
||||
}
|
||||
}
|
||||
}
|
||||
salida
|
||||
}
|
||||
|
||||
/// Lo que `printf` necesita saber de un argumento. Se define acá para que `formato` no dependa del
|
||||
/// intérprete entero.
|
||||
pub enum Arg {
|
||||
Numero(f64),
|
||||
Texto(String),
|
||||
/// De la entrada: `%c` lo trata como cadena, `%d` como número.
|
||||
Entrada(String, Option<f64>),
|
||||
}
|
||||
|
||||
impl Arg {
|
||||
pub fn numero(&self) -> f64 {
|
||||
match self {
|
||||
Arg::Numero(n) => *n,
|
||||
Arg::Texto(s) => crate::valor::prefijo_numerico(s),
|
||||
Arg::Entrada(s, n) => n.unwrap_or_else(|| crate::valor::prefijo_numerico(s)),
|
||||
}
|
||||
}
|
||||
pub fn cadena(&self, convfmt: &str) -> String {
|
||||
match self {
|
||||
Arg::Numero(n) => crate::valor::numero_a_cadena(*n, convfmt),
|
||||
Arg::Texto(s) => s.clone(),
|
||||
Arg::Entrada(s, _) => s.clone(),
|
||||
}
|
||||
}
|
||||
fn caracter(&self, convfmt: &str) -> Option<char> {
|
||||
match self {
|
||||
Arg::Numero(n) => char::from_u32(n.trunc() as u32),
|
||||
Arg::Texto(s) => s.chars().next(),
|
||||
// Un strnum con pinta de número se comporta como número.
|
||||
Arg::Entrada(s, Some(n)) => { let _ = convfmt; char::from_u32(n.trunc() as u32).or_else(|| s.chars().next()) }
|
||||
Arg::Entrada(s, None) => s.chars().next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pruebas {
|
||||
use super::*;
|
||||
|
||||
fn p(fmt: &str, args: Vec<Arg>) -> String { printf(fmt, &args, "%.6g") }
|
||||
|
||||
#[test]
|
||||
fn enteros_con_ancho_y_signo() {
|
||||
assert_eq!(p("%d", vec![Arg::Numero(42.0)]), "42");
|
||||
assert_eq!(p("%5d|", vec![Arg::Numero(42.0)]), " 42|");
|
||||
assert_eq!(p("%-5d|", vec![Arg::Numero(42.0)]), "42 |");
|
||||
assert_eq!(p("%05d", vec![Arg::Numero(-1.0)]), "-0001"); // el cero va tras el signo
|
||||
assert_eq!(p("%+d", vec![Arg::Numero(7.0)]), "+7");
|
||||
assert_eq!(p("%d", vec![Arg::Numero(-3.9)]), "-3"); // trunca hacia cero
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coma_flotante() {
|
||||
assert_eq!(p("%.2f", vec![Arg::Numero(3.14159)]), "3.14");
|
||||
assert_eq!(p("%e", vec![Arg::Numero(150.0)]), "1.500000e+02");
|
||||
assert_eq!(p("%g", vec![Arg::Numero(150.0)]), "150");
|
||||
assert_eq!(p("%g", vec![Arg::Numero(0.0000123)]), "1.23e-05");
|
||||
assert_eq!(p("%.3g", vec![Arg::Numero(3.14159)]), "3.14");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn porciento_c_mira_el_tipo() {
|
||||
assert_eq!(p("%c", vec![Arg::Numero(65.0)]), "A");
|
||||
assert_eq!(p("%c", vec![Arg::Texto("65".into())]), "6");
|
||||
assert_eq!(p("%c", vec![Arg::Texto("héroe".into())]), "h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el_asterisco_toma_la_anchura_del_argumento() {
|
||||
assert_eq!(p("%*d|", vec![Arg::Numero(5.0), Arg::Numero(42.0)]), " 42|");
|
||||
assert_eq!(p("%.*f", vec![Arg::Numero(2.0), Arg::Numero(3.14159)]), "3.14");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadenas_con_precision() {
|
||||
assert_eq!(p("%.3s", vec![Arg::Texto("abcdef".into())]), "abc");
|
||||
assert_eq!(p("%10.3s|", vec![Arg::Texto("abcdef".into())]), " abc|");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faltan_argumentos_no_es_error() {
|
||||
assert_eq!(p("%s-%d", vec![]), "-0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hexadecimal_y_octal() {
|
||||
assert_eq!(p("%x", vec![Arg::Numero(255.0)]), "ff");
|
||||
assert_eq!(p("%#X", vec![Arg::Numero(255.0)]), "0XFF");
|
||||
assert_eq!(p("%o", vec![Arg::Numero(8.0)]), "10");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,249 @@
|
||||
//! Analizador léxico de awk.
|
||||
//!
|
||||
//! Dos cosas hacen a este lexer distinto de uno corriente, y las dos vienen de la gramática de awk:
|
||||
//!
|
||||
//! 1. **`/` es ambiguo**: puede abrir una expresión regular (`/^foo/`) o ser una división (`a / b`).
|
||||
//! No se puede decidir mirando sólo el carácter. La regla —la misma que usan awk, sed y los
|
||||
//! lenguajes con literales de regex— es MIRAR EL TOKEN ANTERIOR: tras un valor (número, cadena,
|
||||
//! identificador, `)`, `]`, `$x`, un incremento) una `/` es división; en cualquier otro sitio
|
||||
//! abre una ERE. Por eso el lexer recuerda el último token emitido.
|
||||
//!
|
||||
//! 2. **El salto de línea es SIGNIFICATIVO**: termina una sentencia. Pero no siempre — tras `{`,
|
||||
//! `&&`, `||`, `,`, `do`, `else` y un `)` de `if`/`for`/`while`, la línea continúa. El lexer
|
||||
//! emite `Nueva` y es el parser quien decide cuándo ignorarla; mezclarlo acá haría falta un
|
||||
//! lexer que entienda de sintaxis.
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Tok {
|
||||
Num(f64),
|
||||
Cad(String),
|
||||
Ere(String),
|
||||
Ident(String),
|
||||
/// Nombre seguido de `(` SIN espacio: sólo así se llama a una función en awk.
|
||||
Funcion(String),
|
||||
PalabraClave(String),
|
||||
/// Operador o signo de puntuación, tal cual.
|
||||
Op(String),
|
||||
Nueva,
|
||||
Fin,
|
||||
}
|
||||
|
||||
const CLAVES: &[&str] = &[
|
||||
"BEGIN", "END", "function", "func", "if", "else", "while", "for", "do", "break", "continue",
|
||||
"next", "nextfile", "exit", "return", "delete", "in", "getline", "print", "printf",
|
||||
];
|
||||
|
||||
pub struct Lexer {
|
||||
src: Vec<char>,
|
||||
i: usize,
|
||||
anterior: Option<Tok>,
|
||||
}
|
||||
|
||||
impl Lexer {
|
||||
pub fn nuevo(src: &str) -> Lexer {
|
||||
Lexer { src: src.chars().collect(), i: 0, anterior: None }
|
||||
}
|
||||
|
||||
fn ver(&self) -> Option<char> { self.src.get(self.i).copied() }
|
||||
fn ver2(&self) -> Option<char> { self.src.get(self.i + 1).copied() }
|
||||
|
||||
/// ¿Una `/` aquí abre una ERE? Depende de lo último que se emitió.
|
||||
fn toca_ere(&self) -> bool {
|
||||
match &self.anterior {
|
||||
None => true,
|
||||
Some(Tok::Num(_)) | Some(Tok::Cad(_)) | Some(Tok::Ere(_)) | Some(Tok::Ident(_)) => false,
|
||||
Some(Tok::Op(o)) => !matches!(o.as_str(), ")" | "]" | "++" | "--" | "$"),
|
||||
Some(Tok::PalabraClave(k)) => !matches!(k.as_str(), "getline"),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn espacios_y_comentarios(&mut self) {
|
||||
loop {
|
||||
match self.ver() {
|
||||
Some(' ') | Some('\t') | Some('\r') => { self.i += 1; }
|
||||
// `\` al final de línea: continuación explícita, no hay token de nueva línea.
|
||||
Some('\\') if self.ver2() == Some('\n') => { self.i += 2; }
|
||||
Some('\\') if self.ver2() == Some('\r') => {
|
||||
self.i += 2;
|
||||
if self.ver() == Some('\n') { self.i += 1; }
|
||||
}
|
||||
Some('#') => { while !matches!(self.ver(), None | Some('\n')) { self.i += 1; } }
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn siguiente(&mut self) -> Result<Tok, String> {
|
||||
let t = self.siguiente_crudo()?;
|
||||
self.anterior = Some(t.clone());
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
fn siguiente_crudo(&mut self) -> Result<Tok, String> {
|
||||
self.espacios_y_comentarios();
|
||||
let c = match self.ver() { None => return Ok(Tok::Fin), Some(c) => c };
|
||||
if c == '\n' { self.i += 1; return Ok(Tok::Nueva); }
|
||||
|
||||
if c.is_ascii_digit() || (c == '.' && matches!(self.ver2(), Some(d) if d.is_ascii_digit())) {
|
||||
return self.numero();
|
||||
}
|
||||
if c == '"' { return self.cadena(); }
|
||||
if c == '/' && self.toca_ere() { return self.ere(); }
|
||||
if c.is_alphabetic() || c == '_' { return Ok(self.palabra()); }
|
||||
|
||||
// Operadores, los largos primero.
|
||||
let tres = self.tramo(3);
|
||||
if tres == "**=" { self.i += 3; return Ok(Tok::Op("^=".into())); } // `**` es el `^` histórico
|
||||
let dos = self.tramo(2);
|
||||
for op in ["==", "!=", "<=", ">=", "&&", "||", "++", "--", "+=", "-=", "*=", "/=", "%=", "^=", "!~", ">>"] {
|
||||
if dos == *op { self.i += 2; return Ok(Tok::Op(op.to_string())); }
|
||||
}
|
||||
if dos == "**" { self.i += 2; return Ok(Tok::Op("^".into())); }
|
||||
self.i += 1;
|
||||
let uno = c.to_string();
|
||||
if "{}()[];,+-*/%^<>=!?:~$|&".contains(c) { return Ok(Tok::Op(uno)); }
|
||||
Err(format!("carácter inesperado: `{c}`"))
|
||||
}
|
||||
|
||||
fn tramo(&self, n: usize) -> String {
|
||||
self.src[self.i..(self.i + n).min(self.src.len())].iter().collect()
|
||||
}
|
||||
|
||||
fn numero(&mut self) -> Result<Tok, String> {
|
||||
let ini = self.i;
|
||||
while matches!(self.ver(), Some(c) if c.is_ascii_digit()) { self.i += 1; }
|
||||
if self.ver() == Some('.') {
|
||||
self.i += 1;
|
||||
while matches!(self.ver(), Some(c) if c.is_ascii_digit()) { self.i += 1; }
|
||||
}
|
||||
if matches!(self.ver(), Some('e') | Some('E')) {
|
||||
let guardado = self.i;
|
||||
self.i += 1;
|
||||
if matches!(self.ver(), Some('+') | Some('-')) { self.i += 1; }
|
||||
if matches!(self.ver(), Some(c) if c.is_ascii_digit()) {
|
||||
while matches!(self.ver(), Some(c) if c.is_ascii_digit()) { self.i += 1; }
|
||||
} else { self.i = guardado; }
|
||||
}
|
||||
let t: String = self.src[ini..self.i].iter().collect();
|
||||
t.parse::<f64>().map(Tok::Num).map_err(|_| format!("número mal formado: `{t}`"))
|
||||
}
|
||||
|
||||
fn cadena(&mut self) -> Result<Tok, String> {
|
||||
self.i += 1; // la comilla de apertura
|
||||
let mut s = String::new();
|
||||
loop {
|
||||
match self.ver() {
|
||||
None | Some('\n') => return Err("cadena sin cerrar".into()),
|
||||
Some('"') => { self.i += 1; return Ok(Tok::Cad(s)); }
|
||||
Some('\\') => {
|
||||
self.i += 1;
|
||||
let e = self.ver().ok_or("`\\` al final de la cadena")?;
|
||||
self.i += 1;
|
||||
match e {
|
||||
'n' => s.push('\n'), 't' => s.push('\t'), 'r' => s.push('\r'),
|
||||
'\\' => s.push('\\'), '"' => s.push('"'), '/' => s.push('/'),
|
||||
'a' => s.push('\x07'), 'b' => s.push('\x08'), 'f' => s.push('\x0c'),
|
||||
'v' => s.push('\x0b'),
|
||||
// Octal: `\101` es `A`. Hasta tres dígitos.
|
||||
'0'..='7' => {
|
||||
let mut v = e.to_digit(8).unwrap();
|
||||
let mut n = 1;
|
||||
while n < 3 {
|
||||
match self.ver() {
|
||||
Some(d) if d.is_digit(8) => { v = v * 8 + d.to_digit(8).unwrap(); self.i += 1; n += 1; }
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
s.push(char::from_u32(v).unwrap_or('\0'));
|
||||
}
|
||||
// POSIX: una barra ante cualquier otro carácter queda INDEFINIDA. awk y gawk
|
||||
// conservan los dos caracteres, que es lo menos sorprendente.
|
||||
otro => { s.push('\\'); s.push(otro); }
|
||||
}
|
||||
}
|
||||
Some(c) => { s.push(c); self.i += 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ere(&mut self) -> Result<Tok, String> {
|
||||
self.i += 1; // la `/` de apertura
|
||||
let mut s = String::new();
|
||||
let mut en_clase = false;
|
||||
loop {
|
||||
match self.ver() {
|
||||
None | Some('\n') => return Err("expresión regular sin cerrar".into()),
|
||||
// Dentro de `[...]` una `/` no cierra: `/[/]/` es la ERE de una barra.
|
||||
Some('[') if !en_clase => { en_clase = true; s.push('['); self.i += 1; }
|
||||
Some(']') if en_clase => { en_clase = false; s.push(']'); self.i += 1; }
|
||||
Some('/') if !en_clase => { self.i += 1; return Ok(Tok::Ere(s)); }
|
||||
Some('\\') => {
|
||||
self.i += 1;
|
||||
let e = self.ver().ok_or("`\\` al final de la expresión regular")?;
|
||||
self.i += 1;
|
||||
// `\/` es una barra literal; el resto de escapes se los queda el motor ERE.
|
||||
if e == '/' { s.push('/'); } else { s.push('\\'); s.push(e); }
|
||||
}
|
||||
Some(c) => { s.push(c); self.i += 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn palabra(&mut self) -> Tok {
|
||||
let ini = self.i;
|
||||
while matches!(self.ver(), Some(c) if c.is_alphanumeric() || c == '_') { self.i += 1; }
|
||||
let p: String = self.src[ini..self.i].iter().collect();
|
||||
if CLAVES.contains(&p.as_str()) {
|
||||
return Tok::PalabraClave(if p == "func" { "function".into() } else { p });
|
||||
}
|
||||
// Llamada a función: el `(` va PEGADO. `f (x)` con espacio es concatenación, no llamada —
|
||||
// regla de POSIX que distingue a awk de casi todo lo demás.
|
||||
if self.ver() == Some('(') { Tok::Funcion(p) } else { Tok::Ident(p) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pruebas {
|
||||
use super::*;
|
||||
|
||||
fn toks(s: &str) -> Vec<Tok> {
|
||||
let mut l = Lexer::nuevo(s);
|
||||
let mut v = Vec::new();
|
||||
loop {
|
||||
let t = l.siguiente().unwrap();
|
||||
if t == Tok::Fin { break; }
|
||||
v.push(t);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn la_barra_es_division_tras_un_valor_y_ere_en_otro_sitio() {
|
||||
assert_eq!(toks("a / b"), vec![Tok::Ident("a".into()), Tok::Op("/".into()), Tok::Ident("b".into())]);
|
||||
assert_eq!(toks("$0 ~ /x/"), vec![
|
||||
Tok::Op("$".into()), Tok::Num(0.0), Tok::Op("~".into()), Tok::Ere("x".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn una_barra_dentro_de_una_clase_no_cierra_la_ere() {
|
||||
assert_eq!(toks("/[/]/"), vec![Tok::Ere("[/]".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llamada_a_funcion_solo_con_el_parentesis_pegado() {
|
||||
assert_eq!(toks("f(1)")[0], Tok::Funcion("f".into()));
|
||||
assert_eq!(toks("f (1)")[0], Tok::Ident("f".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn la_continuacion_de_linea_no_emite_nueva_linea() {
|
||||
assert_eq!(toks("a \\\n b"), vec![Tok::Ident("a".into()), Tok::Ident("b".into())]);
|
||||
assert_eq!(toks("a \n b"), vec![Tok::Ident("a".into()), Tok::Nueva, Tok::Ident("b".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escapes_de_cadena() {
|
||||
assert_eq!(toks(r#""a\tb\101""#), vec![Tok::Cad("a\tbA".into())]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//! qillqa — el awk del producto.
|
||||
//!
|
||||
//! *qillqa* es «escritura» en quechua, y awk es la herramienta de escribir sobre texto. El nombre
|
||||
//! sigue el criterio del proyecto (los nombres propios van en quechua) y el precedente de `simi`:
|
||||
//! **el nombre lo registra este comentario y lo DECIDE un ADR**, igual que el del shell. Lo que no
|
||||
//! está sujeto a decisión es el nombre del comando instalado: la receta enlaza `awk`, porque eso es
|
||||
//! contrato y lo llaman los `configure` de medio corpus.
|
||||
//!
|
||||
//! Uso: qillqa [-F sepc] [-v var=valor]… 'programa' [fichero | var=valor]…
|
||||
//! qillqa [-F sepc] [-v var=valor]… -f fuente.awk [-f más.awk]… [fichero | var=valor]…
|
||||
|
||||
mod ast;
|
||||
mod formato;
|
||||
mod interp;
|
||||
mod lexer;
|
||||
mod parser;
|
||||
mod regex;
|
||||
mod valor;
|
||||
|
||||
use interp::{Flujo, Interprete};
|
||||
use std::io::Write;
|
||||
use valor::Valor;
|
||||
|
||||
fn uso() -> ! {
|
||||
eprintln!("uso: qillqa [-F sepc] [-v var=valor]... 'programa' [fichero...]");
|
||||
eprintln!(" qillqa [-F sepc] [-v var=valor]... -f fuente.awk [fichero...]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let mut fuentes: Vec<String> = Vec::new();
|
||||
let mut asignaciones: Vec<String> = Vec::new();
|
||||
let mut fs: Option<String> = None;
|
||||
let mut texto_programa: Option<String> = None;
|
||||
let mut i = 1;
|
||||
|
||||
while i < argv.len() {
|
||||
let a = &argv[i];
|
||||
if a == "--" { i += 1; break; }
|
||||
if a == "-" || !a.starts_with('-') { break; }
|
||||
// Las opciones se pueden pegar al valor (`-F:`) o separar (`-F :`), como en todo POSIX.
|
||||
let (op, pegado) = {
|
||||
let cs: Vec<char> = a.chars().collect();
|
||||
(cs[1], cs[2..].iter().collect::<String>())
|
||||
};
|
||||
let mut toma = |pegado: &str, i: &mut usize| -> String {
|
||||
if !pegado.is_empty() { return pegado.to_string(); }
|
||||
*i += 1;
|
||||
argv.get(*i).cloned().unwrap_or_else(|| uso())
|
||||
};
|
||||
match op {
|
||||
'F' => {
|
||||
let v = toma(&pegado, &mut i);
|
||||
// `-F '\t'` llega con la barra literal: hay que desescaparlo como una cadena.
|
||||
// Y `-F t` es el tabulador por compatibilidad histórica con el awk original.
|
||||
fs = Some(if v == "t" { "\t".to_string() } else { interp::desescapar(&v) });
|
||||
}
|
||||
'v' => asignaciones.push(toma(&pegado, &mut i)),
|
||||
'f' => {
|
||||
let v = toma(&pegado, &mut i);
|
||||
match std::fs::read_to_string(&v) {
|
||||
Ok(s) => fuentes.push(s),
|
||||
Err(e) => { eprintln!("qillqa: no puedo leer `{v}`: {e}"); std::process::exit(2); }
|
||||
}
|
||||
}
|
||||
_ => uso(),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if fuentes.is_empty() {
|
||||
match argv.get(i) {
|
||||
None => uso(),
|
||||
Some(p) => { texto_programa = Some(p.clone()); i += 1; }
|
||||
}
|
||||
}
|
||||
let fuente = match texto_programa { Some(t) => t, None => fuentes.join("\n") };
|
||||
|
||||
let prog = match parser::Parser::nuevo(&fuente).and_then(|mut p| p.programa()) {
|
||||
Ok(p) => p,
|
||||
Err(e) => { eprintln!("qillqa: {e}"); std::process::exit(2); }
|
||||
};
|
||||
|
||||
let mut it = Interprete::nuevo(prog);
|
||||
|
||||
// ENVIRON, antes que nada: un BEGIN puede leerlo.
|
||||
{
|
||||
let env = it.array_publico("ENVIRON");
|
||||
for (k, v) in std::env::vars() {
|
||||
env.borrow_mut().insert(k, Valor::de_entrada(&v));
|
||||
}
|
||||
}
|
||||
if let Some(f) = fs { it.poner_var("FS", Valor::Cad(f)); }
|
||||
for a in &asignaciones {
|
||||
match interp::posicion_asignacion(a) {
|
||||
Some(p) => it.poner_var(&a[..p], Valor::de_entrada(&interp::desescapar(&a[p + 1..]))),
|
||||
None => { eprintln!("qillqa: `-v {a}` no tiene forma var=valor"); std::process::exit(2); }
|
||||
}
|
||||
}
|
||||
|
||||
// ARGV/ARGC: ARGV[0] es el nombre del programa y los operandos van detrás.
|
||||
{
|
||||
let operandos: Vec<String> = argv[i..].to_vec();
|
||||
let arr = it.array_publico("ARGV");
|
||||
arr.borrow_mut().insert("0".into(), Valor::Cad("qillqa".into()));
|
||||
for (n, o) in operandos.iter().enumerate() {
|
||||
arr.borrow_mut().insert((n + 1).to_string(), Valor::de_entrada(o));
|
||||
}
|
||||
it.poner_var("ARGC", Valor::Num((operandos.len() + 1) as f64));
|
||||
}
|
||||
|
||||
let mut codigo = 0;
|
||||
let mut salir_ya = false;
|
||||
|
||||
match it.ejecutar_begin() {
|
||||
Err(e) => { fin_con_error(&mut it, &e); }
|
||||
Ok(Flujo::Sale(c)) => { codigo = c; salir_ya = true; }
|
||||
Ok(_) => {}
|
||||
}
|
||||
|
||||
// El bucle principal sólo corre si hay reglas que no sean BEGIN. Si el programa es *sólo* un
|
||||
// BEGIN, awk NO lee la entrada — y eso importa: `awk 'BEGIN{print 1}'` no se cuelga esperando.
|
||||
if !salir_ya && (it.hay_main() || it.hay_end()) {
|
||||
'registros: loop {
|
||||
let r = match it.siguiente_registro() { None => break, Some(r) => r };
|
||||
it.poner_registro(r);
|
||||
match it.ejecutar_reglas() {
|
||||
Err(e) => { fin_con_error(&mut it, &e); }
|
||||
Ok(Flujo::Sale(c)) => { codigo = c; salir_ya = true; break 'registros; }
|
||||
Ok(Flujo::SiguienteFichero) => { it.cerrar_fuente_actual(); }
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !salir_ya || it.hay_end() {
|
||||
// POSIX: un `exit` en BEGIN o en el cuerpo SÍ ejecuta los END, salvo que el propio END
|
||||
// llame a `exit`. Por eso se entra acá incluso con `salir_ya`.
|
||||
match it.ejecutar_end() {
|
||||
Err(e) => { fin_con_error(&mut it, &e); }
|
||||
Ok(Flujo::Sale(c)) => { codigo = c; }
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
it.cerrar_todo();
|
||||
if it.codigo_salida != 0 && codigo == 0 { codigo = it.codigo_salida; }
|
||||
std::process::exit(codigo);
|
||||
}
|
||||
|
||||
fn fin_con_error(it: &mut Interprete, e: &str) -> ! {
|
||||
// `@salir:N` es cómo viaja un `exit` desde dentro de una función: no es un error de verdad.
|
||||
if let Some(n) = e.strip_prefix("@salir:") {
|
||||
let c = n.parse().unwrap_or(0);
|
||||
let _ = it.ejecutar_end();
|
||||
it.cerrar_todo();
|
||||
std::process::exit(c);
|
||||
}
|
||||
it.cerrar_todo();
|
||||
let _ = std::io::stderr().flush();
|
||||
eprintln!("qillqa: {e}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
//! Analizador sintáctico de awk: de tokens a `Programa`.
|
||||
//!
|
||||
//! La gramática de awk tiene tres rincones que obligan a escribir el parser de cierta forma, y los
|
||||
//! tres están marcados abajo donde aparecen:
|
||||
//!
|
||||
//! 1. **La concatenación no tiene operador.** `a b` es concatenar. Eso choca con el unario `-`:
|
||||
//! en `a - b` hay una resta, no la concatenación de `a` con `-b`. Se resuelve dando a la
|
||||
//! concatenación una precedencia propia, entre la comparación y la suma, y no dejándola empezar
|
||||
//! por un token que pueda continuar la expresión de la izquierda.
|
||||
//! 2. **`>` es ambiguo dentro de `print`.** `print a > "f"` REDIRIGE; no compara. Por eso el parser
|
||||
//! lleva una bandera `sin_mayor` mientras lee la lista de un `print`.
|
||||
//! 3. **`in` y `|` cambian de significado según el contexto**: `(i,j) in a`, `"cmd" | getline`.
|
||||
|
||||
use crate::ast::*;
|
||||
use crate::lexer::{Lexer, Tok};
|
||||
use crate::regex::Regex;
|
||||
use std::rc::Rc;
|
||||
|
||||
const BUILTINS: &[&str] = &[
|
||||
"length", "substr", "index", "split", "sub", "gsub", "match", "sprintf", "sin", "cos", "atan2",
|
||||
"exp", "log", "sqrt", "int", "rand", "srand", "tolower", "toupper", "system", "close", "fflush",
|
||||
];
|
||||
|
||||
pub struct Parser {
|
||||
toks: Vec<Tok>,
|
||||
i: usize,
|
||||
sin_mayor: bool,
|
||||
rangos: usize,
|
||||
funciones_vistas: Vec<String>,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn nuevo(src: &str) -> Result<Parser, String> {
|
||||
let mut lx = Lexer::nuevo(src);
|
||||
let mut toks = Vec::new();
|
||||
loop {
|
||||
let t = lx.siguiente()?;
|
||||
let fin = t == Tok::Fin;
|
||||
toks.push(t);
|
||||
if fin { break; }
|
||||
}
|
||||
Ok(Parser { toks, i: 0, sin_mayor: false, rangos: 0, funciones_vistas: Vec::new() })
|
||||
}
|
||||
|
||||
fn ver(&self) -> &Tok { &self.toks[self.i.min(self.toks.len() - 1)] }
|
||||
fn avanza(&mut self) -> Tok { let t = self.ver().clone(); self.i += 1; t }
|
||||
fn es_op(&self, o: &str) -> bool { matches!(self.ver(), Tok::Op(x) if x == o) }
|
||||
fn es_clave(&self, k: &str) -> bool { matches!(self.ver(), Tok::PalabraClave(x) if x == k) }
|
||||
|
||||
fn come_op(&mut self, o: &str) -> bool { if self.es_op(o) { self.i += 1; true } else { false } }
|
||||
fn come_clave(&mut self, k: &str) -> bool { if self.es_clave(k) { self.i += 1; true } else { false } }
|
||||
|
||||
fn exige_op(&mut self, o: &str) -> Result<(), String> {
|
||||
if self.come_op(o) { Ok(()) } else { Err(format!("esperaba `{o}` y encontré {:?}", self.ver())) }
|
||||
}
|
||||
|
||||
/// Se come los saltos de línea y los `;` que separan sentencias.
|
||||
fn saltos(&mut self) {
|
||||
while matches!(self.ver(), Tok::Nueva) || self.es_op(";") { self.i += 1; }
|
||||
}
|
||||
fn solo_saltos(&mut self) {
|
||||
while matches!(self.ver(), Tok::Nueva) { self.i += 1; }
|
||||
}
|
||||
|
||||
pub fn programa(&mut self) -> Result<Programa, String> {
|
||||
let mut p = Programa::default();
|
||||
self.saltos();
|
||||
while !matches!(self.ver(), Tok::Fin) {
|
||||
if self.es_clave("function") {
|
||||
self.i += 1;
|
||||
let nombre = match self.avanza() {
|
||||
Tok::Ident(n) | Tok::Funcion(n) => n,
|
||||
t => return Err(format!("nombre de función esperado, encontré {t:?}")),
|
||||
};
|
||||
self.exige_op("(")?;
|
||||
let mut params = Vec::new();
|
||||
if !self.es_op(")") {
|
||||
loop {
|
||||
match self.avanza() {
|
||||
Tok::Ident(n) => params.push(n),
|
||||
t => return Err(format!("parámetro esperado, encontré {t:?}")),
|
||||
}
|
||||
self.solo_saltos();
|
||||
if !self.come_op(",") { break; }
|
||||
self.solo_saltos();
|
||||
}
|
||||
}
|
||||
self.exige_op(")")?;
|
||||
self.solo_saltos();
|
||||
self.funciones_vistas.push(nombre.clone());
|
||||
let cuerpo = self.bloque()?;
|
||||
p.funciones.push(Funcion { nombre, params, cuerpo });
|
||||
} else {
|
||||
let regla = self.regla()?;
|
||||
p.reglas.push(regla);
|
||||
}
|
||||
self.saltos();
|
||||
}
|
||||
p.rangos = self.rangos;
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn regla(&mut self) -> Result<Regla, String> {
|
||||
let mut rango_id = usize::MAX;
|
||||
let patron = if self.come_clave("BEGIN") { Patron::Begin }
|
||||
else if self.come_clave("END") { Patron::End }
|
||||
else if self.es_op("{") { Patron::Siempre }
|
||||
else {
|
||||
let e1 = self.expr()?;
|
||||
if self.come_op(",") {
|
||||
self.solo_saltos();
|
||||
let e2 = self.expr()?;
|
||||
rango_id = self.rangos;
|
||||
self.rangos += 1;
|
||||
Patron::Rango(e1, e2)
|
||||
} else { Patron::Expr(e1) }
|
||||
};
|
||||
let accion = if self.es_op("{") { Some(self.bloque()?) } else { None };
|
||||
Ok(Regla { patron, accion, rango_id })
|
||||
}
|
||||
|
||||
fn bloque(&mut self) -> Result<Vec<Sent>, String> {
|
||||
self.exige_op("{")?;
|
||||
let mut v = Vec::new();
|
||||
self.saltos();
|
||||
while !self.es_op("}") {
|
||||
if matches!(self.ver(), Tok::Fin) { return Err("falta `}`".into()); }
|
||||
v.push(self.sentencia()?);
|
||||
self.saltos();
|
||||
}
|
||||
self.exige_op("}")?;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Una sentencia, que puede ser un bloque o una simple.
|
||||
fn sentencia(&mut self) -> Result<Sent, String> {
|
||||
if self.es_op("{") { return Ok(Sent::Bloque(self.bloque()?)); }
|
||||
if self.come_op(";") { return Ok(Sent::Nada); }
|
||||
|
||||
if self.come_clave("if") {
|
||||
self.exige_op("(")?;
|
||||
let cond = self.expr()?;
|
||||
self.exige_op(")")?;
|
||||
self.solo_saltos();
|
||||
let entonces = Box::new(self.sentencia()?);
|
||||
// El `else` puede venir tras saltos de línea y tras `;`.
|
||||
let guardado = self.i;
|
||||
self.saltos();
|
||||
let si_no = if self.come_clave("else") {
|
||||
self.solo_saltos();
|
||||
Some(Box::new(self.sentencia()?))
|
||||
} else { self.i = guardado; None };
|
||||
return Ok(Sent::Si(cond, entonces, si_no));
|
||||
}
|
||||
if self.come_clave("while") {
|
||||
self.exige_op("(")?;
|
||||
let cond = self.expr()?;
|
||||
self.exige_op(")")?;
|
||||
self.solo_saltos();
|
||||
// `while (c) ;` es un bucle con cuerpo vacío.
|
||||
if self.come_op(";") { return Ok(Sent::Mientras(cond, Box::new(Sent::Nada))); }
|
||||
return Ok(Sent::Mientras(cond, Box::new(self.sentencia()?)));
|
||||
}
|
||||
if self.come_clave("do") {
|
||||
self.solo_saltos();
|
||||
let cuerpo = Box::new(self.sentencia()?);
|
||||
self.saltos();
|
||||
if !self.come_clave("while") { return Err("`do` sin `while`".into()); }
|
||||
self.exige_op("(")?;
|
||||
let cond = self.expr()?;
|
||||
self.exige_op(")")?;
|
||||
return Ok(Sent::HazMientras(cuerpo, cond));
|
||||
}
|
||||
if self.come_clave("for") { return self.para(); }
|
||||
if self.come_clave("break") { return Ok(Sent::Corta); }
|
||||
if self.come_clave("continue") { return Ok(Sent::Sigue); }
|
||||
if self.come_clave("next") { return Ok(Sent::Siguiente); }
|
||||
if self.come_clave("nextfile") { return Ok(Sent::SiguienteFichero); }
|
||||
if self.come_clave("exit") {
|
||||
let e = if self.fin_de_sentencia() { None } else { Some(self.expr()?) };
|
||||
return Ok(Sent::Sale(e));
|
||||
}
|
||||
if self.come_clave("return") {
|
||||
let e = if self.fin_de_sentencia() { None } else { Some(self.expr()?) };
|
||||
return Ok(Sent::Retorna(e));
|
||||
}
|
||||
if self.come_clave("delete") {
|
||||
let nombre = match self.avanza() {
|
||||
Tok::Ident(n) | Tok::Funcion(n) => n,
|
||||
t => return Err(format!("`delete` necesita un array, encontré {t:?}")),
|
||||
};
|
||||
if self.come_op("[") {
|
||||
let mut idx = vec![self.expr()?];
|
||||
while self.come_op(",") { idx.push(self.expr()?); }
|
||||
self.exige_op("]")?;
|
||||
return Ok(Sent::Borra(nombre, idx));
|
||||
}
|
||||
// `delete a` y `delete a()` (la forma con paréntesis que acepta gawk)
|
||||
if self.come_op("(") { self.exige_op(")")?; }
|
||||
return Ok(Sent::BorraTodo(nombre));
|
||||
}
|
||||
if self.es_clave("print") || self.es_clave("printf") {
|
||||
let es_printf = self.es_clave("printf");
|
||||
self.i += 1;
|
||||
return self.print(es_printf);
|
||||
}
|
||||
Ok(Sent::Expr(self.expr()?))
|
||||
}
|
||||
|
||||
fn fin_de_sentencia(&self) -> bool {
|
||||
matches!(self.ver(), Tok::Nueva | Tok::Fin) || self.es_op(";") || self.es_op("}")
|
||||
}
|
||||
|
||||
fn para(&mut self) -> Result<Sent, String> {
|
||||
self.exige_op("(")?;
|
||||
// `for (k in a)` — se distingue de `for (expr; …)` mirando dos tokens adelante.
|
||||
if let Tok::Ident(k) = self.ver().clone() {
|
||||
if matches!(&self.toks[self.i + 1], Tok::PalabraClave(p) if p == "in") {
|
||||
self.i += 2;
|
||||
let arr = match self.avanza() {
|
||||
Tok::Ident(a) | Tok::Funcion(a) => a,
|
||||
t => return Err(format!("`for (k in a)` necesita un array, encontré {t:?}")),
|
||||
};
|
||||
self.exige_op(")")?;
|
||||
self.solo_saltos();
|
||||
return Ok(Sent::ParaEn(k, arr, Box::new(self.sentencia()?)));
|
||||
}
|
||||
}
|
||||
let init = if self.es_op(";") { None } else { Some(Box::new(self.sentencia()?)) };
|
||||
self.exige_op(";")?;
|
||||
self.solo_saltos();
|
||||
let cond = if self.es_op(";") { None } else { Some(self.expr()?) };
|
||||
self.exige_op(";")?;
|
||||
self.solo_saltos();
|
||||
let paso = if self.es_op(")") { None } else { Some(Box::new(self.sentencia()?)) };
|
||||
self.exige_op(")")?;
|
||||
self.solo_saltos();
|
||||
if self.come_op(";") { return Ok(Sent::Para(init, cond, paso, Box::new(Sent::Nada))); }
|
||||
Ok(Sent::Para(init, cond, paso, Box::new(self.sentencia()?)))
|
||||
}
|
||||
|
||||
/// ⚠ RINCÓN 2: dentro de la lista de un `print`, un `>` REDIRIGE, no compara. `sin_mayor` lo
|
||||
/// dice, y se apaga en cuanto entramos en un paréntesis —`print (a > b)` sí compara.
|
||||
fn print(&mut self, es_printf: bool) -> Result<Sent, String> {
|
||||
let mut args = Vec::new();
|
||||
if !self.fin_de_sentencia() && !self.es_op(">") && !self.es_op(">>") && !self.es_op("|") {
|
||||
let previo = self.sin_mayor;
|
||||
self.sin_mayor = true;
|
||||
args.push(self.expr()?);
|
||||
while self.come_op(",") {
|
||||
self.solo_saltos();
|
||||
args.push(self.expr()?);
|
||||
}
|
||||
self.sin_mayor = previo;
|
||||
}
|
||||
// `print (a, b) > "f"`: una lista entre paréntesis es la lista de argumentos, no una
|
||||
// expresión agrupada. Se detecta cuando el único argumento es un grupo con comas dentro —
|
||||
// eso lo resuelve `expr_lista_agrupada` al construirlo.
|
||||
let redir = if self.come_op(">") { Some(Redir::Fichero(Box::new(self.expr()?))) }
|
||||
else if self.come_op(">>") { Some(Redir::Anexa(Box::new(self.expr()?))) }
|
||||
else if self.come_op("|") { Some(Redir::Tuberia(Box::new(self.expr()?))) }
|
||||
else { None };
|
||||
Ok(if es_printf { Sent::Printf(args, redir) } else { Sent::Print(args, redir) })
|
||||
}
|
||||
|
||||
// ── Expresiones, por precedencia de menor a mayor ────────────────────────────────────────────
|
||||
|
||||
pub fn expr(&mut self) -> Result<Expr, String> { self.ternario() }
|
||||
|
||||
fn ternario(&mut self) -> Result<Expr, String> {
|
||||
let cond = self.o_logico()?;
|
||||
if self.come_op("?") {
|
||||
self.solo_saltos();
|
||||
let a = self.ternario()?;
|
||||
self.solo_saltos();
|
||||
self.exige_op(":")?;
|
||||
self.solo_saltos();
|
||||
let b = self.ternario()?;
|
||||
return Ok(Expr::Ternario(Box::new(cond), Box::new(a), Box::new(b)));
|
||||
}
|
||||
// La asignación se reconoce DESPUÉS: en awk `a = b ? c : d` asigna el ternario entero, y
|
||||
// el lado izquierdo de una asignación siempre es un lugar simple.
|
||||
if let Some(op) = self.op_asignacion() {
|
||||
if let Some(lugar) = self.a_lugar(&cond) {
|
||||
self.i += 1;
|
||||
self.solo_saltos();
|
||||
let val = self.ternario()?;
|
||||
return Ok(match op.as_str() {
|
||||
"=" => Expr::Asigna(Box::new(lugar), Box::new(val)),
|
||||
"+=" => Expr::AsignaOp(Box::new(lugar), BinOp::Suma, Box::new(val)),
|
||||
"-=" => Expr::AsignaOp(Box::new(lugar), BinOp::Resta, Box::new(val)),
|
||||
"*=" => Expr::AsignaOp(Box::new(lugar), BinOp::Mult, Box::new(val)),
|
||||
"/=" => Expr::AsignaOp(Box::new(lugar), BinOp::Div, Box::new(val)),
|
||||
"%=" => Expr::AsignaOp(Box::new(lugar), BinOp::Modulo, Box::new(val)),
|
||||
"^=" => Expr::AsignaOp(Box::new(lugar), BinOp::Potencia, Box::new(val)),
|
||||
_ => unreachable!(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(cond)
|
||||
}
|
||||
|
||||
fn op_asignacion(&self) -> Option<String> {
|
||||
match self.ver() {
|
||||
Tok::Op(o) if matches!(o.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "^=") => Some(o.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn a_lugar(&self, e: &Expr) -> Option<Lugar> {
|
||||
match e {
|
||||
Expr::Var(n) => Some(Lugar::Var(n.clone())),
|
||||
Expr::Campo(i) => Some(Lugar::Campo(i.clone())),
|
||||
Expr::Indice(n, idx) => Some(Lugar::Indice(n.clone(), idx.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn o_logico(&mut self) -> Result<Expr, String> {
|
||||
let mut izq = self.y_logico()?;
|
||||
while self.come_op("||") {
|
||||
self.solo_saltos();
|
||||
let der = self.y_logico()?;
|
||||
izq = Expr::O(Box::new(izq), Box::new(der));
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
fn y_logico(&mut self) -> Result<Expr, String> {
|
||||
let mut izq = self.en_array()?;
|
||||
while self.come_op("&&") {
|
||||
self.solo_saltos();
|
||||
let der = self.en_array()?;
|
||||
izq = Expr::Y(Box::new(izq), Box::new(der));
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
fn en_array(&mut self) -> Result<Expr, String> {
|
||||
let mut izq = self.coincidencia()?;
|
||||
while self.es_clave("in") {
|
||||
self.i += 1;
|
||||
let arr = match self.avanza() {
|
||||
Tok::Ident(a) | Tok::Funcion(a) => a,
|
||||
t => return Err(format!("`in` necesita un array, encontré {t:?}")),
|
||||
};
|
||||
// `(i,j) in a` llega como un grupo con lista; `i in a`, como una expresión suelta.
|
||||
izq = match izq {
|
||||
Expr::Grupo(inner) => Expr::En(vec![*inner], arr),
|
||||
otro => Expr::En(vec![otro], arr),
|
||||
};
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
fn coincidencia(&mut self) -> Result<Expr, String> {
|
||||
let mut izq = self.comparacion()?;
|
||||
loop {
|
||||
if self.come_op("~") {
|
||||
let der = self.comparacion()?;
|
||||
izq = Expr::Coincide(Box::new(izq), Box::new(der), false);
|
||||
} else if self.come_op("!~") {
|
||||
let der = self.comparacion()?;
|
||||
izq = Expr::Coincide(Box::new(izq), Box::new(der), true);
|
||||
} else { break; }
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
/// POSIX: los relacionales NO son asociativos (`a < b < c` es un error en la gramática), pero
|
||||
/// todos los awk reales lo aceptan como `(a<b)<c`. Se acepta igual, que romper eso sólo rompe
|
||||
/// guiones que ya funcionaban en otro lado.
|
||||
fn comparacion(&mut self) -> Result<Expr, String> {
|
||||
let izq = self.concatenacion()?;
|
||||
let op = match self.ver() {
|
||||
Tok::Op(o) => match o.as_str() {
|
||||
"<" => CmpOp::Lt, "<=" => CmpOp::Le, "==" => CmpOp::Eq, "!=" => CmpOp::Ne,
|
||||
">=" if !self.sin_mayor => CmpOp::Ge,
|
||||
">" if !self.sin_mayor => CmpOp::Gt,
|
||||
_ => return Ok(izq),
|
||||
},
|
||||
_ => return Ok(izq),
|
||||
};
|
||||
self.i += 1;
|
||||
let der = self.concatenacion()?;
|
||||
Ok(Expr::Cmp(op, Box::new(izq), Box::new(der)))
|
||||
}
|
||||
|
||||
/// ⚠ RINCÓN 1: la concatenación no tiene operador. Continúa mientras lo que viene PUEDE empezar
|
||||
/// una expresión unaria… salvo `+` y `-`, que se leen como binarios (`a - b` es una resta).
|
||||
fn concatenacion(&mut self) -> Result<Expr, String> {
|
||||
let mut izq = self.aditiva()?;
|
||||
while self.empieza_valor() {
|
||||
let der = self.aditiva()?;
|
||||
izq = Expr::Concat(Box::new(izq), Box::new(der));
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
fn empieza_valor(&self) -> bool {
|
||||
match self.ver() {
|
||||
Tok::Num(_) | Tok::Cad(_) | Tok::Ere(_) | Tok::Ident(_) | Tok::Funcion(_) => true,
|
||||
Tok::Op(o) => matches!(o.as_str(), "$" | "(" | "!" | "++" | "--"),
|
||||
Tok::PalabraClave(k) => k == "getline",
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn aditiva(&mut self) -> Result<Expr, String> {
|
||||
let mut izq = self.multiplicativa()?;
|
||||
loop {
|
||||
if self.come_op("+") { let d = self.multiplicativa()?; izq = Expr::Bin(BinOp::Suma, Box::new(izq), Box::new(d)); }
|
||||
else if self.come_op("-") { let d = self.multiplicativa()?; izq = Expr::Bin(BinOp::Resta, Box::new(izq), Box::new(d)); }
|
||||
else { break; }
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
fn multiplicativa(&mut self) -> Result<Expr, String> {
|
||||
let mut izq = self.unaria()?;
|
||||
loop {
|
||||
if self.come_op("*") { let d = self.unaria()?; izq = Expr::Bin(BinOp::Mult, Box::new(izq), Box::new(d)); }
|
||||
else if self.come_op("/") { let d = self.unaria()?; izq = Expr::Bin(BinOp::Div, Box::new(izq), Box::new(d)); }
|
||||
else if self.come_op("%") { let d = self.unaria()?; izq = Expr::Bin(BinOp::Modulo, Box::new(izq), Box::new(d)); }
|
||||
else { break; }
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
fn unaria(&mut self) -> Result<Expr, String> {
|
||||
if self.come_op("!") { return Ok(Expr::No(Box::new(self.unaria()?))); }
|
||||
if self.come_op("-") { return Ok(Expr::Neg(Box::new(self.unaria()?))); }
|
||||
if self.come_op("+") { return Ok(Expr::Pos(Box::new(self.unaria()?))); }
|
||||
self.potencia()
|
||||
}
|
||||
|
||||
/// `^` asocia a la DERECHA (`2^3^2` es 512), y su operando derecho admite unario:
|
||||
/// `2^-1` es válido.
|
||||
fn potencia(&mut self) -> Result<Expr, String> {
|
||||
let base = self.postfija()?;
|
||||
if self.come_op("^") {
|
||||
let exp = self.unaria()?;
|
||||
return Ok(Expr::Bin(BinOp::Potencia, Box::new(base), Box::new(exp)));
|
||||
}
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
fn postfija(&mut self) -> Result<Expr, String> {
|
||||
let e = self.primaria()?;
|
||||
if self.es_op("++") || self.es_op("--") {
|
||||
if let Some(l) = self.a_lugar(&e) {
|
||||
let delta = if self.es_op("++") { 1.0 } else { -1.0 };
|
||||
self.i += 1;
|
||||
return Ok(Expr::Incr(Box::new(l), false, delta));
|
||||
}
|
||||
}
|
||||
Ok(e)
|
||||
}
|
||||
|
||||
fn primaria(&mut self) -> Result<Expr, String> {
|
||||
// Pre-incremento.
|
||||
if self.es_op("++") || self.es_op("--") {
|
||||
let delta = if self.es_op("++") { 1.0 } else { -1.0 };
|
||||
self.i += 1;
|
||||
let obj = self.primaria()?;
|
||||
let l = self.a_lugar(&obj).ok_or("`++` necesita una variable, un campo o un elemento")?;
|
||||
return Ok(Expr::Incr(Box::new(l), true, delta));
|
||||
}
|
||||
if self.come_op("$") {
|
||||
let idx = self.primaria()?;
|
||||
let campo = Expr::Campo(Box::new(idx));
|
||||
// `$1++` incrementa el campo.
|
||||
if self.es_op("++") || self.es_op("--") {
|
||||
let delta = if self.es_op("++") { 1.0 } else { -1.0 };
|
||||
self.i += 1;
|
||||
if let Some(l) = self.a_lugar(&campo) {
|
||||
return Ok(Expr::Incr(Box::new(l), false, delta));
|
||||
}
|
||||
}
|
||||
return Ok(campo);
|
||||
}
|
||||
if self.come_op("(") {
|
||||
let previo = self.sin_mayor;
|
||||
self.sin_mayor = false; // dentro de paréntesis, `>` vuelve a comparar
|
||||
self.solo_saltos();
|
||||
let e = self.expr()?;
|
||||
// `(a, b) in arr` y `print (a, b)`: una lista.
|
||||
if self.es_op(",") {
|
||||
let mut lista = vec![e];
|
||||
while self.come_op(",") { self.solo_saltos(); lista.push(self.expr()?); }
|
||||
self.exige_op(")")?;
|
||||
self.sin_mayor = previo;
|
||||
if self.es_clave("in") {
|
||||
self.i += 1;
|
||||
let arr = match self.avanza() {
|
||||
Tok::Ident(a) | Tok::Funcion(a) => a,
|
||||
t => return Err(format!("`in` necesita un array, encontré {t:?}")),
|
||||
};
|
||||
return Ok(Expr::En(lista, arr));
|
||||
}
|
||||
// Lista suelta: sólo tiene sentido como argumentos de print, y allí se desarma.
|
||||
return Ok(Expr::Llamada("@lista".into(), lista));
|
||||
}
|
||||
self.exige_op(")")?;
|
||||
self.sin_mayor = previo;
|
||||
let g = Expr::Grupo(Box::new(e));
|
||||
// `("cmd" | getline x)`
|
||||
return self.quizas_tuberia(g);
|
||||
}
|
||||
match self.avanza() {
|
||||
Tok::Num(n) => Ok(Expr::Num(n)),
|
||||
Tok::Cad(s) => self.quizas_tuberia(Expr::Cad(s)),
|
||||
Tok::Ere(r) => Ok(Expr::Ere(Rc::new(Regex::nueva(&r)?))),
|
||||
// ⚠ `length` sin paréntesis es el ÚNICO builtin al que POSIX se lo permite, y tiene
|
||||
// que ir ANTES del brazo general de identificador — si no, se lee como una variable
|
||||
// llamada `length`, que siempre vale vacío. El banco lo cazó con `{print length}`.
|
||||
Tok::Ident(n) if n == "length" && !self.es_op("[") => Ok(Expr::Builtin("length".into(), vec![])),
|
||||
Tok::Ident(n) => {
|
||||
if self.come_op("[") {
|
||||
let mut idx = vec![self.expr()?];
|
||||
while self.come_op(",") { idx.push(self.expr()?); }
|
||||
self.exige_op("]")?;
|
||||
return Ok(Expr::Indice(n, idx));
|
||||
}
|
||||
Ok(Expr::Var(n))
|
||||
}
|
||||
Tok::Funcion(n) => {
|
||||
self.exige_op("(")?;
|
||||
let previo = self.sin_mayor;
|
||||
self.sin_mayor = false;
|
||||
let mut args = Vec::new();
|
||||
self.solo_saltos();
|
||||
if !self.es_op(")") {
|
||||
loop {
|
||||
args.push(self.expr()?);
|
||||
self.solo_saltos();
|
||||
if !self.come_op(",") { break; }
|
||||
self.solo_saltos();
|
||||
}
|
||||
}
|
||||
self.exige_op(")")?;
|
||||
self.sin_mayor = previo;
|
||||
if BUILTINS.contains(&n.as_str()) { Ok(Expr::Builtin(n, args)) }
|
||||
else { Ok(Expr::Llamada(n, args)) }
|
||||
}
|
||||
Tok::PalabraClave(k) if k == "getline" => {
|
||||
// `getline`, `getline var`, `getline < f`, `getline var < f`
|
||||
let lugar = self.lugar_opcional()?;
|
||||
if self.come_op("<") {
|
||||
let f = self.concatenacion()?;
|
||||
return Ok(Expr::Getline(Getline::DeFichero(lugar, Box::new(f))));
|
||||
}
|
||||
Ok(Expr::Getline(Getline::Simple(lugar)))
|
||||
}
|
||||
t => Err(format!("expresión esperada, encontré {t:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `expr | getline [lugar]`
|
||||
fn quizas_tuberia(&mut self, izq: Expr) -> Result<Expr, String> {
|
||||
if self.es_op("|") && matches!(&self.toks[self.i + 1], Tok::PalabraClave(k) if k == "getline") {
|
||||
self.i += 2;
|
||||
let lugar = self.lugar_opcional()?;
|
||||
return Ok(Expr::Getline(Getline::DeTuberia(Box::new(izq), lugar)));
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
fn lugar_opcional(&mut self) -> Result<Option<Box<Lugar>>, String> {
|
||||
match self.ver().clone() {
|
||||
Tok::Ident(n) => {
|
||||
self.i += 1;
|
||||
if self.come_op("[") {
|
||||
let mut idx = vec![self.expr()?];
|
||||
while self.come_op(",") { idx.push(self.expr()?); }
|
||||
self.exige_op("]")?;
|
||||
return Ok(Some(Box::new(Lugar::Indice(n, idx))));
|
||||
}
|
||||
Ok(Some(Box::new(Lugar::Var(n))))
|
||||
}
|
||||
Tok::Op(o) if o == "$" => {
|
||||
self.i += 1;
|
||||
let idx = self.primaria()?;
|
||||
Ok(Some(Box::new(Lugar::Campo(Box::new(idx)))))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
//! Motor de expresiones regulares ERE (POSIX.1-2024 §9.4), propio.
|
||||
//!
|
||||
//! # Por qué propio y no un crate
|
||||
//!
|
||||
//! La semántica de POSIX es **leftmost-longest**: entre todas las coincidencias que empiezan en la
|
||||
//! posición más a la izquierda, gana la MÁS LARGA. La familia Perl —y con ella el crate `regex` en
|
||||
//! su modo por defecto— usa leftmost-first: para `a|ab` sobre `"ab"`, POSIX devuelve `ab` y Perl
|
||||
//! devuelve `a`. En un awk eso no es un detalle académico: decide qué borra un `sub()`, dónde parte
|
||||
//! un `split()` y qué valen `RSTART`/`RLENGTH`. Adoptar un motor ajeno sería adoptar su semántica.
|
||||
//!
|
||||
//! # Cómo funciona
|
||||
//!
|
||||
//! Construcción de Thompson: el patrón se compila a un programa de instrucciones y se simula con un
|
||||
//! CONJUNTO de hilos que avanzan a la vez, un carácter por paso. No hay retroceso, así que no hay
|
||||
//! explosión exponencial —`(a*)*b` contra sesenta `a` termina igual de rápido que cualquier otra
|
||||
//! cosa— y quedarse con el final más lejano alcanzado da el «más largo» de POSIX sin esfuerzo extra.
|
||||
//!
|
||||
//! Trabaja sobre `char`, no sobre bytes: en awk los índices de `substr`, `index`, `match` y
|
||||
//! `RSTART` se cuentan en caracteres, y con UTF-8 en el fichero de entrada contar bytes daría
|
||||
//! posiciones que no se pueden usar para cortar.
|
||||
|
||||
/// Una instrucción del programa compilado.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Inst {
|
||||
/// Consume exactamente este carácter.
|
||||
Char(char),
|
||||
/// Consume cualquier carácter (`.`). En awk `.` SÍ casa con `\n` (no hay modo multilínea).
|
||||
Any,
|
||||
/// Consume un carácter si pertenece (o no, si `negada`) al conjunto.
|
||||
Clase { partes: Vec<Parte>, negada: bool },
|
||||
/// Bifurca: sigue por las dos ramas. El orden no importa porque no hay preferencia (longest).
|
||||
Split(usize, usize),
|
||||
/// Salta.
|
||||
Jmp(usize),
|
||||
/// Ancla de principio de cadena (`^`).
|
||||
Bol,
|
||||
/// Ancla de final de cadena (`$`).
|
||||
Eol,
|
||||
/// Aceptación.
|
||||
Match,
|
||||
}
|
||||
|
||||
/// Un trozo de una clase `[...]`.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Parte {
|
||||
Uno(char),
|
||||
Rango(char, char),
|
||||
/// `[:alpha:]` y compañía, resueltas por función para no materializar Unicode entero.
|
||||
Nombrada(Nombrada),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Nombrada {
|
||||
Alpha, Digit, Alnum, Upper, Lower, Space, Blank, Punct, Print, Graph, Cntrl, Xdigit,
|
||||
}
|
||||
|
||||
impl Nombrada {
|
||||
fn de(nombre: &str) -> Option<Nombrada> {
|
||||
Some(match nombre {
|
||||
"alpha" => Nombrada::Alpha, "digit" => Nombrada::Digit, "alnum" => Nombrada::Alnum,
|
||||
"upper" => Nombrada::Upper, "lower" => Nombrada::Lower, "space" => Nombrada::Space,
|
||||
"blank" => Nombrada::Blank, "punct" => Nombrada::Punct, "print" => Nombrada::Print,
|
||||
"graph" => Nombrada::Graph, "cntrl" => Nombrada::Cntrl, "xdigit" => Nombrada::Xdigit,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
fn casa(self, c: char) -> bool {
|
||||
match self {
|
||||
Nombrada::Alpha => c.is_alphabetic(),
|
||||
Nombrada::Digit => c.is_ascii_digit(),
|
||||
Nombrada::Alnum => c.is_alphanumeric(),
|
||||
Nombrada::Upper => c.is_uppercase(),
|
||||
Nombrada::Lower => c.is_lowercase(),
|
||||
Nombrada::Space => c.is_whitespace(),
|
||||
Nombrada::Blank => c == ' ' || c == '\t',
|
||||
Nombrada::Punct => c.is_ascii_punctuation(),
|
||||
Nombrada::Print => !c.is_control(),
|
||||
Nombrada::Graph => !c.is_control() && !c.is_whitespace(),
|
||||
Nombrada::Cntrl => c.is_control(),
|
||||
Nombrada::Xdigit => c.is_ascii_hexdigit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Árbol del patrón, paso intermedio entre el texto y el programa.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Nodo {
|
||||
Vacio,
|
||||
Char(char),
|
||||
Any,
|
||||
Clase { partes: Vec<Parte>, negada: bool },
|
||||
Bol,
|
||||
Eol,
|
||||
Cat(Box<Nodo>, Box<Nodo>),
|
||||
Alt(Box<Nodo>, Box<Nodo>),
|
||||
Estrella(Box<Nodo>),
|
||||
Mas(Box<Nodo>),
|
||||
Opcional(Box<Nodo>),
|
||||
/// `{n,m}`: se expande al compilar. `hasta = None` es `{n,}`.
|
||||
Repite(Box<Nodo>, u32, Option<u32>),
|
||||
}
|
||||
|
||||
pub struct Regex {
|
||||
prog: Vec<Inst>,
|
||||
/// El patrón original, para mensajes de error y para `Regex::fuente`.
|
||||
fuente: String,
|
||||
}
|
||||
|
||||
struct Analizador<'a> {
|
||||
p: &'a [char],
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl<'a> Analizador<'a> {
|
||||
fn fin(&self) -> bool { self.i >= self.p.len() }
|
||||
fn ver(&self) -> Option<char> { self.p.get(self.i).copied() }
|
||||
fn tomar(&mut self) -> Option<char> { let c = self.ver(); if c.is_some() { self.i += 1; } c }
|
||||
|
||||
/// alternancia := concatenación ('|' concatenación)*
|
||||
fn alternancia(&mut self) -> Result<Nodo, String> {
|
||||
let mut izq = self.concatenacion()?;
|
||||
while self.ver() == Some('|') {
|
||||
self.i += 1;
|
||||
let der = self.concatenacion()?;
|
||||
izq = Nodo::Alt(Box::new(izq), Box::new(der));
|
||||
}
|
||||
Ok(izq)
|
||||
}
|
||||
|
||||
/// concatenación := repetición*
|
||||
fn concatenacion(&mut self) -> Result<Nodo, String> {
|
||||
let mut acc: Option<Nodo> = None;
|
||||
while !self.fin() && self.ver() != Some('|') && self.ver() != Some(')') {
|
||||
let n = self.repeticion()?;
|
||||
acc = Some(match acc {
|
||||
None => n,
|
||||
Some(a) => Nodo::Cat(Box::new(a), Box::new(n)),
|
||||
});
|
||||
}
|
||||
Ok(acc.unwrap_or(Nodo::Vacio))
|
||||
}
|
||||
|
||||
/// repetición := átomo ('*' | '+' | '?' | '{n,m}')*
|
||||
fn repeticion(&mut self) -> Result<Nodo, String> {
|
||||
let mut n = self.atomo()?;
|
||||
loop {
|
||||
match self.ver() {
|
||||
Some('*') => { self.i += 1; n = Nodo::Estrella(Box::new(n)); }
|
||||
Some('+') => { self.i += 1; n = Nodo::Mas(Box::new(n)); }
|
||||
Some('?') => { self.i += 1; n = Nodo::Opcional(Box::new(n)); }
|
||||
Some('{') => {
|
||||
// Sólo es intervalo si lo que sigue es un dígito: en ERE un `{` suelto es
|
||||
// literal, y hay guiones que escriben `/x{/` sin escapar.
|
||||
if !matches!(self.p.get(self.i + 1), Some(c) if c.is_ascii_digit()) { break; }
|
||||
let guardado = self.i;
|
||||
self.i += 1;
|
||||
match self.intervalo() {
|
||||
Ok((desde, hasta)) => n = Nodo::Repite(Box::new(n), desde, hasta),
|
||||
Err(_) => { self.i = guardado; break; }
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn intervalo(&mut self) -> Result<(u32, Option<u32>), String> {
|
||||
let mut desde = String::new();
|
||||
while matches!(self.ver(), Some(c) if c.is_ascii_digit()) { desde.push(self.tomar().unwrap()); }
|
||||
if desde.is_empty() { return Err("intervalo sin mínimo".into()); }
|
||||
let d: u32 = desde.parse().map_err(|_| "intervalo demasiado grande".to_string())?;
|
||||
match self.ver() {
|
||||
Some('}') => { self.i += 1; Ok((d, Some(d))) }
|
||||
Some(',') => {
|
||||
self.i += 1;
|
||||
let mut hasta = String::new();
|
||||
while matches!(self.ver(), Some(c) if c.is_ascii_digit()) { hasta.push(self.tomar().unwrap()); }
|
||||
if self.ver() != Some('}') { return Err("intervalo sin cerrar".into()); }
|
||||
self.i += 1;
|
||||
if hasta.is_empty() { Ok((d, None)) }
|
||||
else {
|
||||
let h: u32 = hasta.parse().map_err(|_| "intervalo demasiado grande".to_string())?;
|
||||
if h < d { return Err("intervalo con máximo menor que el mínimo".into()); }
|
||||
Ok((d, Some(h)))
|
||||
}
|
||||
}
|
||||
_ => Err("intervalo mal formado".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn atomo(&mut self) -> Result<Nodo, String> {
|
||||
match self.tomar() {
|
||||
None => Ok(Nodo::Vacio),
|
||||
Some('(') => {
|
||||
let dentro = self.alternancia()?;
|
||||
if self.tomar() != Some(')') { return Err("falta `)`".into()); }
|
||||
Ok(dentro)
|
||||
}
|
||||
Some('[') => self.clase(),
|
||||
Some('.') => Ok(Nodo::Any),
|
||||
Some('^') => Ok(Nodo::Bol),
|
||||
Some('$') => Ok(Nodo::Eol),
|
||||
Some('\\') => match self.tomar() {
|
||||
None => Err("`\\` al final del patrón".into()),
|
||||
// Los escapes de C que awk reconoce dentro de una ERE.
|
||||
Some('n') => Ok(Nodo::Char('\n')),
|
||||
Some('t') => Ok(Nodo::Char('\t')),
|
||||
Some('r') => Ok(Nodo::Char('\r')),
|
||||
Some('f') => Ok(Nodo::Char('\x0c')),
|
||||
Some('b') => Ok(Nodo::Char('\x08')),
|
||||
Some('v') => Ok(Nodo::Char('\x0b')),
|
||||
Some('a') => Ok(Nodo::Char('\x07')),
|
||||
Some('/') => Ok(Nodo::Char('/')),
|
||||
Some(c) => Ok(Nodo::Char(c)),
|
||||
},
|
||||
Some(c) => Ok(Nodo::Char(c)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `[abc]`, `[^a-z]`, `[[:digit:]]`. Un `]` justo tras el `[` (o tras `[^`) es LITERAL.
|
||||
fn clase(&mut self) -> Result<Nodo, String> {
|
||||
let mut negada = false;
|
||||
if self.ver() == Some('^') { negada = true; self.i += 1; }
|
||||
let mut partes: Vec<Parte> = Vec::new();
|
||||
let mut primero = true;
|
||||
loop {
|
||||
let c = match self.tomar() {
|
||||
None => return Err("clase `[` sin cerrar".into()),
|
||||
Some(']') if !primero => break,
|
||||
Some(c) => c,
|
||||
};
|
||||
primero = false;
|
||||
// `[:alpha:]` dentro de la clase.
|
||||
if c == '[' && self.ver() == Some(':') {
|
||||
let guardado = self.i;
|
||||
self.i += 1;
|
||||
let mut nombre = String::new();
|
||||
while matches!(self.ver(), Some(c) if c != ':' && c != ']') { nombre.push(self.tomar().unwrap()); }
|
||||
if self.ver() == Some(':') && self.p.get(self.i + 1) == Some(&']') {
|
||||
self.i += 2;
|
||||
match Nombrada::de(&nombre) {
|
||||
Some(n) => { partes.push(Parte::Nombrada(n)); continue; }
|
||||
None => return Err(format!("clase de caracteres desconocida: [:{nombre}:]")),
|
||||
}
|
||||
}
|
||||
self.i = guardado; // no era una clase nombrada: el `[` es literal
|
||||
}
|
||||
let c = if c == '\\' {
|
||||
match self.tomar() {
|
||||
None => return Err("`\\` al final de una clase".into()),
|
||||
Some('n') => '\n', Some('t') => '\t', Some('r') => '\r',
|
||||
Some('f') => '\x0c', Some('b') => '\x08', Some('v') => '\x0b', Some('a') => '\x07',
|
||||
Some(o) => o,
|
||||
}
|
||||
} else { c };
|
||||
// Rango `a-z`, salvo que el `-` sea el último carácter de la clase.
|
||||
if self.ver() == Some('-') && self.p.get(self.i + 1).map_or(false, |&d| d != ']') {
|
||||
self.i += 1;
|
||||
let hasta = match self.tomar() {
|
||||
None => return Err("rango sin final".into()),
|
||||
Some('\\') => self.tomar().ok_or("`\\` al final de un rango")?,
|
||||
Some(d) => d,
|
||||
};
|
||||
partes.push(Parte::Rango(c, hasta));
|
||||
} else {
|
||||
partes.push(Parte::Uno(c));
|
||||
}
|
||||
}
|
||||
Ok(Nodo::Clase { partes, negada })
|
||||
}
|
||||
}
|
||||
|
||||
/// Emite el programa. Devuelve el índice de la primera instrucción del fragmento.
|
||||
fn emitir(prog: &mut Vec<Inst>, n: &Nodo) {
|
||||
match n {
|
||||
Nodo::Vacio => {}
|
||||
Nodo::Char(c) => prog.push(Inst::Char(*c)),
|
||||
Nodo::Any => prog.push(Inst::Any),
|
||||
Nodo::Bol => prog.push(Inst::Bol),
|
||||
Nodo::Eol => prog.push(Inst::Eol),
|
||||
Nodo::Clase { partes, negada } => prog.push(Inst::Clase { partes: partes.clone(), negada: *negada }),
|
||||
Nodo::Cat(a, b) => { emitir(prog, a); emitir(prog, b); }
|
||||
Nodo::Alt(a, b) => {
|
||||
let split = prog.len();
|
||||
prog.push(Inst::Jmp(0)); // reservado, se reescribe como Split
|
||||
emitir(prog, a);
|
||||
let jmp = prog.len();
|
||||
prog.push(Inst::Jmp(0));
|
||||
let inicio_b = prog.len();
|
||||
emitir(prog, b);
|
||||
let fin = prog.len();
|
||||
prog[split] = Inst::Split(split + 1, inicio_b);
|
||||
prog[jmp] = Inst::Jmp(fin);
|
||||
}
|
||||
Nodo::Estrella(a) => {
|
||||
let split = prog.len();
|
||||
prog.push(Inst::Jmp(0));
|
||||
emitir(prog, a);
|
||||
prog.push(Inst::Jmp(split));
|
||||
let fin = prog.len();
|
||||
prog[split] = Inst::Split(split + 1, fin);
|
||||
}
|
||||
Nodo::Mas(a) => {
|
||||
let inicio = prog.len();
|
||||
emitir(prog, a);
|
||||
let split = prog.len();
|
||||
prog.push(Inst::Jmp(0));
|
||||
let fin = prog.len();
|
||||
prog[split] = Inst::Split(inicio, fin);
|
||||
}
|
||||
Nodo::Opcional(a) => {
|
||||
let split = prog.len();
|
||||
prog.push(Inst::Jmp(0));
|
||||
emitir(prog, a);
|
||||
let fin = prog.len();
|
||||
prog[split] = Inst::Split(split + 1, fin);
|
||||
}
|
||||
Nodo::Repite(a, desde, hasta) => {
|
||||
// Se expande: `x{2,4}` es `xx(x(x)?)?`. Es lo que hace cualquier motor sin contadores, y
|
||||
// el coste es lineal en `hasta`, que en la práctica es un número pequeño.
|
||||
for _ in 0..*desde { emitir(prog, a); }
|
||||
match hasta {
|
||||
None => emitir(prog, &Nodo::Estrella(a.clone())),
|
||||
Some(h) => {
|
||||
let opcionales = h.saturating_sub(*desde);
|
||||
let mut splits = Vec::new();
|
||||
for _ in 0..opcionales {
|
||||
let s = prog.len();
|
||||
prog.push(Inst::Jmp(0));
|
||||
splits.push(s);
|
||||
emitir(prog, a);
|
||||
}
|
||||
let fin = prog.len();
|
||||
for s in splits { prog[s] = Inst::Split(s + 1, fin); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Regex {
|
||||
pub fn nueva(patron: &str) -> Result<Regex, String> {
|
||||
let chars: Vec<char> = patron.chars().collect();
|
||||
let mut an = Analizador { p: &chars, i: 0 };
|
||||
let nodo = an.alternancia()?;
|
||||
if !an.fin() {
|
||||
return Err(format!("basura al final del patrón: `{}`", chars[an.i..].iter().collect::<String>()));
|
||||
}
|
||||
let mut prog = Vec::new();
|
||||
emitir(&mut prog, &nodo);
|
||||
prog.push(Inst::Match);
|
||||
Ok(Regex { prog, fuente: patron.to_string() })
|
||||
}
|
||||
|
||||
pub fn fuente(&self) -> &str { &self.fuente }
|
||||
|
||||
/// Añade un hilo y todos los alcanzables sin consumir (cierre-epsilon).
|
||||
fn agregar(&self, lista: &mut Vec<usize>, visto: &mut [bool], pc: usize, pos: usize, largo: usize) {
|
||||
if visto[pc] { return; }
|
||||
visto[pc] = true;
|
||||
match self.prog[pc] {
|
||||
Inst::Jmp(d) => self.agregar(lista, visto, d, pos, largo),
|
||||
Inst::Split(a, b) => {
|
||||
self.agregar(lista, visto, a, pos, largo);
|
||||
self.agregar(lista, visto, b, pos, largo);
|
||||
}
|
||||
Inst::Bol => { if pos == 0 { self.agregar(lista, visto, pc + 1, pos, largo); } }
|
||||
Inst::Eol => { if pos == largo { self.agregar(lista, visto, pc + 1, pos, largo); } }
|
||||
_ => lista.push(pc),
|
||||
}
|
||||
}
|
||||
|
||||
/// El final del match MÁS LARGO que empieza exactamente en `desde`, si lo hay.
|
||||
pub fn casa_en(&self, s: &[char], desde: usize) -> Option<usize> {
|
||||
let largo = s.len();
|
||||
let mut visto = vec![false; self.prog.len()];
|
||||
let mut actual: Vec<usize> = Vec::new();
|
||||
self.agregar(&mut actual, &mut visto, 0, desde, largo);
|
||||
let mut mejor: Option<usize> = None;
|
||||
let mut pos = desde;
|
||||
loop {
|
||||
// ¿Alguno acepta aquí? Nos quedamos con el más lejano: eso es «longest».
|
||||
if actual.iter().any(|&pc| matches!(self.prog[pc], Inst::Match)) {
|
||||
mejor = Some(pos);
|
||||
}
|
||||
if pos >= largo || actual.is_empty() { break; }
|
||||
let c = s[pos];
|
||||
let mut siguiente: Vec<usize> = Vec::new();
|
||||
let mut visto2 = vec![false; self.prog.len()];
|
||||
for &pc in &actual {
|
||||
let avanza = match &self.prog[pc] {
|
||||
Inst::Char(x) => *x == c,
|
||||
Inst::Any => true,
|
||||
Inst::Clase { partes, negada } => {
|
||||
let dentro = partes.iter().any(|p| match p {
|
||||
Parte::Uno(u) => *u == c,
|
||||
Parte::Rango(a, b) => *a <= c && c <= *b,
|
||||
Parte::Nombrada(n) => n.casa(c),
|
||||
});
|
||||
dentro != *negada
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if avanza { self.agregar(&mut siguiente, &mut visto2, pc + 1, pos + 1, largo); }
|
||||
}
|
||||
actual = siguiente;
|
||||
pos += 1;
|
||||
}
|
||||
mejor
|
||||
}
|
||||
|
||||
/// Primera coincidencia a partir de `desde`: la más a la izquierda y, entre ésas, la más larga.
|
||||
/// Devuelve `(inicio, fin)` en índices de CARÁCTER, con `fin` exclusivo.
|
||||
pub fn buscar(&self, s: &[char], desde: usize) -> Option<(usize, usize)> {
|
||||
for i in desde..=s.len() {
|
||||
if let Some(fin) = self.casa_en(s, i) { return Some((i, fin)); }
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// ¿Casa en algún sitio? Es lo que piden `~`, los patrones de regla y `split`.
|
||||
pub fn casa(&self, s: &str) -> bool {
|
||||
let v: Vec<char> = s.chars().collect();
|
||||
self.buscar(&v, 0).is_some()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//! El modelo de valores de awk. Es la parte que más silenciosamente se equivoca.
|
||||
//!
|
||||
//! awk tiene TRES clases de valor, no dos (POSIX.1-2024 §2.6, «Expressions in awk»):
|
||||
//!
|
||||
//! · **número** — `1`, `x+0`, el resultado de cualquier aritmética.
|
||||
//! · **cadena** — un literal entre comillas, o el resultado de `substr`, `sprintf`…
|
||||
//! · **strnum** — lo que VIENE DE FUERA y *parece* un número: campos (`$1`), `getline`, los
|
||||
//! trozos de `split`, `ARGV`, `ENVIRON`, `FILENAME`. Se compara como número.
|
||||
//!
|
||||
//! La tercera es la que decide, y la diferencia es observable:
|
||||
//!
|
||||
//! ```text
|
||||
//! echo '10 9' | awk '{ print ($1 > $2) }' ⇒ 1 (strnum: comparación NUMÉRICA)
|
||||
//! awk 'BEGIN { print ("10" > "9") }' ⇒ 0 (cadenas: "1" < "9" alfabéticamente)
|
||||
//! ```
|
||||
//!
|
||||
//! Un awk que trate los campos como cadenas ordena mal cualquier tabla de números; uno que los trate
|
||||
//! siempre como números rompe `$1 == "0012"`. Por eso `Valor` lleva la procedencia y no sólo el dato.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Valor {
|
||||
Num(f64),
|
||||
Cad(String),
|
||||
/// Vino de la entrada. Guarda el texto TAL CUAL —`print $1` tiene que imprimir `0012`, no `12`—
|
||||
/// y, si parece un número, también su valor.
|
||||
StrNum(String, Option<f64>),
|
||||
/// Variable nunca asignada: vale `""` y `0` a la vez, y compara igual con los dos.
|
||||
Vacio,
|
||||
}
|
||||
|
||||
/// ¿Este texto es un número en el sentido de awk? Admite espacios alrededor, signo, punto decimal y
|
||||
/// exponente; NO admite basura detrás (`"12x"` es cadena) ni hexadecimal (POSIX lo deja fuera, y
|
||||
/// gawk sólo lo acepta en modo no-POSIX).
|
||||
pub fn como_numero(s: &str) -> Option<f64> {
|
||||
let t = s.trim_matches(|c: char| c == ' ' || c == '\t' || c == '\n');
|
||||
if t.is_empty() { return None; }
|
||||
let b: Vec<char> = t.chars().collect();
|
||||
let mut i = 0;
|
||||
if b[i] == '+' || b[i] == '-' { i += 1; }
|
||||
let mut digitos = false;
|
||||
while i < b.len() && b[i].is_ascii_digit() { i += 1; digitos = true; }
|
||||
if i < b.len() && b[i] == '.' {
|
||||
i += 1;
|
||||
while i < b.len() && b[i].is_ascii_digit() { i += 1; digitos = true; }
|
||||
}
|
||||
if !digitos { return None; }
|
||||
if i < b.len() && (b[i] == 'e' || b[i] == 'E') {
|
||||
let guardado = i;
|
||||
i += 1;
|
||||
if i < b.len() && (b[i] == '+' || b[i] == '-') { i += 1; }
|
||||
if i < b.len() && b[i].is_ascii_digit() {
|
||||
while i < b.len() && b[i].is_ascii_digit() { i += 1; }
|
||||
} else { i = guardado; }
|
||||
}
|
||||
if i != b.len() { return None; }
|
||||
t.parse::<f64>().ok()
|
||||
}
|
||||
|
||||
/// El prefijo numérico del texto, que es lo que usa la CONVERSIÓN explícita (`"12x"+0` es 12).
|
||||
/// Distinto de `como_numero`, que exige que no sobre nada: ahí `"12x"` no es un número.
|
||||
pub fn prefijo_numerico(s: &str) -> f64 {
|
||||
let t = s.trim_start_matches(|c: char| c == ' ' || c == '\t' || c == '\n');
|
||||
let b: Vec<char> = t.chars().collect();
|
||||
let mut i = 0;
|
||||
if i < b.len() && (b[i] == '+' || b[i] == '-') { i += 1; }
|
||||
let mut fin = i;
|
||||
while fin < b.len() && b[fin].is_ascii_digit() { fin += 1; }
|
||||
if fin < b.len() && b[fin] == '.' {
|
||||
fin += 1;
|
||||
while fin < b.len() && b[fin].is_ascii_digit() { fin += 1; }
|
||||
}
|
||||
if fin < b.len() && (b[fin] == 'e' || b[fin] == 'E') {
|
||||
let guardado = fin;
|
||||
let mut j = fin + 1;
|
||||
if j < b.len() && (b[j] == '+' || b[j] == '-') { j += 1; }
|
||||
if j < b.len() && b[j].is_ascii_digit() {
|
||||
while j < b.len() && b[j].is_ascii_digit() { j += 1; }
|
||||
fin = j;
|
||||
} else { fin = guardado; }
|
||||
}
|
||||
let t2: String = b[..fin].iter().collect();
|
||||
t2.parse::<f64>().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Formatea un número como lo hace awk al convertirlo a cadena.
|
||||
///
|
||||
/// La regla de POSIX: si el valor es **entero exacto**, se escribe como entero —`print 1/1` da `1`,
|
||||
/// no `1.000000`—; si no, con `CONVFMT` (por defecto `%.6g`) o con `OFMT` cuando lo imprime `print`.
|
||||
/// Ese «entero exacto» es por VALOR, no por tipo: `2.0` es entero y `1e17` también.
|
||||
pub fn numero_a_cadena(n: f64, fmt: &str) -> String {
|
||||
if n.is_nan() { return "nan".into(); }
|
||||
if n.is_infinite() { return if n > 0.0 { "inf".into() } else { "-inf".into() }; }
|
||||
if n == n.trunc() && n.abs() < 1e17 {
|
||||
let mut s = String::new();
|
||||
let _ = write!(s, "{}", n as i64);
|
||||
return s;
|
||||
}
|
||||
crate::formato::una_conversion(fmt, n)
|
||||
}
|
||||
|
||||
impl Valor {
|
||||
pub fn de_entrada(s: &str) -> Valor {
|
||||
let n = como_numero(s);
|
||||
Valor::StrNum(s.to_string(), n)
|
||||
}
|
||||
|
||||
pub fn cadena(&self, convfmt: &str) -> String {
|
||||
match self {
|
||||
Valor::Num(n) => numero_a_cadena(*n, convfmt),
|
||||
Valor::Cad(s) => s.clone(),
|
||||
Valor::StrNum(s, _) => s.clone(),
|
||||
Valor::Vacio => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn numero(&self) -> f64 {
|
||||
match self {
|
||||
Valor::Num(n) => *n,
|
||||
Valor::Cad(s) => prefijo_numerico(s),
|
||||
Valor::StrNum(_, Some(n)) => *n,
|
||||
Valor::StrNum(s, None) => prefijo_numerico(s),
|
||||
Valor::Vacio => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verdad de awk: un número es verdadero si no es 0; una CADENA, si no está vacía. Y un strnum
|
||||
/// se juzga como número — `$1` valiendo `"0"` es FALSO, aunque la cadena no esté vacía.
|
||||
pub fn verdad(&self) -> bool {
|
||||
match self {
|
||||
Valor::Num(n) => *n != 0.0,
|
||||
Valor::Cad(s) => !s.is_empty(),
|
||||
Valor::StrNum(s, n) => match n { Some(v) => *v != 0.0, None => !s.is_empty() },
|
||||
Valor::Vacio => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// ¿Se compara como número frente a otro? Sí si es número, si es un strnum que parece número, o
|
||||
/// si nunca se asignó.
|
||||
fn numerico(&self) -> bool {
|
||||
matches!(self, Valor::Num(_) | Valor::Vacio | Valor::StrNum(_, Some(_)))
|
||||
}
|
||||
|
||||
/// Comparación de awk: numérica si AMBOS lados lo admiten, de cadena en cuanto uno sea cadena.
|
||||
pub fn comparar(&self, otro: &Valor, convfmt: &str) -> std::cmp::Ordering {
|
||||
if self.numerico() && otro.numerico() {
|
||||
let (a, b) = (self.numero(), otro.numero());
|
||||
return a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal);
|
||||
}
|
||||
self.cadena(convfmt).cmp(&otro.cadena(convfmt))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pruebas {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn strnum_compara_como_numero_y_la_cadena_no() {
|
||||
let campo10 = Valor::de_entrada("10");
|
||||
let campo9 = Valor::de_entrada("9");
|
||||
assert_eq!(campo10.comparar(&campo9, "%.6g"), std::cmp::Ordering::Greater);
|
||||
let cad10 = Valor::Cad("10".into());
|
||||
let cad9 = Valor::Cad("9".into());
|
||||
assert_eq!(cad10.comparar(&cad9, "%.6g"), std::cmp::Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn un_campo_que_no_parece_numero_se_compara_como_cadena() {
|
||||
let a = Valor::de_entrada("12x");
|
||||
let b = Valor::de_entrada("9");
|
||||
assert_eq!(a.comparar(&b, "%.6g"), std::cmp::Ordering::Less); // "12x" < "9"
|
||||
assert_eq!(a.numero(), 12.0); // pero convertido a número es su prefijo
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el_texto_original_se_conserva() {
|
||||
let v = Valor::de_entrada("0012");
|
||||
assert_eq!(v.cadena("%.6g"), "0012");
|
||||
assert_eq!(v.numero(), 12.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enteros_exactos_no_llevan_decimales() {
|
||||
assert_eq!(numero_a_cadena(1.0, "%.6g"), "1");
|
||||
assert_eq!(numero_a_cadena(-0.0, "%.6g"), "0");
|
||||
assert_eq!(numero_a_cadena(0.5, "%.6g"), "0.5");
|
||||
assert_eq!(numero_a_cadena(1.0 / 3.0, "%.6g"), "0.333333");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cero_en_un_campo_es_falso_aunque_la_cadena_no_este_vacia() {
|
||||
assert!(!Valor::de_entrada("0").verdad());
|
||||
assert!(Valor::Cad("0".into()).verdad());
|
||||
assert!(!Valor::de_entrada("").verdad());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn que_es_numero_y_que_no() {
|
||||
assert_eq!(como_numero(" 3.5e2 "), Some(350.0));
|
||||
assert_eq!(como_numero("+.5"), Some(0.5));
|
||||
assert_eq!(como_numero("12x"), None);
|
||||
assert_eq!(como_numero("0x10"), None); // POSIX no admite hexadecimal
|
||||
assert_eq!(como_numero(""), None);
|
||||
assert_eq!(como_numero("."), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Las divergencias que el banco diferencial destapó contra `busybox awk` y `gawk --posix`.
|
||||
//!
|
||||
//! Cada una estuvo ROTA y daba un resultado plausible: ninguna reventaba, ninguna imprimía un
|
||||
//! error. Por eso están acá y no sólo en el banco — el banco compara contra oráculos que hay que
|
||||
//! tener en el store, y esto corre con `cargo test` en cualquier sitio.
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
use std::io::Write;
|
||||
|
||||
fn awk(prog: &str, entrada: &str) -> String {
|
||||
let exe = env!("CARGO_BIN_EXE_qillqa");
|
||||
let mut h = Command::new(exe).arg(prog)
|
||||
.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped())
|
||||
.spawn().expect("no pude lanzar qillqa");
|
||||
h.stdin.as_mut().unwrap().write_all(entrada.as_bytes()).unwrap();
|
||||
let s = h.wait_with_output().unwrap();
|
||||
String::from_utf8_lossy(&s.stdout).trim_end_matches('\n').to_string()
|
||||
}
|
||||
|
||||
/// Leer NF tiene que forzar el partido en campos. Daba 0 hasta que alguien tocara un campo, porque
|
||||
/// el partido es perezoso — y eso es lo que hace barato asignar a `$0`.
|
||||
#[test]
|
||||
fn nf_se_lee_sin_tocar_un_campo() {
|
||||
assert_eq!(awk("{print NF}", " a b \n"), "2");
|
||||
assert_eq!(awk("{print NF, $NF}", "a b c\n"), "3 c");
|
||||
}
|
||||
|
||||
/// `length` sin paréntesis es el único builtin al que POSIX se lo permite, y se leía como una
|
||||
/// variable llamada `length`, que siempre vale vacío.
|
||||
#[test]
|
||||
fn length_sin_parentesis() {
|
||||
assert_eq!(awk("{print length}", "abcd\n"), "4");
|
||||
assert_eq!(awk("BEGIN{print length(\"hola\")}", ""), "4");
|
||||
}
|
||||
|
||||
/// Un array se pasa POR REFERENCIA. Se decide mirando si la función usa el parámetro como array, y
|
||||
/// `a["k"] = 1` tiene el nombre en el LUGAR de la asignación, no en el valor.
|
||||
#[test]
|
||||
fn los_arrays_se_pasan_por_referencia() {
|
||||
assert_eq!(awk(r#"function g(a){a["k"]="puesto"} BEGIN{g(A); print A["k"]}"#, ""), "puesto");
|
||||
// Y los escalares siguen yendo por valor.
|
||||
assert_eq!(awk("function f(x){x=99} BEGIN{y=1; f(y); print y}", ""), "1");
|
||||
}
|
||||
|
||||
/// `substr` con inicio ≤ 0: se recorta a 1 y **el largo no se ajusta**. La lectura estricta de
|
||||
/// POSIX daría `a`; ni busybox ni gawk hacen eso, y lo que hay que ejecutar son los guiones
|
||||
/// escritos contra ellos.
|
||||
#[test]
|
||||
fn substr_con_indices_fuera_de_rango() {
|
||||
assert_eq!(awk(r#"BEGIN{print substr("abcdef", 0, 3)}"#, ""), "abc");
|
||||
assert_eq!(awk(r#"BEGIN{print substr("abcdef", -1, 3)}"#, ""), "abc");
|
||||
assert_eq!(awk(r#"BEGIN{print "["substr("abc", 10)"]"}"#, ""), "[]");
|
||||
// Los no enteros se truncan hacia cero, no se redondean.
|
||||
assert_eq!(awk(r#"BEGIN{print substr("abcdef", 1.5, 2.5)}"#, ""), "ab");
|
||||
}
|
||||
|
||||
/// Una coincidencia VACÍA pegada al final de la anterior no sustituye. Es la única diferencia
|
||||
/// medida entre gawk y el awk de busybox en todo el banco, y gawk es quien sigue a POSIX.
|
||||
#[test]
|
||||
fn gsub_no_sustituye_el_vacio_pegado_a_la_coincidencia_anterior() {
|
||||
assert_eq!(awk(r#"BEGIN{s="xax"; n=gsub(/a*/,"-",s); print n, s}"#, ""), "3 -x-x-");
|
||||
assert_eq!(awk(r#"BEGIN{s="abc"; n=gsub(/x*/,"-",s); print n, s}"#, ""), "4 -a-b-c-");
|
||||
}
|
||||
|
||||
/// El campo que viene de la entrada se compara como número si lo parece, y como cadena si no.
|
||||
#[test]
|
||||
fn strnum() {
|
||||
assert_eq!(awk("{print ($1 > $2)}", "10 9\n"), "1");
|
||||
assert_eq!(awk(r#"BEGIN{print ("10" > "9")}"#, ""), "0");
|
||||
assert_eq!(awk("{print $1}", "0012\n"), "0012");
|
||||
assert_eq!(awk("{print $1+0}", "0012\n"), "12");
|
||||
}
|
||||
|
||||
/// `^` asocia a la DERECHA. El awk de busybox lo hace al revés y da 64.
|
||||
#[test]
|
||||
fn la_potencia_asocia_a_la_derecha() {
|
||||
assert_eq!(awk("BEGIN{print 2^3^2}", ""), "512");
|
||||
}
|
||||
|
||||
/// La ERE es leftmost-LONGEST, no leftmost-first: para `a|ab` sobre `"ab"` gana `ab`.
|
||||
#[test]
|
||||
fn la_regex_es_leftmost_longest() {
|
||||
assert_eq!(awk(r#"BEGIN{print match("ab", /a|ab/), RLENGTH}"#, ""), "1 2");
|
||||
assert_eq!(awk(r#"BEGIN{s="ab"; sub(/a|ab/, "X", s); print s}"#, ""), "X");
|
||||
}
|
||||
|
||||
/// Un `exit` en el cuerpo ejecuta los END; uno dentro de END, no.
|
||||
#[test]
|
||||
fn exit_ejecuta_los_end() {
|
||||
assert_eq!(awk(r#"BEGIN{exit 0} END{print "end corre"}"#, ""), "end corre");
|
||||
}
|
||||
|
||||
/// El `%c` de printf mira el TIPO del argumento.
|
||||
#[test]
|
||||
fn printf_c_mira_el_tipo() {
|
||||
assert_eq!(awk(r#"BEGIN{printf "%c|%c\n", 65, "65"}"#, ""), "A|6");
|
||||
}
|
||||
|
||||
/// Los campos se rehacen con OFS al tocar cualquiera, y `$0` se reparte al asignarlo.
|
||||
#[test]
|
||||
fn campos_y_registro_son_dos_caras_del_mismo_dato() {
|
||||
assert_eq!(awk(r#"BEGIN{OFS="-"} {$1=$1; print}"#, "a b c\n"), "a-b-c");
|
||||
assert_eq!(awk(r#"{$5="X"; print; print NF}"#, "a b\n"), "a b X\n5");
|
||||
assert_eq!(awk(r#"{$0="p q r"; print $2, NF}"#, "a b\n"), "q 3");
|
||||
assert_eq!(awk(r#"{NF=2; print}"#, "a b c d\n"), "a b");
|
||||
}
|
||||
@@ -141,6 +141,16 @@ paquetes = [
|
||||
# `bc` es gavinhoward/bc: `bc` Y `dc` del mismo árbol, C99 sin dependencias, el que usan Alpine y
|
||||
# FreeBSD. La alternativa de GNU arrastra `ed` y `flex`, y su `dc` va en otro paquete.
|
||||
"psmisc", "bc",
|
||||
# `ncurses-tools` (2026-09-22): `clear`, `reset`, `tput`, `tset`, `infocmp`, `tic`. La canónica
|
||||
# `ncurses` instala `install.libs install.includes` y nada más ⇒ `clear` y `reset` en una imagen
|
||||
# de takana eran el applet de busybox. Es el mismo caso que `xz-tools` y `zstd-cli`: variante con
|
||||
# las herramientas, porque ampliar la canónica re-hashearía a sus 15 consumidores.
|
||||
# ⚠ Instala los binarios y NO la base terminfo (~4000 entradas): `clear` y `tput` resuelven con
|
||||
# los `--with-fallbacks` compilados dentro de la librería —linux, vt100, vt220, xterm,
|
||||
# xterm-256color, screen, tmux, tmux-256color, ansi—, que cubren la consola del metal, QEMU y
|
||||
# cualquier ssh moderno. Con un TERM exótico contestan «unknown terminal type» en vez de pintar
|
||||
# basura. Corolario: `tic` e `infocmp` viajan pero no tienen base que consultar.
|
||||
"ncurses-tools",
|
||||
]
|
||||
|
||||
[perfil.cli]
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# ncurses 6.5 — VARIANTE con los PROGRAMAS: `clear`, `reset`, `tput`, `tset`, `infocmp`, `tic`.
|
||||
# NO sustituye a `recipes/ncurses.toml`.
|
||||
#
|
||||
# ── POR QUÉ EXISTE ──────────────────────────────────────────────────────────────────────────────
|
||||
# La canónica instala `make install.libs install.includes`: librería y cabeceras, nada más. Es
|
||||
# deliberado —de ella cuelgan 15 recetas— pero el efecto medido es que `clear` y `reset` en una
|
||||
# imagen de takana eran **el applet de busybox**, con `ncurses` declarado en los 7 perfiles
|
||||
# (`scripts/busybox-censo-proveedores.py`, 2026-09-21). Tocar la canónica re-hashearía a sus 15
|
||||
# consumidores; va como variante, igual que `xz-tools` sobre `xz` y `zstd-cli` sobre `zstd`.
|
||||
#
|
||||
# ── LA DECISIÓN QUE HAY DETRÁS: terminfo ────────────────────────────────────────────────────────
|
||||
# `clear` y `reset` necesitan saber qué secuencias manda cada terminal, y eso sale de la base
|
||||
# terminfo (~4000 entradas, varios MB) o de los `--with-fallbacks` COMPILADOS DENTRO de la librería.
|
||||
# La canónica ya pinea nueve: linux, vt100, vt220, xterm, xterm-256color, screen, tmux,
|
||||
# tmux-256color, ansi. Esta variante instala `install.progs` y **no** `install.data`: los binarios
|
||||
# viajan, la base de datos no. Con eso, `clear` funciona para los nueve TERM que cubren la consola
|
||||
# del metal, QEMU y cualquier ssh desde un emulador moderno, y para un TERM exótico contesta
|
||||
# «unknown terminal type» en vez de pintar basura.
|
||||
#
|
||||
# El corolario, dicho para que nadie lo descubra a la mala: **`tic` e `infocmp` viajan pero no
|
||||
# tienen base que leer**. Sirven para compilar una entrada propia, no para consultar el sistema.
|
||||
|
||||
name = "ncurses-tools"
|
||||
version = "6.5"
|
||||
license = "MIT"
|
||||
|
||||
[source]
|
||||
tarball = "https://ftp.gnu.org/gnu/ncurses/ncurses-6.5.tar.gz"
|
||||
sha256 = "136d91bc269a9a5785e5f9e980bc76ab57428f604ce3e5a5a90cebc767971cc6"
|
||||
|
||||
[build]
|
||||
compiler = "zig-cc"
|
||||
target = "x86_64-linux-musl"
|
||||
link = "static"
|
||||
|
||||
# Mismo `strip_debug` que la canónica: la variante no es sitio para cambiar de política.
|
||||
strip_debug = true
|
||||
|
||||
[deps]
|
||||
build = ["binutils"]
|
||||
|
||||
[build.phases]
|
||||
# `configure` IDÉNTICO al de la canónica —mismos fallbacks, mismo widec— porque los binarios tienen
|
||||
# que hablar la misma librería que ya viaja. Cambiar una perilla acá haría que `tput` resolviera
|
||||
# distinto que la `ncursesw` que enlazan las aplicaciones.
|
||||
configure = "./configure --prefix=/usr --without-shared --with-normal --enable-widec --without-debug --without-ada --without-tests --enable-pc-files --with-pkg-config-libdir=/usr/lib/pkgconfig --with-fallbacks=linux,vt100,vt220,xterm,xterm-256color,screen,tmux,tmux-256color,ansi"
|
||||
# ⚠ Los diez binarios salen DINÁMICOS aunque la receta diga `link = "static"` y el harness inyecte
|
||||
# `LDFLAGS=-static`, y la causa no es un flag que falte: es que ncurses usa `-static`/`-dynamic`
|
||||
# como **marcadores de tramo** alrededor de las librerías —su forma portable de
|
||||
# `-Wl,-Bstatic … -Wl,-Bdynamic`— y no como una decisión global. El Makefile generado trae:
|
||||
#
|
||||
# LIBS_TIC = -L../lib -static -lncursesw -lncursesw -dynamic
|
||||
# LIBS_TINFO = -static -lncursesw -dynamic
|
||||
#
|
||||
# o sea «ncursesw estática, lo demás dinámico», que es exactamente lo que `--with-normal` promete.
|
||||
# El `-dynamic` llega al final de la línea de enlace y gana por orden. Se quita sobreescribiendo las
|
||||
# DOS variables en la línea de `make` (ahí sí: son variables del Makefile, no valores ya sustituidos
|
||||
# por el configure — la diferencia que hizo fallar el primer intento con `psmisc`).
|
||||
#
|
||||
# Es el tercer sabor del agujero que documenta `scripts/static-audit.sh`: cada sistema de build
|
||||
# decide dónde pone el flag, y `link = "static"` es una declaración que hay que ir a COMPROBAR al
|
||||
# binario. Aquí lo comprueba `file`; en el corpus, ese guión.
|
||||
compile = 'make LIBS_TIC="-L../lib -static -lncursesw -lncursesw" LIBS_TINFO="-static -lncursesw"'
|
||||
# Sólo `install.progs`. Ni `install.libs` ni `install.includes`: los publica la canónica, y dos
|
||||
# copias de `libncursesw.a` disputándose la misma ruta hacen que quién gana dependa del orden de
|
||||
# hidratación — que es justo lo que no debe decidir nada. Mismo criterio que `xz-tools`.
|
||||
install = 'make LIBS_TIC="-L../lib -static -lncursesw -lncursesw" LIBS_TINFO="-static -lncursesw" install.progs DESTDIR=/out'
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Banco DIFERENCIAL de awk: corre los mismos casos en varios motores y compara.
|
||||
|
||||
scripts/awk-banco-diferencial.py # qillqa contra busybox awk y gawk --posix
|
||||
scripts/awk-banco-diferencial.py --motor ruta/al/awk # añade otro candidato
|
||||
|
||||
Por qué diferencial y no de salidas esperadas escritas a mano: un banco escrito a mano mide lo que
|
||||
al autor se le ocurrió preguntar, y ahí es donde se cuela el sesgo — es la lección del §paso 3 de
|
||||
`docs/plan-botar-busybox.md`, donde brush pasó 56/57 casos POSIX y no construía una sola receta.
|
||||
Contra un oráculo que ya está en producción, cada divergencia es un hecho, no una opinión.
|
||||
|
||||
⚠ Los oráculos NO coinciden entre sí en todo: busybox awk y gawk difieren en `substr` con índices
|
||||
raros, en `%c` y en el formato de algunos números. Por eso el veredicto no es «igual a X» sino
|
||||
«igual a LOS DOS», y cuando los dos difieren entre ellos el caso se marca `discrepan` y no cuenta
|
||||
como fallo de nadie — se mira a mano y se decide siguiendo POSIX.
|
||||
"""
|
||||
import argparse, json, os, subprocess, sys
|
||||
|
||||
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def del_store(receta, rel):
|
||||
est = json.load(open(os.path.join(RAIZ, "docs/state/build-state.json")))["nodes"]
|
||||
if receta not in est:
|
||||
return None
|
||||
p = "/store/" + est[receta]["hash"].split(":")[1] + "-" + receta + "/" + rel
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
# (nombre, programa, entrada)
|
||||
CASOS = [
|
||||
# ── campos, separadores, registros ──────────────────────────────────────────────────────────
|
||||
("campos-simple", '{print $2}', "a b c\nd e f\n"),
|
||||
("campos-nf", '{print NF, $NF}', "a b c\nx\n"),
|
||||
("campos-vacio", '{print "["$5"]"}', "a b\n"),
|
||||
("campos-cero", '{print $0}', " hola mundo \n"),
|
||||
("fs-espacios-rachas", '{print NF}', " a b \n"),
|
||||
("fs-coma", '-F, {print $2}', "a,b,c\n"),
|
||||
("fs-tab", '-F\t {print $2}', "a\tb\tc\n"),
|
||||
("fs-ere", '-F[0-9]+ {print $2}', "ab123cd\n"),
|
||||
("fs-punto-literal", '-F. {print NF}', "a.b.c\n"),
|
||||
("asignar-campo", '{$2="X"; print}', "a b c\n"),
|
||||
("asignar-campo-lejos", '{$5="X"; print; print NF}', "a b\n"),
|
||||
("asignar-cero", '{$0="p q r"; print $2, NF}', "a b\n"),
|
||||
("nf-recorta", '{NF=2; print; print NF}', "a b c d\n"),
|
||||
("ofs-al-rehacer", 'BEGIN{OFS="-"} {$1=$1; print}', "a b c\n"),
|
||||
("registros-nr-fnr", '{print NR, FNR}', "x\ny\n"),
|
||||
# ── valores: la parte que más se equivoca ───────────────────────────────────────────────────
|
||||
("strnum-numerico", '{print ($1 > $2)}', "10 9\n"),
|
||||
("cadena-alfabetica", 'BEGIN{print ("10" > "9")}', ""),
|
||||
("strnum-cero-falso", '{if ($1) print "si"; else print "no"}', "0\n"),
|
||||
("cadena-cero-verdad", 'BEGIN{ if ("0") print "si"; else print "no"}', ""),
|
||||
("texto-se-conserva", '{print $1}', "0012\n"),
|
||||
("suma-con-basura", '{print $1+0}', "12x\n"),
|
||||
("entero-sin-decimales", 'BEGIN{print 2/1, 1/3}', ""),
|
||||
("convfmt", 'BEGIN{CONVFMT="%.2g"; x=1/3; print x ""}', ""),
|
||||
("ofmt", 'BEGIN{OFMT="%.2f"; print 1/3}', ""),
|
||||
("uninit-es-cero-y-vacio", 'BEGIN{print x+0, "["x"]", (x==0), (x=="")}', ""),
|
||||
# ── aritmética y operadores ─────────────────────────────────────────────────────────────────
|
||||
("potencia-derecha", 'BEGIN{print 2^3^2}', ""),
|
||||
("modulo-real", 'BEGIN{print 5.5 % 2}', ""),
|
||||
("unario-vs-concat", 'BEGIN{a=1;b=2;print a-b; print a" "-b}', ""),
|
||||
("incremento", 'BEGIN{i=5; print i++, i, ++i, i}', ""),
|
||||
("asigna-op", 'BEGIN{x=10; x-=3; x*=2; print x}', ""),
|
||||
("ternario", 'BEGIN{print (1?"a":"b"), (0?"a":"b")}', ""),
|
||||
("comparacion-encadena", 'BEGIN{print (1<2), (2<=2), (3!=3)}', ""),
|
||||
("concatenacion", 'BEGIN{print "a" 1+1 "b"}', ""),
|
||||
# ── expresiones regulares ───────────────────────────────────────────────────────────────────
|
||||
("ere-patron", '/b/{print "hay"}', "abc\nxyz\n"),
|
||||
("ere-tilde", '{print ($0 ~ /^a/), ($0 !~ /^a/)}', "abc\nxbc\n"),
|
||||
("ere-alternancia-larga", 'BEGIN{print match("ab", /a|ab/), RLENGTH}', ""),
|
||||
("ere-clase", 'BEGIN{print match("foo123", /[[:digit:]]+/), RSTART, RLENGTH}', ""),
|
||||
("ere-intervalo", 'BEGIN{print match("aaaa", /a{2,3}/), RLENGTH}', ""),
|
||||
("ere-anclas", 'BEGIN{print ("abc" ~ /^abc$/), ("abc" ~ /^b/)}', ""),
|
||||
("ere-punto", 'BEGIN{print ("a.c" ~ /a\\.c/), ("abc" ~ /a\\.c/)}', ""),
|
||||
("ere-estrella-vacia", 'BEGIN{print match("bbb", /a*/), RLENGTH}', ""),
|
||||
("ere-clase-negada", 'BEGIN{print match("abc1", /[^a-z]/), RSTART}', ""),
|
||||
# ── funciones de cadena ─────────────────────────────────────────────────────────────────────
|
||||
("substr-normal", 'BEGIN{print substr("abcdef", 2, 3)}', ""),
|
||||
("substr-sin-largo", 'BEGIN{print substr("abcdef", 3)}', ""),
|
||||
("substr-inicio-cero", 'BEGIN{print "["substr("abcdef", 0, 3)"]"}', ""),
|
||||
("substr-negativo", 'BEGIN{print "["substr("abcdef", -1, 3)"]"}', ""),
|
||||
("substr-pasado", 'BEGIN{print "["substr("abc", 10)"]"}', ""),
|
||||
("substr-redondeo", 'BEGIN{print substr("abcdef", 1.5, 2.5)}', ""),
|
||||
("index", 'BEGIN{print index("abcabc","bc"), index("abc","z"), index("abc","")}', ""),
|
||||
("length", 'BEGIN{print length("hola"), length()}', ""),
|
||||
("length-campo", '{print length}', "abcd\n"),
|
||||
("toupper-tolower", 'BEGIN{print toupper("aBc"), tolower("AbC")}', ""),
|
||||
("sub-simple", 'BEGIN{s="aaa"; n=sub(/a/,"b",s); print n, s}', ""),
|
||||
("gsub-simple", 'BEGIN{s="aaa"; n=gsub(/a/,"b",s); print n, s}', ""),
|
||||
("gsub-ampersand", 'BEGIN{s="abc"; gsub(/b/,"[&]",s); print s}', ""),
|
||||
("gsub-ampersand-escapado", 'BEGIN{s="abc"; gsub(/b/,"[\\\\&]",s); print s}', ""),
|
||||
("gsub-vacio", 'BEGIN{s="xax"; n=gsub(/a*/,"-",s); print n, s}', ""),
|
||||
("gsub-en-campo", '{gsub(/o/,"0"); print}', "foo boo\n"),
|
||||
("split-fs", 'BEGIN{n=split("a:b:c",A,":"); print n, A[1], A[3]}', ""),
|
||||
("split-ere", 'BEGIN{n=split("a1b22c",A,/[0-9]+/); print n, A[2], A[3]}', ""),
|
||||
("split-vacio", 'BEGIN{n=split("",A,":"); print n}', ""),
|
||||
("split-default", 'BEGIN{n=split(" a b ",A); print n, A[1], A[2]}', ""),
|
||||
("sprintf", 'BEGIN{print sprintf("%03d-%s", 7, "x")}', ""),
|
||||
("match-sin-exito", 'BEGIN{print match("abc",/z/), RSTART, RLENGTH}', ""),
|
||||
# ── printf ──────────────────────────────────────────────────────────────────────────────────
|
||||
("printf-enteros", 'BEGIN{printf "%d|%5d|%-5d|%05d\\n", 42, 42, 42, 42}', ""),
|
||||
("printf-negativo", 'BEGIN{printf "%05d|%+d|% d\\n", -1, 7, 7}', ""),
|
||||
("printf-flotante", 'BEGIN{printf "%.2f|%e|%g\\n", 3.14159, 150, 0.0000123}', ""),
|
||||
("printf-cadena", 'BEGIN{printf "%s|%10s|%-10s|%.2s|\\n", "ab","ab","ab","abcd"}', ""),
|
||||
("printf-caracter-num", 'BEGIN{printf "%c\\n", 65}', ""),
|
||||
("printf-hex", 'BEGIN{printf "%x|%X|%o\\n", 255, 255, 8}', ""),
|
||||
("printf-asterisco", 'BEGIN{printf "%*d|%.*f\\n", 5, 42, 2, 3.14159}', ""),
|
||||
("printf-porciento", 'BEGIN{printf "100%%\\n"}', ""),
|
||||
("printf-trunca", 'BEGIN{printf "%d\\n", 3.99}', ""),
|
||||
# ── control de flujo ────────────────────────────────────────────────────────────────────────
|
||||
("if-else", 'BEGIN{ if (1) print "a"; else print "b" }', ""),
|
||||
("while", 'BEGIN{i=0; while(i<3){printf "%d", i; i++}; print ""}', ""),
|
||||
("do-while", 'BEGIN{i=0; do {printf "%d", i; i++} while(i<3); print ""}', ""),
|
||||
("for-clasico", 'BEGIN{for(i=0;i<3;i++) printf "%d", i; print ""}', ""),
|
||||
("for-in", 'BEGIN{a[1];a[2];a[3]; n=0; for(k in a) n++; print n}', ""),
|
||||
("break-continue", 'BEGIN{for(i=0;i<5;i++){if(i==1)continue; if(i==3)break; printf "%d",i}; print ""}', ""),
|
||||
("next", '/skip/{next} {print}', "uno\nskip esto\ndos\n"),
|
||||
("exit-con-end", 'BEGIN{exit 0} END{print "end corre"}', ""),
|
||||
("patron-rango", '/ini/,/fin/{print}', "a\nini\nx\nfin\nb\n"),
|
||||
("patron-expresion", 'NR==2{print}', "a\nb\nc\n"),
|
||||
("patron-sin-accion", 'NR==2', "a\nb\nc\n"),
|
||||
# ── arrays ──────────────────────────────────────────────────────────────────────────────────
|
||||
("array-subsep", 'BEGIN{a[1,2]="x"; for(k in a){split(k,p,SUBSEP); print p[1],p[2]}}', ""),
|
||||
("array-in-crea", 'BEGIN{ if (a["k"]=="") print "vacio"; print ("k" in a) }', ""),
|
||||
("array-delete", 'BEGIN{a[1]=1;a[2]=2; delete a[1]; n=0; for(k in a)n++; print n}', ""),
|
||||
("array-delete-todo", 'BEGIN{a[1]=1;a[2]=2; delete a; n=0; for(k in a)n++; print n}', ""),
|
||||
("array-length", 'BEGIN{a[1];a[2]; print length(a)}', ""),
|
||||
# ── funciones de usuario ────────────────────────────────────────────────────────────────────
|
||||
("funcion-simple", 'function f(x){return x*2} BEGIN{print f(21)}', ""),
|
||||
("funcion-recursiva", 'function f(n){return n<=1?1:n*f(n-1)} BEGIN{print f(6)}', ""),
|
||||
("funcion-array-ref", 'function g(a){a["k"]="puesto"} BEGIN{g(A); print A["k"]}', ""),
|
||||
("funcion-local", 'function f(x, t){t=x*2; return t} BEGIN{t=9; print f(2), t}', ""),
|
||||
("funcion-sin-return", 'function f(){} BEGIN{print "["f()"]"}', ""),
|
||||
# ── entrada/salida ──────────────────────────────────────────────────────────────────────────
|
||||
("print-redirigido", 'BEGIN{print "x" > "/dev/stderr"}', ""),
|
||||
("print-coma-ofs", 'BEGIN{OFS=":"; print "a","b"}', ""),
|
||||
("print-ors", 'BEGIN{ORS="|"} {print}', "a\nb\n"),
|
||||
("print-parentesis", 'BEGIN{print (1), (2)}', ""),
|
||||
("getline-tuberia", 'BEGIN{ "echo hola" | getline x; print x }', ""),
|
||||
("getline-simple", 'NR==1{ getline; print "leido:" $0 }', "a\nb\nc\n"),
|
||||
("rs-parrafo", 'BEGIN{RS=""} {print NR": "$1"-"$2}', "a\nb\n\nc\nd\n"),
|
||||
("rs-caracter", 'BEGIN{RS=":"} {print NR, $0}', "a:b:c"),
|
||||
# ── rincones del parser ─────────────────────────────────────────────────────────────────────
|
||||
("barra-division", 'BEGIN{a=6; b=3; print a / b}', ""),
|
||||
("print-mayor-redirige", 'BEGIN{print (1>2)}', ""),
|
||||
("comentario", 'BEGIN{ # esto es comentario\nprint "ok"}', ""),
|
||||
("punto-y-coma", 'BEGIN{;;print "ok";;}', ""),
|
||||
("continuacion-linea", 'BEGIN{print "a" \\\n "b"}', ""),
|
||||
("cadena-escapes", 'BEGIN{print "a\\tb\\\\c"}', ""),
|
||||
("expresion-regex-valor", '{print /b/}', "abc\nxyz\n"),
|
||||
]
|
||||
|
||||
|
||||
def corre(motor, caso, entrada):
|
||||
"""Devuelve (salida+error, código)."""
|
||||
args = list(motor)
|
||||
prog = caso
|
||||
# Los casos que empiezan por `-F` traen la opción pegada al programa.
|
||||
if prog.startswith("-F"):
|
||||
i = prog.index(" ")
|
||||
args += [prog[:i]]
|
||||
prog = prog[i + 1:]
|
||||
args.append(prog)
|
||||
try:
|
||||
r = subprocess.run(args, input=entrada, capture_output=True, text=True, timeout=10)
|
||||
return (r.stdout + r.stderr).rstrip("\n"), r.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
return "@TIMEOUT", -1
|
||||
except Exception as e:
|
||||
return f"@ERROR {e}", -1
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--motor", default=os.path.join(RAIZ, "target/release/qillqa"))
|
||||
ap.add_argument("--verbose", action="store_true")
|
||||
a = ap.parse_args()
|
||||
|
||||
bb = del_store("busybox", "usr/bin/awk")
|
||||
gawk = del_store("gawk", "usr/bin/gawk")
|
||||
oraculos = []
|
||||
if bb: oraculos.append(("busybox", [bb]))
|
||||
if gawk: oraculos.append(("gawk", [gawk, "--posix"]))
|
||||
if not oraculos:
|
||||
print("sin oráculos en el store: no se puede comparar", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(f"motor: {a.motor}")
|
||||
print(f"oráculos: {', '.join(n for n, _ in oraculos)}\n")
|
||||
igual = discrepan = distinto = 0
|
||||
fallos = []
|
||||
for nombre, prog, entrada in CASOS:
|
||||
mio, _ = corre([a.motor], prog, entrada)
|
||||
refs = [(n, corre(cmd, prog, entrada)[0]) for n, cmd in oraculos]
|
||||
valores = {r for _, r in refs}
|
||||
if len(valores) > 1:
|
||||
discrepan += 1
|
||||
if a.verbose:
|
||||
print(f" ~ {nombre}: los oráculos discrepan " +
|
||||
" · ".join(f"{n}=[{r}]" for n, r in refs) + f" · qillqa=[{mio}]")
|
||||
continue
|
||||
esperado = refs[0][1]
|
||||
if mio == esperado:
|
||||
igual += 1
|
||||
else:
|
||||
distinto += 1
|
||||
fallos.append((nombre, prog, entrada, esperado, mio))
|
||||
|
||||
print(f" iguales a los oráculos : {igual}")
|
||||
print(f" DIVERGEN : {distinto}")
|
||||
print(f" los oráculos discrepan : {discrepan} (no cuentan: se deciden por POSIX a mano)")
|
||||
if fallos:
|
||||
print("\n── divergencias ──")
|
||||
for n, p, e, esp, mio in fallos:
|
||||
print(f"\n ✗ {n}\n programa: {p}\n entrada : {e!r}\n oráculos: [{esp}]\n qillqa : [{mio}]")
|
||||
return 1 if fallos else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user