El curioso bug de QB64 (on error)

Hace unas semanas me comentaban un curioso bug en QB64. Una herramienta de desarrollo de la que a modo de curiosidad fui de los primeros en evaluar en 2010, pero que nunca llegué a recomendar como herramienta de tipo profesional. Tanto por sus características, por como ha quedado comprobado por su soporte.

Debido a la poca proactividad, ya sabéis, gente que reporte el problema y que pretende que inviertas más tiempo que ellos, cosa que como un tonto hice, decidí aprovechar el trabajo al menos para sacarle más jugo al asunto. Para empezar, mencionaban un problema sobre un algoritmo del que ni siquiera fueron capaces de publicar el código fuente, así que lo reimplementé así:

ON ERROR GOTO ErrorHandler
FOR iError% = 0 TO 71
	ERROR iError%
NEXT
INPUT A
ErrorHandler:
	PRINT "Error"; ERR; "on program file line"; ERL
	RESUME NEXT

Entonces reproduje el problema. Resultaba ser que lanzando los errores por código, todos eran capturados dentro de la rutina de manejo de errores vía «ON ERROR», sin embargo el error 11, es decir, división por cero lanzaba un MessageBox. No indicaban que versión de QB64 lo sufría, así que lo hice con la última 1.4, constatando que era así, que tenía ese problema.

Tras editar el código, para después compilarlo y ejecutarlo:


El curioso bug de QB64 (on error)

Se trataban los errores del 0 al 10:


El curioso bug de QB64 (on error)

Y al llegar al 11, se lanzaba el MessageBox indicando «Division by Zero» quedando abortada la ejecución del programa.


El curioso bug de QB64 (on error)

Probé lo mismo portándolo a PowerBASIC Console Compiler, el código fuente sin usar ninguna directiva de optimización quedó así:

Function PBMain()
	On Error GoTo ErrorHandler
	For iError% = 0 To 71
		Error iError%
	Next
	ErrorHandler:
		Print «Error»; Err; «on program file line»; Erl; «description»; Error$
		Resume Next
End Function

Había dejado de «tarea» que verificase si en QBASIC o en QuickBASIC, los dialectos con los que QB64 pretende ser compatible ocurría o no. Por supuesto no comprobó nada, así que al tiempo que escribo este análisis lo he hecho yo. Podría haberlo probado con Visual BASIC for DOS o con Microsoft Basic PDS 7.1, sin embargo no los tenía a mano y lo hice con el legendario Quick BASIC 4.5.

Como era de esperar Quick Basic no adolecía de ese problema. Todos los errores eran adecuadamente capturados por el manejador o handler de «On Error»:


El curioso bug de QB64 (on error)


El curioso bug de QB64 (on error)

Su alucinante documentación en forma de ayuda sensible al contexto, más aún tratándose un título de 1988, tampoco mencionaba ninguna excepción en cuanto al tratamiento de errores.


El curioso bug de QB64 (on error)

Estaba claro que si QB 4.5 se comportaba así, e incluso lo hacía PB/CC, el problema estaba en el mencionado QB64.

Como ya sabéis, la filosofía de QB64 es generar código C que es compilado, en este caso con el GCC 8.1.0 de MinGW incluido dentro de internal\c\c_compiler\bin, como muestra este es el código que ha producido:

S_0: ;
	do
	{
		error_goto_line = 1;
		if (!qbevent) break;
		evnt(1);
	}
	while (r);
S_2: ;
	fornext_value2 = 0;
	fornext_finalvalue2 = 71;
	fornext_step2 = 1;
	if (fornext_step2 < 0)
	{
		fornext_step_negative2 = 1;
	}
	else
	{
		fornext_step_negative2 = 0;
	}
	if (new_error)
	{
		goto fornext_error2;
	}
	goto fornext_entrylabel2;
	while (1)
	{
		fornext_value2 = fornext_step2 + (* __INTEGER_IERROR);
		fornext_entrylabel2:
		*__INTEGER_IERROR = fornext_value2;
		if (fornext_step_negative2)
		{
			if (fornext_value2 < fornext_finalvalue2)
			{
				break;
			}
		}
		else
		{
			if (fornext_value2 > fornext_finalvalue2)
			{
				break;
			}
			fornext_error2:;
				if (qbevent)
				{
					evnt(3);
					if (r)
					{
						goto S_2;
					}
				}
				do
				{
					error(*__INTEGER_IERROR);
					if (!qbevent)
					{
						break;
					}
					evnt(4);
				}
				while (r);
			fornext_continue_1: ;
		}
		fornext_exit_1: ;
		do
		{
			qbs_print(qbs_new_txt("? "), 0);
			qbs_input_variabletypes[1] = 536870944;
			qbs_input_variableoffsets[1] = & ( * __SINGLE_A);
			qbs_input(1, 1);
			if (stop_program)
			{
				end();
			}
			qbs_cleanup(qbs_tmp_base, 0);
			if (!qbevent)
			{
				break;
			}
			evnt(6);
		}
		while (r);
		LABEL_ERRORHANDLER: ;
		if (qbevent)
		{
			evnt(8);
			r = 0;
		}
		do
		{
			tqbs = qbs_new(0, 0);
			qbs_set(tqbs, qbs_new_txt_len("Error", 5));
			if (new_error) goto skip3;
			makefit(tqbs);
			qbs_print(tqbs, 0);
			qbs_set(tqbs, qbs_add(qbs_str((uint32)(get_error_err())), qbs_new_txt(" ")));
			if (new_error)
			{
				goto skip3;
			}
			makefit(tqbs);
			qbs_print(tqbs, 0);
			qbs_set(tqbs, qbs_new_txt_len("on program file line", 20));
			if (new_error)
			{
				goto skip3;
			}
			makefit(tqbs);
			qbs_print(tqbs, 0);
			qbs_set(tqbs, qbs_add(qbs_str((double)(get_error_erl())), qbs_new_txt(" ")));
			if (new_error)
			{
				goto skip3;
			}
			makefit(tqbs);
			qbs_print(tqbs, 0);
			qbs_print(nothingstring, 1);
			skip3:
				qbs_free(tqbs);
			qbs_cleanup(qbs_tmp_base, 0);
			if (!qbevent)
			{
				break;
			}
			evnt(9);
		}
		while (r);
		do
		{
			if (!error_handling)
			{
				error(20);
			}
			else
			{
				error_handling = 0;
				error_err = 0;
				return;
			}
			if (!qbevent)
			{
				break;
			}
			evnt(10);
		}
		while (r);
		sub_end();
		return;
	}
	if (error_occurred)
	{
		error_occurred = 0;
		if (error_goto_line == 1)
		{
			error_handling = 1;
			goto LABEL_ERRORHANDLER;
		}
		exit(99);
	}

Incluyendo la sobrecarga de gestión del break, la basura que produce es considerable. Me imagino que GCC se lo pasará bien tratando de optimizar todo eso. No me sorprende que sin información de DEBUG el ejecutable resultante sea de 2 MB. cuando haciendo más que eso con PowerBASIC Console Compiler son 11 KB. Como referencia, el código en QB 45 eran 30 KB. una vez desempaquetado el EXEPACK.

Mi primera teoría era que el manejador de excepciones interceptaría la división por cero a nivel de runtime de C, dejando así sin efecto el código generado por QB64. Evidentemente este gestor de excepciones puede modificarse, así que pensé que esa habría sido la causa del problema. Estaba equivocado.

Tras descargarme el código fuente completo desde Github me di cuenta que la función ERROR de BASIC se había implementado directamente con error en C. Se encontraba dentro de internal\c\libqb.cpp, un mastodonte de casi 1,3MB. de código fuente.

void error(int32 error_number)
{
	//critical errors:
	
	//out of memory errors
	if (error_number==257){MessageBox2(NULL,"Out of memory","Critical Error #1",MB_OK|MB_SYSTEMMODAL); exit(0);}//generic "Out of memory" error
	//tracable "Out of memory" errors
	if (error_number==502){MessageBox2(NULL,"Out of memory","Critical Error #2",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==503){MessageBox2(NULL,"Out of memory","Critical Error #3",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==504){MessageBox2(NULL,"Out of memory","Critical Error #4",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==505){MessageBox2(NULL,"Out of memory","Critical Error #5",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==506){MessageBox2(NULL,"Out of memory","Critical Error #6",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==507){MessageBox2(NULL,"Out of memory","Critical Error #7",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==508){MessageBox2(NULL,"Out of memory","Critical Error #8",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==509){MessageBox2(NULL,"Out of memory","Critical Error #9",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==510){MessageBox2(NULL,"Out of memory","Critical Error #10",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==511){MessageBox2(NULL,"Out of memory","Critical Error #11",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==512){MessageBox2(NULL,"Out of memory","Critical Error #12",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==513){MessageBox2(NULL,"Out of memory","Critical Error #13",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==514){MessageBox2(NULL,"Out of memory","Critical Error #14",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==515){MessageBox2(NULL,"Out of memory","Critical Error #15",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==516){MessageBox2(NULL,"Out of memory","Critical Error #16",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==517){MessageBox2(NULL,"Out of memory","Critical Error #17",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==518){MessageBox2(NULL,"Out of memory","Critical Error #18",MB_OK|MB_SYSTEMMODAL); exit(0);}

	//other critical errors
	if (error_number==11){MessageBox2(NULL,"Division by zero","Critical Error",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==256){MessageBox2(NULL,"Out of stack space","Critical Error",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==259){MessageBox2(NULL,"Cannot find dynamic library file","Critical Error",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==260){MessageBox2(NULL,"Sub/Function does not exist in dynamic library","Critical Error",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==261){MessageBox2(NULL,"Sub/Function does not exist in dynamic library","Critical Error",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==270){MessageBox2(NULL,"_GL command called outside of SUB _GL's scope","Critical Error",MB_OK|MB_SYSTEMMODAL); exit(0);}
	if (error_number==271){MessageBox2(NULL,"END/SYSTEM called within SUB _GL's scope","Critical Error",MB_OK|MB_SYSTEMMODAL); exit(0);}
	
	if (!new_error)
	{
		if ((new_error==256)||(new_error==257)) fix_error();//critical error!
		if (error_number<=0) error_number=5;//Illegal function call
		new_error=error_number;
		qbevent=1;
	}
}

Su cometido era sencillo. Clasificaba lo que se consideraban errores críticos de la aplicación, y que conllevaban que la ejecución se abortase acompañadas de un Messagebox, con otros que eran críticos. En general los errores críticos tenían códigos elevados, superiores al 256, y que por tanto no se lanzaban dentro del programa. Sin embargo, ahí aparecía también el error 11, la división por 0. ¿Por qué el QB64Team había optado por gestionarlo así? Eso mejor lo deberían responder ellos, porque eliminando esa linea o comentándola, bastaría para alterar su comportamiento llevando así el flujo a fix_error que es la rutina que obtiene el descriptivo del mensaje de error.

void fix_error()
{
	char *errtitle = NULL, *errmess = NULL, *cp;
	int prevent_handling = 0, len, v;
	if ((new_error >= 300) && (new_error <= 315))
	{
		prevent_handling = 1;
	}
	if (!error_goto_line || error_handling || prevent_handling)
	{
		switch (new_error)
		{
			case 1: cp="NEXT without FOR"; break;
			case 2: cp="Syntax error"; break;
			case 3: cp="RETURN without GOSUB"; break;
			case 4: cp="Out of DATA"; break;
			case 5: cp="Illegal function call"; break;
			case 6: cp="Overflow"; break;
			case 7: cp="Out of memory"; break;
			case 8: cp="Label not defined"; break;
			case 9: cp="Subscript out of range"; break;
			case 10: cp="Duplicate definition"; break;
			case 12: cp="Illegal in direct mode"; break;
			case 13: cp="Type mismatch"; break;
			case 14: cp="Out of string space"; break;
			//error 15 undefined
			case 16: cp="String formula too complex"; break;
			case 17: cp="Cannot continue"; break;
			case 18: cp="Function not defined"; break;
			case 19: cp="No RESUME"; break;
			case 20: cp="RESUME without error"; break;
			//error 21-23 undefined
			case 24: cp="Device timeout"; break;
			case 25: cp="Device fault"; break;
			case 26: cp="FOR without NEXT"; break;
			case 27: cp="Out of paper"; break;
			//error 28 undefined
			case 29: cp="WHILE without WEND"; break;
			case 30: cp="WEND without WHILE"; break;
			//error 31-32 undefined
			case 33: cp="Duplicate label"; break;
			//error 34 undefined
			case 35: cp="Subprogram not defined"; break;
			//error 36 undefined
			case 37: cp="Argument-count mismatch"; break;
			case 38: cp="Array not defined"; break;
			case 40: cp="Variable required"; break;
			case 50: cp="FIELD overflow"; break;
			case 51: cp="Internal error"; break;
			case 52: cp="Bad file name or number"; break;
			case 53: cp="File not found"; break;
			case 54: cp="Bad file mode"; break;
			case 55: cp="File already open"; break;
			case 56: cp="FIELD statement active"; break;
			case 57: cp="Device I/O error"; break;
			case 58: cp="File already exists"; break;
			case 59: cp="Bad record length"; break;
			case 61: cp="Disk full"; break;
			case 62: cp="Input past end of file"; break;
			case 63: cp="Bad record number"; break;
			case 64: cp="Bad file name"; break;
			case 67: cp="Too many files"; break;
			case 68: cp="Device unavailable"; break;
			case 69: cp="Communication-buffer overflow"; break;
			case 70: cp="Permission denied"; break;
			case 71: cp="Disk not ready"; break;
			case 72: cp="Disk-media error"; break;
			case 73: cp="Feature unavailable"; break;
			case 74: cp="Rename across disks"; break;
			case 75: cp="Path/File access error"; break;
			case 76: cp="Path not found"; break;
			case 258: cp="Invalid handle"; break;	
			case 300: cp="Memory region out of range"; break;
			case 301: cp="Invalid size"; break;
			case 302: cp="Source memory region out of range"; break;
			case 303: cp="Destination memory region out of range"; break;
			case 304: cp="Source and destination memory regions out of range"; break;
			case 305: cp="Source memory has been freed"; break;
			case 306: cp="Destination memory has been freed"; break;
			case 307: cp="Memory already freed"; break;
			case 308: cp="Memory has been freed"; break;
			case 309: cp="Memory not initialized"; break;
			case 310: cp="Source memory not initialized"; break;
			case 311: cp="Destination memory not initialized"; break;
			case 312: cp="Source and destination memory not initialized"; break;
			case 313: cp="Source and destination memory have been freed"; break;
			case 314: cp="_ASSERT failed"; break;
			case 315: cp="_ASSERT failed (check console for description)"; break;
			default: cp="Unprintable error"; break;
		}
	}
}

Como aspecto gracioso, una prueba más de la falta de soporte de QB64, en su Wiki mencionan lo siguiente:


El curioso bug de QB64 (on error)

Según ellos el «ERROR 11» no tiene ninguna diferencia, por más que en el código fuente la tenga, y que el comportamiento del programa de ejemplo así lo manifieste.

Pero en la página que habla del comando ERROR, se nos dice lo contrario:


El curioso bug de QB64 (on error)

Medio oculto tras una nota al pie indican: «Note: Don’t use error codes under 97 or over 200 as QB64 may respond to those errors and interrupt the program.»

Es decir, que los errores por debajo de 97 y por encoma de 200 pueden interrumpir el programa. Y esa es la explicación a todo el asunto. QB64 ha optado intencionadamente por comportarse diferente a QBASIC y QuickBASIC. Puede que tenga algo que ver con su eslogan de «BASIC for the modern era», en donde han decidido alterar algo que llevaba 35 años funcionando desde los tiempos del Microsoft PC BASIC Compiler.

Aquí (633 KB. en formato ZIP) podéis descargar el código fuente así como el compilado con QB 4.5 y QB64 1.4.

6 comentarios en “El curioso bug de QB64 (on error)”

  1. Hola Guti! La verdad debo admitir que mis conocimientos no son suficientes para entender bien el bug que explicas, pero me alegro que hayas vuelto a publicar articulos sobre programacion y compiladores antiguos.

    Saludos!!

  2. Javier Gutiérrez Chamorro (Guti)

    La cosa es sencilla Dario, ocurre que QB64 está mal programado. No se comporta como QB que es uno de sus objetivos. El Error 11 («Division by zero») debería ser interceptable con «On Error», pero no lo es en QB64. Solucionarlo sería parchear solamente unas pocas lineas de código, pero parece que nadie está interesado en hacerlo. Para prueba es el poco interés de todos aquellos, más de una cincuentena, que comentaron en la entrada original de QB64. Siendo sinceros es una actitud que me cansa cada vez más, personas que exponen su problema y que pretenden que seas tu el que invierta el tiempo para solucionarlo. Al final lo haces, o lo hacía, sin sacar nada a cambio ni pretenderlo, pero una vez hecho ellos se habían ya olvidado de la urgencia y la importancia del problema que habían reportado.

    De manera que la retro-informática y la retro-programación es algo que por supuesto no voy a abandonar, me agrada el tema arqueológico, pero que en todo caso desarrollaré porque a mi me motiva, no a petición de alguien como ha sido este caso (evidentemente no lo digo por ti).

  3. He seguido el error que comentas (a pesar de no ser usuario de QB64, sí del QB original) y me ha gustado cómo lo has resuelto.

    Sobre el tema de trabajar gratis, ya no es sólo el problema de que la gente no se moleste en investigarlo o dar facilidades, o que luego no lo agradezca, es que al final te toman por soporte de la herramienta y se mosquean el día que no se lo resuelves. En esas cosas se aplica aquello de «la feria de Valverde, el que más pone, más pierde». Pues eso mismo.

  4. Javier Gutiérrez Chamorro (Guti)

    Muy bueno lo de la Feria de Valverde Alejandro, y muy cierto. Siguiendo con el tema es que me deja perplejo como algo que parece crítico, casi de vida o muerte, y que además es urgente, en pocos segundos o incluso minutos deja de serlo y se olvidan de todo. Encima como bien dices, no pasa nada si ellos se olvidan, «están muy ocupados». Pero como seas tu el que te olvides… La que te espera, porque en cuanto te explican el problema, parece que pasa a ser responsabilidad tuya.

  5. JOSE HENRY PEREZ PEREZ

    HOla Javier, buenas tardes, gusto en saludarte. Soy el desarrollador de un software en qbasic de 32 que trabaja con archivos btrieve. Mi aplicacon es muy potente y completa pero esta quedando absoleta por ell Hardware de ahora que no reconice software en 32b. Deseo migrar a QB64 y desearia contar con una asesoria personalizada de alugna persona conocedora del tema. le agradeceria su colaboacion.

  6. Javier Gutiérrez Chamorro (Guti)

    Bienvenido JOSE HENRY PEREZ PEREZ. ¡Qué tiempos con Btrieve! No das muchos detalles de la aplicación en cuestión, así que comenzaremos por algo un poco más general y así esperemos que sirva también para otros lectores con dudas similares. Imagino que has hecho ya un análisis previo del proceso y las necesidades, así que para no repetirlo yo, te planteo algunas preguntas.

    1.- ¿Te has planteado lo que parece la solución más fácil? Es decir, encapsularlo dentro de un DOSBox o algún otro tipo de máquina virtual para que así pueda seguir funcionando en entornos más modernos.

    2.- Otra opción sería migrarlo a nivel de sistema, con NTVDMx64 o proyectos similares podrías ejecutar tus aplicaciones DOS en entornos Win64. ¿Por qué lo descartaste?

    3.- Tienes clara la migración a QB64, lo cual te permitirá aprovechar tu código. ¿Qué harás entonces con Btrieve? Entiendo que no tienes la licencia del código fuente (que además está en C), por lo que esa parte tendrías que migrarla. ¿Qué opciones has valorado?

    4.- ¿Has pensado en alternativas como PowerBasic y PBTree? El código de QB requerirá ciertos cambios, pero ya tienes de un motor de bases de datos disponibles.

    5.- Y puesto que el esfuerzo que hayas estimado, entiendo que será grande. ¿Por qué no reescribirlo del todo? Se me ocurre que podrías usan algún tipo de xBase, o incluso pasarte a Lazarus/Delphi o VB.NET así ofrecer una experiencia GUI.

Deja un comentario