C Plus Prolog
C Plus Prolog:用 Prolog 宏生成 C 代码
Navigation Menu(导航菜单,已省略)
Search or jump to... (搜索或跳转到...,已省略)
needleful/c_plus_prolog
master BranchesTags Go to file (跳转到文件,已省略) Code (代码,已省略)
Folders and files (文件夹和文件)
| Name | Name | Last commit message | Last commit date | | -------- | -------- | ------------------- | ---------------- | | examples | examples | | | notes | notes | | | .gitignore | .gitignore | | | README.md | README.md | | | cpp.pl | cpp.pl | | | cpp_common.pl | cpp_common.pl | | | cpp_ops.pl | cpp_ops.pl | | | cpp_reader.pl | cpp_reader.pl | | | cpp_writer.pl | cpp_writer.pl | | | test.ps1 | test.ps1 | | | test.sh | test.sh | |
View all files (查看所有文件,已省略)
Repository files navigation(仓库文件导航,已省略)
C Plus Prolog
Prolog 是唯一的好编程语言。 我应该知道,我的网站是用 Prolog 编写的。 不幸的是,C 是唯一有用的编程语言。 科学家们已经尝试寻找这个问题的答案近 50 年了。 一些人让他们的 C 更像 Prolog。 另一些人让他们的 Prolog 更像 C。 我提供了一个新的解决方案:简单地将 Prolog 和 C 加在一起。 我称之为“C Plus Prolog”,或简称“C+P”。
:- include(stdio).
int func main
{
puts("Hello, world!");
return 0
}.
如果您熟悉 C,您会注意到这是一种奇怪的、糟糕的 C。 然而,您错了。 这是有效的 Prolog,使用了一些 SWI-Prolog 的非标准功能 用于大括号。 此 Prolog 被读取为术语列表,并转换为有效的 C 代码。 所以这个:
int func main(int var argc, char:ptr:array var argv)
转换为:
int main(int argc, char*( argv[]))
除了 var
和 func
运算符等一些明显的更改之外,这些运算符使语法可以表示为 Prolog,还有一些意想不到的怪癖:
- 以大写字母或下划线开头的符号用于 Prolog,除非用单引号括起来,否则不会转换为 C:
int:ptr var string = 'NULL';
'_Bool' var v = true;
- 由于上述原因,字符文字以
#
开头:
char var c = #c; % 小写字母不需要引号
char var c2 = #'C';
char var nl = #'\n';
- 运算符优先级保持了现有 Prolog 运算符的优先级。 例如,
=
和比较运算符(如==
、<
和>
)具有相同的优先级,因此您需要在 C 中不需要括号的地方使用括号:
greater = (a > b).
这也是箭头运算符 ->
被 @
替换的原因,因为它在 Prolog 中以完全不同的方式使用,并且具有完全不同的优先级。
C+P:
a = this@member;
C:
a = this->member;
- 复杂的类型声明是不同的。 C 样式的声明完全可以用 Prolog 表示,只是我不喜欢它们。
char:ptr:array var argv
变成
char *(argv[])
它还可以帮助您记住 const char *
和 char const *
之间的区别:
const(char):ptr,
char:const(ptr)
这些例子更完整地描述了语法。 最后,它是 C,但更冗长,并且对分号特别挑剔。 并不完全是一个银弹。
让我们介绍一下 *=>
运算符。
The *=>
Operator(*=>
运算符)
max(A, B) *=> A > B then A else B.
我说 C+P 除了将术语转换为 C 之外不做任何处理,但它确实执行了一个小步骤。 编译器将收集所有用 *=>
定义的术语,然后在代码的其余部分中将左侧替换为右侧,直到不再应用任何规则。
因为这在 Prolog 术语上运行,而不是在文本上运行,所以我们不需要在 max(A, B)
宏中添加任何额外的括号,以防止运算符优先级出现问题。 它作为单个术语插入到代码中,并且在使用时看起来与函数调用完全一样:
float var f = max(12.3, 0) + 20;
printf("Should be 32.3: %f\n", f);`
它转换为以下 C 代码:
float f=(((12.3>0) ? 12.3 : 0)+20);
printf("Should be 32.3: %f\n", f);
而且,我厌倦了在所有 printf
语句的末尾添加 \n
。 让我们定义另一个宏 println
:
PrintLn *=> PrintF :-
PrintLn =.. [println,Format| Args],
string_concat(Format, "\n", Format2),
PrintF =.. [printf,Format2| Args].
我们可以使用 :-
在编译时完全访问 Prolog,从而允许我们做任何事情。 此宏获取带有字符串文字 Format
的 println(Format, Args...)
的任何实例,并将其转换为 printf
,并在 Format
附加一个换行符。
够简单吧。 让我们实现简陋的泛型。
Poor Man's Generics in C Plus Prolog(C Plus Prolog 中的简陋泛型)
list[T]
{
struct list[T] {
T:ptr:ptr var items
};
% Let's also define syntax for type-specific functions, in this case “list[T]:capacity”
size_t:ptr func list[T]:capacity(list[T] var this)
{
...
};
...
}.
这将类似于 C++ 模板。 为了简化实现,我们将要求用户实例化模板。
declare(list[int]).
declare(list[const(char):ptr]).
我们可能会通过扫描代码中 list[T]
的任何用法并在其正上方实例化模板来自动执行此操作,但我将其作为练习留给读者。
我们还有一些语法可以通过 list[T]:Method
获取函数名称:
int func main {
list[int] var my_ints = list[int]:new(17);
size_t var size = *(list[int]:capacity(my_ints));
for(int var i = 0; i < size; i += 1)
{
list[int]:append(my_ints, i*i)
};
for(int var i = 0; i < size; i+= 1)
{
printf("%d squared = %d.\n", i, list[int]:get(my_ints, i))
};
return 0
}.
不太像 C++,但它可以保持命名空间清晰。 让我们阅读宏。 它会匹配我们的模板,正如您可能预期的那样:
Name[T] {Body} *=> Comment :-
\+ground(T), atom(Name),
atom_concat('//Defined template type: ', Name, Comment),
我们有一个带有名称 Name
、类型参数 T
和正文 Body
的模板。 该宏会删除此代码并插入注释。 其他一切都在 Prolog 世界中处理。
assertz(
declare(Name[Z]) *=> NewBody
我的天哪。 我们的宏 assert
另一个宏 declare(Name[Z])
。 它也有条件:
:- (
ground(Z),
T=Z,
Body=NewBody,
这三行是宏的主体。 它将模板类型 T
与真实(基础)类型 Z
统一起来,然后返回模板的正文。 这就是将 declare(list[int])
转换为类型代码的原因。
但这还不是全部,我们断言的宏本身断言了更多的宏:
('*mangled_name'(Name[Z]) *=> ZName),
assertz(Name[Z] *=> ZName),
assertz((ZName:Method *=> ZMethod :-
% Crudely hacking some shorthand for method names
Method \= ptr, Method \= array,
( atom(Method)
-> MName = Method,
ZArgs = []
; Method =.. [MName|ZArgs]
),
('*mangled_name'(ZName:MName) *=> MZ_Name),
ZMethod =.. [MZ_Name|ZArgs]
))
)).
这会为 list[int]
和 list[int]:function
之类的东西生成 C 名称。 '*mangled_name'
只是另一个宏,但这个示例足够长,并且都在示例 05 中。*
和单引号没有什么特别的,它们只是为了防止此宏意外地与用户定义的 mangled_name
函数发生冲突。
C+P 不提供任何可用于方法语法的类型信息,例如 my_list.append(...)
,而不是 list[int]:append(my_list, ...)
。
当然,我们可以使用宏来收集函数体中每个变量的类型,并将方法调用替换为适当的函数,但在某种程度上,我只是在宏系统中编写一个完整的 XLang-to-C 编译器,这是一个有趣的想法,但我已经就业了。
我提供了 C Plus Prolog 奇迹的几个其他示例:
- 示例 06 重载
struct
关键字以添加编译时反射。 - 如果目标文件以
.h
或.c
结尾,示例 08a 会编译不同的代码,以在单个文件中声明典型的标头和实现。
我发现用 C+P 编写,添加听起来不可能的功能非常引人入胜。 这就像一个益智游戏,这就是为什么我经常使用 Prolog 的原因。
Installation and Usage(安装和使用)
C Plus Prolog 易于安装。 您只需要一个 C 编译器、SWI-Prolog 和此存储库。 您可以弄清楚。
然后,您可以使用以下语法从此存储库运行 cpp.pl
:swipl -s cpp.pl -- <input file> <output file>
按照惯例,C Plus Prolog 文件的扩展名以 .c+p
结尾。
检查 test.sh
和 test.ps1
以获取更多示例用法,以及运行所有测试的快速方法(可能不是 rootkit)。
What is the point of this?(这有什么意义?)
C Plus Prolog 是对系统编程语言中宏的半认真的探索。 在制作此产品的过程中,我清楚地发现,我更喜欢 D 和 Zig 等语言提供的编译时评估和反射,而不是语法宏。
当然,通过足够的工作,您可以使用宏系统完成所有事情及更多事情,但与单独的代码生成器或 DSL 相比,它实际上_添加_了什么? 也许是一个更好的界面。
在 Common Lisp 中,您可以在编译时拥有完整的代码库,但真正的价值是在运行时重新编译代码,而不是操纵一个看起来有点像代码的列表列表,如果您眯着眼睛透过所有的反引号和逗号。
Rust 的程序宏可能节省了 40 行代码,因为您不必编写自己的分词器,但其他一切都取决于您以及您想要引入的任何库。
我的大多数元编程都希望涉及反映编译器已经知道的信息,例如代码中的类型和注释,而不是原始语法树。 对于一种语言中的语言,语法宏最擅长,我通常宁愿付出额外的努力并制作一个具有专门构建语法的完全独立的 DSL,而不是扭曲问题,例如,S 表达式或 Prolog 术语。
尽管如此,C+P 还是非常接近有用的。 它最大的优势是,它默认生成纯 C,从而允许在不太流行的语言没有支持或由于不同的抽象而必须生成更复杂的 C 的情况下进行高性能跨平台构建。 Prolog 也已经存在了 50 年。 如果删除 SWI-Prolog 扩展,例如,将 func F {Body}
换成 func F => Body
,它可以在大量硬件上构建。
但我不想使用它。 绝对没有验证或错误消息,因此任何错误都会导致 C 损坏或静默失败。 我认为使用 var
作为运算符会很好,但它会产生很多视觉噪音。 如果您认为 C 的分号很烦人,那么在 C Plus Prolog 中,分号是运算符。 连续有两个会破坏事情。 在开头有一个会破坏事情。 在_末尾_有一个会破坏事情。
如果有人想使用 C+P,还有很多更好的替代方案。
- Nim 和 Haxe 语言可以编译为 C 和/或 C++,并且它们提供了开箱即用的良好用户体验,但我不能说它们的代码如何直接转换为 C。
- cmacro 以更友好的格式提供了与 C+P 类似的语法操作级别,由 Common Lisp 提供支持。
- D 语言 拥有由 GCC 和 LLVM 支持的编译器,因此它应该在 C 可以运行的大多数地方工作。
我不知道结论是什么。
About
C plus Prolog (关于,已省略)