【编译原理】实验二——语法分析

实验题目:语法分析表的构造
实验目的:
(1)学习和掌握消除左递归和提取左公因子的方法
(2)学习和掌握求解FIRST和FOLLOW函数的一般方法和步骤
(3)学习和掌握DFA的方法和步骤
(4)掌握判断LL(1)文法和SLR(1)文法的一般算法
(5)掌握构造LL(1)语法分析表和LR(1)语法分析表的一般算法
实验环境:笔记本电脑、Win10、idea Java环境

实验内容及操作步骤:
一、基本数据结构
1)文法符:作为终结符和非终结符的基类,其中SymbolType有三种类型:TERMINAL(终结符),NONTERMINAL(非终结符),NULL(ε)。

public class GrammarSymbol {
  // 名字
  private String name;
  // 文法符的类别
  private SymbolType type;
}

2)终结符定义:其中LexemeCategory定义如下

public class TerminalSymbol extends GrammarSymbol {
  // 终结符的词类
  private LexemeCategory category;
}

3)词法类型定义

public enum LexemeCategory {
  // 空字符
  EMPTY,
  // 整数常量
  INTEGER_CONST,
  // 实数常量
  FLOAT_CONST,
  // 科学计数法常量
  SCIENTIFIC_CONST,
  // 数值运算词
  NUMERIC_OPERATOR,
  // 注释
  NOTE,
  // 字符串常量
  STRING_CONST,
  // 空格常量
  SPACE_CONST,
  // 比较运算词
  COMPARE_CONST,
  // 变量词
  ID,
  // 逻辑运算词
  LOGIC_OPERATOR,
  // 关键字
  KEYWORD
}

4)非终结符定义

public class NonTerminalSymbol extends GrammarSymbol{
  // 有关非终结符构成的产生式
  private ArrayList<Production> pProductionTable;
  // 产生式的个数
  private int numOfProduction;
  // 非终结符的 FIRST 函数值
  private Set<TerminalSymbol> pFirstSet;
  // 非终结符的 FOLLOW 函数值
  private Set <TerminalSymbol> pFollowSet;
  // 求非终结符的 FOLLOW 函数值时,存放所依赖的非终结符。
  private Set <NonTerminalSymbol> pDependentSetInFollow;
}

4)产生式定义

public class Production {
  // 产生式序号,起标识作用
  private int productionId;
  // 产生式体中包含的文法符个数
  private int bodySize;
  // 产生式体中包含的文法符
  private ArrayList<GrammarSymbol> pBodySymbolTable;
  // 产生式的 FIRST 函数值
  private Set<TerminalSymbol> pFirstSet;
}

5)LL(1)语法分析表中,每一格的定义

public class Cell {
  private NonTerminalSymbol nonTerminalSymbol;
  private TerminalSymbol terminalSymbol;
  private Production production;
}

6)LR(0)项目定义

public class LR0Item {
  // 非终结符
  private NonTerminalSymbol nonTerminalSymbol;
  // 产生式
  private Production production;
  // 圆点的位置
  private int dotPosition;
  // 类型。两种:CORE(核心项);NONCORE(非核心项)
  private ItemCategoy type;
}

7)LR(0)项集定义

public class ItemSet {
  // 状态序号
  private int stateId;
  // LR0 项目表
  private ArrayList<LR0Item> pItemTable;
}

8)变迁边定义

public class TransitionEdge {
  // 驱动文法符
  private GrammarSymbol driverSymbol;
  // 出发项集
  private ItemSet fromItemSet;
  // 到达项集
  private ItemSet toItemSet;
}

9)DFA定义

public class DFA {
  // 开始项集
  private ItemSet startupItemSet;
  // 变迁边表
  private ArrayList<TransitionEdge> pEdgeTable;
}

10)LR(1)语法分析表中ACTION部分的定义,其中ActionCategory有三种类型:r(reduce规约,id为产生式)、s(shift移入,id为状态)、a(accept,接受)

public class ActionCell {
  // 纵坐标:状态序号
  private int stateId;
  // 横坐标:终结符
  private String terminalSymbolName;
  // Action 类别
  private ActionCategory type ;
  // Action 的 id
  private int id;
}

11)LR(1)语法分析表中GOTO部分的定义

public class GotoCell {
  // 纵坐标:状态序号
  private int stateId;
  // 横坐标:非终结符
  private String nonTerminalSymbolName;
  // 下一状态
  private int nextStateId;
}

12)产生式概述表定义

public class ProductionInfo {
  // 产生式序号
  private int indexId;
  // 头部非终结符
  private String headName;
  // 产生式体中文法符的个数
  private int bodySize;
}

二、针对LL语法分析,实现如下函数
1)判断是否存在左递归以及左递归的消除实现
判断左递归:判断非终结符的所有产生式,若存在产生式右部的头部和该非终结符相同,则表明存在左递归。
消除左递归:对于含左递归的文法符 X,将其产生式分成含左递归的和不含左递归的两个部分。把 X 的左递归变换成了 X’的右递归,并且新增一个ε产生式。具体过程如下:
在这里插入图片描述

实现函数:

public static NonTerminalSymbol leftRecursion(NonTerminalSymbol nonTerminalSymbol) {
  // 获取产生式左部的非终结符A
  String name = nonTerminalSymbol.getName();
  // 保存A=>Aα的链表
  ArrayList<Production> left = new ArrayList<>();
  // 保存A=>β的链表
  ArrayList<Production> constant = new ArrayList<>();
  // 遍历所有产生式
  for (Production production: nonTerminalSymbol.getpProductionTable()) {
    // 产生式右部的头部文法符名字为A,则将其放入left;否则放入constant
    if (production.getpBodySymbolTable().get(0).getName().equals(name)) {
      left.add(production);
    }
    else {
      constant.add(production);
    }
  }
  // left为空,说明不含有左递归
  if (left.isEmpty()) {
    System.out.println("不存在左递归");
    return null;
  }
  System.out.println("存在左递归");
  // 新建一个非终结符A‘
  NonTerminalSymbol S_dot = new NonTerminalSymbol(nonTerminalSymbol.getName()+'\'', SymbolType.NONTERMINAL);
  // 将A=>β变化为A=>βA'
  for (Production production: constant) {
    production.addSymbolAtLast(S_dot);
  }
  // 将A=>Aα变化为A'=>αA'
  for (Production production: left) {
    // 非终结符A移除产生式
    nonTerminalSymbol.removeProduction(production);
    // 产生式头部去除A
    production.removeFirstSymbol(nonTerminalSymbol);
    // 产生式尾部加上A’
    production.addSymbolAtLast(S_dot);
    // 添加到非终结符A‘的产生式集合中
    S_dot.addProduction(production);
  }
  // 添加A=>ε
  Production production = new Production();
  GrammarSymbol epsilon = new GrammarSymbol("ε", SymbolType.NULL);
  production.addSymbolAtLast(epsilon);
  // 加入非终结符A’的产生式集合中
  S_dot.addProduction(production);
  return S_dot;
}

函数测试:A->Aa,A->b消除左递归
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2)产生式有左公因子的判断,以及左公因子的提取实现
判断左公因子:保存非终结符的所有产生式右部的头部信息,若存在两个及以上产生式右部的头部信息相同,则表明存在左公因子。
提取左公因子:将含有公共头部信息的产生式写做一个公共头部信息+A’。再将剩下的部分由A’推导。具体过程如下:
在这里插入图片描述

实现函数:

public static ArrayList<NonTerminalSymbol> leftCommonFactor(NonTerminalSymbol symbol) {
  Map<GrammarSymbol, ArrayList<Production>> head = new HashMap<>();
  Boolean flag = false;
  for (Production production: symbol.getpProductionTable()) {
    GrammarSymbol s = production.getpBodySymbolTable().get(0);
    // 不存在该头部信息,则表示其第一次出现,加入map中
    if (head.get(s) == null) {
      ArrayList<Production> p = new ArrayList<>();
      p.add(production);
      head.put(s, p);
    }
    // 说明已经出现过,此时存在左公因子
    else {
      head.get(s).add(production);
      flag = true;
    }
  }
  if (flag == false) {
    System.out.println("不存在左公因子");
    return null;
  }
  System.out.println("存在左公因子");
  ArrayList<NonTerminalSymbol> ans = new ArrayList<>();
  // 遍历所有头部信息
  for (GrammarSymbol key: head.keySet()) {
    // 判断具有相同头部信息的个数
    if (head.get(key).size() > 1) {
      // 新建一个非终结符A‘
      NonTerminalSymbol A_dot = new NonTerminalSymbol(symbol.getName()+"\'", SymbolType.NONTERMINAL);
      // 新建一个产生式A->key A'
      Production p = new Production();
      p.addSymbolAtLast(key);
      p.addSymbolAtLast(A_dot);
      symbol.addProduction(p);
      // 遍历左公因子
      for (Production production: head.get(key)) {
        // 删除A->key ……中的key
        production.removeFirstSymbol(key);
        // 删除A->……的产生式左部
        symbol.removeProduction(production);
        // 产生式左部为A’->……
        A_dot.addProduction(production);
      }
      // 添加新的非终结符
      ans.add(A_dot);
    }
  }
  return ans;
}

函数测试:A->ab, A->ac
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3)产生式的FIRST函数求解
实现方法:对于产生式X->Y1Y2…Yn-1Yn,FIRST(Y1)∈FIRST(X)显然成立。但如果从Y1至Yj,0<j<n,全为非终结符,且都含虚产生式,那么FIRST(Yj+1) 属于 FIRST(X)。算法思想如下:
在这里插入图片描述

实现函数:

public static Set<TerminalSymbol> firstOfProduction(Production production) {
  // ε是否持续
  Boolean nullStand = true;
  int i = 0;
  Set<TerminalSymbol> pFirstSet = new HashSet<>();
  ArrayList<GrammarSymbol> pBodySymbolTable = production.getpBodySymbolTable();
  // 新建一个ε文法符用于判断
  TerminalSymbol epsilon = new TerminalSymbol("ε", SymbolType.NULL);
  // 当前面文法符FIRST都包含ε时
  while(nullStand && i < production.getBodySize()) {
    // 获取当前文法符的FIRST
    Set<TerminalSymbol> firstY = firstOfSymbol(pBodySymbolTable.get(i));
    // 判断当前文法符FIRST是否包含ε
    if (firstY.contains(epsilon)) {
      // 跳转到下一个文法符,去掉ε
      i ++;
      firstY.remove(epsilon);
    }
    else {
      // ε不再持续
      nullStand = false;
    }
    // 把当前文法符的FIRST加入结果中
    pFirstSet.addAll(firstY);
  }
  // 如果最终能推导出ε,则FIRST集合中包含ε
  if (nullStand && i == production.getBodySize()) {
    pFirstSet.add(epsilon);
  }
  // 设置产生式的FIRST集合
  production.setpFirstSet(pFirstSet);
  return pFirstSet;
}

函数测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4)非终结符的FIRST函数求解
实现方法:对每个非终结符的产生式,求其FIRST函数,再将其合并即可。
实现函数:

public static Set<TerminalSymbol> firstOfSymbol(GrammarSymbol symbol) {
  Set<TerminalSymbol> ans = new HashSet<>();
  // 当前文法符为终结符或ε,则直接返回其本身
  if (symbol.getType() == SymbolType.TERMINAL || symbol.getType() == SymbolType.NULL) {
    ans.add((TerminalSymbol) symbol);
    return ans;
  }
  // 当前文法符为非终结符,遍历每个产生式
  for (Production production: ((NonTerminalSymbol)symbol).getpProductionTable()) {
    if (production.getpBodySymbolTable().get(0) == symbol) {
      continue;
    }
    // 对每个产生式求其FIRST集
    for (TerminalSymbol s: firstOfProduction(production)) {
      // 将未加入的终结符加入FIRST集合
      if (!ans.contains(s)) {
        ans.add(s);
      }
    }
  }
  // 设置FIRST非终结符的FIRST集合
  ((NonTerminalSymbol) symbol).setpFirstSet(ans);
  return ans;
}

函数测试:文法同上,输出每个非终结符的FIRST函数
在这里插入图片描述

5)非终结符的FOLLOW函数求解
实现方法:穷举所有情形,找出跟在T’后面的终结符。产生式X->Y1Y2…Yn-1Yn蕴含有如下两个FOLLOW信息。
①对于末尾符Yn,如果它为非终结符,那么FOLLOW(X)∈FOLLOW(Yn)。若Yi为终结符(0<i<n),且从Yi+1至Yn全为非终结符,且都含虚产生式,那么FOLLOW(X)属于FOLLOW(Yi)。
②除了末尾符Yn之外,对于产生式右部中任一文法符Yi,其中0<i<n,如果Yi是一个非终结符,那么FIRST(Yi+1)-ε∈FOLLOW(Yi)。如果Yi为非终结符(0<i<n-1),且从Yi+1至Yj(i+1<j<n)全为非终结符,且都含虚产生式,那么FIRST(Yj+1)-ε∈FOLLOW(Yi)。
具体算法如下:
在这里插入图片描述

实现函数:

public static void followOfSymbol(NonTerminalSymbol symbol) {
  // 新建ε
  TerminalSymbol epsilon = new TerminalSymbol("ε", SymbolType.NULL);
  // 遍历产生式
  for (Production production: symbol.getpProductionTable()) {
    // 产生式文法符个数
    int size = production.getBodySize();
    // 获取最后一个文法符
    GrammarSymbol Yn = production.getpBodySymbolTable().get(size - 1);
    // symbol->ε,表示为空,跳过该产生式
    if (Yn.getName().equals("ε")) {
      continue;
    }
    // 最后一个文法符为非终结符,其FOLLOW集合依赖于symbol的FOLLOW集合
    if (Yn.getType() == SymbolType.NONTERMINAL) {
      ((NonTerminalSymbol) Yn).addDependentSetFollow(symbol);
    }
    // ε是否持续(从最后一个开始往前找)
    Boolean nullStand = true;
    // 从倒数第二个开始
    int i = size-2, j = size-1;
    while (i >= 0) {
      GrammarSymbol Yi = production.getpBodySymbolTable().get(i);
      // 当前文法符为非终结符
      if (Yi.getType() == SymbolType.NONTERMINAL) {
        // 遍历其后FIRST连续包含ε的非终结符
        for (int k = i+1; k <= j; k ++) {
          // 若第k个文法符为终结符,将其自身加入FOLLOW集合即可
          if (production.getpBodySymbolTable().get(k).getType() == SymbolType.TERMINAL) {
            ((NonTerminalSymbol)Yi).addFollow((TerminalSymbol) production.getpBodySymbolTable().get(k));
            nullStand = false;
          }
          else {
            NonTerminalSymbol Yk = (NonTerminalSymbol)production.getpBodySymbolTable().get(k);
            Set<TerminalSymbol> firstYk = Yk.removeEpsilon();
            // 若当前终结符的后续非终结符的FIRST集合不包含ε,说明无法持续到最后,nullStand置0
            if (!Yk.containsEpsilon()) {
              nullStand = false;
            }
            // 将其FIRST集合-{ε}加入非终结符的FOLLOW集合
            ((NonTerminalSymbol)Yi).addFollowSet(firstYk);
          }
        }
        // 如果当前文法符的FIRST集合不包含ε,说明FOLLOW集合无法到达后续,将j修改为当前i
        if (!((NonTerminalSymbol)Yi).getpFirstSet().contains(epsilon)) {
          j = i;
        }
        // 如果nullStand仍为true,表示当前文法符仍能到达最后,则其FOLLOW集合依赖于symbol
        if (nullStand) {
          ((NonTerminalSymbol)Yi).addDependentSetFollow(symbol);
        }
      }
      else {
        j = i;
        if (nullStand) {
          nullStand = false;
        }
      }
      i --;
    }
  }
}

根据每个非终结符求出FOLLOW相关信息后,将依赖的非终结符的FOLLOW信息加入其中:

public void addFollowDependent() {
  for (NonTerminalSymbol symbol: pDependentSetInFollow) {
    addFollowSet(symbol.pFollowSet);
  }
}

函数测试:文法同上,先求其FIRST函数,再求FOLLOW函数,最后进行求解
在这里插入图片描述
在这里插入图片描述

6)LL(1)文法的判断
实现方法:对于一个文法,其中的任一非终结符X,设其实产生式有X->α1,X->α2,……, X->αn,若满足 FIRST(X->αi)∩ FIRST(X->αj) =Φ,其中i≠j且0<i,j≤n。如果X还有虚产生式X->ε,若进一步满足 FIRST(X->αi)∩FOLLOW(X)=Φ,其中0<i≤n。具有这种特性的文法被称之为 LL(1)文法。
实现函数:

public static Boolean isLL1 (NonTerminalSymbol symbol) {
  Map<TerminalSymbol, Integer> map = new HashMap<>();
  // 判断是否有X->ε,若存在需将FOLLOW保存进map
  if (symbol.containsEpsilon()) {
    for (TerminalSymbol s: symbol.getpFollowSet()) {
      map.put(s, -1);
    }
  }
  // 遍历产生式
  for (Production production: symbol.getpProductionTable()) {
    // 遍历产生式的FIRST集合,若其未出现过,说明无交集,否则有交集(不为LL(1)文法)
    for (TerminalSymbol s: production.getpFirstSet()) {
      if (map.get(s) == null) {
        map.put(s,production.getProductionId());
      }
      else {
        return false;
      }
    }
  }
  return true;
}

函数测试:文法同上,生成FIRST和FOLLOW函数后,对每个非终结符进行依次判断,若每个非终结符都满足条件,则其为LL(1)文法。
在这里插入图片描述

7)LL(1)语法分析表的填写
实现方法:对于 LL(1)文法,在自顶向下最左推导当中,设当前要推导的非终结符为X,当前词为w,如果w∈FIRST(X->αi),就选择 X->αi进行推导。如果存在有X->ε且w∈FOLLOW(X),就选择X->ε进行推导。
实现函数:

public static ArrayList<Cell> parseTable(ArrayList<NonTerminalSymbol> pNonTerminalSymbolTable) {
  ArrayList<Cell> pParseTableOfLL = new ArrayList<>();
  // 遍历每个非终结符
  for (NonTerminalSymbol symbol: pNonTerminalSymbolTable) {
    // 遍历每个产生式
    for (Production production: symbol.getpProductionTable()) {
      // 若该产生式为X->ε,则选择FOLLOW集合中的终结符填入该产生式
      if (production.isEpsilon()) {
        for (TerminalSymbol t: symbol.getpFollowSet()) {
          Cell cell = new Cell(symbol, t, production);
          pParseTableOfLL.add(cell);
        }
      }
      // 否则选择FIRST集合中的终结符填入该产生式
      else {
        for (TerminalSymbol t: production.getpFirstSet()) {
          Cell cell = new Cell(symbol, t, production);
          pParseTableOfLL.add(cell);
        }
      }
    }
  }
  return pParseTableOfLL;
}

函数测试:文法同上,求出FIRST和FOLLOW函数判断其是否为LL(1)文法,再构造语法分析表
在这里插入图片描述

三、针对LR语法分析,实现如下函数。
1)void getClosure(ItemSet itemSet);
函数作用:基于LR(0)核心项的闭包求解。
实现方法:找到所有待约项目,根据待约项目推导出非核心项。
实现函数:

public static void getClosure(ItemSet itemSet) {
  // 栈:用于保存未求其后续文法符的项目
  Stack<LR0Item> item = new Stack<>();
  // 将所有核心项推入栈中
  for (LR0Item lr: itemSet.getpItemTable()) {
    item.push(lr);
  }
  while (!item.isEmpty()) {
    LR0Item lr = item.pop();
    int pos = lr.getDotPosition();
    // pos在产生式最后面,说明为规约项目
    if (pos == lr.getProduction().getBodySize()) {
      continue;
    }
    else {
      // 找到后续文法符
      GrammarSymbol symbol = lr.getProduction().getpBodySymbolTable().get(pos);
      // 该文法符为非终结符,说明为待约项目
      if (symbol.getType() == SymbolType.NONTERMINAL) {
        // 遍历该非终结符的每个产生式
        for (Production production: ((NonTerminalSymbol)symbol).getpProductionTable()) {
          if (!itemSet.containItem(production, 0)) {
            // 新建一个非核心项,原点位置为0
            LR0Item newItem = new LR0Item((NonTerminalSymbol)symbol, production, 0,
                ItemCategoy.NONCORE);
            // 添加进闭包
            itemSet.addItem(newItem);
            // 推入栈中
            item.push(newItem);
          }
        }
      }
    }
  }
}

其中判断item是否重复的函数如下:

public Boolean containItem(Production production, int pos) {
  for (LR0Item item1: pItemTable) {
    if (item1.getProduction() == production && item1.getDotPosition() == pos) {
      return true;
    }
  }
  return false;
}

函数测试:求E’的闭包I0
在这里插入图片描述

2)void exhaustTransition(ItemSet itemSet)
函数作用:穷举一个LR(0)项集的变迁,其中中包括驱动符的穷举,下一项集的创建,下一项集中核心项的确定,下一项集是否为新项集的判断。
实现方法:首先找到所有驱动符,对每个驱动符创建一个项集,求该项集的核心项及其闭包,再判断该项集是否为新项集。最后创建一条变迁边连接两个项集。
实现函数:

public static ArrayList<TransitionEdge> exhaustTransition(ItemSet itemSet) {
  // 保存新创建的变迁边
  ArrayList<TransitionEdge> edges = new ArrayList<>();
  Map<GrammarSymbol, Vector<LR0Item>> map = new HashMap<>();
  // 穷举所有驱动符,并将其保存在map中
  for (LR0Item item: itemSet.getpItemTable()) {
    int pos = item.getDotPosition();
    // 当前项目为规约项目
    if (pos == item.getProduction().getBodySize()) {
      continue;
    }
    // 获得后续文法符
    GrammarSymbol symbol = item.getProduction().getpBodySymbolTable().get(pos);
    // 该文法符未出现,则新创建一个vector对象
    if (map.get(symbol) == null) {
      Vector<LR0Item> v = new Vector<>();
      v.add(item);
      map.put(symbol, v);
    }
    // 之前出现过,则在后面添加item
    else {
      map.get(symbol).add(item);
    }
  }
  // 遍历所有驱动符
  for (GrammarSymbol symbol: map.keySet()) {
    // 下一项集的建立,id为-1(防止重复导致的状态序号不连续)
    ItemSet toSet = new ItemSet(-1);
    for (LR0Item item: map.get(symbol)) {
      // 新建一个核心项,其type为CORE,pos加一
      LR0Item lr = new LR0Item(item);
      toSet.addItem(lr);
    }
    // 求该项集的闭包
    getClosure(toSet);
    // 判断下一项集是否为新项集
    Boolean isExist = false;
    for (ItemSet set: allItemSet) {
      if (toSet.isSame(set)) {
        isExist = true;
        toSet = set;
        break;
      }
    }
    // 该项集为新项集
    if (!isExist) {
      // 设置该项集为新的项集id,保证连续
      toSet.setStateId();
      allItemSet.add(toSet);
      newItemSet.add(toSet);
    }
    // 创建一条变迁边
    TransitionEdge edge = new TransitionEdge(symbol, itemSet, toSet);
    edges.add(edge);
  }
  return edges;
}

其中变化如下:
1.新维护了一个链表,用于保存所有项集和之后判断新项集

private static ArrayList<ItemSet> allItemSet = new ArrayList<>();
public static void addItemSet(ItemSet set) {
  allItemSet.add(set);
}

2.新增了一个构造函数,使其在原项目基础上原点位置右移,且变为核心项

public LR0Item(LR0Item item) {
  this.nonTerminalSymbol = item.getNonTerminalSymbol();
  this.production = item.getProduction();
  this.dotPosition = item.getDotPosition() + 1;
  this.type = ItemCategoy.CORE;
}

3.判断两项集是否相同:判断两者LR0项目是否相等

public Boolean isSame(ItemSet itemSet) {
  // 项目数目不同,则必不相同
  if (pItemTable.size() != itemSet.getpItemTable().size()) {
    return false;
  }
  // 此时项目数目相同
  for (int i = 0; i < pItemTable.size(); i ++) {
    // 若itemSet中不包含该项目,则两者必然存在差异
    if (!pItemTable.get(i).equals(itemSet.getpItemTable().get(i))) {
      return false;
    }
  }
  return true;
}

函数测试:以项目集I0的变迁为例,先求出其核心项,再求其闭包。
在这里插入图片描述
在这里插入图片描述

3)文法的LR(0)型DFA求解
实现方法:在符号栈中,从状态0开始,穷举所有变迁。对于每一变迁的驱动文法符,求下一状态(即核心项闭包)。如果下一状态是一个新状态,则使用相同策略穷举。如此迭代下去,直到把所有的状态变迁都穷举出来。
实现函数:

public static DFA getDFA(ItemSet start) {
  // 新建一个DFA
  DFA dfa = new DFA(start);
  // 保存未穷举状态的项集
  Deque<ItemSet> queue = new ArrayDeque<>();
  queue.push(start);
  while (!queue.isEmpty()) {
    ItemSet current = queue.pop();
    // 对当前项集进行穷举,得到变迁边
    ArrayList<TransitionEdge> edges = exhaustTransition(current);
    queue.addAll(newItemSet);
    newItemSet.clear();
    // 添加所有变迁表到dfa中
    dfa.addEdges(edges);
  }
  return dfa;
}

函数测试:文法如上,对每个项集求其变迁即可。以下是最终项集
在这里插入图片描述

以下是DFA的开始状态和对应边:
在这里插入图片描述
在这里插入图片描述

4)SLR(1)文法的判断
实现思路:对于每个项集,找到它的移入终结符集合和规约项目集合,
①规约项目FOLLOW集合与移入终结符集合有冲突 ==> 移入-规约冲突
②规约项目FOLLOW集合之间有冲突 ==> 规约-规约冲突
上述两种情况都不发生,则为SLR(1)文法。
实现函数:

public static Boolean isSLR1() {
  // 遍历每个项集
  for (ItemSet set: allItemSet) {
    // 设置规约项目和移入终结符集合
    ArrayList<LR0Item> reduce = new ArrayList<>();
    ArrayList<TerminalSymbol> shift = new ArrayList<>();
    // 遍历项集里面的每个项目
    for (LR0Item item: set.getpItemTable()) {
      Production production = item.getProduction();
      int pos = item.getDotPosition();
      // 该项目为规约项目,添加该项目到规约项目集合
      if (pos == production.getBodySize()) {
        reduce.add(item);
      }
      // 该项目为移入项目,添加终结符到移入终结符集合
      else if (production.getpBodySymbolTable().get(pos).getType() == SymbolType.TERMINAL) {
        shift.add((TerminalSymbol) production.getpBodySymbolTable().get(pos));
      }
    }
    // 判断规约-移入冲突
    if (shift.size() > 0 && reduce.size() > 0) {
      // 遍历每个规约项目
      for (LR0Item item: reduce) {
        // 求其产生式左部的非终结符的FOLLOW集合
        Set<TerminalSymbol> follow = item.getNonTerminalSymbol().getpFollowSet();
        // FOLLOW集合不能与移入终结符集合相交,否则不为SLR(1)文法
        for (TerminalSymbol symbol: follow) {
          if (shift.contains(symbol)) {
            return false;
          }
        }
      }
    }
    // 判断规约-规约冲突
    if (reduce.size() > 1) {
      Set<TerminalSymbol> followReduce = new HashSet<>();
      for (LR0Item item: reduce) {
        // 求其产生式左部的非终结符的FOLLOW集合
        Set<TerminalSymbol> follow = item.getNonTerminalSymbol().getpFollowSet();
        for (TerminalSymbol symbol: follow) {
          // 若该FOLLOW集合中的元素在其他规约项目的FOLLOW集中出现过,则存在规约-规约冲突
          if (followReduce.contains(symbol)) {
            return false;
          }
          // 若未出现,则加入
          else {
            followReduce.add(symbol);
          }
        }
      }
    }
  }
  return true;
}

函数测试:分别用下面两个文法进行测试,第一个文法是SLR(1)文法,但第二个文法存在规约-规约冲突。
在这里插入图片描述

5)LR语法分析表的填写
实现思路:是从 0 状态开始,逐行填写。对于DFA中的每个状态,它的每条出边都要在语法分析表中对应填写一格。
①如果出边的驱动符为终结符,就填到ACTION 部分,在目标状态序号前加s,表示移入(shift)。
②如果出边为非终结符,就填到GOTO 部分,直接填上目标状态序号即可。
③如果包含规约项目,对该产生式头部非终结符的FOLLOW集合中的每个终结符,都要在其对应格中填上规约项的产生式序号,并在产生式序号前加 r,表示规约(reduce)。
④如果包含接受项目,就填到ACTION 部分,在非终结符“#”下填a,表示接受。
实现函数:
1.语法分析表

public static void getCell(DFA dfa) {
  // 遍历所有状态集合
  for (ItemSet set: allItemSet) {
    // 遍历每个项集下的所有项目
    for (LR0Item item: set.getpItemTable()) {
      Production production = item.getProduction();
      // 原点所处位置
      int pos = item.getDotPosition();
      // 该项目为规约项目,找到FOLLOW集合填r
      if (pos == production.getBodySize()) {
        // 其中该项目为接受项目,在“#”上填a
        if (item.getProduction().getProductionId() == 0) {
          // 创建对应ACTION
          ActionCell cell = new ActionCell(set.getStateId(), "#",
              ActionCategory.a, 0);
          pActionCellTable.add(cell);
        }
        // 求其产生式左部的非终结符的FOLLOW集合
        Set<TerminalSymbol> follow = item.getNonTerminalSymbol().getpFollowSet();
        // 遍历FOLLOW集合中的每个终结符
        for (TerminalSymbol symbol: follow) {
          // 创建对应ACTION
          ActionCell cell = new ActionCell(set.getStateId(), symbol.getName(),
              ActionCategory.r, item.getProduction().getProductionId());
          pActionCellTable.add(cell);
        }
        continue;
      }
      // 原点后的文法符
      GrammarSymbol symbol = production.getpBodySymbolTable().get(pos);
      // 找到该文法符驱动的下一状态
      ItemSet nextSet = dfa.findNextSet(set, symbol);
      // 该项目为移入项目,找到终结符填s
      if (symbol.getType() == SymbolType.TERMINAL) {
        // 创建对应ACTION
        ActionCell cell = new ActionCell(set.getStateId(), symbol.getName(),
            ActionCategory.s, nextSet.getStateId());
        pActionCellTable.add(cell);
      }
      // 该项目为待约项目,找到非终结符在GOTO填状态序号
      else {
        // 创建对应GOTO
        GotoCell cell = new GotoCell(set.getStateId(), symbol.getName(),
            nextSet.getStateId());
        pGotoCellTable.add(cell);
      }
    }
  }
}

2.产生式概述表

public static ArrayList<ProductionInfo> createInfo(NonTerminalSymbol symbol) {
  ArrayList<ProductionInfo> productionInfoTable = new ArrayList<>();
  for (Production production: symbol.getpProductionTable()) {
    ProductionInfo info = new ProductionInfo(symbol.getName(), production.getBodySize());
    productionInfoTable.add(info);
  }
  return productionInfoTable;
}

函数测试:文法如上,求出FIRST和FOLLOW集,求出项集及变迁边,创建DFA,基于DFA填写下列表格。
1.FIRST和FOLLOW集
在这里插入图片描述

2.产生式概述表
在这里插入图片描述在这里插入图片描述

3.语法分析表
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

收获与体会:
(1)通过此处实验对Java语言进行了一次回忆,并且对链表、Map等数据结构的使用有了进一步的心得。
(2)通过此次练习,对LL语法分析有了进一步的理解,并且在编码实现的过程中可以逐渐熟练掌握消除左递归和提取左公因子的方法。
(3)对于求FIRST函数和FOLLOW函数也有了进一步的了解,但是对判断依赖环仍有一定挑战。
(4)对于LR(0)项目的闭包求解和变迁能够较好地掌握,其中分析核心项和判断新项集作为核心内容也有了更好地掌握。
(5)基于上一次实验,本次实验求解LR(0)型DFA,在回顾DFA知识的同时掌握了语法分析的基本步骤。
(6)对于判断LL(1)文法和SLR(1)文法有了更加深入的了解,能够区分LL(1)语法分析表和LR语法分析表的差异,并且完成构造。

实验成绩

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
语法分析编译原理中的重要部分,它的作用是将词法分析阶段得到的词法单元序列转换成抽象语法树(AST)或语法分析树(Parse Tree),以便于后续的语义分析、中间代码生成和目标代码生成等环节的进行。在本次实验中,我们将使用Java语言实现一个简单的语法分析器。 实验要求: 1. 实现自顶向下的递归下降分析器。 2. 支持的文法如下: ``` <program> ::= <stmts_list> <stmts_list> ::= <stmt> | <stmts_list> <stmt> <stmt> ::= <if_stmt> | <while_stmt> | <assign_stmt> <if_stmt> ::= if <condition> then <stmts_list> end <while_stmt> ::= while <condition> do <stmts_list> end <assign_stmt> ::= <id> = <expr> <condition> ::= <expr> <relop> <expr> <expr> ::= <term> | <expr> <addop> <term> <term> ::= <factor> | <term> <mulop> <factor> <factor> ::= <id> | <number> | '(' <expr> ')' <relop> ::= '<' | '>' | '=' | '<=' | '>=' | '<>' <addop> ::= '+' | '-' <mulop> ::= '*' | '/' <id> ::= <letter> | <id> <letter> | <id> <digit> <number> ::= <digit> | <number> <digit> <letter> ::= A | B | ... | Z | a | b | ... | z <digit> ::= 0 | 1 | ... | 9 ``` 注意:文法中的关键字 if、then、end、while、do、and 等均为保留字。 3. 实现的语法分析器应具备以下功能: - 能够识别出语法正确的程序,并输出相应的语法分析树或抽象语法树。 - 能够识别出语法错误的程序,并给出相应的错误提示信息。 - 能够处理注释和空格等无意义的字符。 4. 实验提交要求: - 实验报告,包括程序设计和实验结果分析。 - 程序源代码。 实验设计思路: 1. 根据给定的文法,设计语法分析器的语法规则和对应的产生式。 2. 编写相应的Java代码,将文法转换为递归下降分析器所需要的形式。 3. 实现从输入的源代码中读取词法单元序列的功能。 4. 实现递归下降分析器的核心算法,对输入的词法单元序列进行语法分析,并构建相应的语法分析树或抽象语法树。 5. 在语法分析过程中,需要处理注释和空格等无意义的字符,以便于正确识别语法错误。 6. 在语法分析过程中,需要对输入的源代码进行错误检查,并给出相应的错误提示信息。 7. 输出语法分析树或抽象语法树,以便于后续的语义分析、中间代码生成和目标代码生成等环节的进行。 实验结果分析: 经过实验测试,我们的语法分析器能够正确地识别出合法的程序,并输出相应的语法分析树或抽象语法树。同时,它也能够正确地识别出语法错误的程序,并给出相应的错误提示信息。总的来说,本次实验取得了较好的实验效果。 实验源代码: 见下方代码框:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值