nodejs First Follow Select集合 算法分析和代码

本文介绍了LL1编译原理,通过一个实例详细讲解了First、Follow、Select集合的计算过程,并提供了相关算法的代码实现。内容包括文法数据结构、消除二义性和左递归、First集合的优化、Follow集合的复杂计算以及Select集合的生成,最终得出LL1分析表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LL1

前言

LL1是编译原理自上至下分析方法的一种,是其他分析方法的基础。本篇文章将通过一个例子附带代码讲述整个过程。读者一定要喜欢哦。

文法数据结构

const grammar = {
  S: [["M", "H"], ["a"]],
  H: [["L", "S", "o"], [EMPTY]],
  K: [["d", "M", "L"], [EMPTY]],
  L: [["e", "H", "f"]],
  M: [["K"], ["b", "L", "M"]],
};

上述的数据结构表示的文法为

  1. S -> MH | a
  2. S -> LSo | ε
  3. S -> dML | ε
  4. L -> eHf
  5. M -> K | bLM

终结符和非终结符数据结构

const nonTermialSymbol = new Set(["S", "H", "K", "L", "M"]);
const terminalSymbol = new Set([EMPTY, "a", "b", "d" ,"e", "o", "$", "f"]);

上面将 ε 和 $ 归结于终结符的目的是为了后面算法简单。

消除二义性文法

对于多数的二义性文法,可以在建立LL1表的时候给予用户提示,为了消除二义性文法可能会引入多余的文法,这是不值得的,例如对于 if else 的就近匹配问题,我们在后期解决的时候可以选择合适的解决文法冲突。

消除左递归和提取左公因子

请读者自行消除左递归和提取左公因子后放入上述文法数据结构中,由于该步骤人工处理较为简单,可以自行完成。

First 集合

First(X) 集合表示可以从 X 退出所有串首终结符

  1. 若 X 是一个终结符,那么 First(X) = { X }

  2. 若存在表达式 X->Y1Y2Y3…Yn,将Y1的First集合加入First(X)中,若First(X)包含ε则迭代Y2,将Y2的First集合加入First(X) 中,循环迭代至结束。

  3. 若存在表达式 X -> ε,则将ε加入First(X)中。

算法优化:

可以看出,若视ε为终结符,1和3可以合并。

合并后,1和2也可以合并,只要单独判断是否为终结符就可以了。

代码:

const EMPTY = "ε"
const firstSet = {};
const processFirst = () => {
  for (let nts of nonTermialSymbol) firstSet[nts] = new Set();
  for (let nts of nonTermialSymbol) _processFirst(nts);
};

const _processFirst = (nts) => {
  for (let grammarArr of grammar[nts]) {
    const len = grammarArr.length;
    for (let i = 0; i < len; i += 1) {
      const ch = grammarArr[i];
      if (terminalSymbol.has(ch)) {
        firstSet[nts].add(ch);
        break;
      } else {
        _processFirst(ch);
        for (let v of firstSet[ch]) firstSet[nts].add(v);
        if (!firstSet[ch].has(EMPTY)) break;
      }
    }
  }
};

这里的话,有可以改进的地方,例如某个first集合算过后,就可以直接省略计算,即加一个标志位标志一下就可以了,但是由于First集合算法本身不太耗时,而且只是为了导出LL1分析表,拿到LL1分析表后,其实在之后的编译过程中不会重复用到,因此其实优化其实可有可无,感兴趣的读者可以自行加上。

Follow 集合

  1. 若S是开始符号,则将 $ 添加到 Follow (S) 中
  2. 若存在一个产生式 X -> Y1Y2Y3…Yn 则 Follow(Yn) 集合需要加上 Follow(X) 集合,若 First (Yn) 包含 ε 则,向左迭代,即 Follow(Yn-1) 集合需要加上 Follow(X) 集合,若 First (Yn-1) 包含 ε 则,向左迭代… 迭代至表达式结束。
  3. 若存在一个表达式 X -> ABCD 则 Follow(A) 需要加上 First(B) - ε,若First(B) 包含 ε,则Follow(A) 需要加上 First© - ε,向右迭代… 迭代至表达式结束。

从算法层面来看,这个follow集合的计算是比较复杂的,而且需要重复计算,直到先前的follow集合和后来的follow集合相等才停止。

const { cloneDeep, isEqual } = require("lodash");
const START = "S";
const END = "$";
const followSet = {};
let prevFollowSet = {};
const processFollow = () => {
  for (let nts of nonTermialSymbol) followSet[nts] = new Set();
  followSet[START].add(END);
  while (true) {
    let flag = false;
    for (let nts of nonTermialSymbol) {
      _processFollow(nts);
      if (isEqual(prevFollowSet, followSet)) flag = true;
      else flag = false;
      prevFollowSet = cloneDeep(followSet);
    }
    if (flag) break;
  }
};

const _processFollow = (_nts) => {
  // 3 若存在一个表达式 X -> ABCD 则 Follow(A) 需要加上 First(B) - ε,若First(B) 包含 ε,则Follow(A) 需要加上 First(C) - ε,向右迭代... 迭代至表达式结束。
  for (let nts of nonTermialSymbol) {
    for (let grammarArr of grammar[nts]) {
      const len = grammarArr.length;
      let index = -1;
      // 找到 B
      for (let i = 0; i < len; i += 1) {
        const ch = grammarArr[i];
        if (ch === _nts) {
          index = i;
          break;
        }
      }
      if (index === -1) break;
      while (index + 1 < len) {
        const ch = grammarArr[index + 1];
        if (terminalSymbol.has(ch)) {
          followSet[_nts].add(ch);
          break;
        } else {
          const set = firstSet[ch];
          for (let v of set) if (v !== EMPTY) followSet[_nts].add(v);
          if (!set.has(EMPTY)) break;
          index += 1;
        }
      }
    }
  }
  // 2 若存在一个产生式 X -> Y1Y2Y3...Yn 则  Follow(Yn) 集合需要加上 Follow(X) 集合,若 First (Yn) 包含 ε 则,向左迭代,即 Follow(Yn-1) 集合需要加上 Follow(X) 集合,若 First (Yn-1) 包含 ε 则,向左迭代... 迭代至表达式结束。
  const set = followSet[_nts];
  for (let grammarArr of grammar[_nts]) {
    const len = grammarArr.length;
    for (let i = len - 1; i >= 0; i -= 1) {
      const ch = grammarArr[i];
      if (terminalSymbol.has(ch)) break;
      const newSet = followSet[ch];
      for (let v of set) {
        newSet.add(v);
      }
      if (!firstSet[ch].has(EMPTY)) {
        break;
      }
    }
  }
};

由于需要深度克隆和判断集合的值是否相等,因此引入了 lodash

Select 集合

是创造 LL1 表的最后一步工作。为了计算Select集合,首先要先算文法表达式右侧的First集合。

即对 A -> BCD

  1. 计算 First(BCD)。则先计算First(B),若包含ε,计算First©,若仍包含ε,则继续往下迭代。直到遇到终结符或者遇到非εFirst集合。
  2. 若First(BCD) 包含 ε,则Select(A)= First(BCD) 和 Follow(A) 的并集合去掉 ε。若不包含 Select(A) = First(BCD)
const processSelect = () => {
  for (let nts of nonTermialSymbol) {
    for (let grammarArr of grammar[nts]) {
      const set = new Set();
      const len = grammarArr.length;

      // 1. 计算 First(BCD)。则先计算First(B),若包含ε,计算First(C),若仍包含ε,则继续往下迭代。直到遇到终结符或者遇到非εFirst集合。
      for (let i = 0; i < len; i++) {
        const ch = grammarArr[i];
        if (terminalSymbol.has(ch)) {
          set.add(ch);
          break;
        }
        for (let v of firstSet[ch]) {
          set.add(v);
        }
        if (!firstSet[ch].has(EMPTY)) break;
      }
      // 2. 若First(BCD) 包含 ε,则Select(A)= First(BCD) 和 Follow(A) 的并集合去掉 ε。若不包含 Select(A) = First(BCD)
      if (set.has(EMPTY)) {
        selectSet[`${nts} -> ${grammarArr.join(" ")}`] = new Set(
          [...set, ...followSet[nts]].filter((v) => v !== EMPTY)
        );
      } else {
        selectSet[`${nts} -> ${grammarArr.join(" ")}`] = set;
      }
    }
  }
};

LL1 集合

利用 Select 集合填上即可

const processLL1 = () => {
  for (let nts of nonTermialSymbol) {
    LL1Table[nts] = {};
  }
  Object.keys(selectSet).forEach((key) => {
    const vt = key.split(" ->")[0];
    const set = selectSet[key];
    for (let v of set) {
      if (typeof LL1Table[vt][v] !== "undefined") {
        console.warn("---------------");
        console.warn(
          "请检查是否文法中含有二义性文法, 是否消除了左递归以及提取左公因子"
        );
        console.warn(`${key}${LL1Table[vt][v]}, 在 ${v} 存在 LL1 表冲突`);
        console.warn("---------------");
        return;
      }
      LL1Table[vt][v] = key;
    }
  });
};

结果

最后得到的结果如下

Terminal | First          
S        |  { d ε b e a } 
H        |  { e ε }       
K        |  { d ε }       
L        |  { e }         
M        |  { d ε b }     

Terminal | Follow           
S        |  { $ o }         
H        |  { $ o f }       
K        |  { $ o e }       
L        |  { d b e a o $ } 
M        |  { $ o e }       

grammar    | Select         
S -> M H   |  { d b e $ o } 
S -> a     |  { a }         
H -> L S o |  { e }         
H -> ε     |  { $ o f }     
K -> d M L |  { d }         
K -> ε     |  { $ o e }     
L -> e H f |  { e }         
M -> K     |  { d $ o e }   
M -> b L M |  { b }         

LL1 | a      | b          | d          | e          | o        | $        | f     
S   | S -> a | S -> M H   | S -> M H   | S -> M H   | S -> M H | S -> M H |       
H   |        |            |            | H -> L S o | H -> ε   | H -> ε   | H -> ε
K   |        |            | K -> d M L | K -> ε     | K -> ε   | K -> ε   |       
L   |        |            |            | L -> e H f |          |          |       
M   |        | M -> b L M | M -> K     | M -> K     | M -> K   | M -> K   |       
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值