Lv.0 词法分析器

我们假设你已经对 Lisp 有了基本的了解。如果没有,请读一下 前置知识 中的内容。

为了减少大家的工作量,我们提供了 脚手架open in new window。所谓脚手架,也就是代码框架,或者说代码模板之类的。我们提供了针对主流操作系统、主流 IDE 以及 CMake/Xmake 等构建工具的脚手架生成。请前往 这个页面open in new window,按照个人实际和意愿生成并下载脚手架代码。

你下载后会得到一个压缩包,解压后就可以看到这些文件:

  • README.md:脚手架使用说明文件,必读。
  • mini-lisp.slnxmake.luaCMakeLists.txt:这是定义项目相关参数用的,一般情况下不用手动编辑。
  • *.cpp *.h 我们提供的部分代码。
  • rjsj_test.hpp 是我们提供的“测试框架”,在阶段性检查时使用。
  • 以及一系列其它配置文件。

根据 README.md 的提示完成 IDE、构建工具等开发环境的配置后,你应该能正常运行这个项目了。它的运行效果是:

>>> (+ 1 2)                 ; 你在 >>> 后面输入了 (+ 1 2)
(LEFT_PAREN)
(IDENTIFIER +)
(NUMERIC_LITERAL 1.000000)
(NUMERIC_LITERAL 2.000000)
(RIGHT_PAREN)
>>> 

TIP

令输入 EOF 以结束程序。在 Windows 上,按 Ctrl+Z 后按回车;在 macOS 上,按 Ctrl+D。

目前提供给你的这份代码就是一个简单的 Mini-Lisp 词法分析器(Tokenizer)。所谓词法分析,就是将一个字符串,分割为若干个小单元;比如将 (+ 1 2) 分割成 ( + 1 2 ) 这五个单元。这样的单元称为 词法标记(Token)。

在 Mini-Lisp 的标准规范中,一共有如下几种词法标记;它们定义在 token.h 的枚举 TokenType 中:

enum class TokenType {
    LEFT_PAREN,      // 左括号 (
    RIGHT_PAREN,     // 右括号 )
    QUOTE,           // 单引号 '
    QUASIQUOTE,      // 反引号 `
    UNQUOTE,         // 逗号   ,
    DOT,             // 点     .
    BOOLEAN_LITERAL, // 布尔字面量 #f 或 #t
    NUMERIC_LITERAL, // 数型字面量如 42
    STRING_LITERAL,  // 字符串字面量如 "Hello"
    IDENTIFIER,      // 标识符(变量名)如 +
};

WARNING

在 Mini-Lisp 规范中,点(DOT .)不是特殊的词法标记,而是标识符的一种。我们为了简化后续讨论,使用了目前的这种分类。

其中反引号和逗号在入门教程中没有提及,也不是重点,目前可以跳过。其它的词法标记我们或多或少都能理解;比如 #t 就代表一个含义为真值的布尔值等等。

总之,我们提供了一个 Tokenizer 类,它的静态方法 Tokenizer::tokenize 接受一个字符串作为待分析的文本,返回一个 std::deque<TokenPtr>,即解析好的词法标记序列。

class Tokenizer {
public:
    static std::deque<TokenPtr> tokenize(const std::string& input);
};

这里遇到了一个类型 TokenPtr。它是 std::unique_ptr<Token> 的别名。而 Token 则是一个多态类型,它拥有如 BooleanLiteralTokenStringLiteralToken 等子类。所有的 Token 都实现了 toString 方法,用来输出观察。

任务 0.1 阅读代码

接下来,请你阅读 token.htokenizer.hmain.cpp,搞清楚目前程序的大概结构。如果你感兴趣的话,也可以读一读 tokenizer.cpp 或者其它文件,彻底理解目前程序的工作原理。

试回答以下问题:

  1. 为什么 Tokenizer::tokenize 接受的形参类型是 const std::string& 而不是 std::string?可不可以用 std::string&
  2. 为什么使用 TokenPtr,也即 std::unique_ptr<Token>?如果改用 Token*,会有什么影响?
  3. main 函数中 std::cout << *token 是如何实现的?
  4. 当词法分析出现错误时,程序是如何进行错误处理的?
  5. * 使用 std::deque<TokenPtr> 相比 std::vector<TokenPtr> 的好处是什么?

在你的工作文件夹下新建 lv0-answer.txt,将以上问题你的解答与思考写下。

作为 Level 0,本部分没有代码编写的工作。但是如果你认为你已经理解了目前的程序的话,那么这是一个非常好的信号——让我们开始敲代码吧!

阶段性检查

在教学网 大作业 / Lv.0 检查 处,提交 lv0-answer.txt。在 2024 年 5 月 5 日 23:59:59 前提交的,可能获得分数加成。

Last Updated:
Contributors: Guyutongxue