用 C 语言开发一门编程语言 — 字符串与文件加载
目录
前文列表
《用 C 语言开发一门编程语言 — 交互式解析器》
《用 C 语言开发一门编程语言 — 跨平台的可移植性》
《用 C 语言开发一门编程语言 — 语法解析器》
《用 C 语言开发一门编程语言 — 抽象语法树》
《用 C 语言开发一门编程语言 — 异常处理》
《用 C 语言开发一门编程语言 — S-表达式》
《用 C 语言开发一门编程语言 — Q-表达式》
《用 C 语言开发一门编程语言 — 变量元素设计》
《用 C 语言开发一门编程语言 — 基于 Lambda 表达式的函数设计》
《用 C 语言开发一门编程语言 — 条件分支》
字符串
每次我们更新程序并重新运行的时候,重新输入所有函数令人苦恼。在本章中,我们将为 Lispy 添加从文件加载代码的功能。该功能让我们建立一个标准库。
当用户加载文件时,我们需要其提供一个由文件名组成的字符串。我们的语言现在支持符号类型,但仍然不支持包含空格以及其他字符的字符串类型。所以,我们需要添加字符串类型的 lval 来指定我们所需的文件名。
像在其他章节中一样,我们通过在枚举中添加新条目并添加新的 lval 来表示字符串类型的数据:
enum { LVAL_ERR, LVAL_NUM, LVAL_SYM, LVAL_STR,
LVAL_FUN, LVAL_SEXPR, LVAL_QEXPR };
- lval 结构体:
/* Basic */
long num;
char* err;
char* sym;
char* str;
- 构造函数:
lval* lval_str(char* s) {
lval* v = malloc(sizeof(lval));
v->type = LVAL_STR;
v->str = malloc(strlen(s) + 1);
strcpy(v->str, s);
return v;
}
- 删除部分
case LVAL_STR: free(v->str); break;
- 复制部分
case LVAL_STR: x->str = malloc(strlen(v->str) + 1);
strcpy(x->str, v->str); break;
- 等于部分
case LVAL_STR: return (strcmp(x->str, y->str) == 0);
- 类型名部分
case LVAL_STR: return "String";
在此,我们需要做一些额外工作来实现字符串打印函数。Lispy 内部存储字符串与所打印字符串有所区别。我们希望在打印用户所输入的字符串时,会使用转义字符(如 \n
)来表示换行符。
因此,我们需要在打印字符串之前将其转义。幸运的是,我们可以使用mpc函数来为我们做这件事。在打印函数中,我们添加以下内容:
case LVAL_STR: lval_print_str(v); break;
以及:
void (lval* v) {
/* Make a Copy of the string */
char* escaped = malloc(strlen(v->str)+1);
strcpy(escaped, v->str);
/* Pass it through the escape function */
escaped = mpcf_escape(escaped);
/* Print it between " characters */
printf("\"%s\"", escaped);
/* free the copied string */
free(escaped);
}
读取字符串
添加对字符串解析的支持。跟以往相同,添加名称为string的新语法规则到 Lispy 语法分析器中。我们将要使用的字符串表示规则与 C 相同。这意味着字符串实质上是两个引号 "
之间的一系列转义字符或普通字符组成。
string : /\"(\\\\.|[^\"])*\"/ ;
该正则表达式的含义是:该字符串是从 "
字符开始,后面跟着零个或多个跟着任意字符 .
的反斜杠 \\
,或者非 "
的任意字符 [^\\"]
。最后,以 "
收尾。
我们也需要在 lval_read 函数中添加一个case来处理字符串读取:
if (strstr(t->tag, "string")) { return lval_read_str(t); }
因为字符串是以转义形式输入的,所以我们需要创建 lval_read_str 函数来解决这个问题。 这个功能有点棘手,因为它必须解决以下问题。 首先,它必须剥离字符串两侧 "
字符。然后必须对转义字符串进行解码,将一系列转义字符(如 \n
)转换成实际编码字符。最后必须创建一个新的 lval 并清理函数中使用过的内存。
lval* lval_read_str(mpc_ast_t* t) {
/* Cut off the final quote character */
t->contents[strlen(t->contents)-1] = '\0';
/* Copy the string missing out the first quote character */
char* unescaped = malloc(strlen(t->contents+1)+1);
strcpy(unescaped, t->contents+1);
/* Pass through the unescape function */
unescaped = mpcf_unescape(unescaped);
/* Construct a new lval using the string */
lval* str = lval_str(unescaped);
/* Free the string and return */
free(unescaped);
return str;
}
如果这一切都没有问题,我们应该能够在 REPL 中使用字符串:
lispy> "hello"
"hello"
lispy> "hello\n"
"hello\n"
lispy> "hello\""
"hello\""
lispy> head {"hello" "world"}
{"hello"}
lispy> eval (head {"hello" "world"})
"hello"
lispy>
注释
跟 C 语言一样,我们可以使用注释来告知其他人(或我们自己)相关代码的用途或编写原因。 C 语言注释在 /*
和 */
之间,Lisp 注释则以 ;
开头,并读取至行尾。
正则表达式为:
comment : /;[^\\r\\n]*/ ;
和字符串一样,我们需要创建一个新的解析器并在 mpca_lang 中更新语法。 此外,还需将对应解析器添加到 mpc_cleanup,并同步更新解析器个数。
mpca_lang(MPCA_LANG_DEFAULT,
" \
number : /-?[0-9]+/ ; \
symbol : /[a-zA-Z0-9_+\\-*\\/\\\\=<>!&]+/ ; \
string : /\"(\\\\.|[^\"])*\"/ ; \
comment : /;[^\\r\\n]*/ ; \
sexpr : '(' <expr>* ')' ; \
qexpr : '{' <expr>* '}' ; \
expr : <number> | <symbol> | <string> \
| <comment> | <sexpr> | <qexpr>; \
lispy : /^/ <expr>* /$/ ; \
",
Number, Symbol, String, Comment, Sexpr, Qexpr, Expr, Lispy);
mpc_cleanup(8,
Number, Symbol, String, Comment,
Sexpr, Qexpr, Expr, Lispy);
因为注释仅仅是供程序员分析代码,所以用于读取代码的内置函数只是忽略它们。我们可以在 lval_read 中添加一个类似于括号处理方式的子句来处理注释。
if (strstr(t->children[i]->tag, "comment")) { continue; }
注释在 REPL 没有多大用处,但在给代码加上评注方面非常有用。
文件加载函数
我们想构建一个函数,当传入文件名称时加载并对文件中表达式求值。为了实现这个函数,我们需要用到语法解析器,因为我们需要其来读取文件内容、解析表达式并求值。加载函数将依赖于名为 Lispy 的 mpc_parser*
。
因此,就像函数一样,我们需要前向声明解析器指针,并将其放置于文件的顶端:
mpc_parser_t* Number;
mpc_parser_t* Symbol;
mpc_parser_t* String;
mpc_parser_t* Comment;
mpc_parser_t* Sexpr;
mpc_parser_t* Qexpr;
mpc_parser_t* Expr;
mpc_parser_t* Lispy;
我们的 load 函数就像任何其他内置函数一样。其首先需要检查输入参数是否为单个字符串。然后我们调用 mpc_parse_contents 函数通过语法解析器读入文件的内容。就像 mpc_parse 一样,它将文件内容解析为一些其中包含抽象语法树或错误的 mpc_result 对象。
与命令提示符略有不同,在成功解析文件时,我们不应将其视为一个表达式。在输入文件时,我们让用户列出多个表达式并对所有表达式单独求值。为了实现这个需求,我们需要遍历文件内容中的每个表达式并逐个进行求值。如果出现任何错误,我们应该打印错误信息并继续。
若解析出错,我们将提取错误信息并返回一个 error 型 lval。若解析正确,则此内置函数的返回值为一个空表达式。
lval* builtin_load(lenv* e, lval* a) {
LASSERT_NUM("load", a, 1);
LASSERT_TYPE("load", a, 0, LVAL_STR);
/* Parse File given by string name */
mpc_result_t r;
if (mpc_parse_contents(a->cell[0]->str, Lispy, &r)) {
/* Read contents */
lval* expr = lval_read(r.output);
mpc_ast_delete(r.output);
/* Evaluate each Expression */
while (expr->count) {
lval* x = lval_eval(e, lval_pop(expr, 0));
/* If Evaluation leads to error print it */
if (x->type == LVAL_ERR) { lval_println(x); }
lval_del(x);
}
/* Delete expressions and arguments */
lval_del(expr);
lval_del(a);
/* Return empty list */
return lval_sexpr();
} else {
/* Get Parse Error as String */
char* err_msg = mpc_err_string(r.error);
mpc_err_delete(r.error);
/* Create new error message using it */
lval* err = lval_err("Could not load Library %s", err_msg);
free(err_msg);
lval_del(a);
/* Cleanup and return error */
return err;
}
}
命令行参数
我们可以通过文件加载函数添加一些其他编程语言的典型功能。当文件名作为参数提供给命令行时,我们会去尝试运行这些文件。例如,要运行 python
文件,可以编写 python filename.py
。
我们通过使用赋给 main 函数的 argc 和 argv 形参来访问这些命令行参数。这意味着如果 argc 设置为 1,我们调用解释器,否则通过 builtin_load 函数运行每个参数。
/* Supplied with list of files */
if (argc >= 2) {
/* loop over each supplied filename (starting from 1) */
for (int i = 1; i < argc; i++) {
/* Argument list with a single argument, the filename */
lval* args = lval_add(lval_sexpr(), lval_str(argv[i]));
/* Pass to builtin load and get the result */
lval* x = builtin_load(e, args);
/* If the result is an error be sure to print it */
if (x->type == LVAL_ERR) { lval_println(x); }
lval_del(x);
}
}
现在写一些基础程序并尝试去以命令行参数的方式去调用。
lispy example.lspy
打印函数
如果我们从命令行运行程序,我们可能希望它们输出一些数据,而不仅仅是定义函数和其他值。 我们可以在 Lispy 中添加一个 print 函数,该函数复用了现有的 lval_print 函数。
该函数打印由空格分隔的每个参数,然后打印换行符完成整个流程。 函数返回空表达式。
lval* builtin_print(lenv* e, lval* a) {
/* Print each argument followed by a space */
for (int i = 0; i < a->count; i++) {
lval_print(a->cell[i]); putchar(' ');
}
/* Print a newline and delete arguments */
putchar('\n');
lval_del(a);
return lval_sexpr();
}
报错函数
我们还可以利用字符串添加报错函数。 该函数将用户提供的字符串作为输入,并将其提供给 lval_err 作为报错信息。
lval* builtin_error(lenv* e, lval* a) {
LASSERT_NUM("error", a, 1);
LASSERT_TYPE("error", a, 0, LVAL_STR);
/* Construct Error from first argument */
lval* err = lval_err(a->cell[0]->str);
/* Delete arguments and return */
lval_del(a);
return err;
}
最后一步是将以上函数注册为内置函数。 现在我们终于可以开始构建函数库并将其写入文件中。
/* String Functions */
lenv_add_builtin(e, "load", builtin_load);
lenv_add_builtin(e, "error", builtin_error);
lenv_add_builtin(e, "print", builtin_print);
源代码
Github:https://github.com/JmilkFan/Lispy.git
编译:
gcc -g -std=c99 -Wall parsing.c mpc.c -lreadline -lm -o parsing
运行:
$ ./parsing
Lispy Version 0.1
Press Ctrl+c to Exit
lispy> print "Hello World!"
"Hello World!"
()
lispy> error "This is an error"
Error: This is an error
用 C 语言开发一门编程语言 — 字符串与文件加载的更多相关文章
- VSTO学习笔记(三) 开发Office 2010 64位COM加载项
原文:VSTO学习笔记(三) 开发Office 2010 64位COM加载项 一.加载项简介 Office提供了多种用于扩展Office应用程序功能的模式,常见的有: 1.Office 自动化程序(A ...
- 一步一步开发Game服务器(三)加载脚本和服务器热更新(二)完整版
上一篇文章我介绍了如果动态加载dll文件来更新程序 一步一步开发Game服务器(三)加载脚本和服务器热更新 可是在使用过程中,也许有很多会发现,动态加载dll其实不方便,应为需要预先编译代码为dll文 ...
- iOS 开发——实用技术Swift篇&Swift 懒加载(lazy)
Swift 懒加载(lazy) 在程序设计中,我们经常会使用 * 懒加载 * ,顾名思义,就是用到的时候再开辟空间,比如iOS开发中的最常用控件UITableView,实现数据源方法的时候,通常我们都 ...
- Knockout应用开发指南 第六章:加载或保存JSON数据
原文:Knockout应用开发指南 第六章:加载或保存JSON数据 加载或保存JSON数据 Knockout可以实现很复杂的客户端交互,但是几乎所有的web应用程序都要和服务器端交换数据(至少为了本地 ...
- C#开发奇技淫巧二:根据dll文件加载C++或者Delphi插件
原文:C#开发奇技淫巧二:根据dll文件加载C++或者Delphi插件 这两天忙着把框架改为支持加载C++和Delphi的插件,来不及更新blog了. 原来的写的框架只支持c#插件,这个好做 ...
- IOS 开发下拉刷新和上拉加载更多
IOS 开发下拉刷新和上拉加载更多 简介 1.常用的下拉刷新的实现方式 (1)UIRefreshControl (2)EGOTTableViewrefresh (3)AH3DPullRefresh ( ...
- 伟景行 citymaker 从入门到精通(1)——js开发,最基本demo,加载cep工程文件
开发环境:citymaker 7(以下简称cm),jquery,easyui 1.4(界面),visual studio 2012(没有vs,不部署到IIS也行,html文件在本地目录双击打开可用) ...
- ios新手开发——toast提示和旋转图片加载框
不知不觉自学ios已经四个月了,从OC语法到app开发,过程虽然枯燥无味,但是结果还是挺有成就感的,在此分享我的ios开发之路中的小小心得~废话不多说,先上我们今天要实现的效果图: 有过一点做APP经 ...
- Windows开发,关于通过写代码加载PDB的那些事
最近,接到一个活,要写一个程序,用来批量分析一堆dll和对应的PDB, 其实工作很简单,就是根据一堆偏移,通过PDB文件,找到对应dll里面对应位置的明文符号, 简单的需求,实现起来,通常都很麻烦, ...
- Aery的UE4 C++游戏开发之旅(4)加载资源&创建对象
目录 资源的硬引用 硬指针 FObjectFinder<T> / FClassFinder<T> 资源的软引用 FSoftObjectPaths.FStringAssetRef ...
随机推荐
- js实现电子白板
功能:使用画笔绘制笔迹(线条).橡皮檫 <!DOCTYPE html> <html lang="en"> <head> <meta cha ...
- 在Mac系统上使用Qt调用摄像头不出图解决方法
需求:在Mac系统上,调用摄像头,实现旋转.缩放.处理视频帧等功能 问题:使用获取视频帧的方法,在Mac上调不起来摄像头 解决方法: 将视频窗口(QVideoWidget)和视频帧(QVideoFra ...
- openGauss/MogDB调用C FUNCTION
openGauss/MogDB 调用 C FUNCTION 摘要 之前写过一篇关于postgresql 自定义函数实现,通过 contrib 模块进行扩展的帖子,今天和恩墨工程师进行了一些交流,在 M ...
- DevEco Studio 3.1 Beta1版本发布——新增六大关键特性,开发更高效
智能代码编辑.端云一体化开发.低代码开发个性化-- 六大新增关键特性,开发更高效,体验更觉妙! 立即点击链接下载,做DevEco Studio 3.1 Beta1版本尝鲜者! 下载链接:HUAWE ...
- Spring Cloud 核心组件之Spring Cloud Zuul:API网关服务
Spring Cloud Zuul:API网关服务 SpringCloud学习教程 SpringCloud API网关 Spring Cloud Zuul 是Spring Cloud Netflix ...
- Pytorch-均方差损失函数和交叉熵损失函数
均方差损失函数mse_loss()与交叉熵损失函数cross_entropy() 1.均方差损失函数mse_loss() 均方差损失函数是预测数据和原始数据对应点误差的平方和的均值. \[MSE=\f ...
- 数据结构实验代码分享 - 3 (哈夫曼树 / HuffmanTree)
哈夫曼编码/ 译码系统(树应用) [问题描述] 任意给定一个仅由 26 个大写英文字母组成的字符序列,根据哈夫曼编码算法,求得每个字符的哈夫曼编码. 要求: 1)输入一个由 26 个英文字母组成的字符 ...
- Dubbo-go 服务代理模型
简介:HSF 是阿里集团 RPC/服务治理 领域的标杆,Go 语言又因为其高并发,云原生的特性,拥有广阔的发展前景和实践场景,服务代理模型只是一种落地场景,除此之外,还有更多的应用场景值得我们在研发 ...
- 独家深度 | 一文看懂 ClickHouse vs Elasticsearch:谁更胜一筹?
简介: 本文的主旨在于通过彻底剖析ClickHouse和Elasticsearch的内核架构,从原理上讲明白两者的优劣之处,同时会附上一份覆盖多场景的测试报告给读者作为参考. 作者:阿里云数据库OLA ...
- 深度解析PolarDB数据库并行查询技术
简介: 随着数据规模的不断扩大,用户SQL的执行时间越来越长,这不仅对数据库的优化能力提出更高的要求,并且对数据库的执行模式也提出了新的挑战.本文将介绍基于代价进行并行优化.并行执行的云数据库的并行查 ...