首先是 TerminalSymbol.java 即终结符。
package com.taozeyu.taolan.analysis; import java.util.HashSet; import com.taozeyu.taolan.analysis.Token.Type; public class TerminalSymbol { @SuppressWarnings("serial") private final static HashSet<Type> careValueTypeSet = new HashSet<Type>() {{ add(Type.Keyword); add(Type.Sign); }}; static final TerminalSymbol Empty = new TerminalSymbol(null, null); public final Type type; public final String value; final boolean careValue; TerminalSymbol(Type type, String value) { this.type = type; this.value = value; this.careValue = careValueTypeSet.contains(type); } boolean isEmpty() { return this.type == null; } @Override public boolean equals(Object obj) { boolean isEquals = false; if(obj instanceof TerminalSymbol) { TerminalSymbol other = (TerminalSymbol) obj; isEquals = isEquals(this.type, other.type); if(isEquals & careValue) { isEquals = isEquals(this.value, other.value); } } return isEquals; } private boolean isEquals(Object o1, Object o2) { boolean isEquals = false; if(o1 == null & o2 == null) { isEquals = true; } else if(o1 != null & o2 != null) { isEquals = o1.equals(o2); } return isEquals; } @Override public int hashCode() { int hashCode = getHashCode(this.type); if(careValue) { hashCode ^= getHashCode(this.value); } return hashCode; } private int getHashCode(Object obj) { int hashCode = 0; if(obj != null) { hashCode = obj.hashCode(); } return hashCode; } @Override public String toString() { String str; if(this.value != null) { str = " “" + this.value + "”"; } else { if(this.type != null) { str = this.type.toString(); } else { str = "ε"; } } return str; } }
对于 Parser 而言,终结符 Terminal Symbol 与 Tokenizer 的 Token 是对应的。特别的,对于以上代码:
import com.taozeyu.taolan.analysis.Token.Type;
这里非终结符的类型(Type)我就直接用了 Token.java 中定义的内部枚举类啦。
特别的,
@SuppressWarnings("serial") private final static HashSet<Type> careValueTypeSet = new HashSet<Type>() {{ add(Type.Keyword); add(Type.Sign); }};
这里将 Keyword、Sign 这两种类型归于 careValueType。这是什么意思呢?容我稍微说明一下。
对于 Parser 中的终结符,即便它无法继续展开,但该终结符也并非就只能表示唯一的内容。例如,对于一个终结符,已知它是 Identifier,那么它实际上对应的字符串可能是 "hello_world" 或者 "accountBuilder" 之类的。这些内容,我将其称之为终结符的值 value。
显然,Parser 分析语法树的时候根本就不关心 Identifier 的值是什么,因为 Tokenizer 的工作就是将 Token 提取出来并识别其类型,以便让 Parser 无需关心琐碎的内容。
因此,我们就知道了,每一个终结符都对应一类 Token。
但是,对于 Keyword 和 Sign 而言,它的值却影响 Parser 分析语法树。例如 Sign 的值取 “+” 还是 “*” 会让生成的语法树完全不一样。
换句话说,在定义终结符的时候,我应该为每一个 Sign 单独归于一个类型,而不是将它们统称为一个类型。同样的道理适用于 Keyword 类型。
但是,就我个人而言,将 Keyword 和 Sign 拆分成多个类型未免有些繁琐,而且不好维护。于是我在这里做了一点取巧,将终结符分为 careValueType 型和非 careValueType 型。前者(目前有且仅有 Keyword、Sign) Parser 在识别它们的时候,不仅要考虑它们的 type,还要考虑它们的 value。而后者, Parser 仅考虑它们的 type,而 value 会被忽视。
具体请参考前面代码中的如下函数:
equals
hashCode
最后,我定义了一个特殊的非终结符:
static final TerminalSymbol Empty = new TerminalSymbol(null, null);
用来描述 ε。但是这个符号仅仅用于 Parser 初始化阶段,在 Parser 编译源代码的时候这个符号是用不上的。
然后是 NonTerminalSymbol.java 即非终结符。
package com.taozeyu.taolan.analysis; import java.util.ArrayList; import java.util.HashSet; class NonTerminalSymbol { static enum Exp { //TODO } final Exp exp; Character sign = null; final ArrayList<Object[]> expansionList = new ArrayList<>(); final ArrayList<TerminalSymbol> banList = new ArrayList<>(); final ArrayList<HashSet<TerminalSymbol>> firstSetList = new ArrayList<>(); final HashSet<TerminalSymbol> firstSet = new HashSet<>(); NonTerminalSymbol(Exp exp) { this.exp = exp; } NonTerminalSymbol ban(TerminalSymbol...args) { for(TerminalSymbol node:args) { banList.add(node); } return this; } NonTerminalSymbol or(Object...args) { expansionList.add(args); return this; } NonTerminalSymbol sign(char sign) { this.sign = sign; return this; } @Override public String toString() { String str = String.valueOf(exp); if(sign != null) { str += "(" + sign + ")"; } return str; } }
这个类实际上更多的是用来描述非终结符的产生式的。
对于一个非终结符的产生式:
A → abc | de | fg
对于非终结符 A,其对象的 expansionList 字段则会表现成如下形式。
对于里面的 Object 数组,其元素可能为 终结符对象(TerminalSymbol)、非终结符对象(NonTerminalSymbol)、或表达式枚举对象(Exp)。
expansionList = new ArrayList<>() {{ add(new Object[] { a, b, c}); add(new Object[] { d, e}); add(new Object[] { f, g}); }};
其中表达式枚举对象,就是代码中 //TODO 要填写的部分。