REPL para intérprete usando Flex / Bison

He escrito un intérprete para un lenguaje similar a C, usando Flex y Bison para el escáner / analizador. Está funcionando bien al ejecutar archivos de progtwig completos.

Ahora estoy intentando implementar una REPL en el intérprete para uso interactivo. Quiero que funcione como los intérpretes de línea de comandos en Ruby o ML:

  1. Mostrar un aviso
  2. Acepte una o más declaraciones en la línea.
  3. Si la expresión es incompleta
    1. mostrar un aviso de continuación
    2. Permitir al usuario continuar ingresando lineas.
  4. Cuando la línea termina con una expresión completa.
    1. Echo el resultado de evaluar la última expresión.
    2. mostrar el indicador principal

Mi gramática comienza con una producción de top_level , que representa una sola statement en el idioma. El lexer está configurado para el modo interactivo en stdin. Estoy usando el mismo escáner y la gramática en los modos de archivo completo y REPL, porque no hay diferencia semántica en las dos interfaces.

Mi principal ciclo de evaluación está estructurado así.

 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); } } 

Esto funciona bien a excepción de la lógica de solicitud y eco. Si el usuario ingresa varias declaraciones en una línea, este bucle imprime indicaciones y expresiones superfluas. Y si la expresión continúa en varias líneas, este código no imprime avisos de continuación. Estos problemas se producen porque la granularidad de la lógica de solicitud / eco es una statement de nivel top_level en la gramática, pero la lógica de lectura de línea es profunda en el lexer.

¿Cuál es la mejor manera de reestructurar el ciclo de evaluación para manejar las indicaciones y ecos de REPL? Es decir:

  • ¿Cómo puedo mostrar un mensaje por línea?
  • ¿Cómo puedo mostrar el mensaje de continuación en el momento adecuado?
  • ¿Cómo puedo saber cuándo una expresión completa es la última en una línea?

(Preferiría no cambiar el lenguaje del escáner para pasar tokens de nueva línea, ya que eso alterará gravemente la gramática. Modificar YY_INPUT y agregar algunas acciones a la gramática de Bison estaría bien. Además, estoy usando el stock Flex 2.5.35 y Bison 2.3 que se envían con Xcode.)

Después de ver cómo lenguajes como Python y SML / NJ manejan sus REPL, obtuve uno muy bueno trabajando en mi intérprete. En lugar de tener la lógica de solicitud / eco en el bucle del controlador del analizador más externo, lo puse en la rutina de entrada más interna del lexer. Las acciones en el analizador y el lexer establecen indicadores que controlan la solicitud mediante la rutina de entrada.

Estoy usando un escáner de reentrada, por lo que yyextra contiene el estado pasado entre las capas del intérprete. Se ve más o menos así:

 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; 

La rutina de entrada 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 \0 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; } } 

El bucle de intérprete de nivel superior se convierte en:

 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); } } 

El archivo Flex obtiene estas nuevas definiciones:

 %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; } 

YY_USER_ACTION maneja la complicada interacción entre tokens en la gramática del lenguaje y las líneas de entrada. Mi lenguaje es como C y ML, ya que se requiere un carácter especial (‘;’) para finalizar una statement. En el flujo de entrada, ese carácter puede ir seguido de un carácter de nueva línea para indicar el final de la línea, o puede ir seguido de caracteres que forman parte de una nueva statement. La rutina de entrada debe mostrar el indicador principal si los únicos caracteres escaneados desde el último final de la statement son nuevas líneas u otros espacios en blanco; De lo contrario, debería mostrar el mensaje de continuación.

Yo también estoy trabajando en un intérprete de este tipo, todavía no he llegado al punto de hacer un REPL, por lo que mi discusión podría ser algo vaga.

¿Es aceptable si se da una secuencia de declaraciones en una sola línea, solo se imprime el resultado de la última expresión? Porque puedes re-factorizar tu regla gtwigtical de nivel superior así:

top_level = statement top_level | statement

La salida de su top_level podría ser una lista enlazada de declaraciones, y interpreter.result sería la evaluación de la cola de esta lista.