• 用 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 内部存储字符串与所打印字符串有所区别。我们希望在打印用户所输入的字符串时,会使用转义字符(如 )来表示换行符。

    因此,我们需要在打印字符串之前将其转义。幸运的是,我们可以使用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 函数来解决这个问题。 这个功能有点棘手,因为它必须解决以下问题。 首先,它必须剥离字符串两侧 " 字符。然后必须对转义字符串进行解码,将一系列转义字符(如 )转换成实际编码字符。最后必须创建一个新的 lval 并清理函数中使用过的内存。

    lval* lval_read_str(mpc_ast_t* t) {
      /* Cut off the final quote character */
      t->contents[strlen(t->contents)-1] = '';
      /* 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
    "
    "hello
    "
    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('
    ');
      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
    
  • 相关阅读:
    LeetCode-20.Valid Parentheses
    LeetCode-2. Add Two Numbers
    LeetCode-1. Two Sum
    LeetCode Top100 Liked Questions
    位运算
    sublime中文乱码解决办法
    jQuery 参考手册
    jQuery 参考手册
    jQuery 参考手册
    《锋利的Jquery第二版》读书笔记 第三章
  • 原文地址:https://www.cnblogs.com/jmilkfan-fanguiju/p/12789695.html
Copyright © 2020-2023  润新知