【问题标题】:Finite State Machine parser有限状态机解析器
【发布时间】:2011-03-06 07:57:45
【问题描述】:

我想用类似 FSM 的解析器在 C++ 中解析一个自行设计的文件格式(这是一个 teach-myself-c++-the-hard-way-by-doing-something-big-and-difficult 类型的项目 :))。我有一个带有换行符的标记化字符串,表示 euh... 行的结尾。见here for an input example。所有的 cmets 都会和垃圾被过滤掉,所以我有一个像这样的 std::string:

global \n { \n SOURCE_DIRS src \n HEADER_DIRS include \n SOURCES bitwise.c framing.c \n HEADERS ogg/os_types.h ogg/ogg.h \n } \n ...

语法解释:

  • { } 是范围,大写的单词表示后面有一个选项/文件列表。
  • \n 仅在选项/文件列表中很重要,表示列表的结尾。

所以我认为 FSM 足够简单/可扩展,足以满足我的需求/知识。据我所知(并希望我的文件设计如此),我不需要并发状态或类似的东西。一些设计/实施问题:

  1. 我应该为我的状态使用enum 或抽象class + 派生词吗?第一个可能更适合小语法,但以后可能会变得丑陋,而第二个恰恰相反。我倾向于第一个,因为它很简单。 enum exampleclass example。编辑:this suggestion 对于goto 怎么样,我认为它们在 C++ 中是邪恶的?
  2. 阅读列表时,我不需要忽略\n。我通过stringstream 使用string 的首选方式默认会忽略\n。所以我需要一种简单的方式告诉(同样的!)stringstream 在启用某个状态时不要忽略换行符。
  3. 简单的enum 状态是否足以进行多级解析({...{...}...} 范围内的范围)还是需要 hacky 实现?
  4. 这是我想到的草案状态:
    • upper:读取全局、exe、lib+ 目标名称...
    • normal:在作用域内,可以读取 SOURCES...,创建用户变量...
    • list:将项目添加到列表中,直到遇到换行符。

每个作用域都有一种条件(例如 win32:global { gcc:CFLAGS = ... }),并且需要在任何地方以完全相同的方式处理(即使在list 状态下,每个项目) .

感谢您的任何意见。

【问题讨论】:

  • 会飞的意大利面怪物?
  • goto不是邪恶的。 滥用 goto 是邪恶的。正确使用goto 可以使代码更易于阅读和遵循,而我所知道的正确用例之一就是编码 FSM。为什么这么多人坚持goto 是邪恶的?宗教。
  • 问题是滥用造成无法辨认的怪物。

标签: c++ parsing stream fsm


【解决方案1】:

如果您有嵌套范围,那么有限状态机不是正确的方法,您应该查看上下文无关语法解析器。 LL(1) parser 可以编写为一组递归函数,或者 LALR(1) parser 可以使用解析器生成器(例如 Bison)编写。

如果您将堆栈添加到 FSM,那么您将进入pushdown automaton 领域。非确定性下推自动机等价于上下文无关文法(尽管deterministic pushdown automaton 严格来说没有那么强大。)LALR(1) 解析器生成器实际上在内部生成确定性下推自动机。一本好的编译器设计教科书将涵盖从语法构造下推自动机的确切算法。 (通过这种方式,添加堆栈并不是“hacky”。​​)This Wikipedia article 还描述了如何根据您的语法构造 LR(1) 下推自动机,但是 IMO,这篇文章并没有想象的那么清晰。

如果您的作用域嵌套深度有限(即您有uppernormallist 级别,但您没有嵌套lists 或嵌套normals),那么您可以使用没有堆栈的 FSM。

【讨论】:

  • 在我看来,如果您维护一堆机器状态(范围),FSM 可以正常工作。
  • @Zan 我已经编辑了我的答案以回复您的评论。您建议的修改具有很好的理论基础,并且已经非常详细地形式化。我建议阅读编译器设计,以了解更多关于当今解析器生成器的实现方式。
  • 我打算支持无限嵌套,比如SOURCES main.cpp win32:{ msvc:class_winmsvc.cpp gcc:class_wingcc.cpp} mac:{ class_mac.cpp otherstuff.cpp }。也许不是需要这个的最好的编码项目,但它对我来说似乎非常强大。这确实提出了需要在堆栈中保留序列的问题,以准确了解您在范围内的位置。这会自行解决,还是需要特别注意?谢谢
【解决方案2】:

对于解析,我总是尝试使用已经证明有效的东西:ANTLRANTLRWorks,这对设计和测试语法有很大帮助。您可以为 C/C++(和 other languages)生成代码,但您需要为这些语言构建 ANTLR 运行时。

当然,如果您发现 flexbison 更易于使用,您也可以使用它们(我知道它们只生成 C 和 C++,但我可能错了,因为我有一段时间没有使用它们了)。

【讨论】:

    【解决方案3】:

    分析文本输入流以进行解析有两个阶段:

    词法分析: 这是您的输入流被分解为词法单元的地方。它查看一系列字符并生成标记(类似于口语或书面语言中的单词)。有限状态机非常擅长词法分析,前提是您已经对词法结构做出了良好的设计决策。从您上面的数据来看,单个词位可能是您的关键字(例如“global”)、标识符(例如“bitwise”、“SOURCES”)、符号标记(例如“{”“}”、“.”、“/”) )、数值、转义值(例如“\n”)等

    句法/语法分析:在生成标记序列时(或者可能在您这样做时),您需要能够分析结构以确定标记序列是否与你的语言设计。为此,您通常需要某种解析器,但如果语言结构不是很复杂,您可以使用有限状态机来代替。一般来说(因为您特别希望在您的案例中使用嵌套结构)您将需要使用 Ken Bloom 描述的技术之一。

    所以回答你的问题:

    我应该为我的状态使用枚举还是抽象类 + 派生类?

    我发现对于小型标记器,状态/转换值矩阵是合适的,例如next_state = state_transitions[current_state][current_input_char]。在这种情况下,next_statecurrent_state 是一些整数类型(可能包括枚举类型)。当您转换到无效状态时,会检测到输入错误。令牌的结束是基于有效结束状态的状态标识来标识的,在给定下一个输入字符的情况下,没有有效的转换可用于另一个状态。如果您担心空间,则可以改用地图矢量。制作 states 类是可能的,但我认为这可能会让事情变得比你需要的更困难。

    阅读列表时,我不需要忽略\n。

    您可以创建一个名为“\n”的标记,或者创建一个更通用的转义标记(一个以反斜杠开头的标识符。如果您正在谈论识别源中的换行符,那么这些只是您需要的字符在您的状态转换矩阵中创建转换(但是请注意 Unix 和 Windows 换行符之间的区别;您可以创建一个在其中任何一个上运行的 FSM)。

    简单的枚举状态是否足以进行多级解析(范围在 {...{...}...} 范围内)还是需要 hacky 实现?

    这是您需要语法或下推自动机的地方,除非您可以保证嵌套不会超过某个级别。即便如此,它也可能会使您的 FSM 变得非常复杂。

    这是我想到的草案状态:...

    请参阅上面我对词汇和语法分析的评论。

    【讨论】:

    • 我不再需要任何标记。在我的示例中,每个非空白单词都是一个标记,我的状态将根据 string/token 而不是单个 char 更改。 #2 实际上是iostream 特定问题:是否可以设置流的“空白”定义(例如,当使用operator>> 读取新令牌时)。感谢您的意见。
    • @rubenvb:我想我的意思是,您需要明确区分基于 FSM 的词法分析的可能性和不可能性。如果您已经完成了标记化,那么根据您的问题,您需要开始做一些事情来执行句法/语法分析。这可能意味着语法,而不是 FSM。
    • 在 FSM(+stack=pushdown automaton) 中实现的语法怎么样?我不想再写一个语法描述,我希望它在 C++ 中读取并产生输出。我认为(对于我的例子来说肯定是)FSM(+stack)对于语法的东西来说是不可能的,不是吗?
    • @rubenvb:根据您的描述,下推自动机可以工作。问题是您是否可以开发一种比非确定性PDA 更易于实施和使用的确定性PDA。根据我所见,您可能可以为它定义一个确定性 PDA,但我不确定。
    猜你喜欢
    • 1970-01-01
    • 2012-01-07
    • 2016-07-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多