REPL para intérprete usando Flex / Bison

9

Eu escrevi um interpretador para uma linguagem semelhante ao C, usando Flex e Bison para o analisador / analisador. Está funcionando bem ao executar arquivos de programa completos.

Agora estou tentando implementar um REPL no interpretador para uso interativo. Eu quero que ele funcione como os intérpretes de linha de comando em Ruby ou ML:

  1. Mostrar uma solicitação
  2. Aceite uma ou mais instruções na linha
  3. Se a expressão estiver incompleta
    1. exibir um prompt de continuação
    2. permite que o usuário continue inserindo linhas
  4. Quando a linha termina com uma expressão completa
    1. ecoa o resultado da avaliação da última expressão
    2. mostre o prompt principal

Minha gramática começa com uma produção de top_level , que representa uma única instrução no idioma. O lexer é configurado para o modo interativo no stdin. Eu estou usando o mesmo scanner e gramática nos modos full-file e REPL, porque não há diferença semântica nas duas interfaces.

Meu loop de avaliação principal é estruturado assim.

while (!interpreter.done) {
    if (interpreter.repl)
        printf(prompt);
    int status = yyparse(interpreter);
    if (status) {
        if (interpreter.error)
            report_error(interpreter);
    }
    else {
        if (interpreter.repl)
            puts(interpreter.result);
    }
}            

Isso funciona bem, exceto pela lógica de prompt e eco. Se o usuário inserir várias instruções em uma linha, esse loop imprimirá prompts e expressões supérfluos. E se a expressão continuar em várias linhas, este código não imprime os prompts de continuação. Esses problemas ocorrem porque a granularidade da lógica de prompt / echo é uma instrução top_level na gramática, mas a lógica de leitura de linha é profunda na lexer.

Qual é a melhor maneira de reestruturar o ciclo de avaliação para lidar com o prompt e o eco do REPL? Isso é:

  • como posso exibir um prompt por linha
  • como posso exibir o prompt de continuação no momento certo
  • como posso saber quando uma expressão completa é a última em uma linha

(Prefiro não alterar o idioma do scanner para passar tokens de nova linha, já que isso alteraria severamente a gramática. Seria bom modificar o YY_INPUT e adicionar algumas ações à gramática do Bison. estoque Flex 2.5.35 e Bison 2.3 que acompanham o Xcode.)

    
por Jay Lieske 09.07.2011 в 20:56
fonte

2 respostas

5

Depois de observar como linguagens como Python e SML / NJ lidam com seus REPLs, obtive um bom trabalho no meu interpretador. Em vez de ter a lógica de prompt / eco no loop do driver do analisador mais externo, eu o coloco na rotina de entrada do lexer mais interna. Ações no analisador e no lexer definem sinalizadores que controlam a solicitação pela rotina de entrada.

Estou usando um scanner reentrante, então yyextra contém o estado passado entre as camadas do interpretador. Parece mais ou menos assim:

typedef struct Interpreter {
    char* ps1; // prompt to start statement
    char* ps2; // prompt to continue statement
    char* echo; // result of last statement to display
    BOOL eof; // set by the EOF action in the parser
    char* error; // set by the error action in the parser
    BOOL completeLine // managed by yyread
    BOOL atStart; // true before scanner sees printable chars on line
    // ... and various other fields needed by the interpreter
} Interpreter;

A rotina de entrada do lexer:

size_t yyread(FILE* file, char* buf, size_t max, Interpreter* interpreter)
{
    // Interactive input is signaled by yyin==NULL.
    if (file == NULL) {
        if (interpreter->completeLine) {
            if (interpreter->atStart && interpreter->echo != NULL) {
                fputs(interpreter->echo, stdout);
                fputs("\n", stdout);
                free(interpreter->echo);
                interpreter->echo = NULL;
            }
            fputs(interpreter->atStart ? interpreter->ps1 : interpreter->ps2, stdout);
            fflush(stdout);
        }

        char ibuf[max+1]; // fgets needs an extra byte for 
while (!interpreter->eof) {
    interpreter->atStart = YES;
    int status = yyparse(interpreter);
    if (status) {
        if (interpreter->error)
            report_error(interpreter);
    }
    else {
        exec_statement(interpreter);
        if (interactive)
            interpreter->echo = result_string(interpreter);
    }
}
size_t len = 0; if (fgets(ibuf, max+1, stdin)) { len = strlen(ibuf); memcpy(buf, ibuf, len); // Show the prompt next time if we've read a full line. interpreter->completeLine = (ibuf[len-1] == '\n'); } else if (ferror(stdin)) { // TODO: propagate error value } return len; } else { // not interactive size_t len = fread(buf, 1, max, file); if (len == 0 && ferror(file)) { // TODO: propagate error value } return len; } }

O loop do interpretador de nível superior se torna:

%option extra-type="Interpreter*"

#define YY_INPUT(buf, result, max_size) result = yyread(yyin, buf, max_size, yyextra)

#define YY_USER_ACTION  if (!isspace(*yytext)) { yyextra->atStart = NO; }

O arquivo Flex obtém essas novas definições:

typedef struct Interpreter {
    char* ps1; // prompt to start statement
    char* ps2; // prompt to continue statement
    char* echo; // result of last statement to display
    BOOL eof; // set by the EOF action in the parser
    char* error; // set by the error action in the parser
    BOOL completeLine // managed by yyread
    BOOL atStart; // true before scanner sees printable chars on line
    // ... and various other fields needed by the interpreter
} Interpreter;

O YY_USER_ACTION lida com a interação complicada entre os tokens na gramática da linguagem e nas linhas de entrada. Minha linguagem é como C e ML em que um caractere especial (';') é necessário para terminar uma declaração. No fluxo de entrada, esse caractere pode ser seguido por um caractere de nova linha para sinalizar fim de linha ou pode ser seguido por caracteres que fazem parte de uma nova instrução. A rotina de entrada precisa mostrar o prompt principal se os únicos caracteres verificados desde a última instrução final forem novas linhas ou outros espaços em branco; caso contrário, deve mostrar o prompt de continuação.

    
por Jay Lieske 14.08.2011 / 20:20
fonte
0

Eu também estou trabalhando em tal intérprete, eu não cheguei ao ponto de fazer um REPL ainda, então minha discussão pode ser um pouco vaga.

É aceitável se for dada uma seqüência de instruções em uma única linha, apenas o resultado da última expressão é impresso? Porque você pode refatorar sua regra gramatical de nível superior da seguinte forma:

top_level = instrução top_level | declaração;

A saída do seu top_level poderia ser uma lista vinculada de declarações, e o interpreter.result seria a avaliação da cauda dessa lista.

    
por wmercer 11.07.2011 / 00:36
fonte