Nota del traductor: El documento no está traducido en su integridad, si alguien pretende
terminarlo que contacte con: Stan Sieler
Este documento trata sobre como escribir codigo de Pascal de calidad. Se pone enfasis en los aspectos de programación a la hora de escribir codigo en Pascal.
Programa de ejemplo: roman.p.txt, un programa para convertir numeros romanos a arábigos ó arabes.
Ejemplo de mal codigo: trim, un trozo malo de codigo del mundo real.
Nota: El documento usa Pascal/iX, el Pascal en las computadoras HP 3000 computer, pero casi todos los puntos se pueden aplicar a cualquier variante de Pascal, y casi aplicar tambien a otros lenguajes de programación.
Quiero que los temas de este documento cubran las cuatro Ps de la filosofia
de programación: Filosofia, rendimiento, problemas y compatibilidad
(portabilidad). Consideré dividir el documento en cuatro capitulos,
uno para cada una de las P, pero me di cuenta que para varios temas que
pondría en un capitulo, igualmente se podrían poner bueno
argumentos unos dentro de otros. Así es que, en vez de tener un
capítulo por P, he organizado el documento en los capitulos:
Estilo, Elecciones de programación, Problemas de rendimiento
y Compatibilidad.
En cada capítulo las 4 Ps son usadas como principios que contienen un abanico de guias de trabajo. Estas reglas sugeridas no deberían ser vistas como si fueran los 10 (o %12 o $A) mandamientos. Estan mis reglas, las que e usado (en varios formularios para diferentes lenguajes) en la escritura de más de un millón de lineas de código. Las guias de trabajo deberán crecer, tal y como crece la gente. Puedo mirar los programas que escribí en 1970 y ver los cambios en el estilo en los siguientes años. A pesar de los cambios, la impresión general ha sido la misma.
El principal propósito de este documento es animar a los lectores a pensar sobre los aspectos diferentes de estilo y a crear sus propias guias de trabajo de programación. Es una gran ayuda a la hora de escribir programas de calidad en... Pascal o otro lenguaje.
He escrito más de un millón de lineas de codigo fuente
de lenguajes de alto nivel desde 1970. Esto, por supuesto, incluye algunas
lineas blancas, lineas de comentario, y lineas que han sido clonadas de
otros archivos. De todas maneras, toda esta experiencia me ha enseñado
que la aparencia (impresión) de un programa es de una importacia
critica para entender lo que hace el codigo.
Las secciones principales son:
0.1 Código para tirar a la basura (Throw-away code)
Guias de trabajo:
#1-1. No hay nada como el codigo aqui te pillo aqui te mato. Ya que tus guias de trabajo se aplican a todo lo que escribes. Especialmente los programas "throw-away" ó "hechos de un tiro".
Comentario:
#1-1. Me he dado cuenta que siempre uso mis programas "basura" ó "hechos de un tiro". Y, si tengo que ser menos estricto con mis guias de trabajo siempre tengo que pagar por ello más tarde.
(vuelve a la tabla de contenidos)
1. Estilo
Tener o no estilo, puede afectar dramáticamente la calidad de un programa. El código de calidad debe estar escrito de tal manera que sea correcto, eficiente y de fácil puesta al día. Eligiendo, y siguiendo, un estilo en particular puede incrementar la probabilidad de que cada uno de los tres objetivos sean conseguidos. Esta sección primero tratará sobre las variables y después sobre el resto del programa.
1.1 Nombres : Principios Básicos
Una de las claves para conseguir un programa con exito es la elección adecuada de los nombres..
El éxito de un programa de Pascal descansa en un buen entendimiento de los datos que el programa manipula. Este entendimiento se ve reflejado en la elección de las estructuras de datos y variables usadas por el programa.
A lo largo de este documento la palabra "nombres", cuando se use sin un adjetivo, se referirá a los nombres de variables, constantes, tipos, procedimientos y funciones..
Guías de trabajo:
#1-11. Usa nombres para variables, constantes, y tipos; y usa oraciones
(con verbo) para procedimientos y funciones.
#1-12. Los tipos deben usualmente tener "_type" al final de su nombre.
#1-13. Los nombres deben ser entrados enteramente en minúsculas, con un "subrayado" ("_") separando los nombres (ejemplo: contador_cartas).
Commentary:
#1-11. La siguiente linea debería ser suficiente para mostrar la importancia de esta guia de trabajo.
if contador_cartas = 52 then¿Es "contador_cartas" una variable entera o una función? La diferencia es critica para el entendimiento de un programa. La única guia de trabajo que el lector puede tener es su entendimiento de inglés. (Una regla de "poner en mayúsculas" no habría sido suficiente)
Es importante recordar que otras personas leeran el codigo algun dia. ("Otras" personas ¡Te incluyen a tí algun día despues de haber el código! Ellos necesitaran señales que les ayuden a interpretar correctamente el codigo que están leyendo.
De acuerdo con la guia de trabajo #1-11, "contador_cartas" es una variable. Si tuviera que ser una función, entonces su nombre debería ser algo como "contar_las_cartas".
#1-12. Algunos programadores ponen como prefijo: "t_", "typ_", or "type_". Otros postponen: "_t", "_typ", or "_type". Unas pocas almas malguiadas se han enamorado tanto de esta idea que ponen como prefijo "t_" y postponen "_type" a cada tipo..
La idea básica es que hay que diferenciar claramente los nombres de tipos definidos por el usuario desde variables, y procedimientos (o funciones)..
#1-13. Esta guia de trabajo refleja el hecho de que somos hablantes ingleses. Si fueramos hablantes alemanes, nuestros programas deberían probablemente usar mayúsculas al principio de cada variable. Date cuenta de l lo más dificil que es leer esto.
Puedo ver tres razones por las que poner en mayúsculas la primera letra (o todas) de algunos tipos de nombres. Ninguna merezen la pena.
Los nombres para los tipos deberían ser descriptivos, y deberían ser claramente distinguibles como tipos de tal manera de que lo signifiquen cuando son usados en expresiones "sizeof" ó type coercion.
(* TYPE COERCION --> QUE ME EXPLIQUE LO QUE ES ************)
Guias de trabajo:
#1-21. Los tipos genericos que son: "packed arrays of char" deberían ser llamados: "pac" con el número de caracteres.
type pac8 = packed array [1..8] of char; pac80 = packed array [1..80] of char;#1-22. Los tipos que son arrays deberían tener "array" en su nombre. (Excepción: nombres del tipo pac## (ver #1-21).)
#1-23. Los tipos que son punteros deberían tener "ptr" en su nombre. Los tipos que son punteros largos (64 bits) deberían tener "lptr" en su nombre. (En Pascal/iX, los puntos ordinarios son puntos de 32 bits de largo... un puntero largo es aquel que puede apuntar en cualquier sitio en la memoria.
#1-24. Los registros deberían tener nombres de campos con un prefijo que reflejen el registro del cual forman parte.
#1-25. Los tipos simples no deberían normalmente reflejar su
"tamaño".
(* POR QUE PONE SIZE ENTRE COMILLAS ********)
#1-26. Intenta evitar el uso de "true" y "false" para resultados funcionales. Usa un tipo como "tipo_bien_fallido" en lugar de eso.
Comentario:
#1-21. El lenguaje Pascal tiene reglas especiales para variables que son "packed arrays of char" con un BOUND (* QUE SIGNIFICA BOUND *) inferior a 1. Estas reglas especiales fueron añadidas en un intento de hacer el estandar de Pascal más util para la manipulación de texto. Como resultado, muchas implementaciones de Pascal se refieren a "PAC" como una variable (o tipo) que es un packed array of char, con un BOUND inferior que 1. Así es que, es conveniente reflejar este estandar en nuestros propios tipos.
Date cuenta que esta guia de trabajo decía tipos genéricos. Cuando uso TurboIMAGE (un DBMS en el HP 3000), y quiero crear un tipo que corresponde a un item IMAGE, intentaré dar al tipo un nombre que es similar al nombre del item:
{IMAGE items: } { cust_name x20;} type cust_name_type = packed array [1..20] of char;En el ejemplo de arriba, usé "20" en la declaración de tipo solamente porque era un valor obvio (dado el comentario sobre los items IMAGE). Nunca usaria la constante de "20" más tarde en el programa.Si fuera necesario referirse al "20" usaría la construcción del Pascl/iX "sizeof(cust_name_type)" o habría añadido "const cust_name_len = 20" y habría usado la constante para declarar el registro.
#1-24. Esta linea de trabajo hace los campos de un registro inmediatamente identificable cuando son usados en una instrucción "with". Ejemplo:
mal:
type hpe_status = record {name matches MPE/iX's usage} info : shortint; subsys : shortint; end; hpe_status_ptr_type = ^ hpe_status; ... var hs_status_ptr : hpe_status_ptr_type; ... with hs_status_ptr^ do begin info := my_info; subsys := my_subsys; end;bien:
type hpe_status = record {name matches MPE/iX's usage} hs_info : shortint; hs_subsys : shortint; end; ... with hs_status_ptr^ do begin hs_info := my_info; hs_subsys := my_subsys; end;En el ejemplo malo, el lector que no es familiar con el codigo no sabra que los campos de los punteros están siendo cambiados. En el ejemplo bueno es obvio.
#1-25. En SPL (un lenguaje de programación parecido al ALGOL), es bastante frecuente usar un sufijo o prefijo para denotar el tipo de una variable. (Ejemplo: double ktr'd). Con las restricciones de Pascal a la hora de concordancia de tipos esto es mucho menos necesario.
#1-26. "true" y "false" significan poco por sí mismos. Ejemplos:
malo:
function abrir_archivos : boolean; ... abrir_archivos := false; ... if abrir_archivos then ...bien:
type tipo_fallo_bueno = (fallo, bueno); function abrir_archivos : tipo_fallo_bueno; ... abrir_archivos := bueno; ... if abrir_archivos = fallo then ...En el ejemplo malo, el lector no tiene ni idea de si "abrir_archivos" devuelve un valor true eso es bueno o malo.
Esta guia de trabajo es incluso más importante cuando programando en un entorno de multiples lengaujes, porque lenguajes diferentes (y sistemas operativos diferentes) tienen ideas extrañas sobre si "0" (o "false") significan bueno o malo.
El orden del codigo fuente puede afectar dramaticamente al entendimiento de un programa. Las guias de trabajos sobre el orden tocan tres campos: tipos & constantes, variables y procedimientos & funciones.
Guias de trabajo:
#1-31. Las constantes simples deberían ser puestas al principio del area de declaraciones, seguidas de los tipos, constantes estructuradas, y entonces de las variables. Dentro de cada grupo, los identificadores deberían ser arreglados en algun orden. Si no se usa otro orden, el orden alfabetico debería ser usado. Ejemplo:
const max_queues = 10; type aardvark_quantity_type = 0..9; queue_name_type = packed array [1..8] of char; queue_names_type = array [1..max_queues] of queue_name_type; zoo_fed_arrdvarks_type = array [aardvark_quantity_type] of integer;#1-32. Por lo menos una instancia de "type" y "var" es necesitadas, y hasta dos instancias de "const" (una para constantes simples, y otra para constantes estructuradas).
#1-33. Los identificadors en un area de declaración (constantes, tipos, variables) deberían ser declarado uno por linea, en algun orden. El orden alfabeticos es el de por defecto. Si otro orden es usado, debería ser explicado con un comentario.
#1-34. Los identificadores en una area de declación deberían ser entrados de tal manera que los caracteres "=" (los iguales) estuvieran alineados para constantes y tipos, y que los caracteres ":" estuvieran alineados para variables y campos en registros.
#1-35. Los tipos de registro que contienen registros anidados no deberían nunca ser declarados como "tipos anonimos". Ejemplos de malas y buenas prácticas son:
mal:
type payment_record_type = record pay_date : date_type; pay_time : time_type; pay_style : (paid_cash, paid_check, paid_charge); end;bueno:
type payment_style_type = (paid_cash, paid_check, paid_charge); payment_record_type = record pay_date : date_type; pay_time : time_type; pay_style : payment_style_type; end;#1-36. Los registros "Crunched" deberían ser evitados al no ser de la necesidad para el empaquetamiento "tight" de los campos "sobremonte" la perdida de rendimiento que causan.
(*2******** PREGUNTARLE QUE SIGNIFICA CRUNCHED, tight, override*)
El Pascal/iX soporta "registros crunched********************", y una
extensión más alla de un simple "registro packed". En un
registro crunched ***********, no hay bits sin usar. Si declaras un registro
crunched******* con un campo de 1 bit, seguido de un campo de 8 bits, seguido
de un campo de 1 bit, entonces el registro entero ocupara 10 bits, y el
campo de 8 bits requerira codigo extra para cargar/almacenar.
#1-37. Variables de Large outer block (> 256 bytes) deberían ser declaradas en último lugar, incluso si esto viola la guia de trabajo #1-33. (Similarmente, las variables locales large deberían ser declaradas primero) (Notas: Esto es un truco de rendimiento para el Pascal/iX... ¡Otros compiladores en otras maquinas probablemente hagan las cosas de forma diferente!)
#1-38. Los procedimientos y funciones están entremezcladas(ej.:
No separes procedimientos de funciones
(* ************* NO SERÍA JUSTO AL REVES???? ********)
#1-39. Los procedimientos (y funciones) deberían ser declaradas en algun orden (el orden alfabetico el de por defecto)
#1-40. Más de un nivel de procedimientos anidados debería ser evitado.
#1-41. Intrinsicos deberán ser declarados una vez, al nivel "de afuera" despues de todas las constantes, tipos y variables y delante de cualquier procedimento "external", "forward" o "actual". (Un "intrinseco" es una referencia a una clase de declaración de procedimiento externo pre-compilado, soportado por la mayoría de lenguajes en el HP 3000).
#1-42. Los intrinsecos deberían estar en orden alfabeticos, arreglados por archivos intrinsecos. Ejemplo:
Function ascii : shortint; intrinsic; Function binary : shortint; intrinsic; Procedure quit; intrinsic;En el ejemplo de arrriba, dos espacios se han puesto detrás de la palabra "Function", de tal manera que los nombres de todos los intrinsecos deberían estar alineados, independiente de si son funciones o procedimientos. Es muy desafortuando que el Pascal nos haga probar al compilador que sabemos el tipo funcional de cada intrinseco.
Fijate que en la mayusculas "P" and "F" en el ejemplo superior. Es una regla de una disciplicina a la hora de codificar que esta explicada en la guia de trabajo #1-45 más abajo.
#1-43. Todos los procedimientos y funciones deberían estar declaraos con declaraciones "forward", que están en orden alfabético.
#1-44. Los tipos son "llenadores" (********** QUE EXPLIQUE MEJOR QUE ENTIENDE POR "FILLERS"******) que deberían ser declarados como "entero" o "byte" (donde "byte" es declarado como 0..255) en vez de como "char" para sus tipos bases (******** instead of as "char" for their base types ********)
#1-45. Las declaraciones de procedimiento y función: "forward", "external", y "intrinsic" deberían tener la primera letra ede "Procedure" y "Function" en mayuscula.
Comentario:
#1-34. Normalmente alinea los "=" para tipos y constantes en la columna 31, los ":" para campos dentro de un tipo tambien en la columna 31, y los ":" para variables en la columna 19. Si las variables tienen un nombre lo bastante largo, simplemente alineo los ":" en la columna 31. Uso este alineamiento para los ":" usados en las declaraciones con parametros, tales como el nombre de variable que empieza en una columna. (************** PORQUE AL FINAL PONE ?? ********)
procedure parse_gribbitz ( a_token : str80; var status : boolean; anyvar stuff : char) option default_parms ( stuff := nil);En el ejemplo de arriba, fijate que los nombres de parametros están alineados. (Obviamente, esto no es siempre posible, particularmente si el parametro tiene información de $alignment$ especificada.) (Nota: "anyvar" es vista otra vez en 2-9 más abajo).
#1-35. Las variables (y los campos de registros) que son tipo anónimos no pueden nunca ser pasado por referencia (por paramentros "var") a procedimientos ordinarios. De hecho, los registros anonimos normalmente no pueden ser pasado por valores a los procedimientos.
#1-36. Acceder a los campos de registros "crunched" puede ocupar tres veces el numero de instrucciones que se hubiera necesitado si el registro no hubiera sido crunched. Considera los dos tipos siguientes:
type bad_type = crunched record bad_misc : shortint; {Bytes 0, 1} bad_status : integer; {Bytes 2, 3, 4, 5} end; {Total size: 6 bytes} good_type = record good_misc : shortint; {Bytes 0, 1} good_status : integer; {Bytes 4, 5, 6, 7} end; {Total size: 8 bytes}Cuando el campo "bad_status" es accedido, Pascal/iX emite tres instrucciones (LDH, LDH, y DEP). Cuando el campo "good_status" es accedido, Pascal/iX emite uan instrucción (LDW).
#1-37. Pascal/iX puede acceder eficientemente sólo a los primeros 8.192 bytes de los globales y los *ultimos* 8,192 bytes de variables locales. Pascal/iX guarda variable en una (basta) manera: ya visto, ya guardado. Por consiguiente, para variables globales, poner las pequeñas primero y las largas despues tiende a ser más eficiente. Para variables locales, poner las largas primero y las pequeñas despues tiende a ser más eficiente.
Ya que el Pascal/iX guardar las variables outer block ("global") en el orden opuesto que las variables locales, la regla a seguir es:
malo:
var {outer-block variables} big_array : array [0..9999] of integer; {40,000 bytes} ktr : integer; ... procedure foo; var ktr2 : integer; big_array_2 : array [0..9999] of integer; {40K bytes}bueno:
var {outer-block variables} {small variables...} ktr : integer; {big variables...} big_array : array [0..9999] of integer; ... procedure foo; var {big variables...} big_array_2 : array [0..9999] of integer; {40K bytes} {small variables...} ktr2 : integer;En el ejemplo malo, Pascal/iX usará dos instrucciones para acceder "ktr" y "ktr2". En el ejemplo bueno, Pascal/iX usará una sola instrucción para acceder "ktr" y "ktr2".
#1-38. La diferenciación de Pascal de las funciones frente a los procedimentos es de lo más desafortunado. Deberíamos no animar a los diseñadores de lenguajes a perpetuar esta cosa. (***** flaw *******************)
#1-39. La instrucción "$locality" de Pascal/iX's puede ser usada para decir al linker que agrupe procedimientos especificados juntos independiente de su orden en el codigo fuente.
#1-40. Pascal te permite declarar procedimientos que están declarados dentro de procedimientos que están...
Los procedimientos anidados sufren una penalización en el rendimiento cuando acceden a las variables globales de ellos, que son locales para los procedimientos "que le rodean" (******* surrounding ****** una mejor explicación ********)
Procedures nested more than a total of two deep (i.e.: an "outer level" procedure and one inner procedure) usually implies that other design problems exist.
Algunos debuggers tienen dificultad a la hora de establecer breakpoints en procedimientos anidados.
#1-43. Este trabajo extra es recompensado cuando se escribe un "modulo" que será linkado con otros programas. A menudo pongo todas mis declaraciones "forward" en un archivo, y entonces uso los siguientes comandos QEDIT para general un archivo "external" de declaraciones para otros modulos (******* EXPLICAR MEJOR This extra work often pays off well when writing a "module" that will be linked with other programs. I often put all of my "forward" declarations into a file, and then use the following QEDIT commands to generate an "external" declarations file for other modules to $include:*****)
t myfile.forward c "forward"(S)"external"@ k myfile.external(QEDIT es un editor muy usado en los HP 3000.)
#1-44. Debug/iX's Format Virtual command (FV) will produce much more readable output for random data when the base type is numeric instead of character. (Debug/iX is the debugger bundled with MPE/iX on the HP 3000.)
#1-45. With this guideline, and an associated one in the next section, a QEDIT command like:
l "procedure" (s)will list the first line of every procedure declaration. Note that only the actual declaration will be listed, not the "forward" or "external" declarations, since they would have been declared with a capital "P" in "Procedure".
Note: do not tell your editor to automatically upshift all text you search for (e.g.: Set Window (UP) in QEDIT), as that will defeat the purpose of this guideline.
This section discusses the styles of coding for the executable code part of a Pascal program.
Guidelines:
#1-50. All code should be in lower case.
#1-51. Comments should be in English, and should be in mixed case, as is the practice in English.
#1-52. Comments should appear in one of two styles, depending on their size:
{the following loop looks for a null character} null_index := -1; {-1 will mean "not found"} test_inx := 0; {index of first char} done := (len = 0); {don't loop if no data} while not done do begin {see if current character is null...} if buf [test_inx] = chr (0) then begin {found a null!} null_index := test_inx; {remember location} done := true; {terminate loop} end else begin {incr inx, check end} test_inx := test_inx + 1; if test_inx >= len then {inx is 0-based} done := true; end; end; {while not done}#1-53. Multi-line comments may be written with "{" and "}" on every line, or with the "{" and "}" appearing only once, on lines by themselves.
#1-54. The "{" and "}" characters are used to start and terminate comments, never the "(*" and "*)" pairs.
#1-55. "{" is typically not followed by a space, nor is "}" typically preceded by a space (unless it is to align it with the prior line's "}").
#1-56. Lines should be no longer than 72 bytes, even though Pascal/iX allows longer input lines.
#1-57. Blank lines cost nothing at run time, and should be used liberally to separate sections of code. Example:
if ktr > max_ktr then max_ktr := ktr; {remember new high water} done := false; while not done do begin ...#1-58. The "end" statement does not have to have a comment on it. Pascal never checks that your comment matches reality, anyway.
#1-59. The basic unit of indentation is 3, not 4 or 2.
#1-60. Indent "begin"s 3 spaces more than the start of the preceding line. Code after a "begin" (up to and including the "end") is at the same level as the "begin".
#1-61. Continuation lines are indented 6 more than the start of the first line.
#1-62. The "then" of an "if/then" statement is usually on the same line as the rest of the boolean expression, not on the next line by itself (unless necessary for spacing, and then it is indented 6), and never on the same line as the statement following the "then".
#1-63. An "else if" construct may be treated as though it were a new Pascal construct: "elseif". (I.e.: the "if" follows the "else" on the same line.)
#1-64. A "goto 999" is an acceptable method of branching to the end of a procedure (or function).
#1-65. No other "goto"s are necessary.
#1-66. Try to keep procedures below five pages (300 lines).
#1-67. Never use the word "procedure" or "function" in a comment in exactly all lower case. Instead, use "routine" or "PRocedure" or "FUnction".
#1-68. Always terminate a procedure or function with a comment of the form:
end {nameofroutine proc};#1-69. Put a blank between the name of a procedure/function and the "(" of the parameter list.
#1-70. Put a blank after every comma in a parameter list.
#1-71. Put blanks around operators (e.g.: " := ", " + ", " - "), and in front of left brackets (" [").
Commentary:
#1-52. Aligned comments make the code look neater. This practice makes it possible for a reader to easily read the code or the comments.
#1-55. The practice of always following a "{" with a space and preceding a "}" wastes valuable space on the line.
#1-56. Long lines will not list well on most terminals, nor are they acceptable to all editors.
#1-58. I do put a comment on the "end" statement when it is more than about 10 lines from the corresponding "begin".
#1-60. The value of "3" and the injunction against "double indentation" saves space and makes the result more readable. Consider the following two examples:
bad:
for i := 1 to 10 do begin buf [i] := 0; foo [i] := 0; end; if ktr = 0 then begin if not done then begin ...good:
for i := 1 to 10 do begin buf [i] := 0; foo [i] := 0; end; if ktr = 0 then begin if not done then begin ...Many Pascal programmers have seen the "double indentation" style because the professor in charge of UCSD Pascal (in the early 1970s) used this style. Note that he was primarily a teacher, not a programmer.
#1-61. An example:
if (card_counter = max_card_counter) and all_cards_accounted_for then begin ...The goal of indenting continuation lines is to make it clear to the reader that the prior line has continued to the next line. If no extra indentation is used, then it becomes difficult to determine the difference between the next statement and the continuation of the current statement.
When I have a complex "and/or", I try to make it readable when doing continuation lines:
bad:
if ((card_counter = prior_card_counter) and ((number_of_cards_left > cards_for_book)) then begingood:
if ( (card_counter = prior_card_counter) and (number_of_cards_left > cards_for_book) ) then beginIn the bad example, notice how the "begin" is blurred by the "and" starting in the same column.
#1-62. The word "then" is syntactic sugar: it fattens the listing, and has no redeeming value. When the reader sees an "if", he or she automatically knows that a "then" is coming, eventually. The indentation alone would suffice to tell the reader that the "then" statement has been found. Examples:
bad:
if card_counter = max_card_counter then done := true else ...good:
if card_counter = max_card_counter then done := true else ...In the above bad example, the reader has to mentally sift through the excess verbiage ("then") in front of the "done :=" in order to understand the affects of a "true" boolean expression. In the good example, reading the left side of the listing suffices.
#1-63. "else if" constructs are typically found in one of two situations:
if token_check ('EXIT') then wrapup else if token_check ('LIST') then do_list else if token_check ('PRINT') then do_print else writeln ('Unknown command: ', token);Note: in the above style, I will often put 5 extra spaces before the first "token_check", so that a QEDIT command like LIST "token_check" will show all three "token_check" phrases nicely aligned.
An example of the nested "if/then/else":
if card_counter = max_card_counter then if done then ... else discard_current_card else if current_card = joker then try_best_wildcard else calculate_score;A style I recommend against is the following:
if token_check ('EXIT') then wrapup else if token_check ('LIST') then do_list else ...The above style has drastic readability consequences if an untimely "page eject" occurs in the listing between an "else" line and the following "if" line.
#1-64. Pascal lacks an "exit" statement. Both C and SPL have some form of "return from this procedure right now" statements. This is the only place I use a "goto" in Pascal.
#1-67. This guideline means that editor "find" and "list" commands looking for "procedure" and "function" will never accidentally find comment lines instead. (See also #1-45).
#1-68. This makes it very easy to find the end of any (or all) procedure(s) with a "find" command.
#1-69/70/71. Blanks make code more readable, just as they make English more readable. Note the blank after the comma in the previous sentence. Example:
bad:
fid:=fopen(filename,3,0);good:
fid := fopen (filename, 3, 0);
2. Coding Choices
This section deals with choices made in writing executable code.
Guidelines
#2-1. Decide if your style is to have functions that return errors, or procedures that have status parameters (or both), and then stick to it.
#2-2. Don't use "with" for simple pointer deferencing. Only use "with" if indexing into an array.
#2-3. Try to avoid "repeat" loops, using "while" loops instead.
#2-4. Try to avoid using "escape" outside the scope of a "try/recover" block.
#2-5. Avoid "string"s in favor of PACs. A PAC is a Packed Array of Char with a lower bound of 1.
#2-6. Use Pascal/iX extensions when possible, unless portability is a primary goal.
#2-7. The "try/recover" construct in Pascal/iX is very useful for catching errors: both unexpected and deliberately caused. (try/recover is an error catching mechanism somewhat similar to catch/throw found in some other languages)
#2-8. Use $type_coercion 'representation'$. Never use the noncompatible level of type coercion.
#2-9. The "anyvar" parameter type is useful. Consider using it when you want to pass different types of variables to one procedure.
#2-10. The "uncheckable_anyvar" option for a procedure should be used whenever "anyvar" parameters are declared, unless you specifically want Pascal/iX to pass a hidden "actual size" parameter.
#2-11. When using "anyvar", be sure that the formal parameter type matches the alignment restrictions of the expected actual types. I.e.: if the formal parameter is declared as an "integer", then Pascal/iX will assume that all addresses passed into that parameter are a multiple of 4. Use "char" (or another byte-aligned type) as the formal parameter type if you want to pass any kind of addresses safely.
#2-12. The "default_parms" procedure option should be considered as a means of making long actual parameter lists shorter (for ordinary cases).
Commentary:
#2-1. Sometimes, I return a quick overall result with a "good_failed_type", and a detailed error in a status parameter. Example:
function open_files (var status : hpe_status) : good_failed_type; ... if open_files (status) = failed then report_status (status);#2-2. Pascal provides a "with" statement that can, in some instances, provide the compiler with a hint on how to optimize the instructions it emits for your code. Additionally, a "with" statement can save subsequent typing.
An example of a useless "with" is:
var ptr : hpe_status_ptr_type; ... with ptr^ do begin hs_info := 0; hs_subsys := 0; end;This is "useless" because the compiler & optimizer would probably have done just as good a job of emitting optimum code if we had said:
ptr^.hs_info := 0; ptr^.hs_subsys := 0;Note that the code took five lines using a "with" statement, and two lines without it.
Finally, the fact that "hs_info" and "hs_subsys" are actually fields of the record pointed to by "ptr" is somewhat obscured when the "with" is used.
An example of a useful "with" is:
var statuses : array [0..9] of hpe_status_type; ... with statuses [k] do {optimize hs_@ fields} begin hs_info := 0; hs_subsys := 0; end;I consider this example "useful" because the optimizer would have had more work trying to compile optimum code for the equivalent non-with statements:
statuses [k].hs_info := 0; statuses [k].hs_subsys := 0;In the above examples, it is tempting to align the ":="s with the right-most ":=" of the block of assignment statements. I used to often do this when I had four or more similar assignments in a row.
The advantage is increased readability because we make it obvious that the data is related (because of the aligned ":="s). The disadvantage is that a simple QEDIT command designed to list where the "hs_info" field is changed (e.g.: LIST "hs_info :=") will fail. I found that the search-for-assignments capability outweighed the data-is-related benefit, for me.
#2-3. When a "repeat" loop is encountered, the reader won't know what the termination condition is until many more lines of program source are read. This means that he or she will not be able to check for proper setup of the termination condition. A "while" loop avoids this problem because the termination condition is clearly specified at the top of the loop.
A repeat loop can usually be changed into a while loop easily:
before:
repeat begin ... end until buf [inx] = 0;after:
done := false; while not done do begin ... done := (buf [inx] = 0); end; {while not done}#2-4. A "non-local escape" costs thousands of cycles of CPU time to execute. In short, never plan on using this construct as a normal method of returning from a procedure. Examples:
bad:
procedure do_work; {note: no status parameter!} ... if problem then escape (i_failed); ...good:
procedure do_work (var status : hpe_status_type); label 999; ... if problem then begin status := my_failure_status; goto 999; {exit} end; ... 999: end {do_work proc};#2-5. Strings hide an immense amount of slow and somewhat incorrect compiler-generated code. String concatenation, in particular, can result in "memory leaks" where your process runs out of heap space. PACs are messier to deal with, but much more efficient. This is a performance versus esthetics trade off.
#2-6. Pascal/iX is a very useful language precisely because it has a large body of extensions to standard Pascal. If you eschew using them, you would be better off programming in ANSI C or C++.
#2-7. "Try/recover" is not guaranteed to catch any errors that occur within it. Unexpected errors (e.g.: invalid index, bad virtual address) invoke an operating system routine called trap_handler which will "walk" back through your stack markers looking for the most recent "try/recover" block. This "walk" can fail if your stack is corrupted, and the "try/recover" will not be found. If this happens, and if an appropriate trap handler (e.g.: XCODETRAP) has not been armed, your process will be aborted.
#2-8. Type coercion is one of the best extensions in Pascal/iX. It provides a controlled way of overriding Pascal's type checking. The $type_coercion directive tells Pascal/iX what level of type coercion you want to allow in your program. About five different levels exist. The level I strongly recommend is representation. This level allows an expression of one type to be coerced (treated as) another type if, and only if, the two types are exactly the same size (in units of bits, not bytes).
The noncompatible level tells Pascal/iX that there should be no restrictions on type coercion. This leads to interesting bugs in programs. Some MPE/iX system crashes can be traced to using this kind of type coercion incorrectly. The following examples demonstrates how noncompatible can hide errors.
assuming:
var big_ptr : globalanyptr; my_address : integer; small_ptr : localanyptr;bad:
$type_coercion 'noncompatible'$ my_address := integer (big_ptr); {will get half of data!} my_address := integer (small_ptr); {will get 32 bit value}good:
$type_coercion 'representation'$ my_address := integer (big_ptr); {will get syntax error} my_address := integer (small_ptr); {will get 32 bit value}In the bad example, the coercion of big_ptr results in setting my_address to the upper 32 bits of big_ptr (i.e.: the space id), silently losing the bottom 32 bits of the address. In the good example, Pascal/iX will generate a syntax error on the attempt to coerce a 64-bit expression (big_ptr) into a 32-bit value (integer).
#2-9. "anyvar" is a Pascal/iX extension of "var". When a formal parameter is declared as "anyvar", the compiler allows any variable to be passed as the actual parameter. Without such a feature, and without an object-oriented Pascal, you couldn't write a single procedure that would zero (erase) an arbitrary variable (see below for example).
By default, when a parameter is declared as anyvar, Pascal/iX will pass
in the address of the actual parameter *and* a hidden integer-by-value
which records the size of the actual paramter. The following example shows
what is passed for a formal paramter like: "anyvar foo : integer", and
the affect on a "sizeof (foo)" within the procedure:
Actual parameter type | Hidden size field | sizeof (foo) |
char | 1 | 1 |
shortint | 2 | 2 |
integer | 4 | 4 |
longint | 8 | 8 |
real | 4 | 4 |
longreal | 8 | 8 |
hpe_status (see #1-24) | 4 | 4 |
packed array [1..80] of char | 80 | 80 |
#2-10. I use "anyvar" fairly often. One example is:
procedure zero_var (anyvar foo : char); {Purpose: zero every byte in the parameter} var bytes_left : integer; byte_ptr : ^char; begin $push, range off$ byte_ptr := addr (foo); bytes_left := sizeof (foo); {Note: gets actual size!} while bytes_left > 0 do begin {zero one byte} byte_ptr^ := chr (0); bytes_left := bytes_left - 1; byte_ptr := addtopointer (byte_ptr, 1); end; $pop$ {range} end {zero_var proc};Note the comment on the $pop$...allowing me to recall what options the $pop$ is supposedly restoring.
In Pascal/iX, $push$ saves the state of most compiler options, and $pop$ restores them. Thus, $push, range off$ ... $pop$ temporarily turns off the "range" option, and then restores it to the old state ... which is significantly different than simply turning it on when "done"!
Of course, Pascal/iX allows an even faster way of zeroing a variable, which happens to work well with checkable anyvar parameters. The entire code of the above procedure (between "begin" and "end") can be replaced by:
fast_fill (addr (foo), 0, sizeof (foo));#2-12. The following example, a procedure that most users would call with a "false" in the second parameter, shows the usefulness of "default_parms":
const print_with_cr = 0; print_without_cr = 1; procedure print_msg ( msg : str80; cr_or_no_cr : integer) option default_parms ( cr_or_no_cr := print_with_cr); var cctl_val : shortint; begin if cr_or_no_cr = print_without_cr then cctl_val := octal ('320') else cctl_val := 0; print (msg, -strlen (msg), cctl_val); {Note: ignoring errors from print intrinsic} end {print_msg}; ... print_msg ('Starting...', print_without_cr); print_msg ('Ending'); {does a CR/LF at end}Note that I would not use a "default_parm" for a parameter that is omitted less than about 75% of the time.
3. Performance Problems
The two biggest performance problems in Pascal/iX programs are using the built-in I/O, and using strings.
Guidelines:
#3-1. Avoid Pascal I/O. Use intrinsics instead.
#3-2. Avoid Pascal I/O. Use intrinsics instead. This is worth saying twice!
#3-3. Avoid strings in performance critical areas.
#3-4. Turn off range checking ($range off$) only when you are sure your program is running correctly.
#3-5. Use the Pascal/iX optimizer ($optimize on$).
Commentary:
#3-1. If you encapsulate your I/O calls, then their underlying implementation can be easily changed to use MPE intrinsics. This also aids portability across operating systems and across languages. The "print_msg" procedure in commentary #2-12 is an example.
The second reason for avoiding Pascal I/O constructs is efficiency. Pascal/iX I/O routines are extremely inefficient. This guideline is valid for most languages.
#3-3. String expressions cause the compiler to emit a lot of calls to "helper" routines. Instead of allocating a single work area in your stack, these routines allocate (and deallocate) many work areas on your heap. This can be an expensive activity, and can lead to loss of heap space, and eventual process termination.
#3-5. If your program runs correctly unoptimized, and has a problem optimized, then there are probably one (or more) uninitialized variables. The second most common problem is using pointers in a way that the optimizer doesn't expect (especially when accessing local variables via pointers).
4. Portability
The portability of programs written in Pascal/iX can be enhanced with several techniques. Keep in mind, though, that most other Pascal implementations are not as rich as Pascal/iX. Delphi and Turbo Pascal (on IBM PC compatibles) provide some of the same features as Pascal/iX.
#4-1. Avoid these Pascal/iX extensions: extensible, readonly, anyvar, option, uncheckable_anyvar, default_parms, globalanyptr.
#4-2. Avoid most $ directives (e.g.: $extnaddr$).
#4-3. Use type coercion only for types of identical sizes. (A good tip even if you never intend to port your code!) Most PC-based Pascals have a form of type coercion. They may refer to it as "type casting".
#4-4. Avoid "crunched" records. Even most C languages do not have a functional equivalent. This includes avoiding "$HP3000_16$".
#4-5. Encapsulate "strange" constructs where possible.
#4-6. Encapsulate I/O calls. This is not a language portability issue so much as an operating system and/or performance issue.
#4-7. Keep your source code lines short (72 characters or less per line).
#4-8. Use user-defined types like "int16" and "int32" instead of "shortint" or "integer". Note: this is extremely important for C programmers!
#4-9. Be aware that fields in records may be packed differently on different machines.
#4-10. Standard Pascal does not allow pointers to point to variables. (They can only point into the heap.)
5. Summary
The shortest summary of this paper is probably: you can judge a book by its cover. If a program looks nice, it probably is nice.