interpreter, Linux, Mac, Raspberry Pi

Zbudujmy interpreter (5) – zmienne jeszcze raz

Dotychczas nas interpreter obsługiwał zmienne oznaczane w kodzie jedną literą (w sumie mogliśmy wykorzystać 26 zmiennych – od a do z). Dzisiaj dodamy możliwość używania zmiennych, których identyfikator będzie zawierał więcej niż jeden znak. Wykorzystany tu mechanizmy w kolejnych wpisach użyję do wbudowania stałych (np. PI) i funkcji (np. sin() w moim interpreterze)

Zmienne będziemy przechowywać w następującej strukturze:

struct {
               char *name;
               double val;
               struct Symbol *next;
} Symbol;

a do manipulowania listą będziemy używać dwóch funkcji: add – w celu dodawania zmiennych do listy, find – w celu wyszukania i pobrania wartości zmiennej.

Stwórzmy zatem plik symbol.h

#ifndef SYMBOL_H
#define SYMBOL_H

typedef struct Symbol {
               char *name;
               double val;
               struct Symbol *next;
} Symbol;

Symbol *add(char *s, double d);
Symbol *find(char *s);

#endif

oraz plik symbol.c

#include <string.h>;
#include <stdlib.h>;
#include "symbol.h"

static Symbol *symlist = NULL;

extern void yyerror(char *s);

Symbol *add(char *s, double d)
{
  Symbol *sp;

  if ((sp = (Symbol *) malloc(sizeof(Symbol))) == NULL )
  yyerror("out of memory");

  if ((sp->name = (char *) malloc(strlen(s)+1)) == NULL )
  yyerror("out of memory");

  strcpy(sp->name,s);
  sp->val = d;
  sp->next = symlist;
  symlist = sp;
  return sp;
}

Symbol *find(char *s)
{
  Symbol *sp;

  for (sp = symlist; sp != NULL ; sp = sp->next) {
       if (strcmp(sp->name, s) == 0)
            return sp;
  }
  return NULL;
}

Parser

Zmieńmy plik parser.y tak aby parser odpowiednio korzystał ze zmiennych. Pierwszą zmianą będzie zdefiniowanie unii tak aby przechowywać albo nazwę zmiennej albo wartość wyrażenia.

%union {
double val;
char *sym;
}

Tym samym nie będziemy musieli już korzystać z definicji YYTYPE.

#define YYTYPE double

Drugą zmianą jest odpowiednie wskazanie typów dla poszczególnych tokenów i reguł parsera.

%token <val> NUM
%token <sym> VARIABLE
%type <val> statement expression

Trzecia zmiana dotyczy sposoby zapamiętywania nazw zmiennych i wartości zmiennych (fukcja add) oraz pobieranie tych wartości na potrzeby wyliczenia wyrażenia (funkcja find). Odpowiednie zmiany zostały pokazane poniżej (plik parse.y)

%{
#include <stdio.h>
#include "symbol.h"

int yylex(void);
void yyerror(char *s );
%}

%union {
        double val;
        char *sym;
}

%start expression_list
%token <val> NUM 
%token <sym> VARIABLE
%type  <val> statement expression
%left '-' '+'
%left '*' '/'
%nonassoc UMINUS

%%
expression_list:
        expression_list statement '\n' { printf(" = %f\n", $2); } 
        | expression_list '\n' { /* allow blank lines */ }
        | { /* empty string */ }
;

statement: 
        expression
        | VARIABLE '=' expression { add( $1, $3 ); }
         
expression:
        NUM 
        | VARIABLE { Symbol *sym; sym = find($1); if(!sym) yyerror("Unknown variable"); else $$ = sym-&gt;val; }
        | '-' expression %prec UMINUS { $$ = -$2; }
        | '(' expression ')' { $$=$2; }
        | expression '+' expression { $$ = $1 + $3; }
        | expression '-' expression { $$ = $1 - $3; } 
        | expression '*' expression { $$ = $1 * $3; }
        | expression '/' expression { $$ = $1 / $3; }
;
%%

void yyerror(char *s )
{
    fprintf (stderr, "%s\n", s);
}

int yywrap() { return 1; }

int main (int argc, char **argv) {
        return yyparse();
}

Lekser

Poprawmy teraz plik scan.l tak aby lekser obsługiwał łańcuchy znakowe jako nazwy zmiennych.

%{
#include <stdio.h>
#include "parse.h"

void yyerror(char *s);
%}

%%
[a-zA-Z_][a-zA-Z_0-9]* {
                if ((yylval.sym = (char *) malloc(strlen(yytext)+1)) == NULL )
                yyerror("out of memory");

                strcpy(yylval.sym, yytext) ;
                return VARIABLE ;
        }

[0-9]+ { 
                yylval =  atof (yytext);
                return NUM;
        }

(([0-9]+(\.[0-9]*)?)|([0-9]*\.[0-9]+))        {
                yylval = atof (yytext);
                return NUM;
        }

[-+()=/*\n]     return *yytext;

[ \t] ; /* skip whitespace */

.       yyerror("invalid character");
%%

Makefile

Na tym etapie warto jeszcze zmienić plik Makefile tak aby plik symbol.c brał udział w kompilacji.

LEX     = flex
LFLAGS  =
YACC    = yacc
YFLAGS  = -d
LDFLAGS = -ll
OBJ     = scan.o parse.o symbol.o
SRC     = scan.l parse.y symbol.c

all:    interpreter

interpreter: $(OBJ)
        $(CC) $(LDFLAGS) $(CPPFLAGS) $(CFLAGS) $(OBJ) -o $@

scan.o: parse.c

scan.c: scan.l
        $(LEX) $(LFLAGS) -o $@ $^

parse.c: parse.y
        $(YACC) $(YFLAGS) -o $@ $^

clean:
        $(RM) *.o scan.c parse.c parse.h interpreter
Standard

Dodaj komentarz