草庐IT

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

wo883721 2023-09-13 原文

本篇文章内的源码: 这里

当我们自顶向下的语法分析时,就需要采用最左推导方式。
而这个时候,如果产生式左部和产生式右部首字符一样(即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'|ε

有关编译原理-消除左递归算法(java代码实现)的更多相关文章

  1. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  2. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  3. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  4. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  5. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  6. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  7. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  8. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  9. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  10. ruby - Sinatra set cache_control to static files in public folder编译错误 - 2

    我不知道为什么,但是当我设置这个设置时它无法编译设置:static_cache_control,[:public,:max_age=>300]这是我得到的syntaxerror,unexpectedtASSOC,expecting']'(SyntaxError)set:static_cache_control,[:public,:max_age=>300]^我只想将“过期”header设置为css、javaascript和图像文件。谢谢。 最佳答案 我猜您使用的是Ruby1.8.7。Sinatra文档中显示的语法似乎是在Ruby1.

随机推荐