3

我正在为 Python 开发一个宏系统(如此处所讨论的),我一直在考虑的一件事是度量单位。尽管可以在没有宏或通过静态宏(例如提前定义所有单位)的情况下实现度量单位,但我正在玩弄允许在运行时动态扩展语法的想法。

为此,我正在考虑在编译时对代码使用一种部分评估。如果对给定表达式的解析失败,由于其语法的宏不可用,编译器将停止对函数/块的评估,并生成它已经拥有的代码,其中包含未知表达式所在的存根。当此存根在运行时被命中时,该函数将针对当前宏集重新编译。如果此编译失败,将引发解析错误,因为执行无法继续。如果编译成功,新函数将替换旧函数并继续执行。

我看到的最大问题是,在受影响的代码运行之前,您无法找到解析错误。但是,这不会影响很多情况,例如 []、{}、() 和 `` 等组运算符仍然需要配对(我的标记器/列表解析器的要求),以及类和函数等顶级语法不会受到影响,因为它们的“运行时”实际上是加载时间,在那里评估语法并生成它们的对象。

除了实现困难和我上面描述的问题,这个想法还有什么问题?

4

6 回答 6

3

这里有几个可能的问题:

  • 如果出现问题,您可能会发现很难向用户提供有用的错误消息。这似乎很可能,因为任何编译时语法错误都可能只是语法扩展。
  • 性能受到打击。

我试图找到一些关于 Perl 6 中动态解析的优点、缺点和/或实现的讨论,但我找不到任何合适的东西。但是,您可能会发现 Nicklaus Wirth(Pascal 和其他语言的设计者)的这句话很有趣:

1960 年代计算机科学家的幻想是无限的。由于自动语法分析和解析器生成的成功,一些人提出了灵活的或至少可扩展的语言的想法。这个概念是一个程序之前将有句法规则,然后在解析后续程序时指导通用解析器。更进一步:语法规则不仅在程序之前,而且可以散布在整个文本的任何地方。例如,如果有人希望使用一种特别花哨的私有形式的 for 语句,他可以优雅地这样做,甚至在同一程序的不同部分为同一概念指定不同的变体。语言用于人类之间交流的概念已经完全融合,显然,现在每个人都可以即时定义自己的语言。然而,在试图说明这些私人建筑应该意味着什么时遇到的困难很快就使寄予厚望的希望破灭了。结果,可扩展语言的有趣想法很快就消失了。

编辑:这是 Perl 6 的Synopsis 6: Subroutines,不幸的是标记形式,因为我找不到更新的格式化版本;在里面搜索“宏”。不幸的是,它不是有趣,但是您可能会发现一些相关的东西,例如 Perl 6 的一次性解析规则,或者它用于抽象语法树的语法。Perl 6 采用的方法是,宏是一个函数,它在其参数被解析后立即执行并返回一个 AST 或一个字符串;Perl 6 继续解析,就好像源实际上包含返回值一样。提到了错误消息的生成,但它们看起来好像宏返回 AST,你可以做的很好。

于 2009-01-27T12:58:26.413 回答
2

更进一步,您可以进行“惰性”解析,并且始终只解析到足以评估下一条语句的程度。就像某种即时解析器。然后语法错误可能会变成正常的运行时错误,只会引发可以由周围代码处理的正常异常:

def fun():
   not implemented yet

try:
  fun()
except:
  pass

这将是一个有趣的效果,但它是否有用或可取是另一个问题。通常,即使您现在不调用代码,了解错误也是很好的。

在控制到达宏之前不会对宏进行评估,并且解析器自然会知道所有先前的定义。此外,宏定义甚至可以使用程序迄今为止计算的变量和数据(例如为先前计算的列表中的所有元素添加一些语法)。但是,开始为通常可以直接在语言中完成的事情编写自修改程序可能是一个坏主意。这可能会让人感到困惑......

在任何情况下,您都应该确保只解析一次代码,如果第二次执行,则使用已经解析过的表达式,这样就不会导致性能问题。

于 2009-01-27T21:22:02.730 回答
2

以下是我硕士论文中的一些想法,可能有用,也可能没有帮助。这篇论文是关于自然语言的健壮解析。主要思想:给定一种语言的上下文无关语法,尝试解析给定的文本(或者,在你的情况下,是一个 python 程序)。如果解析失败,您将获得部分生成的解析树。使用树结构来建议新的语法规则,以更好地覆盖已解析的文本。我可以把我的论文寄给你,但除非你读过希伯来语,否则这可能没有用。

简而言之:我使用了自下而上的 图表解析器。这种类型的解析器为语法的产生式生成边。每条边都标有被消耗的树的部分。每个边缘都根据它与完全覆盖的接近程度获得分数,例如:

S -> NP . VP

得分为二分之一(我们成功覆盖了 NP,但没有覆盖 VP)。得分最高的边表明新规则(例如 X->NP)。一般来说,图表解析器的效率低于常见的 LALR 或 LL 解析器(通常用于编程语言的类型) - O(n^3) 而不是 O(n) 复杂度,但是你再次尝试比只是解析现有的语言。如果你能对这个想法做点什么,我可以给你发送更多细节。我相信看看自然语言解析器可能会给你一些其他的想法。

于 2009-02-01T07:17:26.157 回答
1

我考虑过的另一件事是使它成为全面的默认行为,但允许语言(意味着一组宏来解析给定语言)在编译时引发解析错误。例如,我系统中的 Python 2.5 会执行此操作。

代替存根的想法,只需重新编译在执行时无法在编译时完全处理的函数。这也将使自修改代码更容易,因为您可以修改代码并在运行时重新编译它。

于 2009-01-23T21:09:05.950 回答
0

您可能需要用未知语法分隔输入文本的位,以便解析语法树的其余部分,除了一些稍后将扩展的字符序列节点。根据您的顶级语法,这可能没问题。

您可能会发现解析算法和词法分析器以及它们之间的接口都需要更新,这可能会排除大多数编译器创建工具。

(更常用的方法是为此目的使用字符串常量,可以在运行时将其解析为小解释器)。

于 2009-01-27T13:52:40.563 回答
0

我认为您的方法不会很好。让我们举一个用伪代码编写的简单示例:

define some syntax M1 with definition D1
if _whatever_:
    define M1 to do D2
else:
    define M1 to do D3
code that uses M1

所以有一个例子,如果你允许在运行时重新定义语法,你就会遇到问题(因为根据你的方法,使用 M1 的代码将由定义 D1 编译)。请注意,验证是否发生语法重新定义是不确定的。可以通过某种类型的系统或某种其他类型的静态分析来计算过度近似值,但 Python 对此并不为人所知:D。

困扰我的另一件事是您的解决方案“感觉”不正确。我发现存储您无法解析的源代码是很邪恶的,因为您可能能够在运行时解析它。

另一个让我想起的例子是:

...function definition fun1 that calls fun2...
define M1 (at runtime)
use M1
...function definition for fun2

从技术上讲,当您使用 M1 时,您无法解析它,因此您需要将程序的其余部分(包括 fun2 的函数定义)保留在源代码中。当你运行整个程序时,你会看到一个你不能调用的 fun2 调用,即使它已经定义了。

于 2009-01-29T23:04:41.003 回答