03 文法产生式的解析<1>

一、概述

1.1 必要性

设计一门语言的时候,首先需要编写它的文法产生式,然后根据文法产生式得到词法、语法的解析规则。
编译器工作的头两个步骤:词法解析、语法解析的功能如下:
词法解析:解析输入字符串,将其转换为一个个有意义的符号(Token)。
语法分析:解析上一步得到的符号,根据一定的规则,转换成一颗抽象语法树。

而本文要做的,是这两步之前的一个动作:根据文法产生式,生成对应的编译器。

至于为啥不对照文法产生式,硬编对应的代码,有以下原因:

  1. 在设计过程中,我的文法产生式可能会随时改变
  2. 复杂度较高,而且容易出错
  3. 我想设计一个通用的工具

1.2 本文想要解析的文法产生式

下面是一个计算器的文法:

cal:
    exec| cal exec
;
exec:
    assign '\n'|'\n'
    ;
assign:
    ID '=' assign
    | newExp
    ;
newExp:
     '(' newExp ')'
    | prim
    | newExp ('*'|'/') newExp
    | newExp ('+'|'-') newExp
    ;
 prim:
    Integer|Decimal|ID

这个文法可以匹配下列语句:

a=2
b=3
a*2+5-6/b
(a+b)*9

我的想法的是用户每输入一行表达式,就可以计算出对应的值并输出.

1.3 产生式的处理步骤

本文将使用LL1算法做语法分析。

对产生式的处理过程如下:

  1. 解析文法文件,得到 rule -> produce 的规则映射关系
  2. 将括号中的内容提取为子规则(subRule)
  3. 消除左递归
  4. 提取左公因子
  5. 计算First集合
  6. 计算Follow集合
  7. 利用以上处理结果,生成对应文法的Lexer(词法解析)、Parser(语法解析)类

然后我需要另外开发一个类,依赖上一步生成的Lexer、Parser,解析该文法所表示的源码,生成对应的语法树

二、功能实现

2.1 定义几个实体类

Rule:表示语法规则
Produce: 具体的产生式

这里对于每一个终结符、非终结符,都会分配一个short类型的ID,这样可以方便计算、存储。
因为,本质上文法的解析更专注于各个符号之间的关系,符号的名字并不重要,一般只是在解析过程中想直观的输出时,才会考虑展现各个符号的名字。

public class Rule {
    public short id;
    public String name;
    public List<Produce> produces = new ArrayList<>();
    public Set<Short> first = new HashSet<>(), follow = new HashSet<>();
    public boolean isDigui = false, isSub = false;
    public Rule(short id, String name) {
        this.id = id;
        this.name = name;
        this.produces.add(new Produce(id));
    }
}
public class Produce {
    public short id;
    public List<Short> nodes = new ArrayList<>();
    public Set<Short> first = new HashSet<>();
    public Produce(short ruleId ) {
        this.id = ruleId;
    }
}

以上代码展示的:

  1. 一个Rule(语法规则)由多个Produce(产生式)组成
  2. 一个Produce(产生式)由多个节点(node)组成,这里只存储节点的ID(short类型)
  3. 每个rule都有自己的first、follow集合,用于判断是否匹配当前的词法符号(Token)
  4. 每个Produce也有自己的first集合,用于判断当前Token是否适用这个Produce
  5. First和Follow集合如何计算、具体怎么用,在接下来的内容会详细解释。

2.2 解析文法文件

第一步当然是做词法解析,将文法文件的内容解析为一个个的词法符号(Token)。
第二步是遍历这些词法符号,将其转换为语法规则(Rule)的集合。
所以先定义一个GmLexer类:

public class GmLexer extends Lexer {
    public static final short StrStart = Lexer.StrTermStart;
    public static final short RuleEnd= StrStart, SubStart= StrStart +1,
    		SubEnd= StrStart +2, Or= StrStart +3, RuleSplit = StrStart + 4;
    public static final String[] Constant = {";","(",")","|",":"};
    public GmLexer(Reader reader) {
        super(reader);
    }
    @Override
    public String[] constantList() {
        return Constant;
    }
}

再然后就是重头戏了,遍历上一步得到的词法符号,转换为Rule集合,这里定义一个GmParser类用于干这些事情:

public class GmParser {
    static String[] defaultSymbol = "ID,STR1,Integer,Decimal,STR2,NewLine,NullTerm,EndTerm".split(",");
    Map<Short, Rule> ruleMap = new HashMap<>();
    GmLexer lexer;
    List<String> termList = new ArrayList<>();
    String startRule;
    List<Token> allTokens = new ArrayList<>();
    short termIndex = Lexer.StrTermStart, ruleIndex = Lexer.RuleStart;
    Map<String, Short> symbolMap = new HashMap<>();
    public GmParser(GmLexer lexer) {
        this.lexer = lexer;
        short id = 1;
        for (String s : defaultSymbol ) {
            symbolMap.put(s, id++);
        }
        symbolMap.put("T_\\n", Lexer.NewLine);
    }
}

然后我需要加载所有符号:

    void loadAllSymbol() {
        Token token ;
        while ((token = lexer.nextToken()) != null) {
            allTokens.add(token);
            switch (token.type) {
                case GmLexer.RuleSplit :
                    Token lastToken = allTokens.get(allTokens.size() - 2);
                    Rule rule = newRule(lastToken.text);
                    symbolMap.put(rule.name, rule.id);
                    if (startRule == null) {
                        startRule = rule.name;
                    }
                    break;
                case Lexer.STR1:
                    if (symbolMap.containsKey("T_"+token.text)) {
                        break;
                    }
                    symbolMap.put("T_"+token.text, termIndex++);
                    termList.add(token.text);
                    break;
            }
        }
    }

这里对于每一个终结符、非终结符,都会分配一个short类型的ID,用symbolMap登记名字与ID之间的关系,第一次扫描到符号的时候,会给它生成一个新的ID,后面就之间从symbolMap取了。
另外为了防止字符串常量与其他符号冲突,所以在前面统一增加"T_"前缀。
termList用于存储所有的字符串常量,为后面的Lexer代码生成做准备。

2.3 提取子规则

这一步是完整的遍历整个文法文件,根据各个产生式的内容,创建对应的Rule、Produce。

从这一步开始,包括后面的消除左递归、提取左公因子,目的都是重组各个产生式:

  1. 使之达到LL1文法的标准
  2. 每个产生式尽量简洁好处理
    最好每个产生式的格式都是 a → b   c ∣ d   e a\to b\ c|d\ e ab cd e这样的格式,一方面用程序好表示,另一方面也方便计算一些集合。

当前的这个步骤涉及到了子规则的处理,比如说,对于下面的产生式:

a → b   c   ( d   ( e ∣ f )   ∣   ( g ∣ h ) ) a \to b\ c\ ( d\ \color{red}( e|f )\color{black}\ |\ \color{green}(g|h)\color{black}) ab c (d (ef)  (gh))
我希望得到的Rule及Produce为:
a → b c a 1 a 1 → d a 2 ∣ a 3 a 2 → e ∣ f a 3 → g ∣ h a \to b \quad c\quad a_1\\ a_1 \to d \quad a_2 | a_3\\ a_2 \to e|f \\ a_3 \to g|h abca1a1da2a3a2efa3gh

所以解析的时候,我需要定义一个produceStack 栈结构。每次解析的时候,将当前Produce压入栈,解析完弹出。 解析中碰到小括号,则创建一个新的subRule,压入栈顶,同时作为当前Produce的一个符号。

void createRule() {
        Stack<Produce> produceStack = new Stack<>();
        for (Token token : allTokens) {
            if (produceStack.empty() && token.type == Lexer.ID) {
                Rule rule = ruleMap.get(symbolMap.get(token.text));
                produceStack.add(rule.produces.get(0));
            } else switch (token.type) {
                case GmLexer.SubStart:
                    Rule rule = newRule("subRule@");
                    rule.isSub = true;
                    produceStack.peek().nodes.add(rule.id);
                    produceStack.add(rule.produces.get(0));
                    break;
                case GmLexer.RuleEnd:
                case GmLexer.SubEnd:
                    produceStack.pop();
                    break;
                case GmLexer.Or:
                    Produce pop = produceStack.pop();
                    Produce produce = new Produce(pop.id);
                    ruleMap.get(pop.id).produces.add(produce);
                    produceStack.add(produce);
                    break;
                case GmLexer.STR1:
                    produceStack.peek().nodes.add(symbolMap.get("T_"+token.text));
                    break;
                case GmLexer.ID:
                    produceStack.peek().nodes.add(symbolMap.get(token.text));
                    break;
                default:
                    break;
            }
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值