用Python编写DSL编译器

7
我正在用Python编写游戏,并决定为地图数据文件创建DSL。我知道我可以用正则表达式编写自己的解析器,但我想知道是否有现有的Python工具可以更轻松地完成这项工作,就像在PHP引擎中使用的re2c一样。
一些额外的信息: - 是的,我需要一个DSL,即使我不需要,我仍然希望在项目中构建和使用它。 - DSL仅包含数据(声明性?),它不会被“执行”。 大多数行看起来像:SOMETHING: !abc @123 #xyz/123。我只需要读取数据树。
8个回答

13

7
这里有一个非常有效的方法。
abc= ONETHING( ... )
xyz= ANOTHERTHING( ... )
pqr= SOMETHING( this=abc, that=123, more=(xyz,123) )

声明式的。易于解析的。

而且...

它实际上是Python。只需几个类声明就可以完成工作。DSL实际上是类声明。

重要的是,DSL仅创建对象。当您定义DSL时,首先必须从对象模型开始。稍后,您在该对象模型周围放置一些语法。您不是从语法开始,而是从模型开始。


我知道你在说什么,但是写那么多注释、括号、等号和前缀会使实际数据变得难以理解。此外,这种方法在更冗长的语言如PHP或Java中移植性不太好。 - too much php
@Peter。不同意。您可以使用位置参数并消除标签和=符号。它可以完美地转换为Java。已经在生产应用程序中使用它来定义声明性DSL。 - S.Lott
我曾经听说过你所提出的方法被称为内部领域特定语言。我喜欢这种方法。可能会有一个问题,就是虽然这种方法可以移植到其他语言(我看到过类似于C#实现的东西),但是你的DSL的精确语法可能会有所改变。映射文件将不可移植。 - Mendelt
@Mendelt:没有看到可移植性是一个要求。你是对的,但在这种情况下似乎不适用。 - S.Lott

4

是的,有很多——太多了——解析工具,但没有一个在标准库中。

据我所见,PLY和SPARK很受欢迎。PLY类似于yacc,但您可以在Python中完成所有操作,因为您在docstrings中编写语法。

就我个人而言,我喜欢从函数式编程中借鉴的解析器组合子的概念,并且我非常喜欢pyparsing:您可以直接在Python中编写语法和动作,很容易上手。不过,我最终还是使用了自己的树节点类型和动作,而不是使用它们默认的ParserElement类型。

否则,您也可以使用现有的声明性语言,例如YAML


对于YAML的参考,点个赞。绝大多数需求都可以通过任何能够处理任意嵌套复合数据结构的系统来满足。YAML非常适合这一需求,而且不需要重新发明轮子。 - dreftymac

2
我在工作中写了类似这样的东西,用于读取SNMP通知定义,并自动生成Java类和SNMP MIB文件。使用这个小型DSL,我可以写20行规范代码,它会生成大约80行Java代码和100行MIB文件。
为了实现这一点,我实际上只是使用了Python字符串处理(split()、切片等)来解析文件。我发现Python的字符串功能对于我的(简单)解析需求来说已经足够了。
除了其他人提到的库之外,如果我要编写更复杂的东西并需要适当的解析能力,我可能会使用ANTLR,它支持Python(和其他语言)。

2

DSL(领域特定语言)是一件好事,所以您无需自卫 :-)
然而,您是否考虑过使用内部DSL?与外部(解析)DSL相比,它们有很多优点,因此至少值得考虑。将DSL与本地语言的强大功能混合使用确实可以为您解决许多问题,而Python在内部DSL方面表现不错,特别是“with”语句非常方便。


1
with语句对于内部领域特定语言有什么好处?我很好奇。 - fferri

2
针对您所描述的“小语言”,我使用简单的split、shlex(请注意,#表示注释)或正则表达式来处理相关技术问题。
>>> line = 'SOMETHING: !abc @123 #xyz/123'

>>> line.split()
['SOMETHING:', '!abc', '@123', '#xyz/123']

>>> import shlex
>>> list(shlex.shlex(line))
['SOMETHING', ':', '!', 'abc', '@', '123']

以下是一个示例,因为我不知道你具体需要什么。
注:此处的 HTML 标签指的是一些用于网页排版和内容呈现的标记符号。在编写 HTML 代码时,需要按照规范使用这些标签。
>>> import re
>>> result = re.match(r'([A-Z]*): !([a-z]*) @([0-9]*) #([a-z0-9/]*)', line)
>>> result.groups()
('SOMETHING', 'abc', '123', 'xyz/123')

1
这里有一个更简单的方法来解决它。
如果我可以使用新的运算符扩展Python语法,引入新的功能到语言中会怎样呢?例如,一个用于交换两个变量值的新运算符<=>。
我该如何实现这样的行为?AST模块就派上用场了。最后一个模块是处理抽象语法树的一个方便工具。这个模块很酷的一点是它允许我编写生成树的Python代码,然后将其编译成Python代码。
假设我们想将超集语言(或类似Python的语言)编译成Python:
    a <=> b

to:

    a , b = b , a

我需要将我的“类Python”源代码转换为标记列表。因此,我需要一个用于Python源代码的词法扫描器和分词器。Tokenize模块 我可以使用相同的元语言来定义新“类Python”语言的语法,然后构建抽象语法树的结构AST 为什么要使用AST呢?
  1. 在评估不受信任的代码时,AST是更安全的选择
  2. 在执行代码之前操作树
from tokenize import untokenize, tokenize, NUMBER, STRING, NAME, OP, COMMA
import io
import ast

s = b"a <=> b\n" # i may read it from file
b = io.BytesIO(s)
g = tokenize(b.readline)
result = []
for token_num, token_val, _, _, _ in g:
    # naive simple approach to compile a<=>b to a,b = b,a
    if token_num == OP and token_val == '<=' and next(g).string == '>':
        first  = result.pop()
        next_token = next(g)
        second = (NAME, next_token.string)
        result.extend([
            first,
            (COMMA, ','),
            second,
            (OP, '='),
            second,
            (COMMA, ','),
            first,
        ])
    else:
        result.append((token_num, token_val))

src = untokenize(result).decode('utf-8')
exp = ast.parse(src)
code = compile(exp, filename='', mode='exec')


def my_swap(a, b):
    global code
    env = {
        "a": a,
        "b": b
    }
    exec(code, env)
    return env['a'], env['b']

print(my_swap(1,10))


其他使用AST的模块,其源代码可能是一个有用的参考:
  • textX-LS:一种DSL,用于描述一组形状并为我们绘制它。

  • pony orm:您可以使用Python生成器和lambda编写数据库查询,这些查询将转换为SQL查询字符串 - pony orm在内部使用AST。

  • osso:基于角色的访问控制框架,处理权限。


我假设这是一个答案,而不是一个新问题。您能否删除修辞问题,因为这会让它看起来不像是一个真正的答案。 - Dharman

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接