一个简易类C语法脚本。语法是C的子集,有少量C++风格的扩展。
用于嵌入一些只需要简单计算,且不想引入较复杂的外部库的C++程序。
例如某些情况下,需要在外部配置文件中执行一个简单的C++过程,且希望程序内部的代码可以不修改直接放到配置文件。
项目名就是“词法”。实际上本垃圾一开始理解错了,以为词法分析要生成语法树,就这样吧。
执行模式为直接对语法树求值,并处理分支和循环。
千万不要用它完成过于复杂的任务,正如描述所说,这只是一个非常简单的语法解析器和执行器。如果你想做更复杂的事,请选用Python或者Lua。如果你喜欢C风格的语法,请选用AngelScript或者ChaiScript,但是这两个库都不小,而且在很多Linux发行版上没有预编译包可用。
有大佬做的js版:https://github.com/whyb/cifa.js,线上演示为:https://whyb.github.io/cifa.js/playground/web.
ps:目前经实测,Cifa已经能在一定程度上替换Lua。
Cifa曾经单独开发,后来为方便改成了mlcc的一部分,目前因为AI的发展,可以迅速为其添加功能,逐渐可以实用化,因此再次转为独立的项目。
在自己的工程中加入Cifa.h和Cifa.cpp即可。例程如下:
#include "Cifa.h"
#include <fstream>
#include <iostream>
using namespace cifa;
int main()
{
Cifa c1;
std::ifstream ifs;
ifs.open("1.c");
std::string str;
getline(ifs, str, '\0');
auto o = c1.run_script(str);
if(o.hasValue() && o.isNumber() && o.isType<double>())
{
std::cout << "Cifa value is: " << o.ref<double>() << "\n";
}
}其中1.c文件即为脚本内容,一个例子为:
int sum = 1;
for (int i = 1; i <= 10; i++)
{
for (int j = 1; j <= 10; j++)
{
int x = 0;
if ((i + j) % 2 == 0)
{
x = -1;
}
else
x = 1;
sum += (i * j) * x;
}
}
return sum;计算的结果为-24。
若脚本只有一个表达式,则结果就是表达式的求值。若脚本包含多行,则需用return来指定返回值,否则返回值是<empty>(强行当成数值则是NaN)。
自定义函数必须可以转化为std::function<cifa::Object(cifa::ObjectVector&)>,其中cifa::ObjectVector即std::vector<cifa::Object>。
以下自定义3个数学函数(省略了检测越界):
using namespace cifa;
Object sin1(ObjectVector& d) { return sin(d[0]); }
Object cos1(ObjectVector& d) { return cos(d[0]); }
Object pow1(ObjectVector& d) { return pow(d[0].value, d[1].value); }
int main()
{
Cifa c1;
c1.register_function("sin", sin1);
c1.register_function("cos", cos1);
c1.register_function("pow", pow1);
//....
}这里函数原型写成了xxx1的形式,只是为了避免与cmath中的数学函数同名,造成一些麻烦。
其实更推荐lambda表达式的形式,例如将上面正弦函数的注册修改为:
c1.register_function("sin", [](ObjectVector& d) { return sin(d[0]); });这样也不必再定义sin1这个函数。
此时再运行如下脚本:
auto pi = 3.1415927;
print(sin(pi / 6));
print(cos(pi / 6));
print(pow(2, 10));输出应是:
0.5
0.866025
1024
需注意语言已经内置了一些函数,如下面的表格所示。如果想覆盖掉内置函数,可以直接注册同名函数即可。
例如脚本为:
myfun(i)
{
return i*i*i+i*i+i+1;
}
print(myfun(3));可以得到输出为40。
注意这种函数不能做类型检查。
通过预定义变量可以模拟一些外置函数的效果。下面这个例子中,将pi预先定义好,并将degree视作一个C++送到Cifa的参数:
c1.register_parameter("degree", 30);
c1.register_parameter("pi", 3.14159265358979323846);脚本为:
print(sin(degree*pi/180));输出应为0.5。
一对大括号{}内定义的变量在该大括号内可见,外部不可见。函数参数在函数体内可见。与C++规则相同。
Cifa中Object的实现其实是std::any,故可以容纳任何类型。但目前数值相关的类型实际都是double,另内置了std::string的支持。
如果用户希望使用自己的类型,需要增加一些功能函数和对应的运算符重载。
例如,增加以下几个函数支持OpenCV中cv::Mat相关的一些功能:
c.register_function("imread", [](cifa::ObjectVector& v) -> cifa::Object
{
int flag = -1;
if (v.size() >= 2)
{
flag = int(v[1]);
}
return cv::Mat(cv::imread(v[0].toString(), flag));
});
c.register_function("imshow", [](cifa::ObjectVector& v) -> cifa::Object
{
cv::imshow(v[0].toString(), v[1].to<cv::Mat>());
return cifa::Object();
});
c.register_function("imwrite", [](cifa::ObjectVector& v) -> cifa::Object
{
cv::imwrite(v[0].toString(), v[1].to<cv::Mat>());
return cifa::Object();
});除此之外,也可以支持用户自定义某些运算符的重载,但是需注意应进行类型检查。下面以增加加号和减号的重载为例:
c.user_add.push_back([](const cifa::Object& l, const cifa::Object& r) -> cifa::Object
{
if (l.isType<cv::Mat>() && r.isType<cv::Mat>())
{
return cv::Mat(l.to<cv::Mat>() + r.to<cv::Mat>());
}
return cifa::Object();
});
c.user_sub.push_back([](const cifa::Object& l, const cifa::Object& r) -> cifa::Object
{
if (l.isType<cv::Mat>() && r.isType<cv::Mat>())
{
return cv::Mat(l.to<cv::Mat>() - r.to<cv::Mat>());
}
return cifa::Object();
});因为变量作用域的关系,用户自定义类型会按照RAII的原则进行管理,无需手动释放资源,即没有必要进行垃圾收集。但是用户原则上不应使用非RAII的类型。
有两种方式创建空数组:
int a[]; // 类型声明方式
b = {}; // 赋值方式两种写法均会产生一个长度为 0 的数组,之后可以用 push_back 等方法添加元素。
- 写入越界:自动扩展数组大小,中间元素为空值。
- 读取越界:同样自动扩展,返回的空值在后续数值运算中可能产生运行时错误。
使用 {} 花括号构造数组,元素之间用逗号分隔。元素类型可以混合(数字、字符串等):
arr = {1, 2, 3, 4, 5};
mixed = {1, "hello", 3.14, "world"};使用整数下标访问元素,下标从 0 开始:
arr = {10, 20, 30};
int x = arr[0]; // x = 10
arr[1] = 99; // 修改元素
int i = 2;
double v = arr[i]; // 变量作为下标数组元素本身也可以是数组(嵌套):
grid = { {1, 2}, {3, 4} };
int v = grid[1][0]; // v = 3使用内置函数 size() 获取数组元素个数:
arr = {1, 2, 3, 4, 5};
int n = size(arr); // n = 5宿主程序可以将 std::vector<double> 注册为脚本内的数组变量:
c1.register_vector("v", std::vector<double>{1.2, 1.45, 77.3});脚本中即可 v[0]、v[1] 访问。
变量可通过字符串下标使用,此时它会变成一个 string → Object 的映射。
dict["name"] = "Alice";
dict["age"] = 30;
string key = "name";
string n = dict[key]; // n = "Alice"
double a = dict["age"]; // a = 30size(dict) 返回 map 中的元素个数。
访问不存在的 key 后使用该值会触发运行时错误。
访问不存在的 key 会自动创建该 key 并赋空值(与 C++ std::map::operator[] 行为一致)。
数组和 Map 支持通过 . 语法调用内置方法,对自身进行原地修改。
| 方法 | 说明 | 返回值 |
|---|---|---|
arr.push_back(x) |
在末尾追加元素 x(支持多个参数) |
新的数组大小 |
arr.pop_back() |
移除最后一个元素 | 新的数组大小 |
arr.resize(n) |
将数组大小调整为 n |
新的数组大小 |
arr.insert(i, x) |
在下标 i 处插入元素 x |
新的数组大小 |
arr.erase(i) |
删除下标 i 处的元素 |
新的数组大小 |
arr.clear() |
清空所有元素 | 0 |
arr.contains(x) |
检查数组中是否存在值 x |
1(存在)或 0(不存在) |
示例:
a = {};
a.push_back(10);
a.push_back(20);
a.push_back(30);
a.insert(1, 15); // a = {10, 15, 20, 30}
a.erase(0); // a = {15, 20, 30}
int has = a.contains(20); // has = 1
a.clear(); // a = {}| 方法 | 说明 | 返回值 |
|---|---|---|
m.erase("key") |
删除指定 key | 新的 map 大小 |
m.clear() |
清空所有键值对 | 0 |
m.contains("key") |
检查 key 是否存在 | 1(存在)或 0(不存在) |
m.keys() |
返回所有 key 组成的数组 | 字符串数组 |
示例:
m["name"] = "Alice";
m["age"] = 30;
int has = m.contains("name"); // has = 1
k = m.keys(); // k = {"age", "name"}(按字典序)
m.erase("age");
int n = size(m); // n = 1Cifa 支持 C 风格的结构体定义,用于将多个字段组合成一个命名类型。
struct Point { int x; int y; };
Point p;struct定义声明字段列表,字段类型名(如int)会被忽略,仅字段名有效。- 结构体名本身作为类型关键字使用,声明变量时自动将其初始化为含所有字段的对象。
struct Point { int x; int y; };
Point p;
p.x = 10;
p.y = 20;
return p.x + p.y; // 30struct Counter { int n; };
Counter cnt;
cnt.n = 5;
cnt.n += 3; // cnt.n == 8结构体实例可以作为参数传入脚本定义的函数:
struct Rect { int w; int h; };
area(r) { return r.w * r.h; }
Rect r;
r.w = 4; r.h = 5;
return area(r); // 20注意:传递时为值拷贝,函数内修改字段不影响外部变量。
struct 定义绑定在 Cifa 实例上,只需定义一次,后续对同一实例的 run_script 调用均可直接使用该类型,无需重复写定义:
Cifa c;
c.run_script("struct Point { int x; int y; };"); // 第一次:定义结构体
auto o = c.run_script(R"(
Point p;
p.x = 3; p.y = 7;
return p.x + p.y;
)"); // 第二次:直接使用,结果为 10- 不支持嵌套 struct 定义(字段类型只能是基本类型)。
- 不支持构造函数、成员函数、继承等 C++ OOP 特性。
字符串长度可用 size() 获取:
string s = "hello";
int n = size(s); // n = 5字符串拼接用 +:
string s = "hello" + " " + "world";类型转换:
to_string(3.14)→ 将数值转为字符串to_number("3.14")→ 将字符串转为数值
| 函数 | 说明 |
|---|---|
print(...) |
输出一个或多个值,不换行 |
println(...) |
输出一个或多个值,最后换行 |
to_string(x) |
将数值转为字符串 |
to_number(s) |
将字符串转为数值 |
size(x) |
返回数组、map 或字符串的大小 |
pow(x, y) |
x 的 y 次方 |
max(a, b, ...) |
多个数中的最大值 |
min(a, b, ...) |
多个数中的最小值 |
random() |
[0, 1) 均匀随机数;random(n) 返回 [0, n);random(a, b) 返回 [a, b) |
ifv(cond, a, b) |
三元选择,等价于 cond ? a : b |
abs / sqrt / round / floor / ceil |
常见数学函数 |
sin / cos / tan / asin / acos / atan |
三角函数 |
sinh / cosh / tanh |
双曲函数 |
exp / log / log10 |
指数对数函数 |
sprintf(fmt, ...) |
C printf 风格格式化,支持 %s %d %f %g %x 等;%% 输出字面 % |
format(fmt, ...) |
{} / {N} 占位符风格格式化;整数不带小数点,{{ / }} 转义为字面括号 |
Cifa 将错误分为两类:语法错误(静态检查阶段)和运行时错误(执行阶段)。
if (c.has_error())
{
// 推荐:返回带源码行和位置指示的错误字符串
std::string err = c.get_errors_str();
std::cerr << err;
// 或者直接打印到 stderr
// c.print_errors();
// 也可逐条访问(建议优先使用上面的字符串接口)
for (auto& e : c.get_errors())
{
// e.line, e.col, e.message
}
}错误输出示例:
未初始化变量(脚本:int y = undef;):
Syntax Error: parameter undef is at right of = but not been initialized
at line 2, col 9: int y = undef;
^
未定义函数(脚本:return foo(1, 2);):
Syntax Error: function foo is not defined
at line 1, col 8: return foo(1, 2);
^
不可赋值的左值(脚本:123 = 5;):
Syntax Error: 123 cannot be assigned
at line 1, col 1: 123 = 5;
^
括号不匹配(脚本:int x = (1 + 2));):
Syntax Error: unpaired right bracket )
at line 1, col 16: int x = (1 + 2));
^
else 无对应 if(脚本:int x = 1; else { x = 2; }):
Syntax Error: else has no if
at line 2, col 1: else { x = 2; }
^
位置信息:下表所有语法错误均记录了精确的行列位置,get_errors_str() / print_errors() 输出时均会附带原始源码行与 ^ 位置指示。
若某个错误节点未能记录位置(内部极少见情况),则仅输出
at line 0, col 0,不附原始行文本。
语法错误在 run_script 返回前会自动打印到 stderr(output_error 为 true 时,这是默认行为),也可手动调用 print_errors() 或 get_errors_str() 获取。
静态检查可检出的语法错误列表:
| 错误 | 说明 |
|---|---|
| unpaired right bracket | 右括号 ) / ] / } 无对应左括号 |
| unpaired left bracket | 左括号 ( / [ / { 无对应右括号 |
| parameter ... is at right of = but not been initialized | 使用了未经赋值的变量 |
| ... cannot be assigned | 赋值左侧是常量或字符串字面量 |
| function ... is not defined | 调用了未注册/未定义的函数 |
| function ... has no operands | 函数缺少参数列表 |
| operator ? has no : | 三元运算符 ? 缺少 : 分支 |
| if/while has empty condition | if() 或 while() 条件为空 |
| if has no condition/statement | if 缺少条件或语句体 |
| else has no if | else 无对应 if |
| while has constant true condition, may cause infinite loop | while(1) / while(true) 静态检测到潜在死循环 |
| for loop may cause infinite loop | for(;;) 等无终止条件 |
| for loop condition is not right | for 循环条件格式不正确 |
| while/do while has no statement/condition | 循环缺少必要部分 |
| switch has no condition/statement | switch 缺少条件或语句体 |
| case has no condition / case missing : | case 格式不正确 |
| default missing : | default 后缺少冒号 |
| missing ; | 语句末尾缺少分号 |
| no parameters inside [] | 下标表达式为空(数组声明 int a[]; 除外) |
| wrong parameters inside [] / () | 括号内参数格式不正确 |
| variable declaration not allowed in non-block body | 在无 {} 的分支/循环/case 体中定义或引入了新变量 |
执行阶段的错误(如类型不兼容、访问不存在的 map 键等):
- 运行时错误在触发时立即自动打印到 stderr(
output_error为true时,这是默认行为)。 - 若要关闭自动输出,可在
run_script前调用c.set_output_error(false)。 run_script的返回值会携带错误标记,可用于程序逻辑判断:
// 运行时错误发生时会自动打印到 stderr
// 若要关闭自动输出:c.set_output_error(false);
auto result = c.run_script(script);
if (result.getSpecialType() == "Error")
{
// 运行时发生了错误(已自动打印到 stderr)
}位置信息:自动输出的运行时错误包含完整调用栈,每个调用帧均附带原始源码行与 ^ 位置指示。
运行时错误输出示例(含调用栈):
类型转换失败(脚本:int bad; return sqrt(bad);):
Runtime Error: type conversion failed: variable 'bad' from <empty> to double
Call Stack (most recent call last):
at func sqrt()
at line 2, col 8: return sqrt(bad);
^
无限递归(脚本:f(n){ return f(n); } return f(0);):
Runtime Error: max call depth exceeded (possible infinite recursion)
Call Stack (most recent call last):
at line 1, col 14: f(n){ return f(n); }
^
at func f()
at func f()
at line 2, col 8: return f(0);
^
循环超限(脚本:for (int i = 0; i < 99999999; i++) {}):
Runtime Error: for loop exceeded max iterations
Call Stack (most recent call last):
at line 1, col 1: for (int i = 0; i < 99999999; i++) {}
^
运行时错误列表:
| 错误 | 说明 |
|---|---|
| type conversion failed: variable '...' from ... to double | 将非数值类型(空值、字符串等)转换为 double 时失败 |
| type conversion failed: variable '...' from ... to string | 将非字符串类型转换为 string 时失败 |
| max call depth exceeded (possible infinite recursion) | 函数递归调用过深,超过最大调用深度 |
| for/while/do-while loop exceeded max iterations | 循环次数超过最大限制 |
| function ... is not defined | 调用了运行时未找到的函数 |
| ...() is not supported on arrays/maps | 对数组或 map 调用了不支持的内置方法 |
| ...() requires an array or map | 对非数组、非 map 的变量调用了内置方法 |
- Cifa的变量定义其实不需要指定类型,但是为了实现“简单C(C++)代码可以直接被Cifa运行”这一目的,auto、int、float、double等类型名会被忽略。- 结构体(struct)名用作类型时同理,声明变量时只需使用结构体名,字段声明中的类型名同样被忽略。- 未经初始化即出现在赋值号右侧的变量值为空,即std::any的
<empty>,相当于强制要求显式初始化。 - 函数调用时,a.func(c)等价于func(a, c)。但对于内置数组/map方法(push_back、erase等),仅能通过
.语法调用,不能写成push_back(arr, x)的形式——这是因为内置方法需要直接修改原始变量,而普通函数调用传的是值的副本,无法修改原始对象。 - 自加算符不支持++++或----这种写法,请不要瞎折腾。
- 没有goto。
以下是使用Cifa计算一个数值的完整用例,包含错误检查和结果处理:
cifa::Cifa cifa;
std::string str1 = "a1 = 2;\na3=a1;\nreturn 5+4*9*(a1+3)/23;";
auto c = cifa.run_script(str1);
if (cifa.has_error()) //检查语法错误
{
//可以选择输出语法错误字符串(已包含源码行和错误位置指示)
std::string err_str = cifa.get_errors_str();
std::cerr << err_str;
//也可以直接打印到 stderr
//cifa.print_errors();
}
//无语法错误,判断结果是否是一个数值
if (c.isNumber())
{
std::print("{}\n", c.toDouble());
}
//若需正常继续计算,需要排除nan和inf
if (c.isEffectNumber())
{
//do something
}- 生成语法树时的检查不太严格,例如if和while后面的条件其实可以不写括号,但是最好要写全(此处若严格处理需要将括号多归约一层,略微影响效率)。
- 报错位置有时不太准确。例如括号被归约后,在语法树上就不再存在,所以会相差一个或多个括号。要解决需要在归约时记录最终位置,比较复杂且意义不是很大,不再处理。
- 变量的括号初始化。
- 遍历最终语法树可以生成执行码,不再处理。