Es un gran tema, pero en lugar de ignorarlo con un pomposo "ve a leer un libro, niño", en su lugar, con gusto te daré consejos para ayudarte a entenderlo.
La mayoría de los compiladores y / o intérpretes trabajan así:
Tokenizar : escanee el texto del código y divídalo en una lista de tokens.
Este paso puede ser complicado porque no puede simplemente dividir la cadena en espacios, debe reconocer que if (bar) foo += "a string";
es una lista de 8 tokens: WORD, OPEN_PAREN, WORD, CLOSE_PAREN, WORD, ASIGNMENT_ADD, STRING_LITERAL, TERMINATOR. Como puede ver, simplemente dividir el código fuente en los espacios no funcionará, debe leer cada carácter como una secuencia, por lo que si encuentra un carácter alfanumérico, sigue leyendo los caracteres hasta que toque un carácter no alfanumérico y esa cadena Acabo de leer es una PALABRA para ser clasificada más adelante Puedes decidir por ti mismo cuán granular es tu tokenizer: si se traga "a string"
como un token llamado STRING_LITERAL para analizarlo más adelante, o si ve"a string"
como OPEN_QUOTE, UNPARSED_TEXT, CLOSE_QUOTE, o lo que sea, esta es solo una de las muchas opciones que tiene que decidir por sí mismo mientras lo codifica.
Lex : Entonces ahora tienes una lista de tokens. Probablemente etiquetó algunos tokens con una clasificación ambigua como WORD porque durante la primera pasada no gasta demasiado esfuerzo tratando de descubrir el contexto de cada cadena de caracteres. Así que ahora lea nuevamente su lista de tokens de origen y reclasifique cada uno de los tokens ambiguos con un tipo de token más específico basado en las palabras clave en su idioma. Por lo tanto, tiene una PALABRA como "if" y "if" está en su lista de palabras clave especiales llamadas símbolo IF, por lo que cambia el tipo de símbolo de ese token de WORD a IF, y cualquier WORD que no esté en su lista de palabras clave especiales , como WORD foo, es un IDENTIFICADOR.
Parse : Así que ahora giró if (bar) foo += "a string";
una lista de tokens lexed que se ve así: IF OPEN_PAREN IDENTIFER CLOSE_PAREN IDENTIFIER ASIGN_ADD STRING_LITERAL TERMINATOR. El paso es reconocer secuencias de tokens como declaraciones. Esto está analizando. Lo haces usando una gramática como:
DECLARACIÓN: = ASIGN_EXPRESSION | IF_STATEMENT
IF_STATEMENT: = IF, PAREN_EXPRESSION, STATEMENT
ASIGN_EXPRESSION: = IDENTIFICADOR, ASIGN_OP, VALOR
PAREN_EXPRESSSION: = OPEN_PAREN, VALUE, CLOSE_PAREN
VALOR: = IDENTIFICADOR | STRING_LITERAL | PAREN_EXPRESSION
ASIGN_OP: = IGUAL | ASIGN_ADD | ASIGN_SUBTRACT | ASIGN_MULT
Las producciones que usan "|" entre términos significa "coincidir con cualquiera de estos", si hay comas entre términos significa "coincidir con esta secuencia de términos"
¿Cómo usas esto? Comenzando con el primer token, intente hacer coincidir su secuencia de tokens con estas producciones. Entonces, primero intenta hacer coincidir su lista de tokens con STATEMENT, así que lee la regla para STATEMENT y dice "una STATEMENT es ASIGN_EXPRESSION o IF_STATEMENT", por lo que intenta hacer coincidir ASIGN_EXPRESSION primero, así que busca la regla gramatical de ASIGN_EXPRESSION y dice "ASIGN_EXPRESSION es un IDENTIFICADOR seguido de un ASIGN_OP seguido de un VALOR, por lo que busca la regla gramatical para IDENTIFICADOR y ve que no hay un gramatical para IDENTIFICADOR, lo que significa que IDENTIFICADOR es un" terminal ", lo que significa que no requiere más análisis para que coincida para que pueda intentar hacerlo directamente con su token, pero su primer token de origen es un IF, y IF no es lo mismo que un IDENTIFICADOR, por lo que falló la coincidencia. ¿Ahora que? Vuelve a la regla STATEMENT e intenta hacer coincidir el siguiente término: IF_STATEMENT. Busca IF_STATEMENT, comienza con IF, busca IF, IF es un terminal, compara el terminal con tu primer token, si el token coincide, es increíble, el próximo término es PAREN_EXPRESSION, busca PAREN_EXPRESSION, no es un terminal, cuál es el primer término, PAREN_EXPRESSION comienza con OPEN_PAREN, busca OPEN_PAREN, es una terminal, combina OPEN_PAREN con tu próximo token, coincide, .... y así sucesivamente.
La forma más fácil de abordar este paso es tener una función llamada parse () a la que le pasa el token de código fuente que está tratando de hacer coincidir y el término gramatical con el que está tratando de hacerlo coincidir. Si el término de gramática no es una terminal, entonces recurre: llama a parse () nuevamente y le pasa el mismo token de origen y el primer término de esta regla de gramática. Es por eso que se llama un "analizador de descenso recursivo". La función parse () devuelve (o modifica) su posición actual en la lectura de los tokens de origen, esencialmente devuelve el último token en la secuencia coincidente, y continúa la siguiente llamada a analizar () desde allí.
Cada vez que parse () coincide con una producción como ASIGN_EXPRESSION, crea una estructura que representa ese fragmento de código. Esta estructura contiene referencias a los tokens de origen originales. Empiezas a construir una lista de estas estructuras. Llamaremos a toda esta estructura el Árbol de sintaxis abstracta (AST)
Compilar y / o ejecutar : Para ciertas producciones en su gramática, ha creado funciones de controlador que, si se les da una estructura AST, compilarían o ejecutarían esa porción de AST.
Así que echemos un vistazo a la parte de su AST que tiene el tipo ASIGN_ADD. Entonces, como intérprete, tiene una función ASIGN_ADD_execute (). Esta función se pasa como parte del AST que corresponde al árbol de análisis, por foo += "a string"
lo que esta función mira esa estructura y sabe que el primer término en la estructura debe ser un IDENTIFICADOR, y el segundo término es el VALOR, por lo que ASIGN_ADD_execute () pasa el término VALOR a una función VALOR_eval () que devuelve un objeto que representa el valor evaluado en la memoria, luego ASIGN_ADD_execute () realiza una búsqueda de "foo" en la tabla de variables y almacena una referencia a lo que devuelve eval_value () función.
Eso es un intérprete. En cambio, un compilador tendría funciones de controlador que traducen el AST en código de bytes o código de máquina en lugar de ejecutarlo.
Los pasos 1 a 3, y algunos 4, se pueden facilitar con herramientas como Flex y Bison. (también conocido como Lex y Yacc), pero escribir un intérprete desde cero es probablemente el ejercicio más enriquecedor que cualquier programador podría lograr. Todos los demás desafíos de programación parecen triviales después de la cumbre de este.
Mi consejo es comenzar poco a poco: un lenguaje pequeño, con una gramática pequeña, e intente analizar y ejecutar algunas declaraciones simples, luego crezca a partir de ahí.
¡Lee esto y buena suerte!
http://www.iro.umontreal.ca/~felipe/IFT2030-Automne2002/Complements/tinyc.c
http://en.wikipedia.org/wiki/Recursive_descent_parser
lex
,yacc
ybison
.