diff --git a/Cargo.lock b/Cargo.lock index c7f876c6..4f3670a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -881,6 +881,13 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qillqa" +version = "0.0.1" +dependencies = [ + "libc", +] + [[package]] name = "quote" version = "1.0.45" diff --git a/Cargo.toml b/Cargo.toml index be5d6b00..4e4e8366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/hammerd", "crates/netup", "crates/simi", + "crates/qillqa", ] [workspace.package] diff --git a/crates/qillqa/Cargo.toml b/crates/qillqa/Cargo.toml new file mode 100644 index 00000000..05224202 --- /dev/null +++ b/crates/qillqa/Cargo.toml @@ -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" diff --git a/crates/qillqa/src/ast.rs b/crates/qillqa/src/ast.rs new file mode 100644 index 00000000..98c5ec8a --- /dev/null +++ b/crates/qillqa/src/ast.rs @@ -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), + Var(String), + Campo(Box), + /// `a[i]`, `a[i,j]` (los índices se unen con SUBSEP). + Indice(String, Vec), + Asigna(Box, Box), + /// `+=`, `-=`, … con su operador. + AsignaOp(Box, BinOp, Box), + Bin(BinOp, Box, Box), + Cmp(CmpOp, Box, Box), + /// `~` y `!~`. El segundo `bool` dice si está negado. + Coincide(Box, Box, bool), + Y(Box, Box), + O(Box, Box), + No(Box), + Neg(Box), + Pos(Box), + Ternario(Box, Box, Box), + /// Concatenación por yuxtaposición: `a b`. + Concat(Box, Box), + /// `++x` / `x++` (y `--`). `previo` = es prefijo. + Incr(Box, bool, f64), + /// `(i, j) in a` y `i in a`. + En(Vec, String), + Llamada(String, Vec), + Builtin(String, Vec), + /// `getline` en sus cuatro formas. + Getline(Getline), + /// `(expr)` agrupada: hay que recordarlo para distinguir `(a,b) in arr` de una lista. + Grupo(Box), +} + +#[derive(Clone)] +pub enum Getline { + /// `getline [lugar]` — de la entrada normal. + Simple(Option>), + /// `getline [lugar] < "fichero"`. + DeFichero(Option>, Box), + /// `"cmd" | getline [lugar]`. + DeTuberia(Box, Option>), +} + +/// Algo a lo que se puede asignar. +#[derive(Clone)] +pub enum Lugar { + Var(String), + Campo(Box), + Indice(String, Vec), +} + +#[derive(Clone)] +pub enum Redir { + /// `> f` + Fichero(Box), + /// `>> f` + Anexa(Box), + /// `| "cmd"` + Tuberia(Box), +} + +#[derive(Clone)] +pub enum Sent { + Expr(Expr), + Print(Vec, Option), + Printf(Vec, Option), + Bloque(Vec), + Si(Expr, Box, Option>), + Mientras(Expr, Box), + HazMientras(Box, Expr), + /// `for (init; cond; paso) cuerpo` + Para(Option>, Option, Option>, Box), + /// `for (k in a) cuerpo` + ParaEn(String, String, Box), + Corta, + Sigue, + Siguiente, + SiguienteFichero, + Sale(Option), + Retorna(Option), + Borra(String, Vec), + /// `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>, + /// Í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, + pub cuerpo: Vec, +} + +#[derive(Clone, Default)] +pub struct Programa { + pub reglas: Vec, + pub funciones: Vec, + pub rangos: usize, +} diff --git a/crates/qillqa/src/formato.rs b/crates/qillqa/src/formato.rs new file mode 100644 index 00000000..5b03176a --- /dev/null +++ b/crates/qillqa/src/formato.rs @@ -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, + pub precision: Option, + 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 = 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, _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 = 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 = 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), +} + +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 { + 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) -> 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"); + } +} diff --git a/crates/qillqa/src/interp.rs b/crates/qillqa/src/interp.rs new file mode 100644 index 00000000..fd8060b6 --- /dev/null +++ b/crates/qillqa/src/interp.rs @@ -0,0 +1,1306 @@ +//! El intérprete: recorre el árbol y ejecuta. +//! +//! Tres piezas merecen leerse antes de tocar nada: +//! +//! * **Los campos y `$0` son un mismo dato con dos caras.** Escribir `$2` reconstruye `$0` uniendo +//! con OFS; escribir `$0` vuelve a partir en campos con FS; y asignar a `$5` cuando sólo hay tres +//! campos CREA los que faltan vacíos y sube NF. Todo eso vive en `partir` / `rehacer_registro`. +//! * **El separador FS tiene cuatro comportamientos**, no uno: `" "` (el de por defecto) parte por +//! rachas de espacios/tabuladores/saltos y descarta los de los extremos; un carácter suelto parte +//! por ese carácter exacto; `"\t"` es ese caso; y cualquier cosa de más de un carácter es una ERE. +//! * **Los arrays se pasan POR REFERENCIA a las funciones y los escalares por valor**, y eso no se +//! declara: se descubre al usar el parámetro. Por eso un parámetro puede empezar indefinido y +//! convertirse en array dentro de la llamada. + +use crate::ast::*; +use crate::formato::{self, Arg}; +use crate::regex::Regex; +use crate::valor::{self, Valor}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Read, Write}; +use std::rc::Rc; + +/// Lo que puede haber en una variable: un escalar o un array. No hay un tercer caso, y confundirlos +/// es un error en awk (`x[1]=1; x=2` es ilegal), pero no lo comprobamos: los awk reales tampoco lo +/// hacen en todos los caminos y romper eso sólo rompe guiones que ya funcionaban. +#[derive(Clone)] +pub enum Celda { + Escalar(Valor), + Array(Rc>>), +} + +/// Por qué se interrumpió la ejecución de un bloque. +pub enum Flujo { + Normal, + Corta, + Sigue, + Siguiente, + SiguienteFichero, + Retorna(Valor), + Sale(i32), +} + +macro_rules! propaga { + ($e:expr) => { + match $e { Flujo::Normal => {}, otro => return Ok(otro) } + }; +} + +pub struct Salida { + escritor: Box, + /// Si es una tubería, hay un hijo al que esperar en `close`. + hijo: Option, +} + +pub struct Entrada { + lector: Box, + hijo: Option, +} + +pub struct Interprete { + pub prog: Programa, + globales: HashMap, + /// Marcos de las funciones. El último es el actual. + marcos: Vec>, + campos: Vec, + registro: String, + /// `$0` ya no refleja los campos (alguien escribió `$2`) y hay que rehacerlo antes de leerlo. + sucio_registro: bool, + /// Los campos ya no reflejan `$0`. + sucio_campos: bool, + rangos_activos: Vec, + salidas: HashMap, + entradas: HashMap, + ere_cache: HashMap>, + pub codigo_salida: i32, + semilla: u64, + semilla_previa: f64, + /// Ficheros de la línea de órdenes pendientes, y el que se está leyendo. + lector_actual: Option>, + indice_argv: usize, + pub stdout: Box, +} + +fn ahora_ms() -> u64 { + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0) +} + +impl Interprete { + pub fn nuevo(prog: Programa) -> Interprete { + let rangos = prog.rangos; + let mut it = Interprete { + prog, + globales: HashMap::new(), + marcos: Vec::new(), + campos: Vec::new(), + registro: String::new(), + sucio_registro: false, + sucio_campos: false, + rangos_activos: vec![false; rangos], + salidas: HashMap::new(), + entradas: HashMap::new(), + ere_cache: HashMap::new(), + codigo_salida: 0, + semilla: 0, + semilla_previa: 0.0, + lector_actual: None, + indice_argv: 1, + stdout: Box::new(std::io::BufWriter::new(std::io::stdout())), + }; + for (k, v) in [("FS", " "), ("OFS", " "), ("ORS", "\n"), ("RS", "\n"), + ("SUBSEP", "\u{1c}"), ("CONVFMT", "%.6g"), ("OFMT", "%.6g"), ("FILENAME", "")] { + it.globales.insert(k.into(), Celda::Escalar(Valor::Cad(v.into()))); + } + for k in ["NR", "NF", "FNR", "RSTART", "RLENGTH"] { + let v = if k == "RLENGTH" { -1.0 } else { 0.0 }; + it.globales.insert(k.into(), Celda::Escalar(Valor::Num(v))); + } + it.semilla = 0; + it + } + + // ── Variables ──────────────────────────────────────────────────────────────────────────────── + + fn celda(&self, n: &str) -> Option<&Celda> { + if let Some(m) = self.marcos.last() { + if let Some(c) = m.get(n) { return Some(c); } + } + self.globales.get(n) + } + + fn en_marco(&self, n: &str) -> bool { + self.marcos.last().map_or(false, |m| m.contains_key(n)) + } + + pub fn leer_var(&self, n: &str) -> Valor { + match self.celda(n) { + Some(Celda::Escalar(v)) => v.clone(), + Some(Celda::Array(_)) => Valor::Vacio, + None => Valor::Vacio, + } + } + + pub fn poner_var(&mut self, n: &str, v: Valor) { + if self.en_marco(n) { + self.marcos.last_mut().unwrap().insert(n.into(), Celda::Escalar(v)); + return; + } + // Escribir NF recorta o extiende el registro; escribir los demás no tiene efecto colateral. + if n == "NF" { + let nuevo = v.numero().max(0.0) as usize; + self.asegurar_campos(); + self.campos.resize(nuevo, String::new()); + self.globales.insert("NF".into(), Celda::Escalar(Valor::Num(nuevo as f64))); + self.sucio_registro = true; + return; + } + self.globales.insert(n.into(), Celda::Escalar(v)); + } + + fn cadena_de(&self, v: &Valor) -> String { v.cadena(&self.convfmt()) } + fn convfmt(&self) -> String { self.leer_var("CONVFMT").cadena("%.6g") } + fn ofmt(&self) -> String { self.leer_var("OFMT").cadena("%.6g") } + + fn array(&mut self, n: &str) -> Rc>> { + if let Some(Celda::Array(a)) = self.celda(n) { return a.clone(); } + let a = Rc::new(RefCell::new(HashMap::new())); + if self.en_marco(n) { + self.marcos.last_mut().unwrap().insert(n.into(), Celda::Array(a.clone())); + } else { + self.globales.insert(n.into(), Celda::Array(a.clone())); + } + a + } + + // ── Campos ─────────────────────────────────────────────────────────────────────────────────── + + /// ⚠ Los cuatro comportamientos de FS. + fn partir(&mut self, linea: &str) -> Vec { + let fs = self.leer_var("FS").cadena("%.6g"); + Self::partir_con(linea, &fs, |p| self.ere(p)) + } + + fn partir_con Rc>(linea: &str, fs: &str, mut ere: F) -> Vec { + if fs == " " { + return linea.split(|c: char| c == ' ' || c == '\t' || c == '\n') + .filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(); + } + if linea.is_empty() { return Vec::new(); } + if fs.chars().count() == 1 && fs != "\\" { + let c = fs.chars().next().unwrap(); + // Un único carácter es literal, salvo que sea el espacio (ya tratado). En POSIX un + // metacarácter suelto —`.` o `|`— también es literal acá, y ése es el punto. + return linea.split(c).map(|s| s.to_string()).collect(); + } + let re = ere(fs); + let cs: Vec = linea.chars().collect(); + let mut trozos = Vec::new(); + let mut ini = 0; + let mut i = 0; + while i <= cs.len() { + match re.buscar(&cs, i) { + Some((a, b)) if b > a => { + trozos.push(cs[ini..a].iter().collect::()); + ini = b; + i = b; + } + // Una coincidencia VACÍA no parte: si no, `FS="x*"` haría un trozo por carácter y + // se comería la cadena entera. + Some((a, _)) => { i = a + 1; } + None => break, + } + } + trozos.push(cs[ini.min(cs.len())..].iter().collect::()); + trozos + } + + fn asegurar_campos(&mut self) { + if !self.sucio_campos { return; } + self.sucio_campos = false; + let reg = self.registro.clone(); + self.campos = self.partir(®); + let nf = self.campos.len() as f64; + self.globales.insert("NF".into(), Celda::Escalar(Valor::Num(nf))); + } + + fn asegurar_registro(&mut self) { + if !self.sucio_registro { return; } + self.sucio_registro = false; + let ofs = self.leer_var("OFS").cadena("%.6g"); + self.registro = self.campos.join(&ofs); + } + + pub fn poner_registro(&mut self, s: String) { + self.registro = s; + self.sucio_campos = true; + self.sucio_registro = false; + } + + fn leer_campo(&mut self, i: usize) -> Valor { + if i == 0 { + self.asegurar_registro(); + return Valor::de_entrada(&self.registro.clone()); + } + self.asegurar_campos(); + match self.campos.get(i - 1) { + Some(s) => Valor::de_entrada(s), + None => Valor::Vacio, + } + } + + fn poner_campo(&mut self, i: usize, v: String) { + if i == 0 { + self.poner_registro(v); + self.asegurar_campos(); + return; + } + self.asegurar_campos(); + if i > self.campos.len() { + self.campos.resize(i, String::new()); + self.globales.insert("NF".into(), Celda::Escalar(Valor::Num(self.campos.len() as f64))); + } + self.campos[i - 1] = v; + self.sucio_registro = true; + } + + // ── Expresiones regulares ──────────────────────────────────────────────────────────────────── + + /// Compila y CACHEA. Sin la caché, un `gsub(/x/,"")` dentro de un bucle recompilaría el patrón + /// en cada vuelta; con ella se compila una vez por texto distinto. + fn ere(&self, patron: &str) -> Rc { + // La caché es interior-mutable de hecho: se usa desde `&self` para poder llamarla desde + // `partir`, que ya tiene prestado el intérprete. Se resuelve compilando sin cachear cuando + // no se puede tomar prestado — es raro y el coste es de una compilación. + if let Some(r) = self.ere_cache.get(patron) { return r.clone(); } + Rc::new(Regex::nueva(patron).unwrap_or_else(|e| { + eprintln!("qillqa: expresión regular inválida /{patron}/: {e}"); + std::process::exit(2); + })) + } + + fn ere_mut(&mut self, patron: &str) -> Rc { + if let Some(r) = self.ere_cache.get(patron) { return r.clone(); } + let r = self.ere(patron); + self.ere_cache.insert(patron.to_string(), r.clone()); + r + } + + /// La ERE de una expresión: si es un literal `/re/`, la suya; si no, se compila su valor. + fn ere_de(&mut self, e: &Expr) -> Result, String> { + if let Expr::Ere(r) = e { return Ok(r.clone()); } + let v = self.eval(e)?; + let s = self.cadena_de(&v); + Ok(self.ere_mut(&s)) + } + + // ── Ejecución ──────────────────────────────────────────────────────────────────────────────── + + pub fn ejecutar_begin(&mut self) -> Result { + let reglas = self.prog.reglas.clone(); + for r in reglas.iter().filter(|r| matches!(r.patron, Patron::Begin)) { + if let Some(a) = &r.accion { + propaga!(self.bloque(a)?); + } + } + Ok(Flujo::Normal) + } + + pub fn ejecutar_end(&mut self) -> Result { + let reglas = self.prog.reglas.clone(); + for r in reglas.iter().filter(|r| matches!(r.patron, Patron::End)) { + if let Some(a) = &r.accion { + propaga!(self.bloque(a)?); + } + } + Ok(Flujo::Normal) + } + + pub fn hay_main(&self) -> bool { + self.prog.reglas.iter().any(|r| !matches!(r.patron, Patron::Begin)) + } + pub fn hay_end(&self) -> bool { + self.prog.reglas.iter().any(|r| matches!(r.patron, Patron::End)) + } + + /// Corre las reglas normales para el registro actual. + pub fn ejecutar_reglas(&mut self) -> Result { + let reglas = self.prog.reglas.clone(); + for r in ®las { + let corre = match &r.patron { + Patron::Begin | Patron::End => continue, + Patron::Siempre => true, + Patron::Expr(e) => self.eval(e)?.verdad(), + Patron::Rango(a, b) => { + let id = r.rango_id; + if !self.rangos_activos[id] { + if self.eval(a)?.verdad() { + // Un rango cuyo final casa en la MISMA línea empieza y acaba ahí. + self.rangos_activos[id] = !self.eval(b)?.verdad(); + true + } else { false } + } else { + if self.eval(b)?.verdad() { self.rangos_activos[id] = false; } + true + } + } + }; + if !corre { continue; } + match &r.accion { + None => { let s = self.leer_campo(0); let t = self.cadena_de(&s); self.escribir_linea(&t, &None)?; } + Some(a) => propaga!(self.bloque(a)?), + } + } + Ok(Flujo::Normal) + } + + fn bloque(&mut self, ss: &[Sent]) -> Result { + for s in ss { propaga!(self.sentencia(s)?); } + Ok(Flujo::Normal) + } + + fn sentencia(&mut self, s: &Sent) -> Result { + match s { + Sent::Nada => {} + Sent::Expr(e) => { self.eval(e)?; } + Sent::Bloque(b) => propaga!(self.bloque(b)?), + Sent::Print(args, redir) => { + let ofs = self.leer_var("OFS").cadena("%.6g"); + let ofmt = self.ofmt(); + let mut partes = Vec::new(); + if args.is_empty() { + let v = self.leer_campo(0); + partes.push(self.cadena_de(&v)); + } else { + let planos = Self::aplanar(args); + for a in &planos { + let v = self.eval(a)?; + // `print` usa OFMT para los no-enteros, no CONVFMT. Es la única diferencia + // entre las dos variables y se nota en cuanto alguien toca OFMT. + partes.push(match &v { + Valor::Num(n) => valor::numero_a_cadena(*n, &ofmt), + otro => self.cadena_de(otro), + }); + } + } + let linea = partes.join(&ofs); + self.escribir_linea(&linea, redir)?; + } + Sent::Printf(args, redir) => { + let planos = Self::aplanar(args); + if planos.is_empty() { return Err("printf sin formato".into()); } + let fv = self.eval(&planos[0])?; + let fmt = self.cadena_de(&fv); + let mut vals = Vec::new(); + for a in &planos[1..] { + let v = self.eval(a)?; + vals.push(match v { + Valor::Num(n) => Arg::Numero(n), + Valor::Cad(s) => Arg::Texto(s), + Valor::StrNum(s, n) => Arg::Entrada(s, n), + Valor::Vacio => Arg::Texto(String::new()), + }); + } + let texto = formato::printf(&fmt, &vals, &self.convfmt()); + self.escribir_crudo(&texto, redir)?; + } + Sent::Si(c, a, b) => { + if self.eval(c)?.verdad() { propaga!(self.sentencia(a)?); } + else if let Some(b) = b { propaga!(self.sentencia(b)?); } + } + Sent::Mientras(c, cuerpo) => { + while self.eval(c)?.verdad() { + match self.sentencia(cuerpo)? { + Flujo::Corta => break, + Flujo::Sigue | Flujo::Normal => {} + otro => return Ok(otro), + } + } + } + Sent::HazMientras(cuerpo, c) => { + loop { + match self.sentencia(cuerpo)? { + Flujo::Corta => break, + Flujo::Sigue | Flujo::Normal => {} + otro => return Ok(otro), + } + if !self.eval(c)?.verdad() { break; } + } + } + Sent::Para(init, cond, paso, cuerpo) => { + if let Some(i) = init { propaga!(self.sentencia(i)?); } + loop { + if let Some(c) = cond { if !self.eval(c)?.verdad() { break; } } + match self.sentencia(cuerpo)? { + Flujo::Corta => break, + Flujo::Sigue | Flujo::Normal => {} + otro => return Ok(otro), + } + if let Some(p) = paso { propaga!(self.sentencia(p)?); } + } + } + Sent::ParaEn(k, arr, cuerpo) => { + let a = self.array(arr); + // Se copian las claves: el cuerpo puede borrar elementos, y recorrer el mapa + // mientras se modifica es justo lo que rompe en los awk que no lo hacen. + let claves: Vec = a.borrow().keys().cloned().collect(); + for c in claves { + if !a.borrow().contains_key(&c) { continue; } + self.poner_var(k, Valor::de_entrada(&c)); + match self.sentencia(cuerpo)? { + Flujo::Corta => break, + Flujo::Sigue | Flujo::Normal => {} + otro => return Ok(otro), + } + } + } + Sent::Corta => return Ok(Flujo::Corta), + Sent::Sigue => return Ok(Flujo::Sigue), + Sent::Siguiente => return Ok(Flujo::Siguiente), + Sent::SiguienteFichero => return Ok(Flujo::SiguienteFichero), + Sent::Sale(e) => { + let c = match e { Some(x) => self.eval(x)?.numero() as i32, None => self.codigo_salida }; + return Ok(Flujo::Sale(c)); + } + Sent::Retorna(e) => { + let v = match e { Some(x) => self.eval(x)?, None => Valor::Vacio }; + return Ok(Flujo::Retorna(v)); + } + Sent::Borra(n, idx) => { + let clave = self.clave(idx)?; + let a = self.array(n); + a.borrow_mut().remove(&clave); + } + Sent::BorraTodo(n) => { let a = self.array(n); a.borrow_mut().clear(); } + } + Ok(Flujo::Normal) + } + + /// `print (a, b)` llega como una «llamada» sintética `@lista`: acá se desarma. + fn aplanar(args: &[Expr]) -> Vec { + if args.len() == 1 { + if let Expr::Llamada(n, lista) = &args[0] { + if n == "@lista" { return lista.clone(); } + } + } + args.to_vec() + } + + fn clave(&mut self, idx: &[Expr]) -> Result { + let subsep = self.leer_var("SUBSEP").cadena("%.6g"); + let mut partes = Vec::new(); + for e in idx { + let v = self.eval(e)?; + partes.push(self.cadena_de(&v)); + } + Ok(partes.join(&subsep)) + } + + // ── Salidas y entradas ─────────────────────────────────────────────────────────────────────── + + fn escribir_linea(&mut self, s: &str, r: &Option) -> Result<(), String> { + let ors = self.leer_var("ORS").cadena("%.6g"); + self.escribir_crudo(&format!("{s}{ors}"), r) + } + + fn escribir_crudo(&mut self, s: &str, r: &Option) -> Result<(), String> { + match r { + None => { let _ = self.stdout.write_all(s.as_bytes()); } + Some(red) => { + let (destino, modo) = match red { + Redir::Fichero(e) => (self.eval(e)?, 'w'), + Redir::Anexa(e) => (self.eval(e)?, 'a'), + Redir::Tuberia(e) => (self.eval(e)?, 'p'), + }; + let nombre = self.cadena_de(&destino); + self.abrir_salida(&nombre, modo)?; + if let Some(sal) = self.salidas.get_mut(&nombre) { + let _ = sal.escritor.write_all(s.as_bytes()); + } + } + } + Ok(()) + } + + fn abrir_salida(&mut self, nombre: &str, modo: char) -> Result<(), String> { + if self.salidas.contains_key(nombre) { return Ok(()); } + let sal = match modo { + 'p' => { + // La tubería la abre un `sh -c`, como manda POSIX: el destino es una LÍNEA DE + // ÓRDENES, no un programa. Por eso `print | "sort -k2 | head"` funciona. + let mut hijo = std::process::Command::new("/bin/sh") + .arg("-c").arg(nombre) + .stdin(std::process::Stdio::piped()) + .spawn().map_err(|e| format!("no pude lanzar `{nombre}`: {e}"))?; + let entrada = hijo.stdin.take().unwrap(); + Salida { escritor: Box::new(entrada), hijo: Some(hijo) } + } + _ => { + if nombre == "/dev/stdout" || nombre == "-" { + Salida { escritor: Box::new(std::io::stdout()), hijo: None } + } else if nombre == "/dev/stderr" { + Salida { escritor: Box::new(std::io::stderr()), hijo: None } + } else { + let f = std::fs::OpenOptions::new() + .write(true).create(true) + .append(modo == 'a').truncate(modo == 'w') + .open(nombre).map_err(|e| format!("no pude abrir `{nombre}`: {e}"))?; + Salida { escritor: Box::new(std::io::BufWriter::new(f)), hijo: None } + } + } + }; + self.salidas.insert(nombre.to_string(), sal); + Ok(()) + } + + pub fn cerrar_todo(&mut self) { + let _ = self.stdout.flush(); + let nombres: Vec = self.salidas.keys().cloned().collect(); + for n in nombres { self.cerrar(&n); } + let nombres: Vec = self.entradas.keys().cloned().collect(); + for n in nombres { self.cerrar(&n); } + } + + fn cerrar(&mut self, nombre: &str) -> i32 { + let mut r = -1; + if let Some(mut s) = self.salidas.remove(nombre) { + let _ = s.escritor.flush(); + drop(s.escritor); + if let Some(mut h) = s.hijo.take() { + r = h.wait().ok().and_then(|e| e.code()).unwrap_or(0); + } else { r = 0; } + } + if let Some(mut e) = self.entradas.remove(nombre) { + drop(std::mem::replace(&mut e.lector, Box::new(std::io::empty()))); + if let Some(mut h) = e.hijo.take() { + r = h.wait().ok().and_then(|x| x.code()).unwrap_or(0); + } else { r = 0; } + } + r + } + + /// Lee un registro del lector dado, respetando RS. Devuelve `None` en fin de fichero. + fn leer_registro(lector: &mut dyn BufRead, rs: &str) -> Option { + if rs == "\n" { + let mut buf = Vec::new(); + match lector.read_until(b'\n', &mut buf) { + Ok(0) | Err(_) => return None, + Ok(_) => {} + } + if buf.last() == Some(&b'\n') { buf.pop(); } + if buf.last() == Some(&b'\r') { buf.pop(); } + return Some(String::from_utf8_lossy(&buf).into_owned()); + } + if rs.is_empty() { + // RS="" es el MODO PÁRRAFO: los registros se separan por líneas en blanco, las de + // delante se descartan, y `\n` pasa a ser separador de campos además de FS. + let mut lineas: Vec = Vec::new(); + loop { + let mut buf = Vec::new(); + match lector.read_until(b'\n', &mut buf) { + Ok(0) | Err(_) => break, + Ok(_) => {} + } + if buf.last() == Some(&b'\n') { buf.pop(); } + let l = String::from_utf8_lossy(&buf).into_owned(); + if l.is_empty() { + if lineas.is_empty() { continue; } // blancos de delante + break; + } + lineas.push(l); + } + if lineas.is_empty() { return None; } + return Some(lineas.join("\n")); + } + // RS de un carácter. + let sep = rs.as_bytes()[0]; + let mut buf = Vec::new(); + match lector.read_until(sep, &mut buf) { + Ok(0) | Err(_) => return None, + Ok(_) => {} + } + if buf.last() == Some(&sep) { buf.pop(); } + Some(String::from_utf8_lossy(&buf).into_owned()) + } + + /// Siguiente registro de la entrada principal (los ficheros de ARGV, o la entrada estándar). + pub fn siguiente_registro(&mut self) -> Option { + let rs = self.leer_var("RS").cadena("%.6g"); + loop { + if self.lector_actual.is_none() && !self.abrir_siguiente_fuente() { return None; } + let lector = self.lector_actual.as_mut().unwrap(); + match Self::leer_registro(lector.as_mut(), &rs) { + Some(r) => { + let nr = self.leer_var("NR").numero() + 1.0; + let fnr = self.leer_var("FNR").numero() + 1.0; + self.poner_var("NR", Valor::Num(nr)); + self.poner_var("FNR", Valor::Num(fnr)); + return Some(r); + } + None => { self.lector_actual = None; } + } + } + } + + pub fn cerrar_fuente_actual(&mut self) { self.lector_actual = None; } + + /// Abre el siguiente operando de ARGV que no sea una asignación. Devuelve false si no queda. + fn abrir_siguiente_fuente(&mut self) -> bool { + let argc = self.leer_var("ARGC").numero() as usize; + while self.indice_argv < argc { + let i = self.indice_argv; + self.indice_argv += 1; + let arr = self.array("ARGV"); + let v = arr.borrow().get(&i.to_string()).cloned().unwrap_or(Valor::Vacio); + let s = self.cadena_de(&v); + if s.is_empty() { continue; } + // `var=valor` entre ficheros: asignación diferida, no un fichero. + if let Some(p) = posicion_asignacion(&s) { + let (n, v) = (s[..p].to_string(), desescapar(&s[p + 1..])); + self.poner_var(&n, Valor::de_entrada(&v)); + continue; + } + self.poner_var("FILENAME", Valor::Cad(s.clone())); + self.poner_var("FNR", Valor::Num(0.0)); + if s == "-" || s == "/dev/stdin" { + self.lector_actual = Some(Box::new(BufReader::new(std::io::stdin()))); + return true; + } + match std::fs::File::open(&s) { + Ok(f) => { self.lector_actual = Some(Box::new(BufReader::new(f))); return true; } + Err(e) => { + eprintln!("qillqa: no puedo leer `{s}`: {e}"); + self.codigo_salida = 2; + continue; + } + } + } + // Ningún operando de fichero: se lee la entrada estándar, una sola vez. + if !self.uso_stdin() { + self.marcar_stdin(); + self.lector_actual = Some(Box::new(BufReader::new(std::io::stdin()))); + return true; + } + false + } + + fn uso_stdin(&self) -> bool { self.globales.contains_key("@stdin-usado") } + fn marcar_stdin(&mut self) { self.globales.insert("@stdin-usado".into(), Celda::Escalar(Valor::Num(1.0))); } + + // ── Evaluación de expresiones ──────────────────────────────────────────────────────────────── + + pub fn eval(&mut self, e: &Expr) -> Result { + Ok(match e { + Expr::Num(n) => Valor::Num(*n), + Expr::Cad(s) => Valor::Cad(s.clone()), + Expr::Grupo(inner) => self.eval(inner)?, + Expr::Ere(r) => { + // Una ERE suelta vale `$0 ~ /re/`. + let reg = self.leer_campo(0); + let s = self.cadena_de(®); + Valor::Num(if r.casa(&s) { 1.0 } else { 0.0 }) + } + // ⚠ Leer NF obliga a PARTIR el registro: si no, `{print NF}` da 0 hasta que alguien + // toque un campo. El estado perezoso (`sucio_campos`) es lo que hace barato asignar a + // `$0`, y el precio es acordarse de forzarlo en cada lectura que dependa de los campos. + Expr::Var(n) if n == "NF" => { self.asegurar_campos(); self.leer_var(n) } + Expr::Var(n) => self.leer_var(n), + Expr::Campo(i) => { + let idx = self.eval(i)?.numero(); + if idx < 0.0 { return Err(format!("intento de acceder al campo {idx}")); } + self.leer_campo(idx as usize) + } + Expr::Indice(n, idx) => { + let clave = self.clave(idx)?; + let a = self.array(n); + // Leer un elemento que no existe lo CREA vacío. Es observable con `in`, y es lo que + // manda POSIX: `if (a["x"] == "") ...` deja `"x"` dentro del array. + let mut m = a.borrow_mut(); + m.entry(clave).or_insert(Valor::Vacio).clone() + } + Expr::Asigna(l, v) => { let val = self.eval(v)?; self.asignar(l, val.clone())?; val } + Expr::AsignaOp(l, op, v) => { + let actual = self.leer_lugar(l)?; + let d = self.eval(v)?; + let r = Valor::Num(aritmetica(*op, actual.numero(), d.numero())); + self.asignar(l, r.clone())?; + r + } + Expr::Bin(op, a, b) => { + let x = self.eval(a)?.numero(); + let y = self.eval(b)?.numero(); + Valor::Num(aritmetica(*op, x, y)) + } + Expr::Cmp(op, a, b) => { + let x = self.eval(a)?; + let y = self.eval(b)?; + let o = x.comparar(&y, &self.convfmt()); + let r = match op { + CmpOp::Lt => o.is_lt(), CmpOp::Le => o.is_le(), CmpOp::Gt => o.is_gt(), + CmpOp::Ge => o.is_ge(), CmpOp::Eq => o.is_eq(), CmpOp::Ne => o.is_ne(), + }; + Valor::Num(if r { 1.0 } else { 0.0 }) + } + Expr::Coincide(a, b, negado) => { + let v = self.eval(a)?; + let s = self.cadena_de(&v); + let re = self.ere_de(b)?; + let casa = re.casa(&s); + Valor::Num(if casa != *negado { 1.0 } else { 0.0 }) + } + Expr::Y(a, b) => { + if !self.eval(a)?.verdad() { Valor::Num(0.0) } + else { Valor::Num(if self.eval(b)?.verdad() { 1.0 } else { 0.0 }) } + } + Expr::O(a, b) => { + if self.eval(a)?.verdad() { Valor::Num(1.0) } + else { Valor::Num(if self.eval(b)?.verdad() { 1.0 } else { 0.0 }) } + } + Expr::No(a) => Valor::Num(if self.eval(a)?.verdad() { 0.0 } else { 1.0 }), + Expr::Neg(a) => Valor::Num(-self.eval(a)?.numero()), + Expr::Pos(a) => Valor::Num(self.eval(a)?.numero()), + Expr::Ternario(c, a, b) => if self.eval(c)?.verdad() { self.eval(a)? } else { self.eval(b)? }, + Expr::Concat(a, b) => { + let x = self.eval(a)?; + let y = self.eval(b)?; + Valor::Cad(format!("{}{}", self.cadena_de(&x), self.cadena_de(&y))) + } + Expr::Incr(l, previo, delta) => { + let antes = self.leer_lugar(l)?.numero(); + let despues = antes + delta; + self.asignar(l, Valor::Num(despues))?; + Valor::Num(if *previo { despues } else { antes }) + } + Expr::En(idx, arr) => { + let clave = self.clave(idx)?; + let a = self.array(arr); + let hay = a.borrow().contains_key(&clave); + Valor::Num(if hay { 1.0 } else { 0.0 }) + } + Expr::Builtin(n, args) => self.builtin(n, args)?, + Expr::Llamada(n, args) => self.llamar(n, args)?, + Expr::Getline(g) => self.getline(g)?, + }) + } + + fn leer_lugar(&mut self, l: &Lugar) -> Result { + Ok(match l { + Lugar::Var(n) => self.leer_var(n), + Lugar::Campo(i) => { let idx = self.eval(i)?.numero() as usize; self.leer_campo(idx) } + Lugar::Indice(n, idx) => { + let clave = self.clave(idx)?; + let a = self.array(n); + let v = a.borrow().get(&clave).cloned(); + v.unwrap_or(Valor::Vacio) + } + }) + } + + fn asignar(&mut self, l: &Lugar, v: Valor) -> Result<(), String> { + match l { + Lugar::Var(n) => self.poner_var(n, v), + Lugar::Campo(i) => { + let idx = self.eval(i)?.numero() as usize; + let s = self.cadena_de(&v); + self.poner_campo(idx, s); + } + Lugar::Indice(n, idx) => { + let clave = self.clave(idx)?; + let a = self.array(n); + a.borrow_mut().insert(clave, v); + } + } + Ok(()) + } + + // ── Funciones del programa ─────────────────────────────────────────────────────────────────── + + fn llamar(&mut self, nombre: &str, args: &[Expr]) -> Result { + let f = self.prog.funciones.iter().find(|f| f.nombre == nombre).cloned() + .ok_or_else(|| format!("función no definida: `{nombre}`"))?; + let mut marco: HashMap = HashMap::new(); + for (i, p) in f.params.iter().enumerate() { + match args.get(i) { + None => { marco.insert(p.clone(), Celda::Escalar(Valor::Vacio)); } + Some(a) => { + // ⚠ Un array se pasa POR REFERENCIA y un escalar por valor. Cuál es cada uno se + // decide mirando qué hay en la variable: si ya es un array, se comparte el Rc. + if let Expr::Var(vn) = a { + let es_array = matches!(self.celda(vn), Some(Celda::Array(_))); + if es_array { + let arr = self.array(vn); + marco.insert(p.clone(), Celda::Array(arr)); + continue; + } + // Una variable nunca usada que se pasa a un parámetro que la función usa + // como array tiene que compartir el array. Se resuelve creándolo ya. + if self.celda(vn).is_none() && self.usa_como_array(&f, p) { + let arr = self.array(vn); + marco.insert(p.clone(), Celda::Array(arr)); + continue; + } + } + let v = self.eval(a)?; + marco.insert(p.clone(), Celda::Escalar(v)); + } + } + } + self.marcos.push(marco); + let r = self.bloque(&f.cuerpo); + self.marcos.pop(); + match r? { + Flujo::Retorna(v) => Ok(v), + Flujo::Sale(c) => Err(format!("@salir:{c}")), + _ => Ok(Valor::Vacio), + } + } + + /// ¿La función usa este parámetro como array? Se mira el cuerpo: `p[...]`, `in p`, `delete p`. + fn usa_como_array(&self, f: &Funcion, p: &str) -> bool { + fn en_sent(s: &Sent, p: &str) -> bool { + match s { + Sent::Expr(e) => en_expr(e, p), + Sent::Print(a, _) | Sent::Printf(a, _) => a.iter().any(|e| en_expr(e, p)), + Sent::Bloque(b) => b.iter().any(|s| en_sent(s, p)), + Sent::Si(c, a, b) => en_expr(c, p) || en_sent(a, p) || b.as_ref().map_or(false, |b| en_sent(b, p)), + Sent::Mientras(c, b) => en_expr(c, p) || en_sent(b, p), + Sent::HazMientras(b, c) => en_expr(c, p) || en_sent(b, p), + Sent::Para(i, c, s2, b) => i.as_ref().map_or(false, |x| en_sent(x, p)) + || c.as_ref().map_or(false, |x| en_expr(x, p)) + || s2.as_ref().map_or(false, |x| en_sent(x, p)) || en_sent(b, p), + Sent::ParaEn(_, a, b) => a == p || en_sent(b, p), + Sent::Borra(n, _) | Sent::BorraTodo(n) => n == p, + Sent::Sale(e) | Sent::Retorna(e) => e.as_ref().map_or(false, |x| en_expr(x, p)), + _ => false, + } + } + fn en_lugar(l: &Lugar, p: &str) -> bool { + match l { Lugar::Indice(n, _) => n == p, Lugar::Campo(e) => en_expr(e, p), _ => false } + } + fn en_expr(e: &Expr, p: &str) -> bool { + match e { + Expr::Indice(n, _) => n == p, + Expr::En(_, n) => n == p, + Expr::Llamada(_, args) | Expr::Builtin(_, args) => args.iter().any(|a| en_expr(a, p)) + || args.iter().any(|a| matches!(a, Expr::Var(v) if v == p)), + Expr::Bin(_, a, b) | Expr::Cmp(_, a, b) | Expr::Y(a, b) | Expr::O(a, b) + | Expr::Concat(a, b) => en_expr(a, p) || en_expr(b, p), + Expr::Coincide(a, b, _) => en_expr(a, p) || en_expr(b, p), + Expr::No(a) | Expr::Neg(a) | Expr::Pos(a) | Expr::Grupo(a) | Expr::Campo(a) => en_expr(a, p), + Expr::Ternario(a, b, c) => en_expr(a, p) || en_expr(b, p) || en_expr(c, p), + // ⚠ `a["k"] = 1` marca a `a` como array, y el nombre está en el LUGAR, no en el + // valor. Mirar sólo el valor dejaba `funcion-array-ref` sin compartir el array. + Expr::Asigna(l, v) => en_lugar(l, p) || en_expr(v, p), + Expr::AsignaOp(l, _, v) => en_lugar(l, p) || en_expr(v, p), + Expr::Incr(l, _, _) => en_lugar(l, p), + _ => false, + } + } + f.cuerpo.iter().any(|s| en_sent(s, p)) + } + + // ── getline ────────────────────────────────────────────────────────────────────────────────── + + fn getline(&mut self, g: &Getline) -> Result { + let rs = self.leer_var("RS").cadena("%.6g"); + match g { + Getline::Simple(l) => { + match self.siguiente_registro() { + None => Ok(Valor::Num(0.0)), + Some(r) => { + match l { + None => { self.poner_registro(r); } + Some(lug) => { let lug = (**lug).clone(); self.asignar(&lug, Valor::de_entrada(&r))?; } + } + Ok(Valor::Num(1.0)) + } + } + } + Getline::DeFichero(l, f) => { + let fv = self.eval(f)?; + let nombre = self.cadena_de(&fv); + if !self.entradas.contains_key(&nombre) { + let lector: Box = if nombre == "-" || nombre == "/dev/stdin" { + Box::new(BufReader::new(std::io::stdin())) + } else { + match std::fs::File::open(&nombre) { + Ok(f) => Box::new(BufReader::new(f)), + // getline devuelve -1 cuando no puede abrir: es un valor, no un error. + Err(_) => return Ok(Valor::Num(-1.0)), + } + }; + self.entradas.insert(nombre.clone(), Entrada { lector, hijo: None }); + } + let ent = self.entradas.get_mut(&nombre).unwrap(); + match Self::leer_registro(ent.lector.as_mut(), &rs) { + None => Ok(Valor::Num(0.0)), + Some(r) => { + match l { + // `getline < f` sin variable pone $0 y NF, pero NO toca NR. + None => { self.poner_registro(r); } + Some(lug) => { let lug = (**lug).clone(); self.asignar(&lug, Valor::de_entrada(&r))?; } + } + Ok(Valor::Num(1.0)) + } + } + } + Getline::DeTuberia(cmd, l) => { + let cv = self.eval(cmd)?; + let nombre = self.cadena_de(&cv); + if !self.entradas.contains_key(&nombre) { + let _ = self.stdout.flush(); // que lo ya impreso salga antes que lo del hijo + let mut hijo = std::process::Command::new("/bin/sh") + .arg("-c").arg(&nombre) + .stdout(std::process::Stdio::piped()) + .spawn().map_err(|e| format!("no pude lanzar `{nombre}`: {e}"))?; + let salida = hijo.stdout.take().unwrap(); + self.entradas.insert(nombre.clone(), Entrada { + lector: Box::new(BufReader::new(salida)), hijo: Some(hijo), + }); + } + let ent = self.entradas.get_mut(&nombre).unwrap(); + match Self::leer_registro(ent.lector.as_mut(), &rs) { + None => Ok(Valor::Num(0.0)), + Some(r) => { + // `"cmd" | getline` SÍ incrementa NR (POSIX lo distingue de `getline < f`). + let nr = self.leer_var("NR").numero() + 1.0; + self.poner_var("NR", Valor::Num(nr)); + match l { + None => { self.poner_registro(r); } + Some(lug) => { let lug = (**lug).clone(); self.asignar(&lug, Valor::de_entrada(&r))?; } + } + Ok(Valor::Num(1.0)) + } + } + } + } + } + + // ── Funciones incorporadas ─────────────────────────────────────────────────────────────────── + + fn builtin(&mut self, n: &str, args: &[Expr]) -> Result { + match n { + "length" => { + if args.is_empty() { + let v = self.leer_campo(0); + return Ok(Valor::Num(self.cadena_de(&v).chars().count() as f64)); + } + // `length(a)` con un array da su número de elementos. + if let Expr::Var(vn) = &args[0] { + if let Some(Celda::Array(a)) = self.celda(vn) { + return Ok(Valor::Num(a.borrow().len() as f64)); + } + } + let v = self.eval(&args[0])?; + Ok(Valor::Num(self.cadena_de(&v).chars().count() as f64)) + } + "substr" => { + let v = self.eval(&args[0])?; + let s: Vec = self.cadena_de(&v).chars().collect(); + let m = self.eval(&args[1])?.numero(); + // ⚠ Los índices empiezan en 1, y los dos rincones de esta función se resolvieron + // MIDIENDO los dos oráculos, no leyendo el estándar — que acá es ambiguo: + // · un inicio ≤ 0 se recorta a 1 y **el largo NO se ajusta**: + // `substr("abcdef", -1, 3)` es `abc` en busybox y en gawk --posix, no `a`. + // La lectura estricta de POSIX daría `a` (las posiciones -1,0,1); ningún awk + // real hace eso, y un guión escrito contra ellos es lo que hay que ejecutar. + // · los no enteros se TRUNCAN hacia cero: `substr("abcdef", 1.5, 2.5)` es `ab`. + let inicio = m.trunc() as i64; + let (desde, largo) = if args.len() > 2 { + let l = self.eval(&args[2])?.numero().trunc() as i64; + (inicio.max(1), l) + } else { + let d = inicio.max(1); + (d, s.len() as i64 - d + 1) + }; + if largo <= 0 || desde > s.len() as i64 { return Ok(Valor::Cad(String::new())); } + let a = (desde - 1) as usize; + let b = ((desde - 1 + largo) as usize).min(s.len()); + Ok(Valor::Cad(s[a..b].iter().collect())) + } + "index" => { + let a = self.eval(&args[0])?; let sa = self.cadena_de(&a); + let b = self.eval(&args[1])?; let sb = self.cadena_de(&b); + let ha: Vec = sa.chars().collect(); + let ag: Vec = sb.chars().collect(); + if ag.is_empty() { return Ok(Valor::Num(if ha.is_empty() { 0.0 } else { 1.0 })); } + for i in 0..ha.len().saturating_sub(ag.len() - 1) { + if ha[i..i + ag.len()] == ag[..] { return Ok(Valor::Num((i + 1) as f64)); } + } + Ok(Valor::Num(0.0)) + } + "split" => { + let v = self.eval(&args[0])?; + let s = self.cadena_de(&v); + let nombre = match &args[1] { + Expr::Var(n) => n.clone(), + Expr::Indice(n, _) => n.clone(), + _ => return Err("split necesita un array como segundo argumento".into()), + }; + let fs = if args.len() > 2 { + match &args[2] { + Expr::Ere(r) => r.fuente().to_string(), + e => { let v = self.eval(e)?; self.cadena_de(&v) } + } + } else { self.leer_var("FS").cadena("%.6g") }; + let trozos = if s.is_empty() { Vec::new() } else { + let mut cache: Vec<(String, Rc)> = Vec::new(); + let t = Self::partir_con(&s, &fs, |p| { + if let Some((_, r)) = cache.iter().find(|(k, _)| k == p) { return r.clone(); } + let r = Rc::new(Regex::nueva(p).unwrap_or_else(|e| { + eprintln!("qillqa: expresión regular inválida /{p}/: {e}"); + std::process::exit(2); + })); + cache.push((p.to_string(), r.clone())); + r + }); + t + }; + let a = self.array(&nombre); + a.borrow_mut().clear(); + for (i, t) in trozos.iter().enumerate() { + a.borrow_mut().insert((i + 1).to_string(), Valor::de_entrada(t)); + } + Ok(Valor::Num(trozos.len() as f64)) + } + "sub" | "gsub" => { + let global = n == "gsub"; + let re = self.ere_de(&args[0])?; + let rv = self.eval(&args[1])?; + let reemplazo = self.cadena_de(&rv); + let destino = if args.len() > 2 { + match &args[2] { + Expr::Var(v) => Lugar::Var(v.clone()), + Expr::Campo(i) => Lugar::Campo(i.clone()), + Expr::Indice(v, i) => Lugar::Indice(v.clone(), i.clone()), + Expr::Grupo(g) => match &**g { + Expr::Var(v) => Lugar::Var(v.clone()), + Expr::Campo(i) => Lugar::Campo(i.clone()), + Expr::Indice(v, i) => Lugar::Indice(v.clone(), i.clone()), + _ => return Err("sub/gsub necesita algo asignable".into()), + }, + _ => return Err("sub/gsub necesita algo asignable".into()), + } + } else { Lugar::Campo(Box::new(Expr::Num(0.0))) }; + let actual = self.leer_lugar(&destino)?; + let texto = self.cadena_de(&actual); + let (nuevo, cuantas) = sustituir(&re, &texto, &reemplazo, global); + if cuantas > 0 { self.asignar(&destino, Valor::Cad(nuevo))?; } + Ok(Valor::Num(cuantas as f64)) + } + "match" => { + let v = self.eval(&args[0])?; + let s = self.cadena_de(&v); + let re = self.ere_de(&args[1])?; + let cs: Vec = s.chars().collect(); + match re.buscar(&cs, 0) { + Some((a, b)) => { + self.poner_var("RSTART", Valor::Num((a + 1) as f64)); + self.poner_var("RLENGTH", Valor::Num((b - a) as f64)); + Ok(Valor::Num((a + 1) as f64)) + } + None => { + self.poner_var("RSTART", Valor::Num(0.0)); + self.poner_var("RLENGTH", Valor::Num(-1.0)); + Ok(Valor::Num(0.0)) + } + } + } + "sprintf" => { + if args.is_empty() { return Ok(Valor::Cad(String::new())); } + let fv = self.eval(&args[0])?; + let fmt = self.cadena_de(&fv); + let mut vals = Vec::new(); + for a in &args[1..] { + let v = self.eval(a)?; + vals.push(match v { + Valor::Num(x) => Arg::Numero(x), + Valor::Cad(s) => Arg::Texto(s), + Valor::StrNum(s, x) => Arg::Entrada(s, x), + Valor::Vacio => Arg::Texto(String::new()), + }); + } + Ok(Valor::Cad(formato::printf(&fmt, &vals, &self.convfmt()))) + } + "sin" => Ok(Valor::Num(self.arg_num(args, 0)?.sin())), + "cos" => Ok(Valor::Num(self.arg_num(args, 0)?.cos())), + "atan2" => { let a = self.arg_num(args, 0)?; let b = self.arg_num(args, 1)?; Ok(Valor::Num(a.atan2(b))) } + "exp" => Ok(Valor::Num(self.arg_num(args, 0)?.exp())), + "log" => Ok(Valor::Num(self.arg_num(args, 0)?.ln())), + "sqrt" => Ok(Valor::Num(self.arg_num(args, 0)?.sqrt())), + "int" => Ok(Valor::Num(self.arg_num(args, 0)?.trunc())), + "rand" => { + // xorshift64*: determinista y suficiente. `rand` de awk NO promete calidad + // criptográfica; lo que sí promete es repetirse con la misma semilla. + self.semilla ^= self.semilla << 13; + self.semilla ^= self.semilla >> 7; + self.semilla ^= self.semilla << 17; + let x = self.semilla.wrapping_mul(0x2545F4914F6CDD1D); + Ok(Valor::Num((x >> 11) as f64 / (1u64 << 53) as f64)) + } + "srand" => { + let previa = self.semilla_previa; + let nueva = if args.is_empty() { ahora_ms() as f64 } else { self.arg_num(args, 0)? }; + self.semilla_previa = nueva; + self.semilla = (nueva as i64 as u64) | 1; // el 0 congelaría el xorshift + Ok(Valor::Num(previa)) + } + "tolower" | "toupper" => { + let v = self.eval(&args[0])?; + let s = self.cadena_de(&v); + Ok(Valor::Cad(if n == "tolower" { s.to_lowercase() } else { s.to_uppercase() })) + } + "system" => { + let v = self.eval(&args[0])?; + let cmd = self.cadena_de(&v); + let _ = self.stdout.flush(); // POSIX: se vacía la salida ANTES de lanzar + let r = std::process::Command::new("/bin/sh").arg("-c").arg(&cmd).status(); + Ok(Valor::Num(r.ok().and_then(|s| s.code()).unwrap_or(-1) as f64)) + } + "close" => { + let v = self.eval(&args[0])?; + let nombre = self.cadena_de(&v); + Ok(Valor::Num(self.cerrar(&nombre) as f64)) + } + "fflush" => { + if args.is_empty() { let _ = self.stdout.flush(); return Ok(Valor::Num(0.0)); } + let v = self.eval(&args[0])?; + let nombre = self.cadena_de(&v); + if let Some(s) = self.salidas.get_mut(&nombre) { let _ = s.escritor.flush(); } + else { let _ = self.stdout.flush(); } + Ok(Valor::Num(0.0)) + } + otro => Err(format!("función incorporada desconocida: `{otro}`")), + } + } + + fn arg_num(&mut self, args: &[Expr], i: usize) -> Result { + match args.get(i) { Some(e) => Ok(self.eval(e)?.numero()), None => Ok(0.0) } + } +} + +fn redondear(x: f64) -> i64 { + // POSIX dice «truncado al entero más cercano», que es redondeo a la mitad hacia arriba. + (x + 0.5).floor() as i64 +} + +fn aritmetica(op: BinOp, a: f64, b: f64) -> f64 { + match op { + BinOp::Suma => a + b, + BinOp::Resta => a - b, + BinOp::Mult => a * b, + BinOp::Div => a / b, + // `%` de awk es `fmod`, no el resto entero: `5.5 % 2` es 1.5. + BinOp::Modulo => a % b, + BinOp::Potencia => a.powf(b), + } +} + +/// `sub`/`gsub`. En el reemplazo, `&` es el texto que casó y `\&` es un `&` literal. +/// +/// ⚠ La regla que decide el caso raro: **una coincidencia VACÍA pegada al final de la anterior no +/// sustituye**. Sin ella, `gsub(/a*/,"-")` sobre `"xax"` da `-x--x-` con 4 sustituciones —que es lo +/// que hace el awk de busybox— en vez de `-x-x-` con 3, que es lo que hacen gawk y el awk original +/// y lo que se deduce de POSIX. El banco diferencial lo destapó como una discrepancia ENTRE los +/// oráculos, que es justo para lo que sirve tener dos. +fn sustituir(re: &Regex, texto: &str, reemplazo: &str, global: bool) -> (String, usize) { + let cs: Vec = texto.chars().collect(); + let mut salida = String::new(); + let mut i = 0; + let mut cuantas = 0; + let mut ultimo_vacio_en: Option = None; + while i <= cs.len() { + match re.buscar(&cs, i) { + None => break, + Some((a, b)) => { + // Una coincidencia VACÍA justo donde terminó la anterior no cuenta: si no, + // `gsub(/x*/,"-")` sobre "xax" produciría guiones de más. + if a == b && ultimo_vacio_en == Some(a) { + if a >= cs.len() { break; } + salida.push(cs[a]); + i = a + 1; + continue; + } + salida.extend(&cs[i..a]); + let casado: String = cs[a..b].iter().collect(); + let mut rc = reemplazo.chars().peekable(); + while let Some(c) = rc.next() { + match c { + '&' => salida.push_str(&casado), + '\\' => match rc.peek() { + Some('&') => { salida.push('&'); rc.next(); } + Some('\\') => { salida.push('\\'); rc.next(); } + _ => salida.push('\\'), + }, + otro => salida.push(otro), + } + } + cuantas += 1; + if !global { + salida.extend(&cs[b..]); + return (salida, cuantas); + } + if a == b { + ultimo_vacio_en = Some(a); + if a >= cs.len() { i = a; break; } + salida.push(cs[a]); + i = a + 1; + } else { + // Tras una coincidencia real, la posición donde termina queda VEDADA para una + // vacía: es la regla de arriba, y es la única diferencia con busybox. + ultimo_vacio_en = Some(b); + i = b; + } + } + } + } + if i <= cs.len() { salida.extend(&cs[i.min(cs.len())..]); } + (salida, cuantas) +} + +/// ¿`s` tiene forma `nombre=valor`? Devuelve la posición del `=`. +pub fn posicion_asignacion(s: &str) -> Option { + let cs: Vec = s.chars().collect(); + if cs.is_empty() { return None; } + if !(cs[0].is_alphabetic() || cs[0] == '_') { return None; } + let mut i = 1; + while i < cs.len() && (cs[i].is_alphanumeric() || cs[i] == '_') { i += 1; } + if i < cs.len() && cs[i] == '=' { + return Some(s.char_indices().nth(i).map(|(p, _)| p).unwrap_or(i)); + } + None +} + +/// Los escapes de una asignación de la línea de órdenes se procesan como en una cadena. +pub fn desescapar(s: &str) -> String { + let mut out = String::new(); + let mut it = s.chars().peekable(); + while let Some(c) = it.next() { + if c != '\\' { out.push(c); continue; } + match it.next() { + None => out.push('\\'), + Some('n') => out.push('\n'), Some('t') => out.push('\t'), Some('r') => out.push('\r'), + Some('\\') => out.push('\\'), Some('"') => out.push('"'), Some('/') => out.push('/'), + Some('a') => out.push('\x07'), Some('b') => out.push('\x08'), + Some('f') => out.push('\x0c'), Some('v') => out.push('\x0b'), + Some(d @ '0'..='7') => { + let mut v = d.to_digit(8).unwrap(); + let mut n = 1; + while n < 3 { + match it.peek() { + Some(x) if x.is_digit(8) => { v = v * 8 + x.to_digit(8).unwrap(); it.next(); n += 1; } + _ => break, + } + } + out.push(char::from_u32(v).unwrap_or('\0')); + } + Some(o) => { out.push('\\'); out.push(o); } + } + } + out +} + +impl Interprete { + /// Acceso al array desde `main` (ENVIRON, ARGV), que es el único sitio de fuera que los toca. + pub fn array_publico(&mut self, n: &str) -> Rc>> { self.array(n) } +} diff --git a/crates/qillqa/src/lexer.rs b/crates/qillqa/src/lexer.rs new file mode 100644 index 00000000..399641bf --- /dev/null +++ b/crates/qillqa/src/lexer.rs @@ -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, + i: usize, + anterior: Option, +} + +impl Lexer { + pub fn nuevo(src: &str) -> Lexer { + Lexer { src: src.chars().collect(), i: 0, anterior: None } + } + + fn ver(&self) -> Option { self.src.get(self.i).copied() } + fn ver2(&self) -> Option { 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 { + let t = self.siguiente_crudo()?; + self.anterior = Some(t.clone()); + Ok(t) + } + + fn siguiente_crudo(&mut self) -> Result { + 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 { + 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::().map(Tok::Num).map_err(|_| format!("número mal formado: `{t}`")) + } + + fn cadena(&mut self) -> Result { + 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 { + 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 { + 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())]); + } +} diff --git a/crates/qillqa/src/main.rs b/crates/qillqa/src/main.rs new file mode 100644 index 00000000..f797f556 --- /dev/null +++ b/crates/qillqa/src/main.rs @@ -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 = std::env::args().collect(); + let mut fuentes: Vec = Vec::new(); + let mut asignaciones: Vec = Vec::new(); + let mut fs: Option = None; + let mut texto_programa: Option = 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 = a.chars().collect(); + (cs[1], cs[2..].iter().collect::()) + }; + 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 = 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); +} diff --git a/crates/qillqa/src/parser.rs b/crates/qillqa/src/parser.rs new file mode 100644 index 00000000..3295e52d --- /dev/null +++ b/crates/qillqa/src/parser.rs @@ -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, + i: usize, + sin_mayor: bool, + rangos: usize, + funciones_vistas: Vec, +} + +impl Parser { + pub fn nuevo(src: &str) -> Result { + 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 { + 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 { + 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, 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 { + 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 { + 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 { + 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 { self.ternario() } + + fn ternario(&mut self) -> Result { + 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 { + match self.ver() { + Tok::Op(o) if matches!(o.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "^=") => Some(o.clone()), + _ => None, + } + } + + fn a_lugar(&self, e: &Expr) -> Option { + 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 { + 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 { + 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 { + 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 { + 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 Result { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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>, 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), + } + } +} diff --git a/crates/qillqa/src/regex.rs b/crates/qillqa/src/regex.rs new file mode 100644 index 00000000..107a043e --- /dev/null +++ b/crates/qillqa/src/regex.rs @@ -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, 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 { + 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, negada: bool }, + Bol, + Eol, + Cat(Box, Box), + Alt(Box, Box), + Estrella(Box), + Mas(Box), + Opcional(Box), + /// `{n,m}`: se expande al compilar. `hasta = None` es `{n,}`. + Repite(Box, u32, Option), +} + +pub struct Regex { + prog: Vec, + /// 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 { self.p.get(self.i).copied() } + fn tomar(&mut self) -> Option { let c = self.ver(); if c.is_some() { self.i += 1; } c } + + /// alternancia := concatenación ('|' concatenación)* + fn alternancia(&mut self) -> Result { + 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 { + let mut acc: Option = 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 { + 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), 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 { + 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 { + let mut negada = false; + if self.ver() == Some('^') { negada = true; self.i += 1; } + let mut partes: Vec = 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, 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 { + let chars: Vec = 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::())); + } + 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, 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 { + let largo = s.len(); + let mut visto = vec![false; self.prog.len()]; + let mut actual: Vec = Vec::new(); + self.agregar(&mut actual, &mut visto, 0, desde, largo); + let mut mejor: Option = 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 = 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 = s.chars().collect(); + self.buscar(&v, 0).is_some() + } +} diff --git a/crates/qillqa/src/valor.rs b/crates/qillqa/src/valor.rs new file mode 100644 index 00000000..5a86867c --- /dev/null +++ b/crates/qillqa/src/valor.rs @@ -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), + /// 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 { + let t = s.trim_matches(|c: char| c == ' ' || c == '\t' || c == '\n'); + if t.is_empty() { return None; } + let b: Vec = 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::().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 = 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::().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); + } +} diff --git a/crates/qillqa/tests/divergencias_medidas.rs b/crates/qillqa/tests/divergencias_medidas.rs new file mode 100644 index 00000000..f2de4c13 --- /dev/null +++ b/crates/qillqa/tests/divergencias_medidas.rs @@ -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"); +} diff --git a/docs/state/targets.toml b/docs/state/targets.toml index 4cb5e37a..a9a26fdf 100644 --- a/docs/state/targets.toml +++ b/docs/state/targets.toml @@ -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] diff --git a/recipes/ncurses-tools.toml b/recipes/ncurses-tools.toml new file mode 100644 index 00000000..f707c4f7 --- /dev/null +++ b/recipes/ncurses-tools.toml @@ -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' diff --git a/scripts/awk-banco-diferencial.py b/scripts/awk-banco-diferencial.py new file mode 100755 index 00000000..51a0f449 --- /dev/null +++ b/scripts/awk-banco-diferencial.py @@ -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())