编译原理——如何将字符流转换为 token 流

前言:好吧,开始对编译器下手了。。。本系列博客大部分内容来自 http://www.craftinginterpreters.com/,以下只是我的学习笔记。

0X00 对原理的讨论

将字符流转换成 token 流是「编译原理」最开始的部分,把这一块的代码叫做「词法分析器」

首先说什么叫做 token:

像这样的每一块都是一个 token

在我要实现的解释器中,每一个 token 有四个属性他们分别是:typelexemeliteralline

先说简单的三个:typelexemeline

type 是 token 的类型,token 的类型一共有:

public enum TokenType {
    // Single-character tokens.
    LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR,

    // One or two character tokens.
    BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL,

    // Literals.
    IDENTIFIER, STRING, NUMBER,

    // Keywords.
    AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR, PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,
    
    EOF
}

lexeme 用来保存这个 token 的字符串,比如 var 这个 token。它的 lexeme 就是 "var"

line 是 token 所在的行,用来报错。

最后说 literal

由于我们使用 Java 实现我们的脚本语言,所以对于有些 token 是必须把字符串解释成 Java 中的 Object。

比如:"lox" 就要解释成 Java 中的 String。

最后回到本质上,词法分析器的本质任务就是将字符串,归类成一个又一个的 token。接下来我们来说说如何将这些字符串进行识别!

0X01 识别特殊符号 token

编程中少不了的就是这些「特殊符号」:+ - * / 之类的,首先将这些特殊符号分类成单字符,和双字符的:

  • 单字符的特殊符号识别
switch (c) {
        case ' ':
            break;
        case '\r':
            break;
        case '\t':
            break;
        case '\n':
            line++;
            break;
        case '(':
            addToken(LEFT_PAREN);
            break;
        case ')':
            addToken(RIGHT_PAREN);
            break;
        case '{':
            addToken(LEFT_BRACE);
            break;
        case '}':
            addToken(RIGHT_BRACE);
            break;
        case ',':
            addToken(COMMA);
            break;
        case '.':
            addToken(DOT);
            break;
        case '-':
            addToken(MINUS);
            break;
        case '+':
            addToken(PLUS);
            break;
        case ';':
            addToken(SEMICOLON);
            break;
        case '*':
            addToken(STAR);
            break;
  • 双字符的特殊符号识别

由于有些双字符,的第一位是单字符的特殊符号,所以有一些特殊的操作:

case '!':
            addToken(match('=') ? BANG_EQUAL : BANG);
            break;
        case '=':
            addToken(match('=') ? EQUAL_EQUAL : EQUAL);
            break;
        case '<':
            addToken(match('=') ? LESS_EQUAL : LESS);
            break;
        case '>':
            addToken(match('=') ? GREATER_EQUAL : GREATER);

match 是为了匹配下一个字符

由于注释的第一个字符是 /,可能是除法也可能是注释的第一个 /。如果是除法,我们添加这个 token,如果是注释,我们把整行注释都消耗掉。

 case '/':
            if (match('/')) {
                // A comment goes until the end of the line.
                while (peek() != '\n' && !isAtEnd())
                    advance();
            } else {
                addToken(SLASH);
            }
            break;

0X02 识别字符串 token

在要实现的解释器中,我们规定字符串是以 " 开头,以 " 结尾,所以一旦发现了 " 就要小心了。

  • 如果识别了一个 \n 那么行数要增加!
  • 到了文件或者命令末尾还不能识别到另一个 " 就要报错了!
private void string() {
        while (peek() != '"' && !isAtEnd()) {
            if (peek() == '\n')
                line++;
            advance();
        }

        // Unterminated string.
        if (isAtEnd()) {
            BookLox.error(line, "Unterminated string.");
            return;
        }

        // The closing ".
        advance();

        // Trim the surrounding quotes.
        String value = source.substring(start + 1, current - 1);
        addToken(STRING, value);
    }

0X03 识别数字 token

识别数字一定要放在识别标识符的前面,因为标识符中也可能出现数字。

数字无非是 123、或者 123.123,但是数字也有可能写错:123.,也就是小数点后面必须有数字,最后的识别代码:

 private void number() {
        while (isDigit(peek()))
            advance();

        // Look for a fractional part.
        if (peek() == '.' && isDigit(peekNext())) {
            // Consume the "."
            advance();
            while (isDigit(peek()))
                advance();
        }

        addToken(NUMBER, Double.parseDouble(source.substring(start, current)));
    }

0X04 识别标识符 token 与 关键字 token

由于我们先识别的是数字,所以标识符第一个字符肯定不是数字。

所以一旦遇到了字母,我们就把他当成标识符的开头,不断获取新的字符,直到拿到整个标识符(数字+字母)

这里的字母除了 a~z 和 A~Z 还有一个字符 _,所以写成:

    private boolean isAlpha(char c) {
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
    }
private void identifier() {
        while (isAlphaNumeric(peek()))
            advance();

        String text = source.substring(start, current);

        TokenType type = keywords.get(text);
        if (type == null)
            type = IDENTIFIER;
        addToken(type);
    }

拿到整个标识符以后,我们还要注意,他可能是「关键字」,这是我们预设好的关键字:

static {
        keywords = new HashMap<>();
        keywords.put("and", AND);
        keywords.put("class", CLASS);
        keywords.put("else", ELSE);
        keywords.put("false", FALSE);
        keywords.put("for", FOR);
        keywords.put("fun", FUN);
        keywords.put("if", IF);
        keywords.put("nil", NIL);
        keywords.put("or", OR);
        keywords.put("print", PRINT);
        keywords.put("return", RETURN);
        keywords.put("super", SUPER);
        keywords.put("this", THIS);
        keywords.put("true", TRUE);
        keywords.put("var", VAR);
        keywords.put("while", WHILE);
    }

最后看这个 Hash 表里面有没有这个字符串,如果有这个字符串,我们就把它识别出来。

最后详细代码:https://github.com/TensShinet/toy_compiler/blob/master/code/MyLox/src/app/MyLox.java

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,498评论 0 5
  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,848评论 0 10
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,949评论 0 38
  • 允许的修饰符 有些修饰符能在所有允许的地方出现, 但并非所有的都这样. 通常, 影响 regex 编译的修饰符(...
    焉知非鱼阅读 1,432评论 0 1
  • 一:感恩清晨的阳光冉冉升起,让万物得到阳光的照耀显得格外的有活力。 二:感恩启慧教育训练营老师 班长每天辛勤的付出...
    cebfc69192b8阅读 168评论 0 2