É um tópico importante, mas, em vez de escová-lo com um pomposo "vá ler um livro, garoto", em vez disso, darei com prazer dicas para ajudá-lo a entender o assunto.
A maioria dos compiladores e / ou intérpretes funciona assim:
Tokenize : digitalize o texto do código e divida-o em uma lista de tokens.
Esta etapa pode ser complicada porque você não pode simplesmente dividir a cadeia de caracteres em espaços, é necessário reconhecer que if (bar) foo += "a string";
há uma lista de 8 tokens: WORD, OPEN_PAREN, WORD, CLOSE_PAREN, WORD, ASIGNMENT_ADD, STRING_LITERAL, TERMINATOR. Como você pode ver, simplesmente dividir o código-fonte em espaços não funcionará, você deverá ler cada caractere como uma sequência; portanto, se encontrar um caractere alfanumérico, continue lendo os caracteres até encontrar um caractere não alfanumérico e a sequência que você acabou de ler é uma PALAVRA para ser posteriormente classificada posteriormente. Você pode decidir por si mesmo qual é a granularidade do seu tokenizador: se ele engole "a string"
como um token chamado STRING_LITERAL para ser analisado posteriormente mais tarde ou se vê"a string"
como OPEN_QUOTE, UNPARSED_TEXT, CLOSE_QUOTE ou qualquer outra coisa, essa é apenas uma das muitas opções que você tem que decidir por si mesma enquanto a codifica.
Lex : Então agora você tem uma lista de tokens. Você provavelmente marcou alguns tokens com uma classificação ambígua como WORD, porque durante a primeira passagem não gasta muito esforço tentando descobrir o contexto de cada sequência de caracteres. Agora, leia a lista de tokens de origem novamente e reclassifique cada um dos tokens ambíguos com um tipo de token mais específico, com base nas palavras-chave em seu idioma. Portanto, você tem uma PALAVRA como "se" e "se" está na sua lista de palavras-chave especiais chamada símbolo SE, para alterar o tipo de símbolo desse token de WORD para IF e qualquer PALAVRA que não esteja na sua lista de palavras-chave especiais , como WORD foo, é um IDENTIFICADOR.
Analisar : agora você transformou if (bar) foo += "a string";
uma lista de tokens lexed parecidos com este: SE OPEN_PAREN IDENTIFER CLOSE_PAREN IDENTIFIER ASIGN_ADD STRING_LITERAL TERMINATOR. A etapa é reconhecer sequências de tokens como instruções. Isso está analisando. Você faz isso usando uma gramática como:
DECLARAÇÃO: = ASIGN_EXPRESSION | IF_STATEMENT
IF_STATEMENT: = SE, PAREN_EXPRESSION, STATEMENT
ASIGN_EXPRESSION: = IDENTIFICADOR, ASIGN_OP, VALUE
PAREN_EXPRESSSION: = OPEN_PAREN, VALUE, CLOSE_PAREN
VALOR: = IDENTIFICADOR | STRING_LITERAL | PAREN_EXPRESSION
ASIGN_OP: = EQUAL | ASIGN_ADD ASIGN_SUBTRACT | ASIGN_MULT
As produções que usam "|" entre termos significa "corresponder a qualquer um destes"; se houver vírgulas entre termos, significa "corresponder a esta sequência de termos"
Como você usa isso? Começando com o primeiro token, tente combinar sua sequência de tokens com essas produções. Então, primeiro você tenta combinar sua lista de tokens com STATEMENT, para ler a regra para STATEMENT e ela diz "uma STATEMENT é ASIGN_EXPRESSION ou IF_STATEMENT", para tentar corresponder ASIGN_EXPRESSION primeiro, e procurar a regra gramatical para ASIGN_EXPRESSION e ele diz "ASIGN_EXPRESSION é um IDENTIFIER seguido por um ASIGN_OP seguido por um VALUE, portanto, você consulta a regra gramatical para IDENTIFIER e vê que não há erros gramaticais para IDENTIFIER, o que significa que o IDENTIFIER é um" terminal ", o que significa que não requer mais análise para combiná-lo para que você possa tentar combiná-lo diretamente com seu token, mas seu primeiro token de origem é um IF e IF não é o mesmo que um IDENTIFIER; portanto, a correspondência falhou. E agora? Você volta à regra STATEMENT e tenta corresponder ao próximo termo: IF_STATEMENT. Você pesquisa IF_STATEMENT, começa com IF, pesquisa IF, IF é um terminal, compare o terminal com seu primeiro token, combina com o token IF, impressionante, o próximo termo é PAREN_EXPRESSION, pesquisa PAREN_EXPRESSION, não é um terminal, qual é o primeiro termo, PAREN_EXPRESSION começa com OPEN_PAREN, pesquisa OPEN_PAREN, é um terminal, corresponde a OPEN_PAREN ao seu próximo token, corresponde a .... e assim por diante.
A maneira mais fácil de abordar esta etapa é ter uma função chamada parse (), a qual você transmite o token do código-fonte que está tentando corresponder e o termo gramatical com o qual está tentando combiná-la. Se o termo gramatical não for um terminal, você deverá recursar: você chama parse () novamente passando o mesmo token de origem e o primeiro termo dessa regra gramatical. É por isso que é chamado de "analisador de descida recursiva". A função parse () retorna (ou modifica) sua posição atual na leitura dos tokens de origem, essencialmente repassa o último token na sequência correspondente e você continua a próxima chamada para parse () a partir daí.
Cada vez que parse () corresponde a uma produção como ASIGN_EXPRESSION, você cria uma estrutura que representa esse trecho de código. Essa estrutura contém referências aos tokens de origem originais. Você começa a criar uma lista dessas estruturas. Vamos chamar toda essa estrutura de Árvore de Sintaxe Abstrata (AST)
Compilar e / ou Executar : Para determinadas produções de sua gramática, você criou funções de manipulador que, se recebidas uma estrutura AST, compilariam ou executariam esse pedaço de AST.
Então, vejamos a parte do seu AST que possui o tipo ASIGN_ADD. Portanto, como intérprete, você tem uma função ASIGN_ADD_execute (). Essa função é passada como parte do AST que corresponde à árvore de análise foo += "a string"
, portanto, essa função olha para essa estrutura e sabe que o primeiro termo na estrutura deve ser um IDENTIFIER e o segundo termo é o VALUE, então ASIGN_ADD_execute () passa o termo VALUE para uma função VALUE_eval () que retorna um objeto que representa o valor avaliado na memória, em seguida, ASIGN_ADD_execute () faz uma pesquisa de "foo" na tabela de variáveis e armazena uma referência ao que foi retornado pelo eval_value () função.
Isso é intérprete. Em vez disso, um compilador teria funções de manipulador que convertem o AST em código de bytes ou código de máquina, em vez de executá-lo.
Os passos 1 a 3, e alguns 4, podem ser facilitados usando ferramentas como Flex e Bison. (aka. Lex e Yacc), mas escrever um intérprete a partir do zero é provavelmente o exercício mais poderoso que qualquer programador pode realizar. Todos os outros desafios de programação parecem triviais após a cimeira deste.
Meu conselho é começar pequeno: uma linguagem minúscula, com uma gramática minúscula, e tentar analisar e executar algumas instruções simples e depois crescer a partir daí.
Leia isso e boa sorte!
http://www.iro.umontreal.ca/~felipe/IFT2030-Automne2002/Complements/tinyc.c
http://en.wikipedia.org/wiki/Recursive_descent_parser
lex
,yacc
ebison
.