Graduation Project——词法分析器

语言处理器的第一个组成部分是词法分析器(lexer),也叫scanner。程序的源代码最初是一长串字符串。从内部来看,源代码中的换行也能用专门的(不可见)换行符表示。所以这一长串代码会首先被处理为一个一个的token,也成为token流。

token流

譬如下面这一行代码:
while i<10{
词法分析器会这样处理:
"while" "i" "<" "10" "{"
这样的5个字符串,被称为token。词法分析器将筛选出程序的解释与执行必须的成分。单词之间的空白和注释会被忽略。譬如
while i<10{ //judge if i >10
这行的处理结果与前面是相同的,都是"while" "i" "<" "10" "{"

实际的单词是Token类的子类的对象,Token对象除了记录该单词对应的字符串,还会保留单词的类型、单词所处位置的行号等信息。Stone语言含有标识符、整型字面量和字符串字面量这三种类型的单词,每种单词都定义了对应Token类的子类。每种子类都覆盖了父类Token类的isIndentifier、isNumber、isString方法,并根据具体类型返回相应的值。
此外Stone语言还定义了一个特别的单词Token.EOF(end of file)来表示程序的结束。Token.EOL(end of line)表示程序的换行。不过它是一个String对象,也就是说,只是一个单纯的字符串。

通过正则表达式定义单词

要设计词法分析器,首先要考虑每一种类型的单词的定义,规定怎样的字符串才能构成一个单词,这里最重要的是不能有歧义,某个特定的字符串只能是某种特定类型的单词。举例来讲,要是字符串123h既能被解释为标识符,又能被解释为整型字面量,之后的处理就会相当麻烦。这种单词的定义方式是不可取的。

package stone;

public abstract class Token {
    public static final Token EOF=new Token(-1){};//EOF 表示文件终结
    public static final String EOL="\\n";//EOL 标识一行终结
    private int lineNumber;

    protected Token(int line){
        this.lineNumber=line;
    }
    public int getLineNumber(){return lineNumber;}
    public boolean isIdentifier(){return false;}//是否是标识符
    public boolean isNumber(){return false;}//是否是数字字面量
    public boolean isString(){return false;}//是否是字符串字面量

    public String getText(){return "";}//具体的字符串

}
package stone;

public class StoneExcetion extends RuntimeException{
    private static final long serialVersionUID = *;

    public StoneExcetion(String s) {
        super(s);
    }
    public StoneExcetion(String msg,ASTree t){
        super(msg+" "+t.location());
    }
}

标识符是指变量名、函数名或者类名等名称。此外+、-等运算符号,以及括号等标点符号也属于标识符。标点符号和保留字有时候被归位另一种类型的单词,不过Stone语言在实现的时候,没有对他们加以区分。
整型字面量就是譬如123、1256等字符序列。
字符串字面量就是一串用于表示字符串的字符序列。

不过,我们希望以一种形式化的语言来进行描述,以便计算机自动进行处理。正则表达式就是一个理想的选择。正则表达式不用赘述,它有2种基本要素:

  • 表达式ε,表示。
  • 对于字符集合中的任意字符a,表达式a表示仅有一个字符a的语言,即{a}。

正则表达式有3种基本运算:

  • 2个正则表达式的,记作X|Y。比如a|b所得的语言就是{a, b}。
  • 2个正则表达式的连接,记作XY。比如令X = a|b,Y = c|d,那么XY所表示的语言就是{ac, bc, ad, bd}。
  • 一个正则表达式的克林闭包,记作X*,表示分别将零个,一个,两个……无穷个X与自己连接。也就是说X* = ε | X | XX | XXX | XXX | ……。

以上三种运算写在一起时克林闭包的优先级高于连接运算,而连接运算的优先级高于并运算。下面我们用正则表达式来描述一下刚才各个词素的规则。
首先是关键字string,刚才我们描述说它是“正好是s-t-r-i-n-g这几个字母按顺序组成”,用正则表达式来表示,那就是s-t-r-i-n-g这几个字母的连接运算,所以写成正则表达是就是string。
先用正则表达式描述“由字母开头”,那就是指,可以是a-z中任意一个字母来开头。这是正则表达式中的并运算:a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z。如果每个正则表达式都这么写,那真是要疯掉了,所以我们引入方括号语法,写在方括号里就表示这些字符的并运算。比如[abc]就表示a|b|c。而a-z一共26个字母我们也简写成a-z,这样,“由字母开头”就可以翻译成正则表达式[a-z]了。接下来我们翻译第二句“后面可以跟零个或多个字母或数字”这句话中的**“零个或多个”**可以翻译成克林闭包运算,最后相信大家都可以写出来,就是[a-z0-9]*

最后,前后两句之间是一个连接运算,因此最后描述标识符“语言”的正则表达式就是[a-z][a-z0-9]*。其中的*运算也意味着“标识符”是一种无穷语言,有无数种可能的标识符。本来就是这样,很好理解对吧?
在这里插入图片描述

Stone语言的标识符包括各类符号,因此下面才是真正完整的表达式,各个模式之间需要通过|连接。
首先来定义整型字面量:(表示0-9的任意数字)
[0-9]+
然后是定义标识符:
[A-Z_a-z][A-Z_a-z0-9]*
这个正则表达式至少需要一个字母、数字或下划线_,且首字符不能是数字,这种表示方式涵盖了常用的名称。根据该定义,对整型字面量和标识符的判断不存在二义性。
因此最后,Stone的语言标识符的真正的完整的正则表达式如下:
[A-Z_a-z][A-Z_a-z0-9]*|==|<=|>=|&&|\|\||\p{Punct}
最后的\p{Punct}表示与任意一个符号字符匹配。
最后需要定义的是字符串字面量,虽然我们可能用不上,但是定义一下也没有坏事。(以后万一有什么功能)。
"(\\"|\\\\|\\n[^"])*

借助java.util.regex设计词法分析器

Lexer类是一个词法分析器,它的构造函数接受一个java.io.Reader对象,它能根据需要逐行读取源代码,供执行词法分析。

package stone;

import stone.Exception.ParseException;
import stone.token.IdToken;
import stone.token.NumToken;
import stone.token.StrToken;
import stone.token.Token;

import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Lexer {
    /*
    \s*((//.*)|([0-9]+)|("(\n|\\\\|\\"|[^"])*")|([A-Z_a-z][A-Z_a-z0-9]*)|==|<=|>=|&&|\|\||\p{Punct})
     \s*                        前导空白符
     (//.*)                     //注释符
     ([0-9]+)                   整形
     ("(\n|\\\\|\\"|[^"])*")    字符串 里面可以有// \"
     ([A-Z_a-z][A-Z_a-z0-9]*)   标识符
     ==
     <=
     >=
     &&
     \|\|                       短路符
     p{Punct}                   POSIX 字符类 表示标点符号
     */

    public static String regexpat =
            "\\s*((//.*)|([0-9]+)|(\"(\\\\\"|\\\\\\\\|\n|[^\"])*\")|([A-Z_a-z][A-Z_a-z0-9]*)|==|<=|>=|&&|\\|\\||\\p{Punct})";

    private Pattern pattern = Pattern.compile(regexpat);
    private ArrayList<Token> queue = new ArrayList<>();
    private boolean hasMore;
    private LineNumberReader reader;

    public Lexer(Reader reader) {
        hasMore = true;
        this.reader = new LineNumberReader(reader);
    }

    public Token read()throws ParseException{
        if (fillQueue(0)){//如果true 意味着queue中还有
            return queue.remove(0);//拿出一个
        }else {
            return Token.EOF;//否则返回文件末
        }
    }
    public Token peek(int i)throws ParseException{
        if (fillQueue(i)){
            return queue.get(i);
        }
        return Token.EOF;
    }

    /**
     * 向queue中填充i个token 如果已经没有可读的了 false
     */
    private boolean fillQueue(int i) throws ParseException {
        while (i>=queue.size()){
            if (hasMore){
                readLine();
            }else {
                return false;
            }
        }
        return true;
    }


    private void readLine() throws ParseException {
        String line;
        try {
            line = reader.readLine();//读取一行
//            System.out.println(line.toString());
        } catch (IOException e) {
            throw new ParseException(e);
        }
        if (line == null) {
            //如果这是最后一行的话
            hasMore = false;
            return;
        }

        int lineNo = reader.getLineNumber();//行号
        Matcher matcher = pattern.matcher(line);//检测匹配
        matcher.useTransparentBounds(true).useAnchoringBounds(false);

        int pos = 0;
        int endPos = line.length();
        //关键逻辑
        while (pos < endPos) {
            matcher.region(pos, endPos);//设定匹配范围
            if (matcher.lookingAt()){//匹配到
                addToken(lineNo, matcher);
                pos = matcher.end();//设定新的起始点
            } else {//有无法匹配的就抛出异常

                throw new ParseException("无法解析此行 " + lineNo);
            }
        }
        queue.add(new IdToken(lineNo, Token.EOL));//解析完一行 增加一个EOL,也就是\n
    }

    private void addToken(int lineNo, Matcher matcher) {
        //matcher.group(0)指的是整个串,1指的是第一个括号,2是第二个括号里的东西,以此类推
        //s*((//.*)|([0-9]+)|("(\n|\\\\|\\"|[^"])*")|([A-Z_a-z][A-Z_a-z0-9]*)|==|<=|>=|&&|\|\||\p{Punct})
        String m=matcher.group(1);
        if (m!=null)//整个匹配有命中
        {
            if (matcher.group(2)==null)//不是注释
            {
                Token token;
                if (matcher.group(3)!=null){//是数字
                    token=new NumToken(lineNo,Integer.parseInt(m));
                }else if (matcher.group(4)!=null){//是字符串
                    token=new StrToken(lineNo,toStringliteral(m));
                }else {
                    token=new IdToken(lineNo,m);
                }
                queue.add(token);
            }
        }

    }

    //正则捕获的是 "wwea"的形式 去掉 "" 同时处理一些转义字符
    private String toStringliteral(String m) {
        StringBuilder sb=new StringBuilder();
        int len=m.length()-2;//去掉""后的长度
        for (int i=1;i<=len;i++){
            char c=m.charAt(i);
            if (c=='\\'&&(i+1)<=len){//有转义字符
                char c2=m.charAt(i+1);
                if (c2=='\\'||c2=='"')//去掉转义的 \\=>\ \"=>"
                {
                    i++;
                    c=m.charAt(i);
                }else  if (c2=='n'){//将字符串\n 变成真正的换行符
                    i++;
                    c='\n';

                }
            }
            sb.append(c);
        }
        return sb.toString();
    }

}

正则表达式保存于regexPat字段。
read和peek是Lexer的两个主要方法,read是逐一获取单词,peek则用于预读,peek(i)将返回read方法即将返回的单词之后的第i个单词,如果i=0,则与read方法相同。
如果单词读取结束,read方法和peek方法豆浆返回Token.EOF。
为什么要有peek方法呢?因为这是语法分析器阶段的的抽象语法树必不可少的方法,用于回溯。
readLine则是从每一行读取单词,由于正则表达式已经事先编译为Pattern对象,所以能够调用matcher方法来获得一个用于实际检查匹配的Matcher对象。

运行:

package stone;

import stone.token.Token;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;

public class LexerRunner {
    public static void main(String[] args) throws FileNotFoundException {
        File code = new File("data/lex.stone");
        Lexer lexer;

        lexer = new Lexer(new FileReader(code));
        for (Token t; (t = lexer.read()) != Token.EOF; ) {
            System.out.println("=> " + t.getText()/*+" "+t.isIdentifier()+" "+t.isNumber()+" "+t.isString()*/);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/No_Game_No_Life_/article/details/84526260