关键词搜索

源码搜索 ×
×

用 C 语言开发一门编程语言 — 字符串的设计与实现

发布2023-03-19浏览1910次

详情内容

目录

前言

通过开发一门类 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  : /\"(\\\\.|[^\"])*\"/ ;  
    
    • 1

    然后,添加 LVAL_STR 数据类型:

    enum { LVAL_ERR, LVAL_NUM,   LVAL_SYM, LVAL_STR,
           LVAL_FUN, LVAL_SEXPR, LVAL_QEXPR };
    
    • 1
    • 2

    ltype_name 类型名部分:

    case LVAL_STR: return "String";
    
    • 1

    lval 结构体部分:

    /* Basic */
    long num;
    char* err;
    char* sym;
    char* str;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    构造函数:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    析构函数部分:

    case LVAL_STR: free(v->str); break;
    
    • 1

    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;
    }
    
      15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    打印函数部分:

    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);
    }
    
      15
    • 16
    • 17
    • 18

    lval_copy 数据复制部分:

    case LVAL_STR:
      x->str = malloc(strlen(v->str) + 1);
      strcpy(x->str, v->str);
      break;
    
    • 1
    • 2
    • 3
    • 4

    lval_eq 等于比较函数部分:

    case LVAL_STR: return (strcmp(x->str, y->str) == 0);
    
    • 1

    如果这一切都没有问题,我们应该能够在 REPL 中使用字符串:

    lispy> "hello"
    "hello"
    lispy> "hello\n"
    "hello\n"
    lispy> "hello\""
    "hello\""
    lispy> head {"hello" "world"}
    {"hello"}
    lispy> eval (head {"hello" "world"})
    "hello"
    lispy>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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();
    }
    
      15
    • 16
    • 17
    • 18

    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;
    }
    
      15

    注释

    注释在 REPL 没有多大用处,但在源文件中则非常有用。C 语言的注释使用 ///**/ 这 2 种形式,我们则使用 ; 开头表示单行注释即可。

    词法规则为:

    comment : /;[^\\r\\n]*/ ;
    
    • 1

    添加 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);
    
      15
    • 16
    • 17
    • 18
    • 19

    因为注释仅用于开发者阅读,所以,在 lval_read 读取注释时,不需要做特别的处理。

    if (strstr(t->children[i]->tag, "comment")) { continue; }
    
    • 1

    文件加载

    继续通过 MPC 库来实现 builtin 的 load 函数。

    1. 首先,检查输入参数是否为单个字符串。
    2. 然后,调用 mpc_parse_contents 函数通过 MPC Parser 来加载源文件的内容。
    3. 最后,MPC Parser 对文件内容进行解析,并返回一个 MPC AST。
      1. 若解析出错,将错误信息返回一个 error 型 lval。
      2. 若解析正确,则此内置函数的返回值为一个空表达式。

    值得注意的是,区别于交互式提示符每次输入只是一条表达式,源文件中包含了多条表达式,所以们需要遍历文件内容中的每个表达式并逐个进行求值。如果出现任何错误,我们应该打印错误信息并继续。

    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;
      }
    }
    
      15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    函数注册

    最后一步是将以上函数注册为内置函数。

    /* String Functions */
    lenv_add_builtin(e, "load",  builtin_load);
    lenv_add_builtin(e, "error", builtin_error);
    lenv_add_builtin(e, "print", builtin_print);
    
    • 1
    • 2
    • 3
    • 4

    命令行参数

    在实现了源文件的解析功能之后,还需要添加一个 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);
      }
    }
    
      15
    • 16
    • 17

    相关技术文章

    点击QQ咨询
    开通会员
    返回顶部
    ×
    微信扫码支付
    微信扫码支付
    确定支付下载
    请使用微信描二维码支付
    ×

    提示信息

    ×

    选择支付方式

    • 微信支付
    • 支付宝付款
    确定支付下载