¿Cómo definen las funciones los lenguajes de programación?


28

¿Cómo definen y guardan los lenguajes de programación funciones / métodos? Estoy creando un lenguaje de programación interpretado en Ruby, y estoy tratando de descubrir cómo implementar la declaración de función.

Mi primera idea es guardar el contenido de la declaración en un mapa. Por ejemplo, si hice algo como

def a() {
    callSomething();
    x += 5;
}

Luego agregaría una entrada en mi mapa:

{
    'a' => 'callSomething(); x += 5;'
}

El problema con esto es que se volvería recursivo, porque tendría que llamar a mi parsemétodo en la cadena, que luego volvería a llamar parsecuando se encontrara doSomething, y finalmente me quedaría sin espacio de pila.

Entonces, ¿cómo manejan esto los idiomas interpretados?


Ah, y esta es mi primera publicación en Programmers.SE, así que infórmeme si estoy haciendo algo mal o si está fuera de tema. :)
Pomo de la puerta el

En el pasado, los he almacenado todos en línea dentro de mis tokens y las llamadas a funciones son solo saltos a un desplazamiento específico (al igual que las etiquetas en el ensamblaje). ¿Estás tokenizando el guión? ¿O analizar cadenas cada vez?
Simon Whitehead

@SimonWhitehead Dividí la cadena en tokens y luego analicé cada token por separado.
Pomo de la puerta

3
Si es nuevo en el diseño y la implementación del lenguaje de programación, es posible que desee consultar parte de la literatura sobre el tema. El más popular es el "Libro del Dragón": en.wikipedia.org/wiki/… , pero hay otros textos más concisos que también son muy buenos. Por ejemplo, la implementación de lenguajes de programación de Aarne Ranta se puede obtener de forma gratuita aquí: bit.ly/15CF6gC .
evilcandybag

1
@ddyer Gracias! Busqué en Google un intérprete de lisp en diferentes idiomas y eso realmente ayudó. :)
Pomo de la puerta

Respuestas:


31

¿Sería correcto asumir que su función "analizar" no solo analiza el código sino que también lo ejecuta al mismo tiempo? Si desea hacerlo de esa manera, en lugar de almacenar el contenido de una función en su mapa, almacene la ubicación de la función.

Pero hay una mejor manera. Requiere un poco más de esfuerzo por adelantado, pero produce resultados mucho mejores a medida que aumenta la complejidad: use un árbol de sintaxis abstracta.

La idea básica es que solo analiza el código una vez, nunca. Luego tiene un conjunto de tipos de datos que representan operaciones y valores, y crea un árbol de ellos, así:

def a() {
    callSomething();
    x += 5;
}

se convierte en:

Function Definition: [
   Name: a
   ParamList: []
   Code:[
      Call Operation: [
         Routine: callSomething
         ParamList: []
      ]
      Increment Operation: [
         Operand: x
         Value: 5
      ]
   ]
]

(Esto es solo una representación de texto de la estructura de un AST hipotético. El árbol real probablemente no estaría en forma de texto). De todos modos, analiza su código en un AST, y luego ejecuta su intérprete directamente sobre el AST, o use un segundo pase ("generación de código") para convertir el AST en alguna forma de salida.

En el caso de su idioma, lo que probablemente haría es tener un mapa que asigne nombres de funciones a AST de funciones, en lugar de nombres de funciones a cadenas de funciones.


Está bien, pero el problema sigue ahí: usa la recursividad. Eventualmente me quedaré sin espacio de pila si hago esto.
Pomo de la puerta

3
@Doorknob: ¿Qué usa la recursión, específicamente? Cualquier lenguaje de programación estructurado en bloques (que es cada lenguaje moderno en un nivel más alto que ASM) está inherentemente basado en un árbol y, por lo tanto, es de naturaleza recursiva. ¿Qué aspecto específico le preocupa obtener desbordamientos de pila?
Mason Wheeler

1
@Doorknob: Sí, esa es una propiedad inherente de cualquier idioma, incluso si está compilada en código máquina. (La pila de llamadas es una manifestación de este comportamiento). De hecho, soy un colaborador de un sistema de script que funciona de la manera que describí. Únase a mí en el chat en chat.stackexchange.com/rooms/10470/… y analizaré algunas técnicas para una interpretación eficiente y minimizar el impacto en el tamaño de la pila con usted. :)
Mason Wheeler

2
@Doorknob: Aquí no hay ningún problema de recursión porque la llamada a la función en el AST hace referencia a la función por su nombre , no necesita una referencia a la función real . Si estaba compilando el código de la máquina, eventualmente necesitaría la dirección de la función, razón por la cual la mayoría de los compiladores realizan múltiples pases. Si desea tener un compilador de una pasada , necesita "declaraciones de reenvío" de todas las funciones para que el compilador pueda asignar direcciones de antemano. Los compiladores de código de bytes ni siquiera se molestan con esto, el jitter maneja la búsqueda de nombres.
Aaronaught

55
@Doorknob: De hecho, es recursivo. Y sí, si su pila solo tiene 16 entradas, no podrá analizar (((((((((((((((( x ))))))))))))))))). En realidad, las pilas pueden ser mucho más grandes, y la complejidad gramatical del código real es bastante limitada. Ciertamente si ese código tiene que ser legible por humanos.
MSalters

4

No deberías llamar a parse al ver callSomething()(supongo que quisiste decir en callSomethinglugar de doSomething). La diferencia entre ay callSomethinges que uno es una definición de método mientras que el otro es una llamada a método.

Cuando vea una nueva definición, querrá hacer verificaciones relacionadas para asegurarse de que puede agregar esa definición, por lo tanto:

  • Compruebe si la función ya no existe con la misma firma
  • Asegúrese de que la declaración del método se realice en el ámbito adecuado (es decir, ¿se pueden declarar los métodos dentro de otras declaraciones de métodos?)

Suponiendo que estas verificaciones pasan, puede agregarlo a su mapa y comenzar a verificar el contenido de ese método.

Cuando encuentre una llamada de método como callSomething(), debe realizar las siguientes comprobaciones:

  • ¿Existe callSomethingen su mapa?
  • ¿Se llama correctamente (el número de argumentos coincide con la firma que ha encontrado)?
  • ¿Son válidos los argumentos (si se usan nombres de variables, ¿se declaran? ¿Se puede acceder a ellos en este ámbito?)?
  • ¿Se puede llamar a SomeSomething desde donde estás (es privado, público, protegido?)?

Si encuentra que callSomething()está bien, entonces, en este punto, lo que desearía hacer realmente depende de cómo desee abordarlo. Estrictamente hablando, una vez que sepa que tal llamada está bien en este momento, solo puede guardar el nombre del método y los argumentos sin entrar en más detalles. Cuando ejecute su programa, invocará el método con los argumentos que debería tener en tiempo de ejecución.

Si desea ir más allá, puede guardar no solo la cadena sino también un enlace al método real. Esto sería más eficiente, pero si tiene que administrar la memoria, puede ser confuso. Te recomendaría que simplemente te aferres a la cuerda al principio. Más tarde puedes intentar optimizar.

Tenga en cuenta que todo esto supone que ha eliminado su programa, lo que significa que ha reconocido todos los tokens en su programa y sabe cuáles son . Eso no quiere decir que sepa si tienen sentido juntos todavía, que es la fase de análisis. Si aún no sabe cuáles son los tokens, le sugiero que primero se concentre en obtener esa información primero.

¡Espero que eso ayude! ¡Bienvenido a Programmers SE!


2

Al leer tu publicación, noté dos preguntas en tu pregunta. El más importante es cómo analizar. Hay muchos tipos de programas de análisis (por ejemplo recursiva analizador de descenso , LR analizadores , de Packrat analizadores ) y generadores de analizadores sintácticos (por ejemplo GNU bisontes , antlr ) se puede utilizar para atravesar un programa textual "recursiva" dada una gramática (explícito o implícito).

La segunda pregunta es sobre el formato de almacenamiento para funciones. Cuando no está haciendo una traducción dirigida por la sintaxis , crea una representación intermedia de su programa, que puede ser un árbol de sintaxis abstracta , o algún lenguaje intermedio personalizado, para poder seguir procesándolo (compilar, transformar, ejecutar, escribir en un archivo, etc.)


1

Desde un punto de vista genérico, la definición de una función es poco más que una etiqueta o marcador en el código. La mayoría de los demás operadores de bucle, alcance y condicional son similares; son sustitutos de un comando básico de "salto" o "goto" en los niveles inferiores de abstracción. Una llamada de función básicamente se reduce a los siguientes comandos de computadora de bajo nivel:

  • Concatene los datos de todos los parámetros, más un puntero a la siguiente instrucción de la función actual, en una estructura conocida como "marco de pila de llamadas".
  • Empuje este marco en la pila de llamadas.
  • Salte al desplazamiento de memoria de la primera línea del código de la función.

Una declaración de "devolución" o similar hará lo siguiente:

  • Cargue el valor que se devolverá en un registro.
  • Cargue el puntero a la persona que llama en un registro.
  • Pop el marco de la pila actual.
  • Salta al puntero de la persona que llama.

Por lo tanto, las funciones son simplemente abstracciones en una especificación de lenguaje de nivel superior, que permiten a los humanos organizar el código de una manera más fácil de mantener e intuitiva. Cuando se compila en un lenguaje ensamblador o intermedio (JIL, MSIL, ILX), y definitivamente cuando se procesa como código de máquina, casi todas esas abstracciones desaparecen.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.