实现脚本解释器 - 词法分析器

本系列介绍

笔者最近正学习编译原理,为了将理论变为实践,所以创作了本系列来记录学习过程中的思考与问题,注意文章中为了理论上描述方便增加了自创的术语。

本系列使用 Java 语言来实现一个脚本解释器,该脚本语言命名为 Foo,其语法参考 JavaScript 语言,本系列代码地址 Github

词法分析器介绍

词法分析器的作用是将输入的字符串转变为一个个的记号(token),记号是由记号名(name)和属性值(value)构成的二元组(unit doublet)。

通过构造有限自动机(finite automata, FA)来识别字符串是否为匹配某种规则(模式),编译原理书中用正规式来描述这种规则,但其描述性不强且不能描述匹配对,故本文统一采用扩展的巴斯克范式(ABNF),具体语法参考 RFC5234

当有限自动机匹配或不匹配输入串会执行不同的动作,具体实现时是匹配则返回对应的记号或者忽略该字符串(例如注释)否则报词法错误,而有限自动机往往通过一段子程序(函数)来实现,将这些子程序组合起来就构成了词法分析器(lexer)。

基本的准备

首先需要编写一个记号类,其包含了记号名和属性值,由于属性值会被赋予不同的类型,所以使用 Object 类型,类中的常量来表示不同的记号名。

public class Token {
    public static final String TOKEN_EOF = "<eof>";
    // omit other token constants

    private private String name = TOKEN_EOF;
    private Object value = null;

    // getters and setters
}

接下来就可以来编写 Lexer 词法分析器类,先抛弃其他一些细节来分析下面定义的两个私有属性和两个个私有方法的作用。其中属性 currentChar 用来存放当前读取的字符,而 nextChar 则是存放下一个字符 。

方法 char readChar() 用来读取下一个字符,当返回 -1 时表明读取完毕,其重载方法 char readChar(int offset) 用来指定偏移多少位置后读取字符,从 0 开始且 0 相当于调用了该方法的无参重载。

public class Lexer {
    private char currentChar = '\0';
    private char nextChar = '\0';

    private char readChar() {
        // ...
    }
    private char readChar(int offset) {
        // ...
    }
}

分析字符串流程

接下来定义 Lexer 类的公有方法 Token nextToken() 来读取一个记号,它分析字符串的流程如下:

  1. currentChar 存放当前需要匹配的字符,若读取到文件末尾则返回 EOF 记号。
  2. 根据匹配的单字符或双字符,调用确定的子过程。
  3. 子过程匹配完毕,读取下一个字符,并返回相对应的记号或者跳转回步骤 1 。

注意若是代码较短,则这里的子过程并不一定需要写成函数。

匹配前缀与匹配状态

整个词法分析器其实就是个不确定的有限自动机(NFA),开始时并不知道匹配何种记号,这里称之为 不确定匹配状态 。通过单个或多个字符就能确定匹配何种记号并可以调用子过程,这时进入了 确定匹配状态,而子过程就是个确定的有限自动机(DFA),称这些字符或字符序列为 匹配前缀

记号可以分为以下几类,这些记号根据匹配前缀可以分为需要双字符和只需单字符确定,双字符确定的记号只有注释和双字符符号,其他都为单字符确定的,这也是为什么前面需要声明 nextChar 变量存放下一个字符。其中的标识符包含了保留字,而符号分为运算符及界符。

  • 注释
  • 空白符号
  • 换行
  • 标识符
  • 数字
  • 字符串
  • 双字符符号
  • 单字符符号
  • 终止记号

消除歧义

有些情况下,单字符确定的匹配会影响双字符确定的匹配,为了消除这种歧义,就需要先进行双字符匹配再进行单字符匹配。

例如单行注释以双字符 // 作为匹配前缀,而单字符符号除号 / 会影响该双字符确定的匹配,若是将单字符确定的匹配放前面,则会匹配成两个除号记号。

匹配换行

在不同的系统中,文件的换行有以下三种:

  • CRLF Windows
  • LF Linux
  • CR Unix

为了兼容考虑,匹配换行具体代码如下所示:

if (currentChar == '\r' || currentChar == '\n') {
    newLine();
    continue;
}

private void newLine() {
    nextChar = readChar();
    if (nextChar == '\n') {
        currentChar = readChar();
    } else {
        currentChar = nextChar;
        nextChar = '\0';
    }
}

待续

原文地址:https://www.cnblogs.com/linzhehuang/p/10850279.html

时间: 2024-11-05 22:08:03

实现脚本解释器 - 词法分析器的相关文章

Ch/CINT — C/C++语言脚本解释器

1.CH Ch是一个跨平台的C/C++脚本解释器,它支持ISO的C语言标准(C90/C99),C++,附带了8000多个函数库并支持众多的工业标准,支持POSIX, socket/Winsock, X11/Motif, OpenGL, ODBC, C LAPACK, GTK+, Win32, XML, 和CGI等等.Ch具有MATLAB的高级数值计算和绘图的功能,且具有良好的交互性,在C/C++语言编程学习方面尤为好用(试试就知道了).Ch也是一个可嵌入的脚本引擎,可以无缝地嵌入到自己的程序中.

Basic脚本解释器移植到STM32

本文来自http://blog.csdn.net/hellogv/ .引用必须注明出处! 上次讲了LUA移植到STM32.这次讲讲Basic脚本解释器移植到STM32. 在STM32上跑Basic脚本,相同能够跟穿戴设备结合.也能够作为刚開始学习的人学习MCU的入门工具,当然前提是有人做好Basic的STM32交互实现.这里使用的是uBasic开源脚本解释器(http://dunkels.com/adam/ubasic/),只是uBasic不支持完整的Basic算法,所以用起来略费心,假设有好的

Shell脚本开发规范

1)开头指定脚本解释器 #!/bin/sh ;#!/bin/bash 2)开头加版本及版权信息 3)脚本中不用中文注释 4)文件以.sh扩展名保存 5)成对的内容一次性写全,防止泄露 6)[ ]中括号两端要有空格 7)流程控制语句一次性写完,再添加语句 提示:你检查脚本明明没有问题,但是就是执行出现莫名其妙的错误,要想到执行dos2unix命令 好习惯:每次执行脚本都dos2unix下 shell调试脚本方法: 1)shell调试之echo关键命令 2)shell调试之bash参数 sh -n

shell脚本复习

最近公司工作量很小,就复习复习,看起了马哥的视频,感觉马哥讲课讲得特别细.这才是深入到系统的讲解,补充了很多我之前只是了解到皮毛的东西. shell编程:弱类型编程语言 强:变量在使用前,必须事先声明,甚至还需要初始化 NULL: 弱:变量用时声明,甚至不区分类型: 变量赋值:VAR_NAME=VALUE 编译器,解释器 编程语言:机器语言.汇编语言.高级语言 静态语言:编译型语言 强类型(变量) 关键字: 事先转换成可执行格式 C.  C++.JAVA.C# 动态语言:解释型语言 on the

Shell脚本编程30分钟入门

什么是Shell脚本 示例 看个例子吧: #!/bin/sh cd ~ mkdir shell_tut cd shell_tut for ((i=0; i<10; i++)); do touch test_$i.txt done 示例解释 第1行:指定脚本解释器,这里是用/bin/sh做解释器的 第2行:切换到当前用户的home目录 第3行:创建一个目录shell_tut 第4行:切换到shell_tut目录 第5行:循环条件,一共循环10次 第6行:创建一个test_1…10.txt文件 第7

Java中使用Lua脚本语言(转)

Lua是一个实用的脚本语言,相对于Python来说,比较小巧,但它功能并不逊色,特别是在游戏开发中非常实用(WoW采用的就是Lua作为脚本的).Lua在C\C++的实现我就不多说了,网上随便一搜,到处都是这方面的介绍,我想说的是如何在Java下使用Lua以提高编程效率.增强你的程序可扩展性. 首先,要在Java上使用Lua脚本,必须有关于Lua脚本解释器以及Java程序可以访问这些脚本的相关API,即相关类库.我使用的是一个叫做LuaJava的开源项目,可以在: http://www.keple

使用EJS脚本将字符串转换成Base64编码

此博客为9925.org的镜像,登录9925.org可以查看到最新博文. 原文出处:http://ily.so/VVfyim 由于EJS脚本解释器是完美支持 ECMA-262 标准的,因此EJS脚本是JavaScript的真超集. 说了一段废话后,我其实是想说我在网上copy了一段JS支持的将字符串转换成Base64编码的代码,不知出处,这里斗胆将代码贴出,如有侵权请联系删除. var Base64 = {     // 转码表     table : [             'A', 'B

在EJS脚本内使用“#include”预编译指令

此博客为9925.org的镜像,登录9925.org可以查看到最新博文. 原文出处:http://ily.so/26bMBz 预编译指令是Easton JavaScript脚本解释器对JavaScript语言拓展的重要功能之一,使用预编译指令可以引用外部的JS脚本代码,类似于HTML内的<script>标签引用外部JS脚本. #include语法解释 以“#include”指令开始,一行一个指令,指令后面加不加空格都无所谓,但是为了方便阅读通常情况下都加一个空格. 例如: //引用运行库内的A

shell脚本--(符号和语法)

shell脚本 shell脚本:一直都在用,但是没有系统的学习过,只是在linux命令行模式敲一些简单的常用的指令,现在系统的学习一番. 一.创建shell脚本,一般以.sh结尾(linux下后缀只是种显示,没有任何具体含义)文件创建后需要chmod 0755 name 给它加上权限,之后就可以用./运行了 进入文件需要在最顶部加上shell脚本解释器,这个文件中的所有命令都会被bash拿到去解释.下面会说到shell的工作模式. 二.shell的工作模式: 我是这样理解的当我们在脚本中写这样一