MYC编译器源码之词法分析

前文  .NET框架源码解读之MYC编译器 和 MYC编译器源码分析之程序入口 分别讲解了 SSCLI 里示例编译器的架构和程序入口,本文接着分析它的词法分析部分的代码。

词法解析的工作都由Tok类处理,其构造函数接受一个Io对象做文件处理,下面是Tok构造函数的源码:

public Tok(Io ihandle)
{
    io = ihandle;
    // 初始化Token(字符归类)字典
    InitHash();            // initialize the tokens hashtable
    // 读入文件的第一个字符
    io.ReadChar();
    // 逐个扫描文件里的字符,获取
    // 第一个字符归类(Token)
    scan();
}

构造函数中第一个函数调用InitHash的目的是将关键字和操作符解析成更容易识别的字符类型识别号 - Token,这样做的目的是为了便于语法解析器parser处理。例如,对于下面这条C语句:

int foo(int a)

与其让语法解析器去逐个处理单个字符,词法解析器的作用是将去上面一行语句归类成类似下面的格式:

T_INT T_IDENT ‘(‘ T_INT T_IDENT ‘)’

因为T_INT,T_IDENT都是一个整数型常量,而’(‘这样的单个字符也可以当作整数型常量对待,这样语法解析器在分析语法的时候工作会更轻松些。所以在InitHash函数里,其把编程语言里所有的关键字和多字符操作符(如左移赋值操作符 <<=)都设置了类型标识号(Token),在Tok对象的scan()函数扫描源文件时,会逐一在这个字典里查询关键字的标识号:

public void InitHash()
{
    // 为字符类型识别号对照表 – tokens分配空间
    tokens = new Hashtable();
    AddTok(T_LEFT_ASSIGN,    "<<=");
    // ... ...
    AddTok(T_IF,        "if");
    // ... ...
    AddTok(T_STATIC,        "static");
    AddTok(T_INT,        "int");
    // ... ...
}

而对应的每个标识号(Token)的定义,则可以在Tok.cs源文件的最上面找到:

public const int T_LEFT_ASSIGN    = 10001;
// ... ...
public const int T_IF            = 20001;
// ... ...
public const int T_STATIC        = 30002;
// ... ...
public const int T_INT        = 40003;
// ... ...
public const int T_IDENT         = 50001;
public const int T_DIGITS         = 50002;
public const int T_UNKNOWN        = 99999;
public const int T_EOF         = -1;

字符类型识别号对照表初始化完毕后,语法分析器就可以调用Tok对象的scan函数进行语法处理了,scan函数每次只处理并返回一个字符类型:

public void scan()
{
    // 跳过注释、换行符、空格等字符
    skipWhite();
    // 先判断当前读取的字符是不是一个字母
    // 如果是字母开头的话,要么是关键字,
    // 要么就是变量名
    if (Char.IsLetter(io.getNextChar()))
      // 逐个扫描后面的字符,直到识别出关键字
      // 或者变量名为止才退出
      LoadName();
    // 如果当前的字符是 0 - 9的数字
    else if (Char.IsDigit(io.getNextChar()))
      // 扫描完后面的数字并归类
      LoadNum();
    // 如果是操作符,扫描完后面的操作符字符串
    else if (isOp(io.getNextChar()))
      LoadOp();
    // 如果文件已经读取完毕了
    else if (io.EOF())
      {
      // 返回特殊的识别符 T_EOF,表示文件读取完毕
      value = null;
      token_id = T_EOF;
      }
    else
      {
      // 这个字符不是一个合法的字符,归类成T_UNKNOWN
      // T_UNKNOWN没有被任何语法引用
      // 如果语法分析器在扫描语法的过程中
      // 看到这个识别符,很有可能是源码里有语法错误
      value = new StringBuilder(MyC.MAXSTR);
      value.Append(io.getNextChar());
      token_id = T_UNKNOWN;
      io.ReadChar();
      }
    skipWhite();
    // 条件编译,如果是myc.exe是调试版本,则在命令行里
    // 打印出当前识别的字符类型,便于myc.exe的开发者排错
#if DEBUG
    Console.WriteLine("[tok.scan tok=["+this+"]");
#endif
}

scan函数是Tok对象里最核心的函数,它实际上是完成前面myc语法里这些词法规则(还有隐含的关键字和操作符识别):

letter ::= "A-Za-z";
digit ::= "0-9";

name ::= letter { letter | digit };
integer ::= digit { digit };

我们再通过说明LoadName函数来解释词法分析的细节:

void LoadName()
{
  // 缓存读取到的字符
  value = new StringBuilder(MyC.MAXSTR);
  skipWhite();
  // 错误验证 - 确保第一个字符是字母
  if (!Char.IsLetter(io.getNextChar()))
    throw new ApplicationException("?Expected Name");
  // 后面跟着的字符只能是数字或者字母
  while (Char.IsLetterOrDigit(io.getNextChar()))
    {
    // 缓存字符,以便判断是变量名,还是关键字
    value.Append(io.getNextChar());
    // 从源文件里读取下一个字符
    io.ReadChar();
    }
  // 在字符类型识别表里查询读取到的词组是不是关键字
  token_id = lookup_id();
  // 不是关键字的话,那么就是变量名(或函数名)
  if (token_id <= 0)
    token_id = T_IDENT;
  skipWhite();
}

上面基本上就是词法分析的关键代码了,不过在说明的时候,我特意跳过了构造函数的 io.ReadChar()这个函数,这个函数从字面意义上看是读取一个字符,但实际上从源文件一个字符一个字符的读取效率实在是太低了,因此一般都是从源文件里读取一大段字符并缓存在内存里,提高效率:

// Io.cs – ReadChar函数

public void ReadChar()
{
  // 判断是不是读到文件末尾了
  if (_eof)            // if already eof, nothing to do here
    return;
  // 如果缓存还没有实例化,或者缓存里的字符
  // 已经处理完毕了,创建一个新的缓存
  // 对于老的缓存数组,丢给垃圾回收机制处理
  if (ibuf == null || ibufidx >= MyC.MAXBUF)
    {
    ibuf = new char[MyC.MAXBUF];
    _eof = false;
    // 从源文件里读取一大块内容到缓存里
    ibufread = rfile.Read(ibuf, 0, MyC.MAXBUF);
    ibufidx = 0;
    if (buf == null)
      buf = new StringBuilder(MyC.MAXSTR);
    }
  // 从缓存里读取下一个字符
  look = ibuf[ibufidx++];
  // 判断这次读取时,是否已经到源文件末尾了
  if (ibufread < MyC.MAXBUF && ibufidx > ibufread)
    _eof = true;

  /*
   * track the read characters
   */
  // 保存当前读取的字符,以便在生成IL源文件的时候
  // 可以把C源码跟生成的IL源码对应起来
  buf.Append(look);
  // 如果碰到换行,更新行号,行号在报告语法错误
  // 的时候会用到,告知具体语法出错的行号便于
  // 程序员找到错误
  if (look == ‘\n‘)
    bufline++;
}

在Io.ReadChar函数里,会保存读取的C源码,当要生成IL源文件的时候,这个信息用来保存C语句跟IL语句的对应关系,如用下面的命令编译myc里自带的测试源码文件:

效果如下图:

时间: 2024-08-11 03:31:24

MYC编译器源码之词法分析的相关文章

MYC编译器源码分析

前文.NET框架源码解读之MYC编译器讲了MyC编译器的架构,整个编译器是用C#语言写的,上图列出了MyC编译器编译一个C源文件的过程,编译主路径如下: 首先是入口Main函数用来解析命令行参数,读取源文件,并开始编译过程.Main函数在MyC.cs文件,而IO.cs文件主要保存读取源码文件的相关操作.下表是Main函数的源码(批注用注释的方式显示),IO.cs文件用单独的一个小节说明: public static void Main() { try { // 看源码注释,代码是99年写的,也就

MYC编译器源码之语法分析

MyC编译器采用自顶向下的方法进行语法解析,这种语法解析方式,一般是从最左边的Token开始,然后自顶向下看哪一条语法规则可能包含这个Token,如果包含这个Token,则自左向右根据这条语法规则逐一匹配后面的Token.自顶向下的语法解析我会在其他文章中说明,在前文我们已经列出了MyC的语法规则: program ::= ( outer_decl | func_decl ); outer_decl ::= [ class ] type ident { "," ident } &quo

MYC编译器源码之代码生成

前面讲过语法的解析之后,代码生成方面就简单很多了.虽然myc是一个简单的示例编译器,但是它还是在解析的过程中生成了一个小的语法树,这个语法树将会用在生成exe可执行文件和il源码的过程中. 编译器在解析时,使用emit类来产生中间的语法树,语法树的数据结构和操作方法在iasm这个类型里完成,源程序的语法解析完毕后,Exe和Asm两个类分别遍历生成的语法树产生最终的代码. 我们来看几个代码的例子,下表的函数 Parser.program 里,在函数开始和结束的地方分别调用了 prolog 和 ep

方舟编译器源码过一遍流程

不管是被带节奏还是啥,在年初放出方舟编译器的消息后,我真的很期待的,毕竟这是我本科一直很想去的华为编译器部门出品的,并且迫不及待地更新了最新的EMUI,体验一波所谓的方舟编译器.不过目前确实,没看到有啥实质性的.明眼可以看的东西. 跨语言编译的事,有一个比较成熟的graal在做了,其实也不算什么新思想.不过放在移动端,甚至是IoT领域,确实是前无古人. 昨天下载了代码,但是在火车上还没看,今天大致看了一下. 其实该吐槽的别人都吐槽了. 文档啥的确实写得不怎么样,看完文档确实没懂应该怎么做才能跑起

淘宝数据库OceanBase SQL编译器部分 源码阅读--生成逻辑计划

body, td { font-family: tahoma; font-size: 10pt; } 淘宝数据库OceanBase SQL编译器部分 源码阅读--生成逻辑计划 SQL编译解析三部曲分为:构建语法树,生成逻辑计划,指定物理执行计划.第一步骤,在我的上一篇博客淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树里做了介绍,这篇博客主要研究第二步,生成逻辑计划. 一. 什么是逻辑计划?我们已经知道,语法树就是一个树状的结构组织,每个节点代表一种类型的语法含义.如

淘宝数据库OceanBase SQL编译器部分 源码阅读--生成物理查询计划

SQL编译解析三部曲分为:构建语法树,制定逻辑计划,生成物理执行计划.前两个步骤请参见我的博客<<淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树>>和<<淘宝数据库OceanBase SQL编译器部分 源码阅读--生成逻辑计划>>.这篇博客主要研究第三步,生成物理查询计划. 一. 什么是物理查询计划 与之前的阅读方法一致,这篇博客的两个主要问题是what 和how.那么什么是物理查询计划?物理查询计划能够直接执行并返回数据结果数

淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树

OceanBase是 阿里巴巴集团自主研发的可扩展的关系型数据库,实现了跨行跨表的事务,支持数千亿条记录.数百TB数据上的SQL操作.在阿里巴巴集团 下,OceanBase数据库支持了多个重要业务的数据存储,包括收藏夹.直通车报表.天猫评价等.截止到2013年4月份,OceanBase线上业务 的数据量已经超过一千亿条. 看起来挺厉害的,今天我们来研究下它的源代码.关于OceanBase的架构描述有很多文档,这篇笔记也不打算涉及这些东西,只讨论OceanBase的SQL编译部分的代码. Ocea

用VC编译lua源码,生成lua语言的解释器和编译器

用VC编译lua源码,生成lua语言的解释器和编译器 1.去网址下载源码 http://www.lua.org/download.html 2.装一个VC++,我用的是VC6.0 3.接下来我们开始编译源码,我们需要编译: 一个静态库 一个动态库 一个lua语言解释器 一个lua编译器 建立一个工静态库工程 打开VC-->文件-->(点击)新建--(弹出框中选择)工程-->(win32 static library) 创一个空的工程 工程名为luaLib 把lua中所有的源码添加,去掉其

Lua源码编译之CL编译器编译

通过使用VC下的CL编译器,可方便地编译Lua源码,而无需构造工程并设置各种选项: 以下以源码Lua5.3.1版本为例,将通过CL编译选项直接编译源码,为方便编译将采用批处理脚本,脚本放置在Lua解压后的目录下如:Lua.bat,具体内容如下: mkdir bin cd src del *.obj cl /O2 /W3 /c /DLUA_BUILD_AS_DLL *.c del lua.obj luac.obj link /Dll /out:../bin/lua-5.3.1.dll *.obj