|
|
Hasta el momento, lo que se ha visto ha sido una serie de operaciones aritméticas y de manipulación de la pila. Todas éstas indicadas de manera implícita a través de una serie de términos y convenciones que es necesario definir de forma más precisa antes de continuar con otra cosa.
Existen dos formas en las que puede representarse una pila y su funcionamiento; de ésta dependerá la terminología que se emplee. Nosotros hemos venido usando una representación que conceptualiza a la pila como un arreglo que "crece" o es utilizado desde su base (el primer nivel) hacia arriba. Los datos entran y se toman de la base y desplazan o "empujan" a los elementos, que ya pudieran estar introducidos en la pila, hacia arriba. Identificaremos a los diversos elementos involucrados acorde al siguiente diagrama:
| Niveles | ... | ^ | Tope | v Base |
|||||||||
|
|
||||||||||
| ^ | Ingreso |
Llamaremos nivel a cada una de las localidades con que cuente la pila y los enumeraremos a partir de 1. La base es el primer nivel. El tope de la pila lo identificaremos como el nivel en el que se encuentre el elemento con ingreso más antiguo. Representaremos con una elipsis (...) el resto de las localidades o elementos en la pila. Por último, mientras que nos hemos limitado a usar números en la representación de elementos que pueden ser introducidos en la pila, no estamos restringidos a esto. Dependiendo de su implementación una pila puede albergar más que números. De forma genérica nos referiremos a estos elementos como objetos.
Los diagramas que se han usado para ilustrar el contenido de la pila antes y después de la aplicación de una instrucción (que aparece como una operación, función, operador o nombre en el código del programa) serán identificados como diagramas de pila. En estos diagramas la elipsis se incluirá sólo cuando sea necesario recalcar la existencia del resto de la pila o su contenido; mientras no se indique lo contrario, siempre debe considerarse a la pila de Stop como infinita y factible de tener un contenido.
Adicionalmente, en la presente descripción:
font no proporcional.Ya se ha mencionado que la representación que emplearemos para la pila de Stop será una en la que ésta "crece" a partir de la base, que es sobre la que descansa, y por donde entran y salen los diversos objetos. Al entrar, los datos desplazan o "empujan" hacia arriba a los elementos que ya pudieran estar introducidos en la pila. Al salir, los elementos restantes en la pila "caen" a niveles inferiores. En ningún momento es posible tener localidades vacías intermedias.
En la introducción de las primeras instrucciones para la manipulación de elementos en la pila durante el capítulo anterior se menciona que contamos con un soporte para hacer lo que se define, detalles técnicos sobre la implementación de estas acciones se darán en la sección dedicada a la implementación del lenguaje. Por el momento sólo se mencionará que deben tenerse presente dos cosas muy importantes: a) la pila de Stop es una pila y como tal sólo cuenta con un punto de entrada y salida de datos, que como se ha mencionado es la base; b) tanto en la manipulación de elementos con las instrucciones que hemos definido como en las que se definirán posteriormente, lo que se observa es el efecto final de una serie de operaciones internas y no la posibilidad de extraer o insertar elementos directamente de o a niveles intermedios de la pila.
Las operaciones de manipulación de objetos en la pila que vimos en el capítulo anterior las etiquetaremos como operaciones básicas de pila. Como se habrá podido observar estas operaciones están atadas a determinados niveles de la pila. Esto presenta algunos inconvenientes. Por ejemplo, si tuviéramos la pila
|
y deseáramos manipular el cuarto objeto, nos enfrentaríamos a un problema. Las operaciones ROTATE y REVOLVE, que son las que tienen un mayor alcance, están limitadas al tercer nivel. La única forma de alcanzar el 9 es eliminar uno de los niveles previos. Es en este punto donde surge la necesidad de contar con operaciones que puedan manipular otros niveles de la pila.
Antes de hablar de posibilidades de expansión o sobrecarga de las operaciones básicas, debe considerarse cómo es que se va a lograr que una operación tenga la capacidad de alcanzar un nivel arbitrario en la pila. Una consideración crucial al respecto es que para poder indicar el alcance de la operación es necesario proporcionarle dicha información con un argumento o parámetro. Con esto en mente veremos que no es posible extender el funcionamiento de las operaciones ya creadas sin incurrir en un problema de ambigüedad (cuando tomar un argumento, cuando no). La solución más viable es entonces crear un grupo adicional de instrucciones.
Llamaremos al nuevo grupo operaciones generalizadas de pila y cada una de éste será una extensión de una operación básica; así para ROTATE tenemos
|
ROLL UP==> |
|
Todas estas instrucciones toman de la pila el argumento a usar (un valor numérico entero puesto que los niveles de la pila se identifican con valores enteros), la pila se ajusta y entonces se lleva a cabo la operación. En este caso se toma el nivel del cual se moverá su contenido para ser insertado en la base de la pila empujando a los elementos que previamente se encontraban bajo éste.
Para REVOLVE:
|
ROLL DOWN==> |
|
se toma el nivel al cual se moverá el contenido de la base de la pila (una vez retirado el argumento) empujando hacia abajo a partir del nivel indicado.
En la generalización de PUSH se implica que el argumento señala la cantidad de elementos a duplicar:
|
DUPLICATE==> |
|
Para OVER:
|
INSERT==> |
|
se copia (nótese la diferencia con ROLL DOWN) la base de la pila en el nivel indicado por el argumento.
Para BELOW:
|
PICK==> |
|
se copia el contenido de un determinado nivel de la pila a la base. Nótese la diferencia con la operación ROLL UP.
Para DROP:
|
CLEAR==> |
|
Se eliminan el número de elementos indicado por el argumento tomado, contando a partir de la base de la pila y una vez retirado el argumento.
Finalmente, para SWAP definiremos una operación que nos permitirá intercambiar la base de la pila con cualquier nivel de ésta.
|
INTERCHANGE==> |
|
Como se ha mencionado, éstas operaciones buscan sobrepasar la limitante impuesta en las operaciones básicas de estar asociadas con niveles específicos de la pila. La definición de todas estas operaciones ha partido de lo anterior teniendo presente mantener la simplicidad de la acción. Lo anterior podrá parecer obvio pero en el caso de DUPLICATE y de INTERCHANGE se presentan dos interesantes problemas. En la primera tendríamos la posibilidad de duplicar una determinada cantidad de niveles o de crear una n cantidad de copias de un valor. Se ha optado por la primera opción por considerarse sería de mayor utilidad y sentido práctico, ya que lo que se busca es liberarse de las ataduras a niveles determinados (crear n copias de un mismo nivel no parece liberarnos de dicha limitación). Con INTERCHANGE, ha aplicado un razonamiento similar. En este caso las posibilidades eran intercambiar dos niveles o dos grupos de niveles. La última opción parece menos frecuente o sin mayor sentido que la primera. Por supuesto se ha decidido continuar trabajando con la base de la pila, ya que es la principal localidad de trabajo y el considerar intercambiar dos localidades arbitrarias requeriría dos parámetros.
La generalización de las operaciones básicas de pila nos permitirá ahora manipular cualquier nivel en ésta. Sin embargo esto no es lo único a lo que podemos aspirar. Todavía podemos extender y generalizar aún más el concepto.
El siguiente paso consiste en permitir poder tomar el argumento de una operación generalizada de un nivel indicado previamente por otro. Extenderemos la sintaxis del grupo de operaciones previamente definido mediante un calificador que nos permita indicar a la operación a ejecutar que primero debe obtener información de donde tomará el argumento con el que se concretará la acción. Este calificador será la palabra INDIRECT.
Siguendo la filosofia de funcionamiento ya establecida, INDIRECT tomará su argumento de la base de la pila, la cual se ajustará tras esto. El valor indicará de donde deberá tomarse el argumento de la operación generalizada a llevarse a cabo. Una implicación importante que se desprende de esto, es el hecho de que en este punto ya no estaríamos trabajando con la base de la pila sino en otro nivel de ésta. Como se recordará una operación generalizada toma su argumento de la base de la pila, que llamaremos una acción destructiva, y que no presenta mayor problema con los principios y filosofía de trabajo que hablan de respetar el principio operacional de una pila. Sin embargo, el trabajar tomando argumentos de un nivel intermedio presenta algunas contradicciones a este principio.
Quizás una primer idea sería decir que en estos casos la instrucción generalizada no se comporte de manera destructiva, copiando únicamente sui argumento del nivel indicado y trabajando a partir de la base. Esto hablaría de una excepción en el funcionamiento regulado por controles internos. Algo que suena algo complicado. Por otra parte, hasta donde hemos visto en las operaciones generalizadas, el único propóosito del argumento es indicar el nivel o cantidad de elementos a trabajar y tras lo cual ya no se necesitaría más. Entonces, por lo anterior, parecería contradictorio hacer una excepción manteniendo un valor que no necesitaríamos después de usarlo.
Considerando el razonamiento anterior, veremos que nos parecerá mucho más más viable hacer una excepción sobre el manejo que hemos venido haciendo de la pila y concluiremos indicando que para el caso de las instrucciones indirectas, tomaremos de forma destructiva el argumento de la operación a realizar de forma indirecta. El siguiente es un ejemplo de todo lo anterior. Recuérdese que toda operación generalizada puede llevarse a cabo de manera indirecta.
|
INDIRECT CLEAR==> |
|
Concluiremos la presente sección con la siguiente nota final. Para quienes están familiarizados con C/C++ lo anterior deberá recordarles los conceptos de variable, apuntador y apuntador de apuntador, que fue precisamente algo de lo que se pretendió reflejar. Esta misma idea volverá a presentarse en la sección 8.- Procesamiento simbólico usando la pila.
En el capítulo I ya se ha mencionado la carencia de la declaración de tipos de datos. Esto no quiere decir que Stop no cuente con tipos de datos sino que simplemente estos no se declaran, y la razón ya ha sido dada.
Por tipos de datos nos referiremos no a una declaración sino a la naturaleza de un objeto capaz de ser contenido en la pila de Stop. Los tipos de datos a los que puede pertenecer uno de estos objetos son:
0x. Se les llama binarios porque se toma como un valor crudo sin considerarse codificado de alguna forma (letra, cadena o número). Por ejemplo:Los arreglos de Stop corresponden a lo que se denomina arreglos asociativos. Los índices son usados como llave mas que como un indicador de posición. Hasta este momento se ha ilustrado el manejo de los arreglos accediendo individualmente a cada uno de sus elementos, sin embargo es posible manipular a todo el arreglo haciendo referencia al nombre seguido de paréntesis vacíos. Esto resulta útil para pasar la referencia de un arreglo a un procedimiento y para el procesamiento simbólico, e.g 'A(). La referencia a un arreglo en esta forma siempre debe ser hecha como objeto inhibido.
Stop cuenta con un grupo de operadores aritméticos, lógicos y relacionales, así como con funciones para el cálculo de ciertos valores. Todos estos son bien conocidos por su presencia en otros lenguajes por lo que se omite la presentación de ejemplos. El manejo de cadenas está contemplado a través de la sobrecarga de los operadores artiméticos (para mayor detalle al respecto debe consultarse las Especificaciones de Implementación).
Los operadores definidos para Stop se encuentran clasificados en tres categorías principales. La clasificación parte del uso más común asociado con el símbolo empleado. Sin embargo, debe considerarse que no es posible determinar el tipo de operandos con los que se encontrará un operador en tiempo de ejecución. Con el fin de disminuir la cantidad de errores por incompatibilidad en tipos de datos se ha sobrecargado a la mayoría de éstos (consúltese la sección dedicada a la conversión de tipos de datos). A continuación se describen estas tres categorías, indicando las diversas acciones que con cada uno de los operadores puede llevarse a cabo.
Funciones para el cálculo de ciertos valores son incluidos como parte del lenguaje, estas son:
En diversas situaciones es necesario obtener información del estado de la pila. Stop cuenta con una instrucción que le permite alterar el tamaño de la pila (lo que afecta en su comportamiento) y con dos instrucciones que le permiten obtener información sobre ésta. Estas tres instrucciones se conocen como funciones de pila.
La función de pila TOP permite conocer cuantos elementos se encuentran en la pila, lo que está directamente relacionado con el número de niveles ocupados en ésta. Así:
|
TOP==> |
|
La función de pila TYPE devuelve un valor entero que identifica al tipo de objeto que se encuentre en la base de la pila. Esto es lo que se llama una prueba de tipo destructivo ya que el objeto cuyo tipo de dato se está determinando se pierde. Por ejemplo:
|
TYPE==> |
|
Para más información sobre los distintos valores identificadores de tipos de datos favor de consultar el apéndice 4.
Por último, se cuenta con la función de pila SIZE, ésta tiene un comportamiento específico dependiendo del valor que tenga el argumento en la base de la pila. Si este valor es cero, se establece que la pila de Stop es de tamaño infinito (valor por defecto). Si, por otra parte, el valor es mayor a cero, se establece que la pila tendrá un número de localidades igual a este valor. Cuando esto ocurre: a) si previamente se tenía una pila con mayor número de niveles, el contenido de éstos se perderá, b) si previamente la pila tenía una cantidad menor de niveles, los niveles adicionales se crean vacíos. Por ejemplo:
|
SIZE==> |
|
En los siguientes ejemplos, se asume que la pila tiene un tamaño de sólo cuatro niveles (esto es, con anterioridad se aplicó el efecto de 4 SIZE). Cuando la pila tiene un valor finito, y en caso de suceder, no se marca un error en caso de que la pila se llene. Si la pila está llena y se introduce un nuevo objeto en ésta, el objeto en el tope se pierde. Por ejemplo:
|
4==> |
|
Por otra parte, si la pila se ha llenado y entonces se extraen elementos, el valor que haga llegado a estar en el tope se duplica.
|
DROP==> |
|
En Stop, un nombre es una cadena de caracteres que permiten identificar a un procedimiento o una variable. Un nombre puede ser formado por una letra o guión bajo (_) al inicio seguido por más letras, guiones bajos o números. Ejemplos de nombres válidos son:
Abaco, A78_09, __abajo, A_Z_U_L, _0987 o X45.
Para recuperar el valor asociado a una variable sólo se tiene que nombrarla para que el valor contenido en ésta se inserte en la pila. Por ejemplo, supongamos que la variable Abaco contiene el valor numérico entero 128; para ingresar este valor en la pila sólo tiene que hacerse:
|
Abaco==> |
|
Aunque en Stop no existe una forma para declarar tipos de datos, existen dos formas de declarar una variable. Esta declaración tiene que ver más con la definición de la visibilidad de la variable a lo largo del programa, módulos y procedimientos que sobre el tipo de dato, que como ya hemos visto carece de utilidad. Recuérdese que en Stop las variables no son mas que una extensión de las localidades de la pila. Siendo éstas capaces de albergar cualquiera de los tipo de datos existentes y dado que no es posible determinar con certeza qué cosa tendrá la pila al momento de llevar a cabo la asiganción de un valor a una variable, resulta lógico pensar que las variables no tengan un tipo de dato asociado y que son capaces de almacenar cualquiera de éstos.
La primer forma de declarar una variable es en el momento de la asignación de un valor. En los lenguajes tradicionales, que emplean una notación infija, la asignación de un valor a una variable tiene la forma variable=valor. Siendo que Stop trabaja empleando la notación polaca inversa podríamos pensar en que la instrucción de asignación debería expresarse como valor variable =. El problema que aquí se nos presenta es la ambigüedad en el empleo de =, ya que como hemos definido anteriormente éste es el símbolo elegido para la comparación por igualdad. Por supuesto podemos elegir otro símbolo para servir como operador de asignación, pero si meditamos un poco sobre el asunto, veríamos que: a) la asignación de un valor a una variable es tomar un valor de la pila para almacenarlo en memoria, sirviendo el valor de la variable como etiqueta o dirección de donde fue almacenado dicho valor; b) esta operación es como cualquiera de las otras que hemos definido para manipular el contenido de la pila, en las cuales empleamos una palabra para su nombramiento y no un símbolo. Así la primer forma en que declaramos una variable es la forma como declaramos a una variable global, visibles a lo largo de todo el programa, módulos y procedimientos desde su creación.
Así entonces, la decisión más acorde a como hemos venido trabajando es emplear una palabra para la definición de la operación de asignación. De entre las diversas palabras que podríamos seleccionar quizas la más idónea es la que nos da a entender que lo que estamos haciendo es almacenar un valor en memoria, por lo que emplearemos la palabra STORE para nombrar a esta operación.
En este punto nos adelantaremos un poco a la noción de procesamiento simbólico que definiremos con mayor precisión más adelante. Primeramente debemos considerar que, como toda operación de Stop, los argumentos se toman de la pila, y siendo que uno de estos argumentos u operandos es la variable en sí, nos vemos en la necesidad de que el nombre de la variable también se introduzca en la pila. Esto nos lleva al problema de determinar cómo hacer para introducir el nombre de una variable en la pila; obviamente esto no puede hacerse nombrando a la variable ya que como se ha visto esto recuperaría el contenido de ésta. Por el momento nos concretaremos a indicar que esto se logrará antecediendo al nombre de la variable con un apóstrofe. De esta forma la asignación del valor "a" a la variable Abaco se logra con:
|
'Abaco==> |
|
STORE==> |
|
Con respecto a los diagramas cabe hacer mención que aunque se ilustra a los objetos con sus delimitadores al ser introducidos en la pila, esto se hace sólo por facilidad de ilustración. En la práctica los delimitadores no son parte del nombre del objeto.
Como podrá inferirse, la creacion de una variable es extremadamente dinámica y en realidad se lleva a cabo en tiempo de ejecución. En tiempo de compilación las declaraciones de variables sirven para validar la existencia y alcance de éstas. Tales validaciones podrían incluirse en la definición del lenguaje pero, como veremos más adelante, las posibilidades de procesamiento simbólico que estamos construyendo harán que dichas validaciones al final carezcan de sentido. Ciertamente esto implica un problema para la posterior depuración y seguimiento de un programa, pero esta es una complicación propia de este tipo de lenguajes.
Antes de describir la otra forma de declaración de variables debemos definir el concepto de una subrutina o procedimiento en Stop. Un procedimiento es un nombre asociado a un bloque de instrucciones que opcionalmente puede llevar una lista de argumentos formales (ninguno, uno o más de uno separados por comas) con sus elementos delimitados por llaves ({ }). Al igual que con los arreglos, no debe haber espacios entre el nombre y la llave que abre.
Nombre{ param1, param2, ...} [ .
.
.
.
instrucciones
.
.
.
]
La declaración de parámetros formales constituye la segunda forma de denotar la existencia de una variable. Por supuesto, en este caso la variable tiene un periodo de vida y visibilidad limitado al procedimiento en el que fue declarada.
Por compatibilidad con versiones anteriores de Stop, la siguiente definición de un procedimiento es válida y equivalente a un procedimiento con una lista de parámetros formales vacía:
Nombre [
.
.
.
instrucciones
.
.
.
]
Al igual que las variables, los procedimientos se invocan con sólo nombrarlos, lo mismo que en cualquier otro lenguaje. Por la misma razón de compatibilidad, el nombre puede no ir seguido de una lista de argumentos si ésta se definió vacía o estuvo ausente al declarar el procedimiento. Sin embargo, y a diferencia de las variables el invocar un procedimiento pasándole parámetros presenta algunas implicaciones.
Como en cualquier otro lenguaje de programación, como parámetro de un procedimiento es posible incluir expresiones o resultados de algún otra función. En Stop ocurre lo mismo.
Durante el paso de un valor como parámetro es posible llevar a cabo operaciones para pasar un valor calculado o producto de una serie de acciones. El paso de parámetros no está limitado a valores constantes o valores contenidos en una variable. Por la naturaleza del lenguaje aplican las siguientes consideraciones:
Los siguientes son ejemplos de invocación de procedimientos con parámetros.
Con parámetros constantes |
calcula{ 1, 2.3E14, 0xBDF5, "Cadena" } |
Empleando variables |
traza{ X1, Y1, largo, ancho, color } |
Con parámetros calculados |
impuestos{ salario 1.25 *, tabulador(3) factor * 2.333 / } |
Con parámetros calculados |
vacaciones{ bisiesto{ hoy }, dias{trabajados 1.13 *} } |
Con parámetros calculados |
Factores{ 2 PUSH PUSH * *, 2 PUSH 3 ^ SWAP /} |
Con parámetros por referencia |
referencia{ 'A, 'B, 'C() } |
Las reglas y ejemplos dados aplican también para la indicación de índices de arreglos.
Como en cualquier otro lenguaje de programación, es necesario contar con instrucciones que permitan la comunicación del procesador con el mundo exterior. Las primeras instrucciones que seguramente vendrán a nuestra mente son las que necesitaremos para leer datos del teclado y para desplegar información en pantalla.
La entrada de datos por teclado implica la inserción de un objeto (lo que se haya tecleado) en la pila. Algunos lenguajes recurren a instrucciones específicas para la lectura de determinados tipos de datos: una instrucción para la lectura de números y otra para la de caracteres, inclusive otra muy distinta para la lectura de cadenas. Esto funciona muy bien en el caso de lenguajes con declaraciones de tipos de datos, ya que la variable que recibe el resultado de la entrada por teclado es la adecuada para lo que dicha instrucción devolverá. En un lenguaje sin tipos de datos la idea de tener instrucciones específicas para lectura de cierto tipo de datos parece carecer de sentido, mucho más si lo que se recibe de entrada es directamente enviado a la pila, la cual puede contener cualquier tipo de dato en cada una de sus localidades.
¿Pero por qué introducir los datos a la pila y no a una variable? En Stop la pila es el ambiente alrededor del cual todo gira. Las variables sólo son un almacenamiento auxiliar a ésta. Ya hemos visto que podemos efectuar cálculos y mantener resultados en la pila sin necesidad de recurrir a su almacenamiento en una variable, por lo que se ha tomado la decisión introducir las entradas en la pila y dejar que sea el programador si éstas deberán o no ser almacenadas en una variable.
Ahora, ¿qué deberá introducirse en la pila tras una operación de entrada por teclado? Tal vez con letras y cadenas de letras no haya problema decidiendo que debe entrar pero ¿qué hay de un número? ¿Entra como cadena o como número? Como podemos ver existen dos tipos de datos que podemos esperar como entrada desde teclado: cadenas y números. Podemos definir una instrucción generica de lectura por teclado que nos permita leer una cadena y posteriormente proceder a su conversión si lo que esperamos es un número. Es de esperarse que este tipo de conversiones sean muy frecuentes por lo que conviene definir dos instrucciones de entradas de datos, una genérica para recibir cadenas y otra específica para cualquier entidad númerica (reales, enteros y binarios).
Identificaremos la operación de entrada genérica de datos a la pila con la palabra ENTER y su comportamiento será el de hacer una pausa en el programa a la espera de recibir una entrada por teclado. El programa resume su funcionamiento al ser presionada la tecla Enter o Return, introduciéndose en la pila una cadena formada por todos los caracteres correspondientes a las teclas presionadas antes de las teclas indicadas (sin incluirlas). En el siguiente ejemplo, si ante la espera por datos se teclea: 0xAB3445FEnter, en la pila se introduce:
|
ENTER==> |
|
Nótese que aunque la secuencia de teclas presionada forman una correcto número binario, el resultado entra como cadena.
Para la segunda opción, definiremos ENTRY, que tiene un comportamiento similar a la anterior, salvo que al presionar Enter o Return lo que procede es a efectuar una validación sobre lo que se pretende introducir para determinar si puede identificarse como entidad numérica. De ser así se introduce en la pila, en caso contrario se indicaría el problema y nuevamente se esperaría por la entrada numérica. Tomando como base al ejemplo anterior,
|
ENTRY==> |
|
que en este caso la entrada se deja como un valor binario.
Con base en el primer ejemplo veamos la operación de salida a pantalla. En este caso se trata de una operación sobrecargada en funcionalidad. Ya habíamos mencionado con anterioridad que la palabra POP se usaría para algo más que eliminar elementos de la pila. Acorde al tradicional concepto asociado a esta instrucción, la usaremos para sacar un elemento de la pila y mostrarlo en pantalla. Esta instrucción será capaz de presentar adecuadamente el tipo de dato extraído de la pila. Por ejemplo,
|
POP==> |
|
Muestra la cadena (sin delimitadores) 0xAB3445F. Se ha mencionado que se trata de una instrucción sobrecargada, esto se debe a que no emplearemos instrucciones específicas de presentación en pantalla por tipo de dato. La instrucción POP determinará el formato adecuado para presentar correctamente el dato que es extraído de la pila. Claro, existe una razón complementaria de mucho peso para evitar definir instrucciones adicionales de salida por tipo de datos: la imposibilidad de poder saber en tiempo de compilación el tipo de dato que estará en la base de la pila al tiempo de ejecución. Así POP deberá encargarse de determinar la forma más adecuada de presentación del dato que está siendo extraído de la base de la pila.
En lo que respecta a instrucciones para manipulación de archivos sólo se cuentan con 4 instrucciones básicas. Aquí se describe su propósito; para mayor detalle sobre los valores que llegaran a tomar o devolver a la pila consultar los diagramas de pila abstractos
OPEN.- Abre un archivo.ARCHIVE.- Graba en un archivo.RECOVER.- Recupera de un archivo.CLOSE.- Cierra un archivo.Una de las posibilidades que nos brinda el que a la pila podamos introducir distintos tipos de datos es la de poder introducir objetos de forma que podemos obtener los elementos esenciales del procesamiento simbólico. Ya en la sección 6 hemos presentado un esbozo de esto al definir la forma como se podrá asignar valores a una variable.
Como se habrá podido observar en la sección citada, todo está al rededor de un operador especial al que llamaremos operador de inhibición y que corresponde al apóstrofe ('). Con la misma idea que la primitiva SETQ de Lisp, este operador impide que un objeto sea ejecutado y que literalmente se tome tal cual para su introducción en la pila en lugar del valor que de éste resulte en su evaluación.
La posibilidad de "congelar" la ejecución de una instrucción, no sólo de poderla demorar sino también de poderla insertar en la pila para poder ser manipulada, abre todo un mundo de posibilidades. Este mundo es muy difícil de ser explorado a través de una descripción, debe ser experimentado para poder apreciar su potencia y complejidad. La descripción proporcionada en los siguientes párrafos es tan sólo un vistazo de este mundo de posibilidades.
El operador de inhibición únicamente aplica a objetos e instrucciones que pueden insertarse en la pila o que tienen un efecto sobre la pila. Una vez en ésta está sujeto a ser manipulado como cualquier otro objeto. Por ejemplo, retomando el caso del nombre de la variable (que por cierto aplica también para procedimientos), podríamos hacer:
|
OVER==> |
|
STORE==> |
|
En este punto la variable A contiene el valor 8, de aplicar otra vez la instrucción STORE substituiríamos su contenido por el valor 5.
Como se mencionaba con anterioridad, los nombres de procedimientos son susceptibles de ser inhibidos e introducidos en la pila (implícitamente). Claro, una operación STORE no tiene efecto sobre el nombre de un procedimiento y deberá generarse un error en tiempo de ejecución por tipo de argumento equivocado (lo mismo si en lugar de un nombre apareciera un operador inhibido, una cadena o un número).
En este punto seguramente sale a la luz la idea de que, independientemente de la manipulación de los elementos inhibidos en la pila, debería haber una forma de "evaluar" a uno de estos objetos, esto es "descongelarlo" y llevar a cabo su acción pospuesta. Considérense los dos siguientes casos:
a)
|
'SWAP==> |
|
b)
|
STORE==> |
|
STORE==> |
|
En el primer caso tenemos una operación básica de pila inhibida; en el segundo hemos almacenado el nombre de una variable en otra. De manera similar a la primitiva EVAL de Lisp, crearemos la operación EVALUATE, que nos permitirá "descongelar" (desinhibir) y ejecutar. Así para el caso a) tendríamos:
|
EVALUATE==> |
|
Para el caso b), si se tuviera la pila:
|
EVALUATE==> |
|
La aplicación de un segundo EVALUATE. Recuperaría el valor 5 almacenado en la variable A.
Un efecto similar podría obtenerse nombrando a la variable B, colocando su contenido en la pila. Sin embargo en este punto requeriríamos de la funcionalidad de EVALUATE para extraer el valor del nombre dejado en la pila. Considerando este tipo de situaciones, sería muy cómodo poder llevar a cabo una evaluación continua de valores simbólicos hasta llegar a un punto en el que ya no fuera posible hacerlo. Esto nos ahorraría la realización de sucesivas operaciones EVALUATE.
Sobrecargar o extender el próposito de una instrucción es algo que no debe tomarse con ligereza. Una instrucción sobrecargada puede resultar lenta y demandante de recursos, pero el principal riesgo es la creación de una complicada estructura sintáctica compleja, con parámetros y excepciones. Extender la funcionalidad de EVALUATE para realizar evaluaciones sucesivas nos presenta el problema de controlar el número de evaluaciones a realizara, i.e. si debe hacerse una o varias. Este control se puede establecer mediante parámetros adicionales o condicionantes en la evaluación, que como puede verse es lo que da origen a lo que precisamente queremos evitar.
Una solución alternativa y que parece mucho más prometedora la ofrece la operación POP. Revisemos el caso de POP. Ésta es la que empleamos para extraer objetos de la pila y desplegarlos en pantalla. El darle la posibilidad a POP de extraer el contenido de una variable que está siendo referenciada por otra es algo muy atractivo, si además consideramos que una situación en la que nos veríamos forzados a ejecutar varias acciones EVALUATE sucesivamente será cuando deseemos extraer algo para presentarlo en pantalla, nos parecerá mucho más atractivo. Claro que podemos requerir realizar una serie de evaluaciones sucesivas tan solo para recuperar un valor y emplearlo para una operación posterior (suma, multiplicación, operación básica de pila, etcétera). En este caso tendríamos que considerar la sobrecarga de EVALUATE o la creación de alguna otra operación para tal propósito. Si en la práctica se requerirá la realización de evaluaciones sucesivas más frecuentemente por la recuperación de un valor para una instrucción posterior o para su presentación es algo que sólo podrá averiguarse hasta ver las diversas aplicaciones y necesidades para las que será utilizado el lenguaje que está bajo diseño. Por el momento tomaremos la decisión de extender la funcionalidad de POP. Así
|
POP==> |
|
Desplegaríamos en pantalla el valor 5.
Hasta el momento lo que hemos visto ha sido tomando como argumento un objeto inhibido. La funcionalidad de EVALUATE puede extenderse para manejar cadenas también. ¿Por qué requeriríamos manejar cadenas? Pues bien, la necesidad viene de la naturaleza propia de la operación ENTER. Como se recordar´, ésta operación deja como resultado una cadena, si no vamos a definir instrucciones para el ingreso específico de ciertos tipos de datos, necesitamos la posibilidad de transformar una cadena en un valor numérico. Y para tal propósito la instrucción EVALUATE parece ser el candidato ideal, ya que sólo se requiere sobrecargar la funcionalidad de la operación sin extender su sintaxis.
Al ser aplicada a una cadena, la operación EVALUATE devolverá un valor numérico que identificará el tipo de dato más cercanos al que la cadena puede ser convertida. Por ejemplo.
|
EVALUATE==> |
|
Para una lista más detallada de los valores devueltos por esta operación sírvase consultar el apéndice 4.
Esta es la mitad del proceso de transformación, el cual lo haremos en dos etapas y lo que nos deja la posibilidad también de forzar la conversión a ciertos tipos de datos cuando ya se conoce el tipo de dato al que hay que convertirlo o el identificador devuellto no es el más adecuado. Para la segunda etapa del proceso requeriremos forzosamente de una operación adicional. Esta operación será etiquetada con la palabra TRANSFORM.
TRANSFORM tomará una cadena y un identificador de tipo de datos para llevar a acabo la conversión. Terminando así el ejemplo iniciado antes:
|
TRANSFORM==> |
|
TRANSFORM y su concepto pueden ser usados y extender la utilidad de la operación. Por ejemplo, ¿qué pasaría si el argumento con el que se toparía la operación no fuera una cadena? ¿Debería marcarse error? ¿Tendría sentido llevar a cabo la transformación de un objeto a otro? Después de todo, ¿no es esto lo que estamos haciendo con las cadenas? La respuesta a todo este cuestionamiento debiera ser sí. En otros lenguajes de programación existe lo que se conoce como casting, la tranformación de un tipo de dato específico a otro. Podemos usar muy bien a TRANSFORM para tal propósito. Por ejemplo, la transformación de un real a un entero (que al mismo tiempo sirve como función de truncamiento) se vería:
|
TRANSFORM==> |
|
Hasta el momento nos hemos dedicado a la enumeración de diversos elementos del lenguaje. Un último aspecto que deberemos considerar es la unión de todos estos elementos y su control. En esta sección describiremos precisamente los elementos adicionales que requeriremos para formar estructuras que delimiten y definan de forma precisa las diversas secciones de un programa y el flujo en éste.
El concepto de un bloque de instrucciones ya fue establecido con anterioridad al hablar de la definición de nombres para procedimientos. Los caracteres [ y ] nos permiten agrupar instrucciones para identificar una sección de código y así crear rutinas que son identificadas e invocadas a través de nombre. Una rutina o grupo de rutinas por sí mismas no constituyen necesariamente un programa. En Stop un programa es una colección de módulos; una rutina o grupo de rutinas forman un módulo.
La historia del desarrollo de software ha demostrado que la modularización es un concepto que permite atacar la complejidad de un proyecto y su posterior mantenimiento de forma simple y rápida; un concepto entre muchos otros desarrollados por la ingeniería de software. La implementación en Stop del concepto de modularización es a través de la creación de grupos de rutinas en archivos separados y que son referenciados desde algún otro. Un programa en Stop se forma a partir de la inclusión de los diversos módulos desde uno principal.
Siendo que hasta el momento sólo hemos mencionado de lo que consta un módulo, un mecanismo de control importante es el establecer cual de todas las rutinas que integran a un módulo es lo que deberemos considerar como la rutina principal. Esto es de suma importancia tanto para el módulo principal como para aquellos que son incluidos. Este mecanismo de control se implementará a través del nombre del módulo. El nombre identificará la rutina de acceso, inicio o arranque de un módulo. Para el resto de los módulos que son incluidos, representa la única rutina visible para el módulo que está haciendo la inclusión, cualquier otra rutina permanecerá invisible a todos los demás. Las rutinas sólo serán accesibles dentro del módulo en que son definidas, con lo que implementamos el concepto de ocultamiento de datos y código.
Así la estructura de un módulo se define como:
MODULE Nombre del módulo |
Una inclusión se lleva a cabo usando la palabra INCLUDE seguida de una cadena en la que se da la ruta absoluta o relativa del archivo conteniendo un módulo Stop. Las inclusiones son opcionales y pueden ponerse tantas como se requiera, pero siempre antes de la definición de procedimientos.
Todo lo anterior nos permite establecer la estructura que tendrá un programa en Stop. Esto nos brinda cierto control en cuanto al control del flujo del programa pero no en su totalidad. Para completar este aspecto requerimos de ciertos elementos sintácticos adicionales que nos permitirán construir secciones de código condicionadas en su ejecución así como estructuras repetitivas.
La tres bien conocidas estructuras condicionales de decisión simple, compuesta y múltiple con sus diversas implementaciones se representarán en Stop con las siguientes construcciones. Para la decisión simple tenemos:
IF operación de pila |
Extendiendo la estructura a una decisión compuesta:
IF operación de pila |
La formación de una estructura sintáctica no es la única forma en la que podemos controlar o condicionar eventos en Stop, y lo siguiente es una muestra de como implementar esta idea. Definiremos lo que conoceremos como operador ternario y que denotaremos con el símbolo ?. Como su nombre lo implica, se trata de una operación que aplica sobre el contenido de la pila y que permite discriminar entre su contenido. Este operador toma el valor de la base de la pila para determinar si debe dejar el segundo o tercer nivel, equivalente a una acción IF ... THEN o a una IF ... ELSE, respectivamente. Ambos casos se ilustran a continuación.
|
?==> |
|
para una acción THEN, y
|
?==> |
|
para una acción ELSE.
La decisión múltiple la implementaremos a través de las siguientes construcciones. La primera presenta un comportamiento similar al CASE de Pascal (la primera condición exitosa forza la salida de la estructura):
CHOOSE ONE |
La estructura debe tener al menos una clásula OPTION y al final puede o no aparecer una claúsula ELSE, ésta es la cláusula por defecto (se ejecuta únicamente si ninguna clásula OPTION se ejecutó). Una variante de esta estructura es la que sigue, con las mismas reglas gramaticales ya indicadas pero siendo su comportamiento similar al de la clásula de decisión múltiple de C: se prueban todas las condiciones posibles, excepto la que está por defecto (que será ejecutada únicamente si ninguna otra comparación previa se ha ejecutado).
CHOOSE ALL |
En lo que respecta a las estructuras repetitivas tenemos a las siguientes construcciones. Para las estructuras cíclicas pre y post condicionales, respectivamente:
WHILE operación de pila |
y,
REPEAT operación de pila |
Para la realización de ciclos preestablecidos se cuenta con la construcción:
FOR nombre inhibido |
Para esta última estructura cabe hacer mención que el valor inicial, final e incremento se tomarán de la pila. Todos deben ser introducidos en el orden mencionado.
Finalmente, mencionaremos una de las principales estructuras presente en todo lenguaje de programación: los comentarios. Los comentarios en Stop se pueden colocar en cualquier parte de un módulo en dos formas: en forma de bloque y de línea. La primera forma se logra delimitando los comentarios por un caracter o combinación especial de caracteres, en el caso de Stop usando dos signos continuos de número:
## comentarios ##
La segunda forma consiste en indicar que a partir de un determinado punto en la línea física, lo que continue debe ignorarse. Se ha seleccionado el caracter ";" para tal propósito, así:
... instrucciones ; comentarios fin de línea física
Un comentario puede aparecer en cualquier parte del programa mientras no interrumpa una estructura sintáctica.


| Ultima actualización: . |