【问题标题】:How to Implement EBNF Repetition as Java Code?如何将 EBNF 重复实现为 Java 代码?
【发布时间】:2020-02-03 18:43:30
【问题描述】:

我正在用 Java 编写一个简单的递归体面解析器,它使用词法分析器将源代码从假设的命令式编程语言(看起来有点像伪代码)从文本文件拆分为单独的标记,然后检查它们是否遵循EBNF 语法规则。如果不遵守规则,程序会输出存在语法错误(不是它是什么或发生在哪里)。

EBNF 语法如下:

<program> -> program begin <statement_list> end
<statement_list> -> <statement> {;<statement>}
<statement> -> <assignment_statement> | <if_statement> | <loop_statement> 
<assignment_statement> -> <variable> = <expression>
<variable> -> identifier (An identifier is a string that begins with a letter followed by 0 or more letters and/or digits)
<expression> -> <term> {(+|-) <term>}           
<term> -> <factor> {(* | /) <factor>}
<factor> -> identifier | int_constant | (<expr>)
<if_statement> -> if (<logic_expression>) then <statement> 
<logic_expression> -> <variable> (< | >) <variable> (Assume logic expressions have only less than or greater than operators)
<loop_statement> -> loop (<logic_expression>) <statement>

这是一个没有任何语法错误的伪语言示例程序:

program
begin

sum1 = var1 + var2;
sum2 = var3 + var2 * 90;
sum3 = (var2 + var1) * var3;

if (sum1 < sum2) then
    if (var1 > var2) then
        var4 = sum2 - sum1;

loop (var1 < var2)
    var5 = var4/45

end

如您所见,每个程序都必须遵循 program begin &lt;statement_list&gt; end 格式。只有最后一条语句的末尾不需要分号。

现在,我有一个基本程序正在运行。词法分析器部分没有问题,因为用于调试目的的标记输出表明它们都已被检查,但至于 EBNF 解析,我只有将语句识别为简单变量(从小处开始)。我无法继续前进,因为我不知道如何实施涉及大括号的规则,例如 &lt;statement_list&gt; -&gt; &lt;statement&gt; {;&lt;statement&gt;}(见上文)。

根据扩展巴科斯-瑙尔形式的维基百科页面,花括号表示重复是可选的。因此,在这种语法的情况下,例如,&lt;statement_list&gt; 至少是一个 &lt;statement&gt;,可选地后跟任意数量的分号 + &lt;statement&gt;。因此,如果存在多个&lt;statement&gt;s,则除了最后一个之外,所有的都以分号结尾。

我只是不确定如何根据我的代码来实现它。我知道您必须检查语句后的标记是否是分号后跟另一个语句,但由于这是一个递归的体面解析器,并且我有返回boolean 的方法,因此递归在这里不起作用,这会留下迭代。然而,由于我实现了词法分析器以在检查后从缓冲区中删除标记,这使得通过迭代进行检查非常困难。有没有更好的方法来做到这一点?

到目前为止,这是我的 Java 程序:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.StringTokenizer;

public class SyntacticAnalysis {

    private static class Parser {

        // Parser has buffer -- which contains the source code as text -- it must read from
        private StringBuilder buffer;

        public Parser(File file) {

            // Reading from text file using simple scanner
            Scanner sc = null;

            try {

                sc = new Scanner(file);

                // Define buffer length as file length
                buffer = new StringBuilder((int)file.length());

                // Stops reading when no more text
                while(sc.hasNext()) {

                    // Token will be sequence of characters
                    String token = sc.next();

                    // Since token character sequence may contain characters such as ()+-*/=;
                    // We need to further break down token to separate special character and rest
                    // E.g. if(sum1<sum2) token will be further tokenized into: if ( sum1 < sum2 )
                    // This way spacing doesn't matter  
                    StringTokenizer tokens = new StringTokenizer(token, "()+-*/=;", true);

                    // Add tokens to buffer
                    while (tokens.hasMoreTokens())
                        buffer.append(tokens.nextToken() + " ");

                }


            } catch (FileNotFoundException fnfe) {

                System.out.print("File not found.");
                fnfe.printStackTrace();

            } finally {

                sc.close();

            }

        }

        private String lexicalAnalyzer() {

            int i = buffer.indexOf(" ");

            // Lexeme will be from beginning of buffer (location of current token) to where whitespace is
            String lexeme = buffer.substring(0, i);

            // Delete lexeme from buffer since now stored in variable
            buffer.delete(0, i + 1);

            // Print what tokens are read for debugging purposes
            System.out.println(lexeme);

            return lexeme;

        }

        // Method for main
        // Returns program() since <program> -> program begin <statement_list> end is checked first
        public boolean analyzeCode() {

            return program();

        }

        private boolean program() {

            // Every program must have program followed by begin
            if (!lexicalAnalyzer().toLowerCase().equals("program")) return false;
            if (!lexicalAnalyzer().toLowerCase().equals("begin")) return false;

            if (!statementList()) return false;

            // Every program must finish with end keyword
            if (!lexicalAnalyzer().equals("end")) return false;

            return true;

        }

        private boolean statementList() {

            if (!statement()) return false;

            /* Repetition to check further statements goes here -- how to implement? */

            return true;

        }

        private boolean statement() {

            // Define statement as variable for now just as a test
            if (!variable()) return false;

            return true;

        }

        private boolean variable() {

            // Regular expression to determine valid identifier
            // [a-zA-Z] means starts with any letter
            // [a-zA-Z0-9]* means any amount of alphanumeric characters (0 - infinity)
            if (!lexicalAnalyzer().matches("[a-zA-Z][a-zA-Z0-9]*")) return false;

            return true;

        }

    }

    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);

        System.out.print("Enter file name: ");

        String name = sc.next();

        File file = new File(name);

        Parser parser = new Parser(file);

        if (parser.analyzeCode())
            System.out.print("There are no syntax errors in the program.");
        else
            System.out.print("The program contains one or more syntax errors.");

    }

}

任何帮助将不胜感激。

【问题讨论】:

  • 这里的Scanner 完全没有意义,浪费时间和空间。 lexer 与您的问题无关。重复只是递归下降循环的问题。不清楚你在问什么。

标签: java parsing bnf ebnf


【解决方案1】:

花括号的作用是接受它们所包含的序列的 0 个或多个项目。这通常被实现为一个循环,即,当当前令牌是一个可以启动当前序列的令牌时,处理该序列。 (注意,前面的伪代码):

    while(FIRSTS contain lookAhead()){
      processCurrentBlock()
    }

这是什么“FIRSTS”?它是预先计算的一组标记,可以开始您当前正在分析的序列。例如,FIRSTS(;&lt;statement&gt;) 是“;”。

lookAhead 是您的词法分析器/扫描器中的一个函数,它返回当前令牌(不使用它)。

一般来说,序列的 FIRSTS 集不得包含该序列的 FOLLOW 集中存在的任何标记。 FOLLOW 是可能出现在该序列之后的标记集。

幸运的是,您语法中的三个重复块的 FIRSTS 集与它们的 FOLLOW 集不同。即,序列;&lt;statement&gt;的FIRSTS是[';'],序列(+|-) &lt;term&gt;的FIRSTS是['+', '-'],并且序列(* | /) &lt;factor&gt;的FIRSTS是['*', '/']

只需为每个块实现该算法即可。

【讨论】:

  • 这很有帮助!这个概念现在对我来说很有意义。我知道我需要使用某种类型的lookAhead(),所以直觉就在那里,但由于这是我第一次接触解析器,我不知道如何去做。谢谢人:)
猜你喜欢
  • 2019-04-15
  • 2015-12-06
  • 1970-01-01
  • 1970-01-01
  • 2013-02-15
  • 1970-01-01
  • 2020-06-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多