自制简易解释器


title: 自制简易解释器
date: 2019-02-18 22:00:01
origin: 自制简易解释器


自制简易解释器

用C语言写了一个简单的动态语言解释器, 代码放在了github上面:hedegehog. 编译, 运行可以看看github上的readme.

先简单介绍下这门语言. hedgehog 的多数设计和 python 比较相似, 无需声明变量类型, if,for等语句没有块级作用域. 语法上又有点像 go 语言: if, for不需要(), 但是后面的代码块都必须加{}; 没有while, 不过有for condition {}来替代. 不过行尾必须加;这点和 go 不同. 大多数设计都是为了简化实现方式, 比如必须加{}, ;是为了简化语法的解析.

已实现的功能

  • 数据类型

    a = 10;//int
    b = 3.14;//float
    c = true;//boolean
    d = null;//null
    s = "Hello, World!";//string
    
  • 控制语句

      a = 10;
      if a > 10 { // `()` is not necessary.
          b = a+20;
      } elsif a==10 {
          b = a+10;
      } else {
          b = a-10;
      }
      print(b);
      // block has no local environment, 
      // so 'b' is a global variable.
    
  • 循环

      for i=0; i<10; i=i+1 {
          print(i);
          if i>=4 {break;}
      }
      i = 0;
      for i<10 {
          if i<5 {continue;}
          print(i);
      }
    
  • 函数
    function也被看作一种值(基本数据类型), 不过目前还没有对它实现垃圾回收, 所以直接以函数赋值或者其他操作会出现内存错误.

      // 模仿python首页的函数:)
      func fbi(n) {
          a, b = 0, 1;
          for a<n {
              print(a);
              a, b = b, a+b;//支持这种赋值方式
          }
      }
      fbi(100);
    
      func factorial(n) {
          if n==0 {return 1;}
          return n*factorial(n-1);
      }
      print(factorial(5));
    

    目前只实现了一个原生函数print. print接收一个基本数据类型作为参数, 输出并换行, 或者无参数, 直接换行.

  • 运算符
    大多数与c保持一致, 除了&, |. 因为没有提供位运算的功能, 所以直接用这两个符号表示逻辑与和逻辑或.

    b = 2;
    a = 10;
    if a>20 & b<10 {
        print("`b` is less than 10 and `a` is greater than 20");
    }
    if a>20 | b<10 {
        print("`b` is less than 10 or `a` is greater than 20");
    }
    

概述

image

首先词法分析和语法分析, 构建分析树. 这里以a=1+2*3+fn();为例, 介绍一下表达式分析树的构建.

首先, 它是一个赋值表达式, 把右边的值1+2*3+fn()赋给左边的变量a. 而1+2*3+fn() 又由加法表达式, 乘法表达式, 函数调用表达式构成. yacc中编写的规则会约束表达式构建的顺序, 构建过程大概是这样的:

a = 1 + 2 * 3 + fn()
identifier = value + value * value + function_call // 词法分析
identifier = value + value * value + value // function_call 约归为value
identifier = value + multiply_expression + value // 根据规则, 先生成乘法表达式
identifier = add_expression + value // 把前两项归约为加法表达式
identifier = add_expression // 继续归约(加法运算遵循从左到右)
identifier = expression // 归约为更一般的普通表达式
assgin_expression // 匹配到赋值表达式的规则, 归约为赋值表达式
expression // 赋值表达式归约为一般表达式

然后就可以构建了下面的分析树了:

image

求值的时候从最底层依次往上求值, 就能得到表达式的值.

语法与词法分析

这部分直接使用lex做词法分析, yacc做语法分析. 这两个工具在大多数UNIX上都有预装, GUN提供的版本分别叫bison, flex. 直接用bison生成的文件可能和yacc有些区别(需要修改生成文件的的文件名, 或者改c语言文件中包含的头文件名), 不过在Linux下安装了bison可以直接使用yacc命令. flex 与 lex 生成文件没有区别.

yacc与lex网上有大量的资料, 而且使用比较简单, 这里就仅作简单的介绍.

lex可以使用正则表达式做匹配:

"func" return FUNCTION;//func 函数定义关键字
[A-Za-z_][A-Za-z0-9_]* {
    //辨识符匹配
    yylval.identifier = initString(yytext);
    return IDENTIFIER;
}

这里的FUNCTIONIDENTIFIER被称为token, 一般在yacc的文件中定义. 在生成的C语言文件中token用枚举变量表示.

lex词法分析的结果会交给yacc处理. yacc使用类似BNF的规范来编写规则.

// 加法表达式由乘法表达式归约, 这样可以限制乘法(除法, 取模)优先于
//加法(减法)运算.
ADD_EXPRESSION:
    MUL_EXPRESSION
    |
    ADD_EXPRESSION ADD MUL_EXPRESSION {
        $$ = initBinaryExpression(ADD_OPERATOR, $1, $3);
    }
    |
    ADD_EXPRESSION SUB MUL_EXPRESSION {
        $$ = initBinaryExpression(SUB_OPERATOR, $1, $3);
    }
    ;

MUL_EXPRESSION:
    UNARY_EXPRESSION//单个单目运算表达式直接归约到乘法表达式
    |
    MUL_EXPRESSION MUL UNARY_EXPRESSION {
        $$ = initBinaryExpression(MUL_OPERATOR, $1, $3);
    }
    |
    MUL_EXPRESSION DIV UNARY_EXPRESSION {
        $$ = initBinaryExpression(DIV_OPERATOR, $1, $3);
    }
    |
    MUL_EXPRESSION MOD UNARY_EXPRESSION {
        $$ = initBinaryExpression(MOD_OPERATOR, $1, $3);
    }
    ;

词法分析和语法分析属于解释器的前端, 这部分没有自己编写, 主要把精力放在了后端.

表达式与语句

表达式与语句是整个后端最重要的两个模块, 大部分的逻辑都在两者中实现. 这里主要介绍一下表达式, 语句与之类似.
大部分的代码都用C语言实现了面向对象, 这里必须推荐一下刘大的C语言:春节回家过年,我发现只有我没有对象!.

// expression.h
struct ExpressionTag {
    void (*free)(Expression *self);

    Value (*evaluate)(Expression *self, Environment *env);

    Expression *pre;
};

这是表达式接口的结构体, freeevaluate是C语言中的函数指针, 定义了所有表达式都应该具备的方法. 这个pre看起来有些突兀, 它其实是为了函数传参和多变量同时赋值时链接表达式使用的. 比如a, b = 1, 2;, 1, 2会分别解析为两个表达式, 通过pre链接. 这样的设计可能不符合面向对象, 不过为了实现链表更加简单, 就暂时这样写了.

所有需要释放内存的结构体都有free函数指针, 所以可以定义一个简单的宏#define del(x) x->free(x), 使用del(obj)就可以释放内存了.

下面以赋值表达式介绍一下赋值表达式的实现过程.

// expression.h
void *initAssignExpression(String *id, Expression *expression) 

向外提供的唯一接口是initAssignExpression, 也就是说所有一般的表达式在模块外引用时都会被向上转型为Expression, 只有freeevaluate两个方法.

// expression.c
typedef struct {
    Expression base;//base必须放在第一个, 保证类型转换时的正确性.
    String *id;
    Expression *expression;
} AssignExpression;

static Value evaluateAssignExpression(Expression *_self, Environment *env) {
    AssignExpression *self = (AssignExpression *) _self;//向下转型为 `AssignExpression`
    Value value = self->expression->evaluate(self->expression, env);
    // 字符串采用引用计数
    on_self(self->id, refer);
    //把变量加到environment, 后文会介绍
    env->addVariable(env, initVariable(self->id, value));
    return value;
}

static void freeAssignExpression(Expression *_self) {
    AssignExpression *self = (AssignExpression *) _self;
    del(self->expression);
    on_self(self->id, release);
    free(self);
}

//绑定函数
const static Expression AssignExpressionBase = {freeAssignExpression,evaluateAssignExpression};

void *initAssignExpression(String *id, Expression *expression) {
    AssignExpression *exp = malloc(sizeof(AssignExpression));
    exp->expression = expression;
    //给base赋值, 函数的绑定在new的时候完成.
    exp->base = AssignExpressionBase;
    exp->id = id;
    return exp;
}

AssignExpression的结构体和除init外方法都在.c文件中实现, 并且标记为static, 从而就实现了封装. 在init中实现函数的绑定, 以Expression引用的时候就能调用相应的方法, 这就实现了多态.

其他的表达式根据具体的功能如法炮制.

语句的实现也是类似:

struct StatementTag {
    StatementResult (*execute)(Statement *self, Environment *env);

    void (*free)(Statement *self);

    Statement *next;
};

解释器与运行环境

Environment

struct EnvironmentTag {
    void (*addVariable)(Environment *self, Variable *var);

    Variable *(*findVariable)(Environment *self, String *id);

    void (*free)(Environment *self);

    VariableTrie *trie;
};

void *initEnvironment();

运行环境主要负责变量和函数的保存, 查找. Global environment保存所有的全局变量和函数. 函数有独立于 global environment的local environment. for, if等语句块没有environment, 它们使用所在函数或者全局的environment.

Environment中的trie是字典树, 负责记录变量名和函数名.

Interpreter

typedef struct InterpreterTag {
    Environment *globalEnv;
    StatementList *list;

    void (*free)(struct InterpreterTag *);

    void (*compile)(struct InterpreterTag *, FILE *);

    void (*interpret)(struct InterpreterTag *);
} Interpreter;

Interpreter *initInterpreter();

Interpreter *getCurrentInterpreter();

Interpreter 中compile实现分析树的构建, interpret实现语句的执行. 因为全局只需要一个 interpreter, 所以别的地方可以通过getCurrentInterpreter获取当前interpreter.

总结

整个解释器的大概构成就是这样. 目前只实现了一些简单的功能, 数组, 字典, 垃圾回收(目前只对字符串做了引用计数回收), 文件IO等特性都还没有写. 而且完全没有优化, 运行效率极低. 不过写这个解释器的时候还是收获不少: 深入学习了C语言, 对编译原理有了大概的了解, 更加深刻地理解了面向对象...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342