我正在尝试实现一个 Lisp 版本的Processing,为此我正在使用macro_lisp
crate 在编译时将 Lisp 代码转换为 Rust。
当我像这样构建我的代码时,它会起作用:
主文件
fn main() {
include!("hello.lisp");
}
Run Code Online (Sandbox Code Playgroud)
你好.lisp
fn main() {
include!("hello.lisp");
}
Run Code Online (Sandbox Code Playgroud)
请注意,我必须将hello.lisp
in lisp!()
、 in的内容包装起来hello.lisp
本身。
我希望它的结构如下:
主文件
fn main() {
lisp!(include!("hello.lisp"));
}
Run Code Online (Sandbox Code Playgroud)
你好.lisp
println "hello"
Run Code Online (Sandbox Code Playgroud)
但这给了我以下错误:
lisp!(println "hello")
Run Code Online (Sandbox Code Playgroud)
那里不应该有 EOF,应该有 hello "list"
.
我究竟做错了什么?我需要打补丁macro_lisp
吗?
不幸的是,你想要的并不容易实现。
宏的工作方式与函数有很大不同。这个问题的重要部分是“嵌套宏调用”是从“外到内”评估的(与函数不同,首先评估参数,所以“从内到外”)。我们可以看到这个小程序的效果:
macro_rules! foo {
($x:literal) => { "literal" };
($x:ident ! ()) => { "ident ! ()" };
}
macro_rules! bar {
() => { 3 };
}
fn main() {
let s = foo!(bar!());
println!("{}", s);
}
Run Code Online (Sandbox Code Playgroud)
正如您在 Playground 上看到的,它打印ident ! ()
. 这意味着bar!()
宏在被评估之前没有foo!
被评估。(此外,编译器甚至会警告未使用的宏定义bar
。)
include!
也不例外,因此我们不能将其用作其他宏的参数。那么你怎么能做到呢?我能想到两种方法,都不是特别容易或优雅:
编写一个程序宏来加载文件并发出令牌流lisp! { ... }
,...
文件内容在哪里。正确设置所有路径可能很棘手(并确保在 lisp 文件更改时正确重新编译所有内容),但理论上它应该可以工作。
使用构建脚本手动将include!("*.lisp")
源代码中的字符串替换为文件内容。显然,您实际上并不想修改真正的源代码(已签入 git),但必须对此有一点聪明。一些板条箱使用这种策略,但只是出于非常特殊的原因。我不建议在你的情况下这样做。
在您的情况下,我会三思而后行在 Rust 中使用大量 LISP 代码是否是个好主意,因为没有任何好的方法可以使其工作(据我所知)。
小智 1
虽然这个问题已经有 4 年了,但我是一个箱子的作者,它以可维护的方式解决了这个确切的问题,我想通过分享如何解决这个问题来节省其他人重新实现所需样板的精力使用CPS板条箱。
Lukas 的回答强调,Rust 需要一些项目外部代码,例如构建脚本或 proc-macro,来实现由内而外的宏评估。CPS 是一个包,它可以为您完成所需的 proc-macro 工作,并引入了let
绑定,允许您将所需的宏编写lisp!
为宏示例:
#[cps::cps]
macro_rules! lisp_include {
($source:literal) =>
let $($lisp_source:tt)* = cps::include!($source) in
{
lisp!($($lisp_source)*)
}
}
Run Code Online (Sandbox Code Playgroud)
之后,您可以lisp!
在代码中的其他位置使用该宏的扩展版本:
lisp_include!("hello.lisp");
Run Code Online (Sandbox Code Playgroud)