编译原理-消除左递归算法(java代码实现)

本篇文章内的源码: 这里

当我们自顶向下的语法分析时,就需要采用最左推导方式。
而这个时候,如果产生式左部和产生式右部首字符一样(即A→Aα),那么推导就可能陷入无限循环。
例如:

文法G
1.  S -> Sa | b

推导
S => Sa => Saa => ... => Sa...a 

因此对于:

  • 含有 A→Aα 形式产生式的文法称为是直接左递归
  • 如果文法中一个非终结符A,存在一步以上的推导,形成了 A =>+ Aα,称为间接左递归

    例如:A→BβB -> Aβ 可以得到 A => Bβ => Aββ

文法中不能包含这两种形式,不然最左推导就没办法进行。

那有人问了,如果产生式右部中间包含和产生式左部相同的字符,允不允许呢?

  • 大多数情况下,是允许的,因为我们采用的是最左推导,只要这个产生式右部在这个非终结符之前有其他终结符(即A→αA),那么就可以确定推导时,用不用这个产生。
  • 如果产生式右部在这个非终结符之前是非终结符(即A→BAβ),那么就要看 B 能不能推导出空串ε 了,如果可以,那么最终还是会推导出 A =>+ Aβ, 也属于间接左递归

一. 算法描述

1.1 消除直接左递归

例如:

文法G
1.  S -> Sa | b

那么该如何改造呢?
其实我们仔细分析一个这组产生式,我们发现它可以进行如下推导:

S => b
或者
S => Sa
  => Saa 
  ... 
  => Sa...a 
  => ba..a

你会惊奇的发现,它能推导出 b 和 (a)* (即由0a或者无数个a生成的文法符号串)。其实就可以改造成:

文法G
1.  S -> bS'
2.  S' -> aA' | ε

S' 就可以表示 (a)* ,因此S'必须有空产生式 S' -> ε,否则就得不到0a的空串。

因此消除直接左递归 算法的一般形式:

  A -> Aα1 | Aα2 | ... | Aαn | β1 | β2 | ... | βm
(其中这里数字表示下标,而不是一个终结符, α 和 β都是文法符号串)
         (αi != ε, 且 βi 不以A开头)
                改造成
A -> β1A' | β2A' | ... | βmA'
A' ->  α1A' | α2A' | ... | αnA' |  ε

1.2 消除间接左递归

例如:

文法G:
1. S -> Aa | b
2. A -> c | Sd

间接左递归
S => Aa
  => Sda
  => Aada
  => Sdada

消除间接左递归的方法就是直接带入消除,即

文法G:
1. S -> Aa | b
2. A -> c | Aad | bd

即将能形成间接左递归产生式中的非终结符,替换成这个非终结符的产生式组。

因此消除间接左递归 算法的一般形式:

按照某个顺序将非终结符进行排序,为A1,A2,...,An
for (从1到n的每个i) {
      for (从1到i-1的每个j) {
            将每个形如Ai -> Ajβ的产生式,用Aj 的产生式组Aj -> α1 | α2 | ... | αk 替换;
            得到产生式 Ai -> Ajβ 替换后的产生式组 Ai -> α1β | α2β | ... | αkβ。
    }
}

这个算法看起来描述很多,其实理解起来很简单:

  • 先将这些非终结符进行排序,然后开始遍历这些非终结符的产生式组
  • 判断当前非终结符Ai的每一个产生式右部首字符,是不是前面已经遍历过的非终结符Aj

    这个就是为什么需要对非终结符进行排序

  • 如果是那么就会形成间接左递归,就要用前面非终结符Aj的产生式组当前非终结符Ai这个产生式中的非终结符Aj,这样当前这个产生式变成了多个产生式了。

二. 算法实现

2.1 Symbol

/**
 * @author by xinhao  2021/8/13
 * 表示文法中字母表中一个符号,包括终结符和非终结符
 */
public class Symbol implements Comparable<Symbol> {
    public static final String  EPSILON = "ε";
    // 文法结束符号
    public static final Symbol END = new Symbol("$", true);
    // 表示符号的值, 这里用 String 表示,而不是 char,
    // 是因为有些符号我们不好用一个 char 表示。 比如 A 对应的 A'
    private final String label;

    private final boolean isTerminator;

    // 外部不能创建
    Symbol(String label, boolean isTerminator) {
        this.label = label;
        this.isTerminator = isTerminator;
    }

    public String getLabel() {
        return label;
    }

    public boolean isTerminator() {
        return isTerminator;
    }

    // 这里只判断 label 值
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Symbol symbol = (Symbol) o;
        return Objects.equals(label, symbol.label);
    }

    @Override
    public int hashCode() {
        return Objects.hash(label);
    }

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

    @Override
    public int compareTo(Symbol o) {
        return label.compareTo(o.label);
    }
}

用来表示文法中字母表中一个符号,包括终结符和非终结符

2.2 Alphabet

/**
 * @author by xinhao  2021/8/13
 * 字母表, 这个类不用实例化。用来记录字母表中所有的符号
 */
public abstract class Alphabet {

    /**
     * 字母表中所有的符号
     */
    public static final Map<String, Symbol> ALL_SYMBOL = new HashMap<>();

    // 初始化
    static {
        for (int index = 0; index <= 9; index++) {
            ALL_SYMBOL.put("" + index, new Symbol("" + index, true));
        }
        // a-z
        for (char ch = 97; ch <= 122; ch++) {
            ALL_SYMBOL.put(String.valueOf(ch), new Symbol(String.valueOf(ch), true));
        }
        // A - Z
        for (char ch = 65; ch <= 90; ch++) {
            ALL_SYMBOL.put(String.valueOf(ch), new Symbol(String.valueOf(ch), false));
        }
        ALL_SYMBOL.put("+", new Symbol("+", true));
        ALL_SYMBOL.put("-", new Symbol("-", true));
        ALL_SYMBOL.put("*", new Symbol("*", true));
        ALL_SYMBOL.put("(", new Symbol("(", true));
        ALL_SYMBOL.put(")", new Symbol(")", true));
    }

    public static Symbol addSymbol(char ch) {
        return addSymbol(ch, true);
    }

    public static Symbol addSymbol(char ch, boolean isTerminator) {
        return addSymbol(String.valueOf(ch), isTerminator);
    }

    public static Symbol addSymbol(String label) {
        return addSymbol(label, true);
    }

    public static Symbol addSymbol(String label, boolean isTerminator) {
        Symbol symbol = new Symbol(label, isTerminator);
        ALL_SYMBOL.put(label, symbol);
        return symbol;
    }

    public static Symbol getSymbol(char ch) {
        return getSymbol(String.valueOf(ch));
    }

    public static Symbol getSymbol(String label) {
        return ALL_SYMBOL.get(label);
    }
}

表示字母表,用来记录字母表中所有的符号。

2.3 Production

/**
 * @author by xinhao  2021/8/13
 * 文法中的产生式
 */
public class Production {
    public static final List<Symbol> EMPTY = new ArrayList<>(0);
    /** 产生式左边,而且必须是非终结符 */
    private final Symbol left;

    /** 这里的 List<Symbol> 希望是不可变的,你们可以自己引入 ImmutableList */
    private final List<Symbol> right;

    /** 表示这个产生式不是左递归产生式 */
    private final boolean isLeftEliminate;
   ....
}

用来表示文法中的一个产生式,例如 A -> bB

2.4 Grammar

public class Grammar {

    // 开始符号
    private final Symbol start;
    /**
     * 这里为什么用 map 类型?
     * S -> aA | bB
     * A -> aaaa
     * A -> abab | ε
     * B -> bbb
     * B -> bababa
     * B -> ε
     * 所以对于同一个左部 Symbol (A) 其实是有多个产生式的,就是产生式租
     * 使用 LinkedHashMap ,因为产生式是要有记录顺序的
     */
    private final LinkedHashMap<Symbol, List<Production>> productions;
   ...
}

表示一个文法,包含文法开始符号和产生式组,终结符号集合非终结符号集没有包含在这个里面。

2.5 消除直接左递归算法

public class Production {
 ......
    public boolean isLeftEliminate() {
        if (right.isEmpty()) {
            return false;
        }
        // 产生式左部 与 产生式第一个字母相等,表示它是一个直接递归的产生式。
        return left.equals(right.get(0));
    }
} 


    /**
     * 消除直接左递归
     * 就是将这种格式  P -> Pa|Pb|Pc|Pd|e|f|g|h
     * 转成:
     * P -> eP'|fP'|gP'|hP'
     * p'-> aP'|bP'|cP'|dP'|ε
     *
     */
    public static Grammar eliminateDeepLeftRecursion(Grammar grammar) {
        List<Production> productions = new ArrayList<>();
        for (Map.Entry<Symbol, List<Production>> entry : grammar.getProductions().entrySet()) {
            // 不包含直接左递归的 产生式集合
            List<Production> noLeftEliminateProductions = new ArrayList<>();
            // 包含直接左递归的 产生式集合; 即 A -> Aa 这种形式
            List<Production> leftEliminateProductions = new ArrayList<>();
            for (Production production : entry.getValue()) {
                if (production.isLeftEliminate()) {
                    leftEliminateProductions.add(production);
                } else {
                    noLeftEliminateProductions.add(production);
                }
            }

            // 表示这个 entry.getKey() 对应的产生式组 List<Production> 没有直接左递归产生式
            if (leftEliminateProductions.isEmpty()) {
                // 不用做任何修改,直接将 noLeftEliminateProductions 添加到 productions
                productions.addAll(noLeftEliminateProductions);
            } else {
                // 需要进行转换了, 即 A 变成 A'
                Symbol newKey = Alphabet.addSymbol(entry.getKey().getLabel() + "'", false);
                // 将原来的 P -> e|f|g|h
                // 变成    P -> eP'|fP'|gP'|hP'
                for (Production production : noLeftEliminateProductions) {
                    // 创建新的产生式右部  e 变成 eP' 或者  f 变成 fP'
                    List<Symbol> newRight = production.addSymbolToRight(newKey);
                    // 这个产生式的 left 还是 原来的
                    productions.add(Production.create(entry.getKey(), newRight));
                }

                // 将原来的 P -> Pa|Pb|Pc|Pd
                // 变成    P -> aP'|bP'|cP'|dP'|ε
                for (Production production : leftEliminateProductions) {
                    // 创建新的产生式右部  Pa 变成 aP' 或者  Pd 变成 dP'
                    List<Symbol> newRight = production.addSymbolToRightAndRemoveFirst(newKey);
                    // 这个产生式的左部,就必须是新的 key 了
                    productions.add(Production.create(newKey, newRight));
                }

                // 最后对这个新左部,必须添加 空产生式
                productions.add(Production.createEmpty(newKey));
            }
        }
        return Grammar.create(grammar.getStart(), productions);
    }

其实算法很简单,就是遍历文法中,每一个非终结符对应的产生式组,查看是否包含直接左递归的形式,如果包含就将它变成两个产生式组:

P -> Pa|Pb|Pc|Pd|e|f|g|h
变成:
P -> eP'|fP'|gP'|hP'
 p'-> aP'|bP'|cP'|dP'|ε

2.6 消除间接左递归算法

    /**
     * 消除间接左递归
     * 如同 S -> Aa|b   A -> Ac|Sd
     * 会产生  S ==> Aa ==> Sda
     * 消除间接左递归变成
     * S -> Aa|b
     * A -> Ac|Aad|bd
     */
    public static Grammar eliminateIndirectLeftRecursion(Grammar grammar) {
        // 记录之前遍历的 产生式
        LinkedHashMap<Symbol, List<Production>> preProductionsMap = new LinkedHashMap<>();
        for (Map.Entry<Symbol, List<Production>> entry : grammar.getProductions().entrySet()) {

            List<Production> entryProductions = new ArrayList<>();
            for (Production production : entry.getValue()) {
                // 得到产生式右部第一个字符,例如 A -> Sd 其中 S 就是第一个字符
                Symbol symbol = production.getRightFirst();
                // 这个产生式右部第一个字符之前出现过,例如 S -> Aa A -> Sd ,这个 S 就在之前出现过,
                // 那么就产生了间接左递归,那么就需要做转换了。就使用 S 的产生式组,替换它在A的中产生式。
                // symbol = null 没有关系,因为我们不会将 null 存入 preProductionsMap 集合中
                if (preProductionsMap.containsKey(symbol)) {
                    // 将 symbol 添加到 leftEliminateSet 集合中,表示它是间接左递归
                    List<Production> eliminateList = preProductionsMap.get(symbol);
                    for (Production eliminateProduction : eliminateList) {
                        // 例如 S -> Aa|b   A -> Ac|Sd,
                        // 这里 eliminateList 就是 [Aa, b], 要用它替换产生式[Sd],变成 [Aad, bd]
                        entryProductions.add(Production.create(production.getLeft(),
                                production.replaceRightFirst(eliminateProduction)));
                    }
                } else {
                    // 不是间接左递归,那么就直接添加
                    entryProductions.add(production);
                }
            }
            preProductionsMap.put(entry.getKey(), entryProductions);
        }
        return Grammar.create(grammar.getStart(), preProductionsMap);
    }

算法其实也很简单,按照一定顺序,遍历文法中非终结符对应的产生式组,如果发现有产生式右部的首字符与之前遍历过的非终结符相同,说明有间接左递归现象,那么就要这个非终结符对应的产生式组替换这个非终结符。

但是注意这里判断是否有间接左递归,只是靠产生式右部的首字符与之前遍历过的非终结符相同;其实这个是不完整的。

  • 例如有三个产生式 A -> B B -> CA C -> ε,那么非终结符 B 是可以推导出 B => CA => A 的,所以这种情况也属于间接左递归。
  • 所以判断间接左递归,不止要看首字符,还要包括产生式右部包含前面出现的字符(即 B -> αA),那么文法符号串 α的串首终结符集FIRST(α) 是否包含空串 ε。这个算法以后再介绍。

2.7 例子

public static void main(String[] args) {
        Symbol start = Alphabet.getSymbol('S');
        List<Production> productions = new ArrayList<>();
        productions.addAll(Production.createList(start, "Aa", "b", "ε"));
        productions.addAll(Production.createList(Alphabet.getSymbol('A'), "Ac", "Sd"));
        Grammar grammar = Grammar.create(start, productions);
        Grammar indirectGrammar =  GrammarUtils.eliminateIndirectLeftRecursion(grammar);
        Grammar deepGrammar =  GrammarUtils.eliminateDeepLeftRecursion(indirectGrammar);
        System.out.println(grammar);
        System.out.println(indirectGrammar);
        System.out.println(deepGrammar);
}

运行结果:

开始符号:S
产生式:
                S->Aa|b|ε
                A->Ac|Sd

开始符号:S
产生式:
                S->Aa|b|ε
                A->Ac|Aad|bd|d

开始符号:S
产生式:
                S->Aa|b|ε
                A->bdA'|dA'
                A'->cA'|adA'|ε
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容