语法树是如何将文字转化为行为的?

602 阅读5分钟

1、为什么要有语法树

当我们使用一门计算机语言时,往往是通过键盘输入一串字符串,计算机解析字符串,用对应语言的“翻译出来”,形成让计算机能够处理的结构,交予执行引擎执行,这之中一个重要的形态就是语法树。

1.1、语法树的应用场景

语法树的应用方面广泛,包括但不限于以下地方:

  • c、Java、python等编程语言从文本文件编译为相关可运行的文件。
  • 通过命令行交互的软件如redis、mysql等。
  • latex等文本编辑软件
  • 部分对机器的操作
  • etc

1.2、语法树的优良性质

语法树在解析和使用上,都有着良好的性能,因此被广泛使用。

  • 语法能够将符合相关语言规范的输入转化为软件能够处理的数据结构。
  • 因为语法树处于语言表述和执行引擎的中间状态,因此对于部分基础问题,不需要更改上层语法规则,只需要更改对语法树的执行引擎即可(甚至可以直接更换引擎,如MySQL底层具有包括InnoDB与MyISAM等许多引擎让使用者选择)。
  • etc

2、如何构建语法树

2.1、语法规则

在使用语法树前,首先应有的就是便是语法规则,有了规则方才有语法的解析。

这里我们不妨先把后文示例的语法来先阐述下,这样能够方便理解。

2.1.1、简例

定义这样一门语言,能够驱使小车运行。语言以program开始,中间跟着程序列表,以end结束;对小车的基本操作有go left right三种方向的行为,因此一个简单的小车程序可以这样写

program 
    go
    left
    right
end

额外的,我们还定义循环语句,循环语句以repeat开始,以end结束,如果是一个让小车练习前进三次的语句,可以这样写

program
    repeat 3
        go
    end
end

2.1.2、BNF语法

BNF(Backus Normal Form)语法是一种用来描述语法的范式,上例可以这样为BNF的一种变种语法EBNF表示。

<program>::=program <command list>
<command list>::=<command> * end
<command>:=<repeat command>|<primitive command>
<repeat command>::=repeat <number><command list>
<primitive command>::= go|left|right

如何理解这一范式呢?首先了解这一范式的语法

  • 每一行的开头由尖括号括起来的是被描述语法的一种结构组成,该行后续的语言都是描述这个结构的
  • “::=”符号的含义为定义,将符号左侧的结构定义为符号右侧的结构
  • “::=”右侧即为具体的定义体,不由尖括号括起来的是常量,在语法中不可变,由尖括号括起来的是另外的语法结构,在上下文中也由其的定义
  • “*”代表该符号左侧的文字可以重复0次以上
  • “|”代表或

这样说依旧是抽象的,我们再举例说明:

<program>::=program <command list> //表示<program>结构由program关键字起头,后续跟着command list
<command list>::=<command> * end //表示<command list>由0个以上<command> 组成,以end结束
<command>:=<repeat command>|<primitive command> //表示一个<command>为<repeat command>或是<primitive command>组成
<repeat command>::=repeat <number><command list> //表示<repeat command>以repeat开始,后续跟着一个数字,再后跟着<command list>即循环体
<primitive command>::= go|left|right //代表<primitive command>为 go或leftright

2.2、生成语法树

仍然以上诉为例,有了上面的规则,当输入一个符合规则的语句时,应当生成这样的一棵树:

stateDiagram-v2

ProgramNode -->CommandListNode
CommandListNode --> ReapeatCommandNode
CommandListNode --> PrimitiveNode
ReapeatCommandNode-->CommandListNode
PrimitiveNode-->go
PrimitiveNode-->right
PrimitiveNode-->left

2.3、操作语法树

当语法树形成后,我们从跟节点开始遍历语法树,从而一步步对树所描述的结构进行解析。 再作一个简单例子

program
     go
end

这是一个简单的程序,生成的语法树如下:

stateDiagram-v2

ProgramNode -->CommandListNode
CommandListNode --> PrimitiveNode
PrimitiveNode-->go
  • 首先访问根节点ProgramNode
  • 再访问Program中的程序列表CommandListNode
  • 列表中只有一句基础的go语句,因此CommandListNode只指向一个PrimitiveNode
  • 该PrimitiveNode指向基本语句go

这样一来,从规则是什么样,到如何用EBNF语言来表述规则,再到如何将符合规则的输入语句转化为具体语法树的过程我们已经展示完了,接下来是对上面描述的简例的具体实现。

3、具体实现

需要提前声明的是,本文是笔者在学习结城浩先生的《图解设计模式》时写下的,应用的例子也是书中的。

然而该例子仅仅将输入语句转化为一定结构,并不是传统意义上的规范语法树,没有对外输出结构以及构建专门的引擎进行处理,限于时间也仅使用书中的案例,望见谅,后续若有时间会增加规范的示例。

3.1、文本工具类Context

在该类中借助StringTokenizer,完成对词的分隔,提供了访问当前词,下一词,当前数字的API

public class Context {
    private StringTokenizer tokenizer;
    private String currentToken;

    public Context(String text) {
        tokenizer=new StringTokenizer(text);
        nextToken();
    }

    public String nextToken() {
        if (tokenizer.hasMoreTokens()) {
            currentToken = tokenizer.nextToken();
        } else {
            currentToken=null;
        }
        return currentToken;
    }

    public String getCurrentToken() {
        return currentToken;
    }

    public void skipToken(String token) throws ParseException {
        if (!token.equals(currentToken)) {
            throw new ParseException("Warning:"+token+" is expected,but"+currentToken+"is found");
        }
        nextToken();
    }

    public int currentNumber() throws ParseException {
        int num=0;
        try {
            num = Integer.parseInt(currentToken);
        } catch (NumberFormatException e) {
            throw new ParseException("Warning:"+e);
        }
        return num;
    }
}

3.2、异常类ParseException

public class ParseException extends Exception{
    public ParseException(String msg) {
        super(msg);
    }
}

3.3、父类Node

在所有节点之上定义了其父类,规范所有节点应该有解析语句的方法

public abstract class Node {
    public abstract void parse(Context context)throws ParseException;
}

3.4、ProgramNode

public class ProgramNode extends Node{
    private Node commandListNode;
    @Override
    public void parse(Context context) throws ParseException {
        context.skipToken("program");
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }

    @Override
    public String toString() {
        return "[program"+commandListNode+"]";
    }
}

3.5、CmmandListNode

public class CommandListNode extends Node{
    private ArrayList<Node> list=new ArrayList();
    @Override
    public void parse(Context context) throws ParseException {
        while (true) {
            if (context.getCurrentToken() == null) {
                throw new ParseException("Missing 'end'!");
            } else if (context.getCurrentToken().equals("end")) {
                context.skipToken("end");
                break;
            } else {
                Node commandNode = new CommandNode();
                commandNode.parse(context);
                list.add(commandNode);
            }
        }
    }

    @Override
    public String toString() {
        return list.toString();
    }
}

3.6、CommandNode

public class CommandNode extends Node{
    private Node node;
    @Override
    public void parse(Context context) throws ParseException {
        if (context.getCurrentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node=new PrimitiveCommandNode();
            node.parse(context);
        }
    }

    @Override
    public String toString() {
        return node.toString();
    }
}

3.7、CommandNode

public class CommandNode extends Node{
    private Node node;
    @Override
    public void parse(Context context) throws ParseException {
        if (context.getCurrentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node=new PrimitiveCommandNode();
            node.parse(context);
        }
    }

    @Override
    public String toString() {
        return node.toString();
    }
}

3.8、RepeatCommand

public class RepeatCommandNode extends Node{
    private int num;
    private Node commandListNode;
    @Override
    public void parse(Context context) throws ParseException {
        context.skipToken("repeat");
        num=context.currentNumber();
        context.nextToken();
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }

    @Override
    public String toString() {
        return "[repeat"+num+" "+commandListNode+"]";
    }
}

3.9、PrimitiveCommandNode

public class PrimitiveCommandNode extends Node{
    private String name;
    @Override
    public void parse(Context context) throws ParseException {
        name=context.getCurrentToken();
        context.skipToken(name);
        if(!name.equals("go")&&!name.equals("left")&&name.equals("right"))  {
            throw new ParseException(name+" is undefined");
        }
    }

    public String toString() {
        return name;
    }
}

3.10、运行测试

输入语句

program
    repeat 3
    go
    end
    left
    right
end

测试类:

public class Main {
    public static void main(String[] args) {
        ProgramNode programNode = new ProgramNode();
        try {
            programNode.parse(new Context("program\n" +
                    "    repeat 3\n" +
                    "    go\n" +
                    "    end\n" +
                    "    left\n" +
                    "    right\n" +
                    "end"));
            System.out.println(programNode);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

image.png