用 C 语言开发一门编程语言 — 字符串的设计与实现
目录
文章目录
- 目录
- 前言
- 前文列表
- 字符串与文件库
- 字符串
- print 关键字函数
- error 关键字函数
- 注释
- 文件加载
- 函数注册
- 命令行参数
前言
通过开发一门类 Lisp 的编程语言来理解编程语言的设计思想,本实践来自著名的《Build Your Own Lisp》。
- 代码实现:https://github.com/JmilkFan/Lispy
前文列表
《用 C 语言开发一门编程语言 — 交互式解析器》
《用 C 语言开发一门编程语言 — 语法解析器运行原理》
《用 C 语言开发一门编程语言 — 波兰表达式解析器》
《用 C 语言开发一门编程语言 — 表达式存储器》
《用 C 语言开发一门编程语言 — 符号表达式解析器》
《用 C 语言开发一门编程语言 — 引用表达式解析器》
《用 C 语言开发一门编程语言 — 变量的设计与实现》
《用 C 语言开发一门编程语言 — 基于 Lambda 表达式的函数设计与实现》
《用 C 语言开发一门编程语言 — 条件分支的设计与实现》
字符串与文件库
文件库功能,可以使我们每次启动交互式指令行的时候自动加载一系列预先定义好的功能函数,以此便于我们更灵活的进行使用。另外,可以从源文件中读取源代码并进行解析,也是编程语言的最终实现方式。
在实现函数库文件加载功能之前,我们要先引入字符串数据类型作为文件名的识别。
- 完整源代码:https://github.com/JmilkFan/Lispy.git
运行效果:
# 指令行
lispy> print "Hello World!"
"Hello World!"
()
lispy> error "This is an error"
Error: This is an error# 文件库
$ cat hello.lspy
(print "hello world.")$ lispy hello.lspy
"hello world."
字符串
首先,在 MPC Parser 中添加字符串的词法分析规则。标识字符串是两个双引号 "
之间的一系列转义字符或普通字符组成。
string : /\"(\\\\.|[^\"])*\"/ ;
然后,添加 LVAL_STR 数据类型:
enum { LVAL_ERR, LVAL_NUM, LVAL_SYM, LVAL_STR,LVAL_FUN, LVAL_SEXPR, LVAL_QEXPR };
ltype_name 类型名部分:
case LVAL_STR: return "String";
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;
lval_read 读取函数部分:
if (strstr(t->tag, "string")) { return lval_read_str(t); }/*** 读取函数* 首先,它必须剥离字符串两侧 `"` 字符。* 然后,必须对转义字符串进行解码,将一系列转义字符(如 `\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;
}
打印函数部分:
case LVAL_STR: lval_print_str(v); break; // 打印字符串时自动加入换行符// 打印函数
void lval_print_str(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);
}
lval_copy 数据复制部分:
case LVAL_STR:x->str = malloc(strlen(v->str) + 1);strcpy(x->str, v->str);break;
lval_eq 等于比较函数部分:
case LVAL_STR: return (strcmp(x->str, y->str) == 0);
如果这一切都没有问题,我们应该能够在 REPL 中使用字符串:
lispy> "hello"
"hello"
lispy> "hello\n"
"hello\n"
lispy> "hello\""
"hello\""
lispy> head {"hello" "world"}
{"hello"}
lispy> eval (head {"hello" "world"})
"hello"
lispy>
print 关键字函数
基于现有的 lval_print 函数实现一个 print 关键字,以便进行字符串的打印。
/*** 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();
}
error 关键字函数
在源文件中,我们可以通过 error 关键字来打印错误警报信息。
/*** error 错误反馈函数* 将用户提供的字符串作为输入,并将其提供给 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;
}
注释
注释在 REPL 没有多大用处,但在源文件中则非常有用。C 语言的注释使用 //
和 /**/
这 2 种形式,我们则使用 ;
开头表示单行注释即可。
词法规则为:
comment : /;[^\\r\\n]*/ ;
添加 Comment 到语法解析器中:
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; }
文件加载
继续通过 MPC 库来实现 builtin 的 load 函数。
- 首先,检查输入参数是否为单个字符串。
- 然后,调用 mpc_parse_contents 函数通过 MPC Parser 来加载源文件的内容。
- 最后,MPC Parser 对文件内容进行解析,并返回一个 MPC AST。
- 若解析出错,将错误信息返回一个 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;}
}
函数注册
最后一步是将以上函数注册为内置函数。
/* String Functions */
lenv_add_builtin(e, "load", builtin_load);
lenv_add_builtin(e, "error", builtin_error);
lenv_add_builtin(e, "print", builtin_print);
命令行参数
在实现了源文件的解析功能之后,还需要添加一个 CLI 操作入口。实现函数库文件的启动加载。
这里应用了 C 语言的 main CLI 参数特性:
/* 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);}
}
用 C 语言开发一门编程语言 — 字符串的设计与实现相关推荐
- 用 C 语言开发一门编程语言 — 字符串与文件加载
目录 文章目录 目录 前文列表 字符串 读取字符串 注释 文件加载函数 命令行参数 打印函数 报错函数 源代码 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器> <用 C ...
- 用 C 语言开发一门编程语言 — 变量元素设计
目录 文章目录 目录 前文列表 变量 变量语法规则 变量的读取和存储 将变量加入 Lisp Value 体系 变量的计算 变量的定义与赋值 异常处理优化 源代码 前文列表 <用 C 语言开发一门 ...
- 用 C 语言开发一门编程语言 — 条件分支
目录 文章目录 目录 前文列表 条件分支 排序函数 等于函数 if 函数 递归函数 源代码 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器> <用 C 语言开发一门编程语 ...
- 用 C 语言开发一门编程语言 — 基于 Lambda 表达式的函数设计
目录 文章目录 目录 前文列表 函数 Lambda 表达式 函数设计 函数的存储 实现 Lambda 函数 函数的运行环境 函数调用 可变长的函数参数 源代码 前文列表 <用 C 语言开发一门编 ...
- 用 C 语言开发一门编程语言 — S-表达式
目录 文章目录 目录 前文列表 使用 S-表达式进行重构 读取并存储输入 实现 S-Expression 语法解析器 实现 S-Expression 存储器 实现 lval 变量的构造函数 实现 lv ...
- 用 C 语言开发一门编程语言 — 异常处理
目录 文章目录 目录 前文列表 异常捕获 定义 Lisp Value 函数 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器l> <用 C 语言开发一门编程语言 - 跨平台 ...
- 用 C 语言开发一门编程语言 — 抽象语法树
目录 文章目录 目录 前文列表 抽象语法树的结构 使用递归来遍历树结构 实现求值计算 抽象语法树与行为树 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器l> <用 C 语 ...
- 用 C 语言开发一门编程语言 — 语法解析器
目录 文章目录 目录 前文列表 编程语言的本质 词法分析 语法分析 使用 MPC 解析器组合库 安装 快速入门 实现波兰表达式的语法解析 波兰表达式 正则表达式 代码实现 前文列表 <用 C 语 ...
- 用 C 语言开发一门编程语言 — Q-表达式
目录 文章目录 目录 前文列表 Q-表达式 读取并存储输入 实现 Q-Expression 语法解析器 读取 Q-Expression 实现 Q-Expression 的函数 Head & T ...
最新文章
- SQLite中的运算符表达式
- 张家口张北:加速“云计算”产业集聚
- IPV4报头格式详解
- 全面解析RS232、RS485、RS422、RJ45接口的区别和各自的应用
- boost::spirit模块使用 phoenix 对逗号分隔的数字列表求和的解析器
- 解决ECLIPSE 卡死的方法
- 一亩地等于多少平方米?
- 计算机考试用户名和密码,计算机模块考试忘记了用户名和密码怎么办
- Apache Camel,Spring Boot 实现文件复制,转移 (转)
- 如何在xshell中查看某一接口的服务_【干货分享】如何使用远程工具进入Linux系统...
- NRF52832学习笔记
- Linux目录/usr/bin和 /usr/local/bin区别
- css定位、position与float同时使用的情况
- Java Swing线程之SwingUtilities.invokeLater解释
- 发射功率dBm 换算表
- vue如何整个页面添加loading
- 高得地图 +数据绑定(databinding) + BaseQuickAdapter 自定义地图选点!
- 如何开发一个动物识别系统毕业设计毕设作品
- 用python实现词语接龙游戏
- js钢琴(含钢琴按键音效包)
热门文章
- NoSQL 简单介绍
- 服务器信号满格但上不了网,新版tplink路由器wifi信号满格上不了网怎么办?
- 【搬家】【数据库】【优化】SQL 优化学习小结——索引和语句优化
- 第三章:3.3 傅里叶级数收敛分析
- 2022大健康展,2022山东健康产业展,艾灸设备展9月举办
- 深刻理解RGB色彩空间与HSI色彩空间的转换关系
- 随机森林之信贷风险模型特征工程
- Git 提交规范之GIt工作流规范(Husky+eslint+stylelint+pretty+Commitlint+ lint-staged)
- python面向对象:光学元件类的实现
- java 简易的闹钟设计,用java设计智能闹钟