#!/usr/bin/env python3 """Banco DIFERENCIAL de awk: corre los mismos casos en varios motores y compara. scripts/awk-banco-diferencial.py # akllay 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/akllay")) 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" · akllay=[{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 akllay : [{mio}]") return 1 if fallos else 0 if __name__ == "__main__": sys.exit(main())