关于lisp:在-Lisp-中使用-reader-macro-支持-JSON-语法

在 Lisp 中应用 reader macro 反对 JSON 语法什么是 reader macro?Reader macro 是 Common Lisp 提供的泛滥乏味个性之一,它让语言的使用者可能自定义词法分析的逻辑,使其在读取源代码时,如果遇到了特定的一两个字符,能够调用相应的函数来个性化解决。此处所说的“特定的一两个字符”,被称为 macro character,而“相应的函数”则被称为 reader macro function。举个例子,单引号'就是一个 macro character,能够用函数get-macro-character来获取它对应的 reader macro function。 CL-USER> (get-macro-character #\')#<FUNCTION SB-IMPL::READ-QUOTE>NIL借助单引号,能够简化一些代码的写法,例如表白一个符号HELLO自身能够写成这样。 CL-USER> 'helloHELLO而不是上面这种等价但更繁琐的模式。 CL-USER> (quote hello)HELLOCommon Lisp 中还定义了由两个字符形成的 reader macro,例如用于书写simple-vector字面量的#(。借助它,如果想要表白一个顺次由数字 1、2、3 形成的simple-vector类型的对象,不须要显式地调用函数vector并传给它 1、2、3,而是能够写成#(1 2 3)。 反对 JSON 语法后有什么成果?非法的 JSON 文本不肯定是非法的 Common Lisp 源代码。例如,[1, 2, 3]在 JSON 规范看来是一个由数字 1、2、3 组成的数组,但在 Common Lisp 中,这段代码会触发 condition。(condition 就是 Common Lisp 中的“异样”、“出情况”了) CL-USER> (let ((eof-value (gensym))) (with-input-from-string (stream "[1, 2, 3]") (block nil (loop (let ((expr (read stream nil eof-value))) (when (eq expr eof-value) (return)) (print expr))))))[1 ; Evaluation aborted on #<SB-INT:SIMPLE-READER-ERROR "Comma not inside a backquote." {1003AAD863}>.这是因为依照 Common Lisp 的读取算法,左方括号[和数字 1 都是规范中所指的 constituent character,它们能够组成一个 token,并且最终被解析为一个符号类型的对象。而紧接着的字符是逗号,,它是一个 terminating macro char,依照规范,如果不是在一个反引号表达式中应用它将会是有效的,因而触发了 condition。 ...

May 23, 2023 · 2 min · jiezi

关于lisp:使用-callcc-实现计数循环

什么是计数循环计数循环就是从一个数字$i$开始始终遍历到另一个数字$j$为止的循环过程。例如,上面的 Python 代码就会遍历从 0 到 9 这 10 个整数并一一打印它们 for i in range(10): print(i)如果是在 C 语言中实现同样的性能,代码会更显著一些 #include <stdio.h>int main(int argc, char *argv[]){ for (int i = 0; i < 10; i++) { printf("%d\n", i); } return 0;}在 C 语言的例子中,显式地指定了计数器变量i从 0 开始并且在等于 10 的时候完结循环,比之 Python 版本更有循环的滋味。 拆开循环计数的语法糖应用 C 语言的while语句同样能够实现计数循环,示例代码如下 #include <stdio.h>int main(int argc, char *argv[]){ int i = 0; while (i < 10) { printf("%d\n", i); i++; } return 0;}如果将while也视为if和goto的语法糖的话,能够进一步将计数循环写成更原始的模式 ...

May 7, 2023 · 2 min · jiezi

关于lisp:用lisp写一个-柯里化curry的宏

curry.lisp (define-macro curry (lambda (fn) ( (define curry0 (lambda (args body) ( (if (nil? args) (body) ( `(lambda ((,(car args))) (,( curry0 (cdr args) body ))) )) ))) (if (procedure? (apply fn)) ( (define args (car (cdr fn))) (define body (cdr (cdr fn))) (curry0 args body) ) (fn)))))应用 (define add3 (curry(lambda (x y z) (+ x y z))))(((add3 1) 2) 3)介绍这条语句 (curry(lambda (x y z) (+ x y z)))等价于上面这条语句 ...

October 29, 2022 · 1 min · jiezi

关于lisp:用rust写lisp解释器2-实现一个简单的异步模型channel-thread-go

背景前段时间实现了一个 call-with-tcp-listener 过程(函数) (call-with-tcp-listener "127.0.0.1:8088" ( lambda (in) ( (display (req-read-string in)) "HTTP/1.1 200 OK\r\n\r\n hello word" )))如果是简略的返回数据还不存在问题,然而当波及到io的时候就会呈现阻塞的状况,最简略的解决方案就是一个申请一个线程,然而这种模型开销比拟大,所以想到了线程池,而后就实现了一个及其简略的版本 实现应用到了 thread + channel + lambda-lep + apply 这几个过程首先定义一个线程池async.lisp ( (define thread-qty (* (get-os-cpu-num) 2)) (define thread-count 1) (define barrier (make-barrier thread-qty)) (define channel (make-channel)) (while (< thread-count thread-qty) ( (thread-run (lambda () ( (channel-for-each (lambda (task) ( ( task) )) channel) (barrier-wait barrier) ;; (loop ((<- channel))) ))) (set! thread-count (+ thread-count 1)))) (define go (lambda-lep (task) ( (apply (`(-> ,channel ,task))) ))) (export go))如何应用 ...

July 2, 2022 · 1 min · jiezi

关于lisp:用java写lisp-解释器-2-扩展无限可能

在上一篇咱们曾经领有了一个简略lisp 解释器 (JLispInter)它反对: 变量:a绑定:(define a 5)四则运算: + - * /函数 (lambda (x) (exp))调用 (g exp)过后咱们还留下了一些问题: 如解析器还不反对字符串,高阶函数中四则运算 define 等还不能作为入参,不反对匿名函数 ,分支判断 等在这篇文章中咱们先疏忽语法树解析器,专一于解释器,花了1个多小时的工夫写了一个更加欠缺的解释器,为了不便解说我会将代码一段一段的放出,这样能够让咱们更聚焦一些,在开始前,画了一个表达式语法元素的形成图,这是形象之后(疏忽了如 Characters,Strings,Vectors , Dotted pairs, lists 等)前面如Dotted pairs 咱们能够通过函数的模式实现。 其中 var expression 是援用类型也就是能够对其进行化简,var在此处咱们用symbols进行代指;number, characters, boolean 是根底类型或原子类型(atom)不能对其进行化简,其就是最简模式;functional则是介于两者之前,在作为参数时它是一个箱子,在对其进行apply(调用)时则是关上的箱子,箱子里关上后能够是atom,也能够是functional,如果是后者则是高阶函数,也就是能够再次对其进行apply(调用)直到关上后为atom为止。 在前面咱们会看到 :“道生一 , 毕生二, 二生三 ,三生万物。” 准备阶段 咱们还须要对 Env 进行一些革新,对解析器进行的革新咱们在这里先疏忽掉, Cons 对应的是前文 ExpNode (此处 Cons是ExpNode 的父类) Env public class Env { private final Map<String, Object> env = new HashMap<>(); private Env parent; public static Env newInstance(Env parent) { Env env1 = new Env(); env1.parent = parent; return env1; } public void setEnv(Symbols symbols, Object val) { setEnv(symbols.getName(),val); } public void setEnv(String key, Object val) { env.put(key, val); } public Optional<Object> env(Symbols symbols) { String symbolsName = symbols.getName(); return Optional.ofNullable(env.containsKey(symbolsName) ? env.get(symbolsName) : (parent != null ? parent.env(symbols).orElse(null) : null)); } public boolean contains(Symbols symbols){ return env.containsKey(symbols.getName()); } public Env parent(){ return parent; }}道生一JLispInter2 ...

February 8, 2022 · 7 min · jiezi

关于lisp:用java写一个lisp-解释器

起初最早听到lisp这个名字是一个偶尔的机会,留下了很牛的印象,工夫匆匆五年就过来了,前些日子看sicp,外面又再次提到了这个名字,从网上找了几个入门文档学习了一下根底语法,便又持续看起了sicp;从写下第一行(+ 1 2)代码,日子转瞬一个月就过来了,不得不说 lisp的前缀表达式的形式还是很不错的,不知怎得缓缓有了写一个lisp 解释器的想法,而后想到了之前王垠仿佛写过一篇文章《怎么写一个解释器》 如果你对如何写一个解释器感兴趣能够看下这篇文章,还是有所启发的。 回来了吗?哈哈哈 咱们持续旅途 写一个lisp解释器能够分三步: 1.将lisp表达式的字符串转换成一个树性构造的数组 2.解释这棵树 3.反对变量和办法调用 这里应用的语言是java 结构语法树首先第一步:如何将lisp表达式的字符串转换成一个树性构造的数组?咱们看一几个lisp表达式 剖析一下他的形成 (+ 1 2)(+ 1 2 (- 3 4))(+ 1 2 (- 3 4) 5)(+ 1 2 (- 3 4) 5 (+ 6 7 (+ 8 9)))(+ 1 2 (- 3 4) (+ 5 6 (+ 7 8)))(+ 1 2 (- 3 4) (+ 5 6 (+ 7 8)) 9)能够看到以上表达式外面能够分成两种元素:一个是不可分割的最小元素如 + - 1 2 3 这种 ,还有 (- 3 4)这种复合元素,而复合元素也是由最小的根底元素形成,于是咱们失去了第一个规定(复合元素能够被拆分成更小的根底元素和复合元素)。以(+ 1 2 (- 3 4) 5 (+ 6 7 (+ 8 9)))这个表达式为例,它如果是一棵树长什么样子呢?上面让咱们画出它的状态:咱们有了它的样子,但要如何将一个字符串模式的表达式转换成这样一棵树呢? 这是咱们接下来要剖析的问题duang duang duang duang...让咱们回到咱们的第一个规定 这里还有什么暗藏信息呢?1.复合元素可拆分2.根底元素不可拆分3.复合元素是被“()”包裹的元素有了这三项咱们就能够在进一步的思考了,树树树,树的元素是什么?1.节点2.叶子节点头绪,头绪,有了头绪节点对应的是复合元素,根底元素对应的是叶子节点,那如何辨别复合元素和根底元素呢?“3.复合元素是被“()”包裹的元素”,是它,是它,就是它。复合节点里的第一个元素是“(”后的第一个元素,最初一个元素是“)”前的第一个元素,咱们又失去了第二个规定,有了下面两个规定咱们开始构建咱们的第一棵树:代码: ...

February 7, 2022 · 3 min · jiezi

关于lisp:非递归遍历二叉树到底有什么用

筹备过互联网公司的服务端岗位面试的人,对于二叉树的三种遍历形式想必是一五一十。假如以类BinaryTree定义一棵二叉树 class BinaryTree: def __init__(self, left, right, value): self.left = left self.right = right self.value = value实现一个前序遍历的算法便是信手拈来的事件 def preorder_traversal(tree, func): """前序遍历二叉树的每个节点。""" if tree is None: return func(tree.value) preorder_traversal(tree.left, func) preorder_traversal(tree.right, func)随着行业曲率的增大,要求写出不应用递归的版本也没什么过分的 def iterative_preorder_traversal(tree, func): nodes = [tree] while len(nodes) > 0: node = nodes.pop() func(node) if node.left is not None: nodes.append(node.right) if node.left is not None: nodes.append(node.left)始终以来,我感觉这种用一个显式的栈来代替递归过程中隐式的栈的做法就是镜花水月。但最近却找到了它的一个用武之地——用于实现iterator。 iterator是个啥?这年头,iterator曾经不是什么陈腐事物了,许多语言中都有反对,维基百科上有一份清单列出了比拟出名的语言的iterator个性。依照Python官网的术语表中的定义,iterator示意一个数据流,重复调用其__next__办法能够一个接一个地返回流中的下一项数据。将内置函数iter作用于list、str、tuple类型的对象,能够取得相应的迭代器 $ cat get_iter.py# -*- coding: utf8 -*-if __name__ == '__main__': values = [ [1, 2, 3], 'Hello, world!', (True, None), ] for v in values: print('type of iter({}) is {}'.format(v, type(iter(v))))$ python get_iter.pytype of iter([1, 2, 3]) is <class 'list_iterator'>type of iter(Hello, world!) is <class 'str_iterator'>type of iter((True, None)) is <class 'tuple_iterator'>写一个前序遍历的iterator一个iterator对象必须要实现__iter__和__next__办法: ...

May 3, 2021 · 2 min · jiezi

关于lisp:Elisp-12兔子洞

前言:不知多久能学会 Elisp 上一章:动静模块 从本章开始,进入这份 Elisp 教程的第三局部。这部分内容侧重于利用,在假使不得不引入没学过的 Elisp 语法之时,则阐明所偏重的利用必然是好的。 一个游戏当初思考来写一个很简略的小游戏:在一个一维的世界里寻找兔子洞。 该如何示意这个一维世界呢?用缓冲区便可示意。该如何在这个世界里行走呢?在缓冲区内挪动插入点。 在这个世界里,如何示意兔子洞呢?这须要三思。 缓冲区变量我想用两个既不是全局变量也不是局部变量的变量来示意兔子洞的入口和进口。在 Elisp 语言里,这样的变量能够是缓冲区变量。上面是这两个变量的定义: (setq hole-entrance "@#")(setq hole-exit "#@")(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (make-local-variable 'hole-exit))尽管 hole-entrance 和 hole-exit 一开始是定义成了全局变量,然而在 Elisp 解释器执行了 find-file 之后,以后缓冲区便是寄存 world.txt 文件内容的缓冲区,这两个变量在 make-local-variable 的作用下,就变成了以后缓冲区内的变量。 任何一个缓冲区都能间接应用同一个全局变量,也将它变成本人的变量,然而各个缓冲区都会感觉本人能够独占它,批改它的值,而且这种批改对其余缓冲区没有任何影响。就像是一个人同时呈现在多个世界里,他不晓得其余世界里的本人是怎么的境况。 简略的世界当初,曾经有了兔子洞的入口和进口的示意模式了,因而能够为上述的游戏轻易结构一个世界: ********@#一个兔子洞#@********@#又一个兔子洞#@**************************@# 这也是一个兔子洞#@****************这个世界里,有三个兔子洞。我将这个世界保留在文本文件 world.txt 里,待 Elisp 程序应用 (find-file) 将其载入缓冲区。接下来的工作便是如何在寄存这个世界的缓冲区内找出这些兔子洞。 search-forward分明了要解决的问题是什么,还有分明本人有哪些资源能够利用以及如何利用,此二者的充沛联合,便诞生了算法。要找出兔子洞,能够利用 Elisp 函数 search-forward,同之前用过的 re-search-forward 类似,可在缓冲区内前向递进搜寻与指定文本相匹配的文本,只是后者反对正则表达式匹配。在寻找兔子洞的过程中,不须要用正则表达式,因而用 search-forward 更为适宜。 当初试试 search-forward: (setq hole-entrance "@#")(setq hole-exit "#@")(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (search-forward hole-entrance))Elisp 解释器对上述程序的求值后果是 11,这正是第一个兔子洞入口 @# 之后的地位,亦即 search-forward 返回的后果是缓冲区内与 hole-entrance 的值匹配的文本的开端地位。 ...

April 24, 2021 · 2 min · jiezi

关于lisp:不知多久能学会-Elisp

引言在一个春天的夜晚,良久也没怎么出门的我,偶尔发现 Emacs Lisp 程序可能像脚本程序那般运行,而不仅仅是用于编写 Emacs 的配置文件或其插件。这个发现,让我感觉无心中发现了一宗瑰奇的宝藏。 对于 Lisp 语言的源远流长及其与世上最好的文本编辑器 Emacs 的莫逆之交,有很多书籍和文章早已给出了庄重的介绍,在此我就不用再行考据和论述了……没人发稿费,就没必要凑字数。上面,大抵总结一下为什么我会感觉能像脚本程序那般运行的 Emacs Lisp 程序蕴含着一宗宝藏: Emacs Lisp 具备着通用的编程语言应该具备的元素,可用于编写在计算机上解释运行的程序。Emacs Lisp 是一种 Lisp 方言,继承了 Lisp 语言的一些重要个性,例如反对泛函编程(Functional Progarmming),可基于宏实现语法扩大。与那些更好的 Lisp 方言相比,Emacs Lisp 存在一些差距 1,却也无伤大雅,因为在精通 Emacs Lisp 的根底上,再学习其余更好的 Lisp 方言,仅须要再了解寥寥几个新的概念,诸如续延(Continuation)、卫生宏(Hygienic macro)等。应用 Emacs Lisp 语言编写的程序具备跨平台性。Emacs 可在 GNU/Linux,Windows,Mac OS 以及 FreeBSD 等零碎上运行,因此应用 Emacs Lisp 语言编写的程序通常可毫无阻碍地这些零碎中运行。Emacs Lisp 程序可调用 C 程序库里的函数 2,因此其性能瓶颈可基于 C 程序予以补救。Emacs Lisp 具备其余编程语言可能没有的一些个性,这个个性并不来自语言,而是来自 Emacs 本身。迄今为止,Emacs 仍然称得上世上最好的文本编辑器,它在文本处理方面长期以来凝聚了泛滥智慧,而这些智慧早已积淀造成了一个宏大的 Emacs Lisp 代码库。这是否意味着,在我应用 Emacs Lisp 编写一个程序用于解决某种特定格局的文本时,有近乎取之不尽的代码可用呢?诚然,Emacs Lisp 有一些先天不足 3。不过,对于文本处理方面的工作而言,Emacs 本身的存在足以证实这些先天不足是次要矛盾。我所说的能像脚本程序那般运行的 Emacs Lisp 程序蕴含着一宗宝藏,次要针对编写文本处理程序而言。 ...

April 10, 2021 · 1 min · jiezi

关于lisp:Elisp-11动态模块

上一章:宏 Emacs 从版本 25 开始反对动静模块。所谓动静模块,即 C 语言编写的共享库 1 。Emacs 的动静模块,就是 Elisp 的动静模块。因而,假使 Elisp 语言编写的程序某些环节存在性能瓶颈,可借 C 语言之力予以缓解。对于其余编程语言,只有可能调用 C 程序库,皆能用于编写 Emacs 的动静模块。本章仅讲述如何应用 C 语言实现此事,所用的 C 编译器为 gcc。 又一个 Hello world!Hello world 程序总是可能帮忙咱们疏忽大量的细节,而把握一个程序的根本面貌,这一教训对于如何编写 Emacs 动静模块仍然实用。 我倡议应用 Emacs 然而并不禁止应用其余文本编辑器创立 C 程序源文件 foo.c,在其中郑重其事地写下 #include <emacs-module.h>int plugin_is_GPL_compatible;应用 C 语言为 Emacs 编写的任何一个动静模块皆以上述代码作为结尾。 接下来应该写 main 函数了。每个 C 程序皆以 main 函数作为程序的入口和进口。然而,Emacs 动静模块的入口和进口不是 main,而是 int emacs_module_init (struct emacs_runtime *ert){ return 0;}跟 C 程序的 main 函数类似,返回 0 示意胜利,返回其余整型数值意味着失败。 还记得 C 程序的 Hello world 吗? ...

April 10, 2021 · 3 min · jiezi

关于lisp:Elisp-10宏

上一章:库 上一章实现了只定义了一个函数的库 newbie.el。事实上,这个函数能够不必定义成函数,定义成宏也能够,而且能让调用代码的执行效率微不足道地更高一些。因为,调用函数,就像是去车站乘坐客车,而调用宏,犹如乘坐自家的私家车。这是一个不是很精确的比喻,所以它仅仅是个比喻。 定义宏先定义一个什么也干不了的宏, (defmacro foo ())在模式上,定义宏,仿佛跟定义函数差不多,只是 defun 换成了 defmacro。 调用一个宏,也跟调用一个函数差不多,例如调用上述定义的什么也干不了的宏 foo; (foo)对于这个宏调用,Elisp 的求值后果是 nil。为什么是 nil 呢?因为 Elisp 解释器遇到宏调用语句,会用宏的定义替换它,此即宏的开展。上述 (foo) 语句会被替换为 就是什么都没有。什么都没有,就是 nil。 假使是让 foo 的定义有点什么,例如 (defmacro foo () t)那么宏调用语句的开展后果就是 t。 宏也能够像函数那样领有参数,例如 (defmacro foo (x) x)宏调用 (foo "Hello world!") 的开展后果便是 "Hello world!"。 像结构数据一样构造程序宏的定义,展示的是 Lisp 语言的一个很重要的个性,在程序里能够像结构数据一样地构造程序。例如 (defmacro foo () (list '+ 1 2 3))Elisp 解释器会对宏定义里的表达式予以求值。上述宏定义里的 (list '+ 1 2 3),求值后果就是 (+ 1 2 3)。因而,宏调用语句 (foo) 会被 Elisp 解释器开展为 (+ 1 2 3),而后 Elisp 解释器会对宏的开展后果持续进行求值,因而 (foo) 的求值后果是 6。利用 Elisp 解释器对宏的定义和调用的解决机制,便能够在程序里像结构数据一样地构造程序。 ...

April 9, 2021 · 3 min · jiezi

关于emacs:emacs学习系列-emacs学习lisp各种数据类型

一、参考根本数据类型二、数值类型emacs 的数字分为整数和浮点数 (1)整数的范畴能够通过 most-positive-fixnum, most-negative-fixnum 2.1 进制同一个整数,能够应用 2~36进制来示意 2.2 迷信计数法 2.3 NaN值 not a number 2.4 测试函数lisp的测试函数个别都是结尾加上 p, predicate的缩写 如果函数名是一个单词通常,加上 p如果函数名是多个单词,通常加上 -p 例如: hellop hello-world-p 2.5 比拟函数 2.6 数的转换 2.7 运算

April 8, 2021 · 1 min · jiezi

关于lisp:Elisp-09库

上一章:文本跨行提取 从第 1 章就定义了的 princ\' 函数被我一路应用至今,我始终感觉它很有用途,特地是在我调试程序的时候。我抵赖,用这种方法调试程序很原始。不过,筷子也很原始。为了不再张贴残缺的代码之时附上它的定义,我决定建设一个 Elisp 库,用于寄存它的定义以及今后我定义的其余函数。 库文件我将这个库的全副代码寄存在一份名为 newbie.el 文件里,目前只有 princ\' 的定义: (defun princ\' (x) (princ x) (princ "\n"))然而,newbie.el 该当放在何处? EMACSLOADPATH零碎环境变量 EMACSLOADPATH 可为库文件指定门路。假如我将 newbie.el 放在 $HOME/.my-scripts/elisp 目录里,那么在我的机器上,因为我用的 Shell 是 Bash,因而可在 $HOME/.bashrc 文件里设定 export EMACSLOADPATH=$HOME/.my-scripts/elisp:$EMACSLOADPATH在以后终端里执行 $ source $HOME/.bashrc或从新关上一个终端窗口,令上述设定失效。 库的载入假如我要写一个 foo.el 程序,它须要调用 newbie.el 里定义的函数 princ\',只需在调用 princ\' 之前,载入 newbie.el 即可。例如 (load "newbie")(princ\' "Hello world!")在终端执行 foo.el 程序, $ emacs -Q --script foo.el程序输入 Loading /home/garfileo/.my-scripts/elisp/newbie.el (source)...Hello world!load 函数是 Elisp 的内建函数,它的第一个参数是库文件名,然而没有扩展名。因为 load 函数会在 $EMACSLOADPATH 指定的目录中主动搜寻三种库文件。对于上例,load 函数会依序搜寻 newbie.elc,newbie.el,newbie.ext 这三个文件,只有搜到其中之一,搜寻过程便终止,而后将搜到的文件内容载入至以后程序。newbie.ext 中的「ext」取决于零碎平台,在 Linux 零碎里,「ext」是「so」;在 Windows 零碎里,「ext」是「dll」;亦即 newbie.ext 能够是 C 语言接口的共享库。没错,Elisp 能够载入 C 库,然而须要为它们编写接口绑定,这是后话,暂且不表。 ...

April 7, 2021 · 1 min · jiezi

关于lisp:emacs学习系列-emacs学习lisp

一、参考Emacs Lisp 扼要教程二、scratch缓存区进入scratch缓存区, 模式抉择 lisp-interaction-mode (注: 能够通过 m-x lisp-interaction-mode ret 切换模式) 三、小试牛刀3.1 hello world (message "hello world")3.2 两个执行命令(1) c-x c-e 执行lisp代码的命令为 c-x c-e, 即函数 eval-last-sexp 函数解释如下: 执行光标之前的表达式 (2) c-j,即函数 val-print-last-sexp 函数解释如下: 执行光标之前的表达式,和c-x c-e的不同之处是,执行后果不仅在底部的mini buffer中输入,而且在 current buffer中也会显示 留神: 一次只会解释执行一个表达式 四、函数4.1 函数定义 函数由3个局部组成, (1)函数名称 (2)函数参数列表 (3)函数体 函数体和函数定义之间,能够通过"doc string"增加函数文档 4.2 函数执行 4.3 留神留神, (1) 如果须要执行新定义的函数 hello-world,须要先在函数定义结尾,执行c-x c-e解析执行该新函数,否则会报错 Debugger entered--Lisp error: (void-function hello-world) (2) 当执行过函数定义后,光标在函数名时候,通过命令 c-h f能够查看该函数的文档 五、变量5.1 setq 能够通过 c-h v查看变量的文档 ...

April 7, 2021 · 1 min · jiezi

关于lisp:Elisp-08文本跨行提取

上一章:命令行程序界面 在上一章的结语里,我说这个教程是否会有第二局部,取决于我是否遇到了新的文本处理问题。后果很快如愿以偿。 问题上面是 XML 文件 foo.xml 的内容: <bib> <title>foo</title></bib><attachment> <resource src="files/foo.html"/> <title>foo</title></attachment><bib> <title>bar</title></bib><attachment> <resource src="files/bar.html"/> <title>bar</title></attachment>我须要从 <attachment>...<attachment> 块里提取以下条目: <resource src="files/foo.html"/><title>foo</title><resource src="files/bar.html"/><title>bar</title>文本跨行匹配当初假如已用 Elisp 函数 find-file 将 foo.xml 文件内容全副载入了缓冲区,即 (find-file "foo.xml")而后发现,之前学过的 Elisp 常识简直派不上用场了。之前学过的文本匹配和提取办法仅实用于单行文本,而当初面临的问题是多行文本的匹配和提取,即从以后缓冲区内提取 <attachment> <resource src="files/foo.html"/> <title>foo</title></attachment><attachment> <resource src="files/bar.html"/> <title>bar</title></attachment>莫说提取,仅仅是如何匹配 <attachment>...</attachment> 块就曾经不好解决了。例如,以下程序 (find-file "foo.xml")(let ((x (buffer-string))) (string-match "<attachment>\\(.+\\)</attachment>" x) (princ\' (match-string 1 x)))输入 nil,意味着 string-match 在以后缓冲区内容中匹配 <attachment>...</attachment> 块失败。导致失败的起因也很简略,因为正则表达式 . 尽管能够匹配任意一个字符,但它不包含换行符。 瞒天过海实现文本的跨行匹配,并非不可行,然而须要比当初更多的 Elisp 的正则表达式常识 1。然而,我想说的是,对于上述问题,现有的 Elisp 常识其实也是足够用,只须要转换一下思路。 文本为什么是多行的?是因为在输出文本的时候,每一行开端由人或程序增加了换行符。假使能将这些换行符长期替换为一个很非凡的记号,那么多行文本就变成了单行文本。在文本匹配和解决完结后,再将这个非凡记号再替换为换行符,单行文本又还原为多行文本。此为瞒天过海之计。 将以后缓冲区内所有的换行符替换为一个非凡记号,可基于第 6 章所讲的缓冲区变换办法予以实现。本章给出一个更快捷的办法。Elisp 函数 replace-string 可在以后缓冲区内应用指定字串替换所有指标字串,例如 (let ((x "") (y "") (one-line (generate-new-buffer "one-line"))) (find-file "foo.xml") (setq x (buffer-string)) (with-current-buffer one-line (insert x) (goto-char (point-min)) (replace-string "\n" "<linebreak/>") (setq y (buffer-string))) (princ\' y))执行上述程序后,在新创建的缓冲区 one-line 里寄存的便是 foo.xml 缓冲区的单行化后果。假使将上述代码里的 (princ\' y) 语句替换为 ...

April 6, 2021 · 2 min · jiezi

关于emacs:Elisp-07命令行程序界面

上一章:缓冲区变换 很多程序是有图形界面的,就是日常所见的那些有菜单和按钮的窗口以及对话框之类。在终端里运行的程序,通常也叫命令行程序,它们也有界面,即一组选项和参数。这两种程序,各有千秋,也各有所短。 我之所以学习 Elisp 语言,是因为感觉它的短处适宜编写文本处理程序,例如上一章所写的一个简略的文本处理程序,它能够将文本由 Markdown 格局翻译为 HTML 格局。像这样的文本处理程序,它们的运行通常并不需要图形界面,否则我为何不间接为 Emacs 写一个插件呢? 命令行选项和参数如同函数能够有参数,命令行程序也能够有一些参数。但凡函数或程序无奈决断的一些因素,可形象为一组参数,交由函数或程序的使用者决断。命令行选项实质上也是命令行参数,只不过它相当于程序的一些性能开关,可用于开启或关闭程序的一些性能,也可用于润饰其余参数。 选项偏向于定性,而参数偏向于定量。当二者对立为程序的参数时,便可使得程序可能明确咱们要用它解决什么问题。有些问题只须要定性的角度去解决。有些问题只须要从量化的角度去解决,因而对二者作辨别,也是有意义的。 在 Linux 零碎里,命令行程序占据了半壁甚至更多的江山。这些命令行程序的选项,通常以 - 或 -- 作为前缀,参数则没有前缀,于是在模式上对于程序的使用者而言,二者有着显著的区别。 为一个命令行程序设计界面在上一章里,我写了个可将文本由 Markdown 格局变换为 HTML 格局的程序。这个程序尽管在性能上远不健全,然而曾经到了要为它设计选项和参数的时候了。 假如这个程序名为 mdc.el,执行这个程序时,它反对 -i 和 -o 两个选项。-i 选项用于指定输出文件名,-o 选项用于指定输入文件名,其中输出文件名和输出文件名都是与选项对应的参数。例如 $ emacs -Q --script mdc.el -i foo.md -o foo.html假使不向 mdc.el 提供任何选项和参数,或者提供了它不意识的选项和参数,它也不示意任何不称心,仅仅是在终端输入: 用法:emacs -Q --script mdc.el -i 输出文件 -o 输入文件命令行界面的实现嵌入在 Emacs 外部的 Elisp 解释器,它可能从终端里取得所有的选项和参数,将后果保留为一个列表变量 argv,这是个全局变量。于是,在 mdc.el 程序里,只需拜访这个列表,便能够取得所需的选项和参数。当然,这须要对 argv 进行遍历,而后做一些文本匹配方面的工作。这些工作不再有任何难度,所须要的常识,在后面的章节里曾经使用得很纯熟了吧。 假如 mdc.el 的实现如下: (defun princ\' (x) (princ x) (princ "\n"))(while (not (null argv)) (princ\' (car argv)) (setq argv (cdr argv)))留神,在判断列表是否为空,我始终是应用 (not (null 列表对象)) 的形式,因为我始终不想抵赖 Elisp 语言里非 nil 即为真的规矩。然而,当初感觉,入乡还是随俗吧,抵赖 (not (null 列表对象)) 等价于 列表对象。 ...

April 5, 2021 · 3 min · jiezi

关于lisp:Elisp-06缓冲区变换

上一章:文本匹配 在第一章「缓冲区和文件」和第二章「文本解析」里已初步介绍了缓冲区的基本知识。应用 Elisp 语言编写文本处理程序时,充分利用缓冲区,仿佛是着实是在施展 Elisp 的一项短处。因此本章要思考和解决的一个事实问题是,缓冲区能够用来做什么。 文本变换将文本由一种模式变换为另一种模式,在「物理」上,可体现为一个字符串变换为另一个字符串,也可体现为一个文件变换为另一个文件。这是其余编程语言里常见的想法,而 Elisp 语言提供了一个新的思维,文本变换能够体现为缓冲区变换。 为什么要进行文本变换呢?因为人类总心愿用更少的语言去讲更多的话。 例如,假如有一份文件 foo.md,其内容为 # 明天要整顿厨房我在一份 Elisp 教程里揭示本人,明天肯定要整顿厨房……当初我想将上述内容变换为 <h1>明天要整顿厨房</h1><p>我在一份 Elisp 教程里揭示本人,明天肯定要整顿厨房……</p>实现这样的变换,后面几章所述的 Elisp 语法和函数曾经足够用了。 算法设计要解决上一节所述的文本变换问题,首先须要设计一个有针对性的算法。这个算法天然是很简略的,简略到了任何一本以传授算法为主旨的教科书都不愿波及的水平。 假如 x 为 foo.md 文件里的任意一行文本,对于上一节提出的问题额演,它只可能属于以下三种状况之一: ^#+[[:blank:]]+.+$;^[[:blank:]]*$;不属于上述两种状况的状况。还记得上一章所讲的正则表达式吗?上述第一种状况,就是以一个或多个 # 结尾且 # 之后能够有一个或多个空格的文本行。第二种状况是空行。 只需基于上述三种状况,对 x 进行变换。第一种状况,将 x 变为 <hn>...</hn> 的模式,n 为 # 的个数。第二种状况,将 x 变换为空字符串。第三种状况,则在 x 的结尾和结尾减少 <p> 和 </p>。如此,问题便得以解决。下文将逐渐实现这个算法。 文本变换函数为一个具体的问题设计一个具体的算法,我认为,这相当于是要站在一个高处对问题的鸟瞰。算法设计进去之后,在着手实现算法时,我倡议这个过程该当自下而上进行。因为底层的逻辑是最简略的。 在实现「### foo」到「<h3>foo</h3>」的变换时,为了谋求简略,我甚至能够假如曾经将前者拆分为「###」和「foo」两个局部了,而后只须要依据前者蕴含的了多少个 #,便能够确定 <hn>...</hn> 里的 n 是多少了。于是,上一节里第一种状况的变换,其实现如下: (defun section-n (level name) (let ((n (length level))) (format "<h%d>%s</h%d>" n name n)))其中,Elisp 函数 length 函数在第二章里曾经用过,它能够算出字符串蕴含多少个字符,也能够算出列表蕴含多少个元素。Elisp 函数 format 是第一次应用,该函数能够结构一个字符串模板,而后特定类型的变量或数据对象的求值后果填充到模板里,从而生成一个字符串。如果学过 C 语言,对这种结构字符串的办法肯定不生疏,因为 C 语言里罕用的 printf 便是相似的函数。 ...

April 5, 2021 · 3 min · jiezi

关于lisp:Elisp-05文本匹配

上一章:迭代 在第二章「文本解析」所实现的解析器程序里,为了判断一行文本是否以 \`\`\` 结尾,我定义了一个函数: (defun text-match (source target) (setq n (length target)) (if (< (length source) n) nil (string= (substring source 0 n) target)))事实上,Elisp 提供了更弱小的文本匹配函数。如何的弱小呢?弱小到了反对正则表达式匹配。 正则表达式,就像现代官府捉拿江洋大盗时在城门边上张贴的通缉告示上的罪犯画像。罪犯的长相越有特点,他的画像便越有用途。我还感觉古代的机器学习程序在辨认照片里的人脸,其原理也像是在城门边上张贴通缉告示。 如何给一段文本画像呢?具体而言,如何给以 \`\`\` 作为结尾的文本画像呢?很简略,只有像上面这样画 ^```^ 的意思是「结尾」,前面紧跟着 \`\`\`,就示意结尾是 \`\`\`。 Elisp 的 string-match 函数能够用正则表达式形成的字符串对象去匹配另一个字符串对象,例如: (string-match "^```" "```lisp")留神,为了便于讲述,从当初开始,诸如字符串对象(或字符串类型的实例),列表对象(或列表类型的实例),若没有非凡申明,通通简称为字符串、列表。应该不会导致误会。 上述示例中,因为字符串 "\`\`\`lisp" 是以 \`\`\` 结尾的,所以 string-match 的求值后果不是 nil,否则是 nil。对于 Elisp 解释器而言,非 nil 即为真,亦即若一个值即不是 nil,也不是 '(),那么无论它是什么,Elisp 都会将其等价于 t。还记得吗,之前说过的,nil 与 '() 等价。要牢记住这些。事实上,上例的求值后果是 0,但 0 即不是 nil 也不是 '()。 为什么上例的求值后果是 0 呢?因为 string-match 在字符串的结尾就找到了与正则表达式相匹配的局部。字符串的结尾,亦即字符串第一个字符的索引(或下标),它的值是 0。再看一个例子: ...

April 5, 2021 · 2 min · jiezi

关于emacs:迭代

上一章:变量 迭代,亦称循环,示意一段反复运行的程序,其状态可在每次反复运行的过程中发生变化。 基于递归函数能够模仿迭代过程。例如以下程序 (defun princ\' (x) (princ x) (princ "\n"))(defun current-line () (buffer-substring (line-beginning-position) (line-end-position)))(defun every-line () (if (= (point) (point-max)) (princ "") (progn (princ\' (current-line)) (forward-line 1) (every-line))))(find-file "foo.md")(every-line)Elisp 解释器在上述程序最初一个表达式 (every-line) 求值时,会转而对 every-line 函数定义里的每个表达式进行求值,然而当 Elisp 解释器在函数 every-line 的定义里又遇到了表达式 (every-line),导致它不得不再次对 every-line 的定义里的每个表达式进行求值。该过程周而复始,在每一次重复对 every-line 的定义进行求值时,princ\' 会一直输入以后文本行,而 forward-line 又一直将插入点挪动到下一行的结尾。于是,上述程序便解决了读取以后缓冲区内的每一行文本并输入于终端这个问题。 咱们活着,也是相似的递归吧。于是,有人说,太阳每天都是新的。还有人说,人不能两次踏进同一条河流。如同咱们有寿命一样,Elisp 对函数的递归深度也有限度。every-line 这个函数,最多只能令 Elisp 解释器重复对其求值 max-lisp-eval-depth 次。` max-lisp-eval-depth 是 Elisp 解释器的全局变量,它定义了函数递归深度。应用 (princ\' max-lisp-eval-depth)可查看它的值,在我的机器上,后果 800,这意味着上述的 every-line 函数只能令 Elisp 解释器重复对其求值 800 次。这也意味着,假使以后缓冲区内的文本行数超过 800 行时,every-line 函数的定义会令 Elisp 解释器因解体而终止工作。它的临终遗嘱是 ...

April 4, 2021 · 1 min · jiezi

关于lisp:变量

上一章:文本解析 上一章实现的解析器程序——当然仅仅是玩具,有几处颇为俊俏,还有一处存在着平安问题。 全局变量平安第一。先从平安问题开始。察看以下代码: (defun text-match (src dest) (setq n (length dest)) (if (< (length src) n) nil (string= (substring src 0 n) dest)))上述代码定义的这个函数可判断字符串对对象 src 的内容是否以字符串对象 dest 的内容作为结尾,例如 (princ\' (text-match "I have a dream!" "I have"))输入 t。这不是问题。问题在于假使紧接着执行 (princ\' n)输入 6。 问题是什么呢?在 text-match 这个函数定义的内部,可能拜访在函数的定义外部的一个变量,宛若别人的手指能够涉及我的内脏……这是不是一个平安问题? 这种匪夷所思的景象之所以呈现,是因为 setq 定义的变量是全局变量。在一个程序里,假使有一个全局变量,那么在这个程序的任何一个角落皆能拜访和批改这个变量。 全局变量不能够没有,但不可滥用。对于 text-match 这样的函数,在其定义里应用全局变量,属于滥用。 局部变量回顾一下 simple-md-parser.el 里的代码里 every-line 函数的定义: (defun every-line (result in-code-block) (if (= (point) (point-max)) result (progn (if (text-match (current) "```") (progn (if in-code-block (progn (setq result (cons '代码块完结 result)) (setq in-code-block nil)) (progn (setq result (cons '代码块开始 result)) (setq in-code-block t)))) (progn (if in-code-block (setq result (cons '代码块 result)) (setq result (cons '未知 result))))) (forward-line 1) (every-line result in-code-blcok))))在这个函数里,我在多处用 setq 重复定义了两个变量 result 和 in-code-block,然而假使调用这个函数之后再执行以下程序 ...

April 3, 2021 · 2 min · jiezi

关于lisp:文本解析

上一章:缓冲区和文件 本章介绍 Elisp 的变量、列表、符号、函数的递归以及一些更便捷的插入点挪动函数。这些常识将围绕一个理论问题的解决过程逐渐开展。 问题假如有一份文档 foo.md,内容如下: # Hello world!上面是 C 语言的 Hello world 程序源文件 hello.c 的内容:```#include <stdio.h>int main(void) { printf("Hello world!\n") return 0;}```... ... ...其中有一部分内容被蕴含在以 \`\`\` 为结尾的两个文本行之间,如何应用 Elisp 编写一个程序,从 foo.md 中辨认它们? 解析器foo.md 文件中每一行文本无非为以下三种状况之一。这三种状况是 以 \`\`\` 结尾的文本行;位于两个 \`\`\` 结尾的文本行之间的文本行;非上述两种状况的文本行。假如我要编写的程序是 simple-md-parser.el,只有它可能断定每一行文本的状况,并将断定后果记录下来,那么问题便得以解决。这个程序尽管简略,但着实称得上是解析器(Parser)。 变量和列表simple-md-parser.el 对 foo.md 文件每一行文本的断定后果可存储于 Elisp 的列表类型的变量里。 在 Elisp 语言里,变量是绑定到某种类型的数据对象的符号,因此定义一个变量,就是将一个符号与一个数据对象绑定起来。例如, (setq x "Hello world!")将一个符号 x 与一个字符串类型的数据绑定起来,于是便定义了变量 x。 列表变量,就是一个符号绑定到了列表类型的实例,后者可由 list 函数创立,例如 (setq x (list 1 2 3 4 5))将符号 x 绑定到列表对象 (1 2 3 4 5),于是便定义了一个列表变量 x。 ...

April 3, 2021 · 4 min · jiezi

关于lisp:缓冲区和文件

假使将 Elisp 的利用场景固定为文本处理,学习 Elisp,我认为无需像学习其余任何一门编程语言那样亦步亦趋,所以本章间接从文件读写开始动手,通过一些小程序,建设对 Elisp 语言的初步感触。 Hello world!尽管我已决定从文件读写开始学习 Elisp,然而我还是心愿像学习任何一门编程语言那样,从写一个可能输入 Hello world! 的程序开始。 用 Emacs 新建一份文本文件,名曰 hello-world.el。当然,也能够应用其余文本编辑器实现此事,然而要保证系统已装置了 Emacs 且可用。 hello-world.el 的内容只有一行: (princ "Hello world!\n")在终端(或命令行窗口)里,将工作目录(当前目录)切换至 hello-world.el 文件所在的目录,而后执行 $ emacs -Q --script ./hello-world.el终端会随即显示 Hello world!从这个 Hellow world 程序里,能学到哪些 Elisp 常识呢? 首先,princ 是一个函数,确切地说,是 Elisp 的内建函数。什么是函数?在数学里,y=f(x) 是函数,f 可将 x 映射为 y。princ 也是这样的函数,它将 "Hello world!\n 这个对象映射为显示于终端的对象,权且这样认为。 其次,"Hello world!\n" 是 Elisp 的字符串类型,用于示意一段文本。文本是数据。数据未必是文本。若将 Elisp 作为用于解决文本的语言,字符串就是根本且外围的数据类型。 最初,这个作为示例的 Elisp 程序的最小单位是一个函数调用。我向 princ 函数提供一个字符串类型的值,便可令其工作,且足以形成一个程序。Emacs 里有 Elisp 解释器。Elisp 程序是由 Elisp 解释器解释运行的,相似于计算机程序是由计算机的 CPU 「解释」运行。换言之,Elisp 解释器可能读懂 Elisp 程序,并实现这个程序所形容的工作,例如在终端里输入 Hello world!。 ...

April 2, 2021 · 2 min · jiezi

关于emacs:走在-Elisp-的歧路上-缓冲区和文件

假使将 Elisp 的利用场景固定为文本处理,学习 Elisp,我认为无需像学习其余任何一门编程语言那样亦步亦趋,所以本章间接从文件读写开始动手,通过一些小程序,建设对 Elisp 语言的初步感触。 Hello world!尽管我已决定从文件读写开始学习 Elisp,然而我还是心愿对初学者敌对一点,毕竟我也是初学者。这种敌对应该像学习任何一门编程语言那样,从写一个可能输入 Hello world! 的程序。 用 Emacs 新建一份文本文件,名曰 hello-world.el。当然,也能够应用其余文本编辑器实现此事,然而要保证系统已装置了 Emacs 且可用。hello-world.el 的内容只有一行: (princ "Hello world!")在终端(或命令行窗口)里,将工作目录(当前目录)切换至 hello-world.el 文件所在的目录,而后执行 $ emacs -Q --script hello-world.el终端会随即显示 Hello world!从这个 Hellow world 程序里,能学到哪些 Elisp 常识呢? 首先,princ 是一个函数,确切地说,是 Elisp 的内建函数。什么是函数?在数学里,y=f(x) 是函数,f 可将 x 映射为 y。princ 也是这样的函数,它将 "Hello world! 这个对象映射为显示于终端的对象。 其次,"Hello world!" 是 Elisp 的字符串类型,用于示意一段文本。文本是数据。数据未必是文本。若将 Elisp 作为用于解决文本的语言,字符串就是根本且外围的数据类型。 最初,这个作为示例的 Elisp 程序的最小单位是一个函数调用。我向 princ 函数提供一个字符串类型的值,便可令其工作,且足以形成一个程序。Emacs 里有 Elisp 解释器。Elisp 程序是由 Elisp 解释器解释运行的,相似于计算机程序是由计算机的 CPU 「解释」运行。换言之,Elisp 解释器可能读懂 Elisp 程序,并实现这个程序所形容的那些工作,例如,在终端里输入 Hello world!。 ...

April 1, 2021 · 2 min · jiezi

调用C标准库的exit函数

在上一篇文章中,实现了对大于号(>)的处理,那么对if表达式的编译也就是信手拈来的事了,不解释太多。在本篇中,将会讲述一下如何产生可以调用来自于C语言标准库的exit(3)函数的汇编代码。 在Common Lisp中并没有一个叫做EXIT的内置函数,所以如同之前实现的_exit一样,我会新增一种需要识别的(first expr),即符号exit。为了可以调用C语言标准库中的exit函数,需要遵循调用约定。对于exit这种只有一个参数的函数而言,情形比较简单,只需要跟对_exit一样处理即可。刚开始,我写下的代码是这样的 (defun jjcc2 (expr globals) ;; 省略不必要的内容 (cond ;; 省略不必要的内容 ((member (first expr) '(_exit exit)) ;; 暂时以硬编码的方式识别一个函数是否来自于C语言的标准库 `((movl ,(get-operand expr 0) %edi) (call :|_exit|)))))对(exit 1)进行编译,会得到如下的代码 .data .section __TEXT,__text,regular,pure_instructions .globl _main_main: MOVL $1, %EDI CALL _exit不过这样的代码经过编译链接之后,一运行就会遇到段错误(segmentation fault)。经过一番放狗搜索后,才知道原来在macOS上调用C函数的时候,需要先将栈对齐到16字节——我将其理解为将指向栈顶的指针对齐到16字节。于是乎,我将jjcc2修改为如下的形式 (defun jjcc2 (expr globals) ;; 省略不必要的内容 (cond ;; 省略不必要的内容 ((member (first expr) '(_exit exit)) ;; 暂时以硬编码的方式识别一个函数是否来自于C语言的标准库 `((movl ,(get-operand expr 0) %edi) ;; 据这篇回答(https://stackoverflow.com/questions/12678230/how-to-print-argv0-in-nasm)所说,在macOS上调用C语言函数,需要将栈对齐到16位 ;; 假装要对齐的是栈顶地址。因为栈顶地址是往低地址增长的,所以只需要将地址的低16位抹掉就可以了 (and ,(format nil "$0x~X" #XFFFFFFF0) %esp) (call :|_exit|)))))结果发现还是不行。最后,实在没辙了,只好先写一段简单的C代码,然后用gcc -S生成汇编代码,来看看究竟应当如何处理这个栈的对齐要求。一番瞎折腾之后,发现原来是要处理RSP寄存器而不是ESP寄存器——我也不晓得这是为什么,ESP不就是RSP的低32位而已么。 ...

July 10, 2019 · 2 min · jiezi

编译大于运算符

原定的计划中这一篇应当是要讲如何编译if表达式的,但是我发现没什么东西可以作为if的test-form的部分的表达式,所以觉得,要不还是先实现一下比较两个数字这样子的功能吧。说干就干,我决定用大于运算符来作为例子——大于运算符就是指>啦。所以,我的目标是要编译下面这样的代码 (> 1 2)并且比较之后的结果要放在EAX寄存器中。鉴于现在这门语言还非常地简陋,没有布尔类型这样子的东西,所以在此仿照C语言的处置方式,以数值0表示逻辑假,其它的值表示逻辑真。所以上面的表达式在编译成汇编代码并最终运行后,应当可以看到EAX寄存器中的值为0。 为了编译大于运算符,并且将结果放入到EAX寄存器中,需要用到新的指令CMP、JG,以及JMP了。我的想法是,先将第一个操作数放入到EAX寄存器,将第二个操作数放入到EBX寄存器。然后,使用CMP指令比较这两个寄存器。如果EAX中的数值大于EBX,那么就使用JG指令跳到一个MOV指令上,这道MOV会将寄存器EAX的值修改为1;否则,JG不被执行,执行后续的一道MOV指令,将数值0写入到EAX寄存器,然后使用JMP跳走,避免又执行到了刚才的第一道MOV指令。思路还是挺简单的。 在修改jjcc2之前,还需要在inside-out/aux中对>予以支持,但没什么特别的,就是往member的参数中加入>这个符号而已。之后,将jjcc2改为如下的形式 (defun jjcc2 (expr globals) "支持两个数的四则运算的编译器" (check-type globals hash-table) (cond ((eq (first expr) '+) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (addl %ebx %eax))) ((eq (first expr) '-) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (subl %ebx %eax))) ((eq (first expr) '*) ;; 将两个数字相乘的结果放到第二个操作数所在的寄存器中 ;; 因为约定了用EAX寄存器作为存放最终结果给continuation用的寄存器,所以第二个操作数应当为EAX `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (imull %ebx %eax))) ((eq (first expr) '/) `((movl ,(get-operand expr 0) %eax) (cltd) (movl ,(get-operand expr 1) %ebx) (idivl %ebx))) ((eq (first expr) 'progn) (let ((result '())) (dolist (expr (rest expr)) (setf result (append result (jjcc2 expr globals)))) result)) ((eq (first expr) 'setq) ;; 编译赋值语句的方式比较简单,就是将被赋值的符号视为一个全局变量,然后将eax寄存器中的内容移动到这里面去 ;; TODO: 这里expr的second的结果必须是一个符号才行 ;; FIXME: 不知道应该赋值什么比较好,先随便写个0吧 (setf (gethash (second expr) globals) 0) (values (append (jjcc2 (third expr) globals) ;; 为了方便stringify函数的实现,这里直接构造出RIP-relative形式的字符串 `((movl %eax ,(get-operand expr 0)))) globals)) ((eq (first expr) '_exit) ;; 因为知道_exit只需要一个参数,所以将它的第一个操作数塞到EDI寄存器里面就可以了 ;; TODO: 更好的写法,应该是有一个单独的函数来处理这种参数传递的事情(以符合calling convention的方式) `((movl ,(get-operand expr 0) %edi) (movl #x2000001 %eax) (syscall))) ((eq (first expr) '>) ;; 为了可以把比较之后的结果放入到EAX寄存器中,以我目前不完整的汇编语言知识,可以想到的方法如下 (let ((label-greater-than (intern (symbol-name (gensym)) :keyword)) (label-end (intern (symbol-name (gensym)) :keyword))) ;; 根据这篇文章(https://en.wikibooks.org/wiki/X86_Assembly/Control_Flow#Comparison_Instructions)中的说法,大于号左边的数字应该放在CMP指令的第二个操作数中,右边的放在第一个操作数中 `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (cmpl %ebx %eax) (jg ,label-greater-than) (movl $0 %eax) (jmp ,label-end) ,label-greater-than (movl $1 %eax) ,label-end)))))然后便可以在REPL中运行下列代码了 ...

July 3, 2019 · 2 min · jiezi

insideoutaux如何支持对exit的调用

在上一篇文章中,新增了两个函数:inside-out以及inside-out/aux——曾经想过将inside-out/aux放到前者的函数中用labels来定义,但担心不好调试,所以剥离了出来成为一个独立的函数——inside-out基本上只是驱动了后者,真正地将嵌套表达式拆解开来的还是inside-out/aux。因此,为了让让这个编译器最终可以处理如下形式的代码 (_exit (+ (+ 1 2) 3))就需要先对inside-out/aux进行一番改造,使其可以处理上述代码。 在此之前,先处理一下inside-out/aux目前的一些问题。在之前的实现中,由于使用了setf对输入参数expr进行了修改,因此在example3中的列表实际上在第二次运行的时候已经不是代码中看到的那样子了。所以,先将inside-out/aux改写为更pure的形式 (defun inside-out/aux (expr result) "将嵌套的表达式EXPR由内而外地翻出来" (check-type expr list) ;; 出于简单起见,暂时只处理加法运算 (cond ((member (first expr) '(+ - * /)) (let ((operands '())) (if (listp (second expr)) ;; 第一个操作数也是需要翻出来的 ;; 翻出来后,result中的第一个元素就是一个没有嵌套表达式的叶子表达式了,可以作为setq的第二个操作数 (let ((var (gensym))) (setf result (inside-out/aux (second expr) result)) (let ((val (pop result))) (push `(setq ,var ,val) result) (push var operands))) (push (second expr) operands)) (if (listp (third expr)) (let ((var (gensym))) (setf result (inside-out/aux (third expr) result)) (let ((val (pop result))) (push `(setq ,var ,val) result) (push var operands))) (push (third expr) operands)) (push (cons (first expr) (nreverse operands)) result) result)) (t (push expr result) result)))其实改动很简单,就是使用一个新的列表operands来承载被修改后的符号或原本的表达式而已。接下来可以开始支持_exit函数了。 ...

June 26, 2019 · 2 min · jiezi

拆解嵌套的表达式

在上一篇文章中,jjcc2函数已经可以处理加减乘除运算表达式中的变量了。也就是说,现在它可以处理如下的代码了 (progn (setq a (+ 1 2)) (+ a a))在我的电脑上,在SLIME中依次运行下面的代码 (defvar *globals* (make-hash-table))(stringify (jjcc2 '(progn (setq a (+ 1 2)) (+ a a)) *globals*) *globals*)会得到下列的汇编代码 .dataA: .long 0 .section __TEXT,__text,regular,pure_instructions .globl _main_main: MOVL $1, %EAX MOVL $2, %EBX ADDL %EBX, %EAX MOVL %EAX, A(%RIP) MOVL A(%RIP), %EAX MOVL A(%RIP), %EBX ADDL %EBX, %EAX movl %eax, %edi movl $0x2000001, %eax syscall现在所需要的,就是要实现一个功能(一般是一个函数),可以将 (+ (+ 1 2) (+ 1 2))自动转换为上面所给出的progn的形式了。我这里给的例子不好,上面这段代码就算能够自动转换,也不会是最上面那段progn的形式的,起码会有两个变量哈哈。好了,那么怎么把上面的含有嵌套表达式的代码给转换成progn的形式呢? ...

June 14, 2019 · 2 min · jiezi

支持四则运算中的变量

在上一篇文章中,jjcc2函数实现了对setq这个语句的编译。这么一来,便可以将加减乘除运算中的嵌套表达式都替换为变量了。比如,将 (+ (+ 1 2) 3)中的嵌套的表达式(+ 1 2)用一个变量G564代替,变成 (PROGN (SETQ #:G564 (+ 1 2)) (+ #:G564 3))PS:上面的结果中的#:G564只是打印出来的时候长这个样子而已,实际地输入这段代码的话,两个#:G564其实是不同的符号,会导致未绑定的变量的错误的。 言归正传。既然如此,现在就要来支持编译(+ #:G564 3)这样的表达式了。其实这个真的是太简单了,只需要将这个符号塞入到jjcc2的第二个参数的globals中,然后在生成的“汇编指令”的S表达式中,嵌入这个符号即可。 我刚开始的时候也是这么想的,后来发现这样出来的代码编译不过,哭 折腾了一小段时间后,才知道原来有一种叫做“RIP-relative”的东西——好吧,我的X64的汇编语言知识也是赶鸭子上架的,遇到什么问题就放狗搜,所以完全不成体系——总之,我找到了解决办法,就是将原本放入一个符号的操作数,替换为类似于下面这样的内容 G564(%RIP)所以对于操作数,实际上还需要先判断一下其类型。如果是整数,就按照原来的方式原样输出;如果是符号,就生成像上面这样的RIP-relative的结构。这部分太经常出现了,于是提炼出了一个专门处理四则运算的操作数的辅助函数get-operand (defun get-operand (expr n) "从EXPR中提取出第N个操作数,操作数的下标从0开始计算" (check-type expr list) (check-type n integer) (let ((e (nth (1+ n) expr))) (etypecase e (integer e) (symbol (format nil "~A(%RIP)" e)))))借助它重写jjcc2,结果如下 (defun jjcc2 (expr globals) "支持两个数的四则运算的编译器" (check-type globals hash-table) (cond ((eq (first expr) '+) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (addl %ebx %eax))) ((eq (first expr) '-) `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (subl %ebx %eax))) ((eq (first expr) '*) ;; 将两个数字相乘的结果放到第二个操作数所在的寄存器中 ;; 因为约定了用EAX寄存器作为存放最终结果给continuation用的寄存器,所以第二个操作数应当为EAX `((movl ,(get-operand expr 0) %eax) (movl ,(get-operand expr 1) %ebx) (imull %ebx %eax))) ((eq (first expr) '/) `((movl ,(get-operand expr 0) %eax) (cltd) (movl ,(get-operand expr 1) %ebx) (idivl %ebx))) ((eq (first expr) 'progn) (let ((result '())) (dolist (expr (rest expr)) (setf result (append result (jjcc2 expr globals)))) result)) ((eq (first expr) 'setq) ;; 编译赋值语句的方式比较简单,就是将被赋值的符号视为一个全局变量,然后将eax寄存器中的内容移动到这里面去 ;; TODO: 这里expr的second的结果必须是一个符号才行 ;; FIXME: 不知道应该赋值什么比较好,先随便写个0吧 (setf (gethash (second expr) globals) 0) (values (append (jjcc2 (third expr) globals) ;; 为了方便stringify函数的实现,这里直接构造出RIP-relative形式的字符串 `((movl %eax ,(get-operand expr 0)))) globals))))现在,如果运行下面的这个example1函数 ...

June 11, 2019 · 2 min · jiezi

如何编译setq

Common Lisp中的setq类似于其它语言中的赋值语句,它可以给一个符号对象设定一个值,类似于将一个值赋值给一个变量一样。简单起见,在jjcc2中,我会将所有的符号都作为全局的一个label来实现。也就是说,如果代码中出现了 (setq a 1)这样的代码,那么在最后生成的代码中,就会相应的在.data段中有一个同名的label,其中存放着数值1。 既然都是全局变量,那么只需要准备一个容器来盛这些变量名即可。现阶段,暂时认为所有的变量都是数值类型即可。简单起见,这个容器直接用Common Lisp内置的HASH-TABLE来表示。 当在jjcc2函数中遭遇到setq这个符号时,整个表的形态是这样的 (setq var form)这时候,首先要将var放入到记录全局变量的哈希表中。然后,递归地调用jjcc2函数,先编译form,得到一系列的汇编代码。然后,生成一条mov语句,将eax寄存器中的内容放到var所指的内存位置中。最终的jjcc2的代码如下 (defun jjcc2 (expr globals) "支持两个数的四则运算的编译器" (check-type globals hash-table) (cond ((eq (first expr) '+) `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (addl %ebx %eax))) ((eq (first expr) '-) `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (subl %ebx %eax))) ((eq (first expr) '*) ;; 将两个数字相乘的结果放到第二个操作数所在的寄存器中 ;; 因为约定了用EAX寄存器作为存放最终结果给continuation用的寄存器,所以第二个操作数应当为EAX `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (imull %ebx %eax))) ((eq (first expr) '/) `((movl ,(second expr) %eax) (cltd) (movl ,(third expr) %ebx) (idivl %ebx))) ((eq (first expr) 'progn) (let ((result '())) (dolist (expr (rest expr)) (setf result (append result (jjcc2 expr globals)))) result)) ((eq (first expr) 'setq) ;; 编译赋值语句的方式比较简单,就是将被赋值的符号视为一个全局变量,然后将eax寄存器中的内容移动到这里面去 ;; TODO: 这里expr的second的结果必须是一个符号才行 ;; FIXME: 不知道应该赋值什么比较好,先随便写个0吧 (setf (gethash (second expr) globals) 0) (values (append (jjcc2 (third expr) globals) ;; 为了方便stringify函数的实现,这里直接构造出RIP-relative形式的字符串 `((movl %eax ,(format nil "~A(%RIP)" (second expr))))) globals))))然后还需要修改stringify函数,现在它需要处理传给jjcc2的全局变量的哈希表,将其转化为对应的.data段的声明。代码如下 ...

June 2, 2019 · 2 min · jiezi

jjcc系列第三篇如何编译progn

progn是什么玩意progn是Common Lisp里面的一个special operator,见这份文档的说明:http://clhs.lisp.se/Body/s_pr... 为嘛要编译这东西现在已经支持了二元四则运算了,但现在这里有一个大问题,就是这四个运算没办法嵌套着组合使用。比如,遇到下面这样的代码,jjcc2函数就懵逼了 (+ 1 (+ 2 3))那要编译这种代码的话怎么办呢?一个比较直观的做法,是引入临时变量,来保存嵌套在其中的表达式的求值结果,然后再用变量来代替原本嵌套的表达式。修改后的代码可能长这个样子 (setq a (+ 2 3))(+ 1 a)显然,这是有先后的时间依赖关系的两条语句,因此应当使用progn将它们包裹起来,结果如下 (progn (setq a (+ 2 3)) (+ 1 a))这样整个表达式的求值结果,或者说它被编译之后的运行结果,应当就是在寄存器EAX中放入整数6了。所以,本篇将来解决对progn的编译问题。 如何编译progn其实很简单,progn可能有不定数量的form在其中,那么只需要按照顺序对它们一个个进行编译,输出汇编代码就可以了,因此最终jjcc2被修改为如下的样子 (defun jjcc2 (expr) "支持两个数的四则运算的编译器" (cond ((eq (first expr) '+) `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (addl %ebx %eax))) ((eq (first expr) '-) `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (subl %ebx %eax))) ((eq (first expr) '*) ;; 将两个数字相乘的结果放到第二个操作数所在的寄存器中 ;; 因为约定了用EAX寄存器作为存放最终结果给continuation用的寄存器,所以第二个操作数应当为EAX `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (imull %ebx %eax))) ((eq (first expr) '/) `((movl ,(second expr) %eax) (cltd) (movl ,(third expr) %ebx) (idivl %ebx))) ((eq (first expr) 'progn) (let ((result '())) (dolist (expr (rest expr)) (setf result (append result (jjcc2 expr)))) result))))就酱就足够了。下一篇,是时候讲一下如何编译setq了。 ...

May 27, 2019 · 1 min · jiezi

支持减乘以及除

在上一篇文章中,初步搭建了一个输入Common Lisp代码,输出汇编代码的编译器的骨架,实现了二元整数的加法运算。在这个基础上,要想实现减法、乘法,以及除法就是手到擒来的事情了。只需依葫芦画瓢,补充更多的分支情况即可。 我自己模仿着x64的调用约定,规定四则运算的结果始终放在EAX这个寄存器中。在稍后给出的代码中,对于减法和除法运算,都是把运算符的左操作数放到EAX寄存器中,再从EAX中减去或者除掉右操作数。 在摸索除法的汇编代码怎么生成时,遇到了个费解的问题,最后才知道,原来需要把EAX寄存器的符号扩展到高位的EDX寄存器中去。对于as这个汇编器来说,需要用到CLTD指令。 最后,jjcc2和stringify两个函数被修改为如下的样子 (defun jjcc2 (expr) "支持两个数的四则运算的编译器" (cond ((eq (first expr) '+) `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (addl %ebx %eax))) ((eq (first expr) '-) `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (subl %ebx %eax))) ((eq (first expr) '*) ;; 将两个数字相乘的结果放到第二个操作数所在的寄存器中 ;; 因为约定了用EAX寄存器作为存放最终结果给continuation用的寄存器,所以第二个操作数应当为EAX `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (imull %ebx %eax))) ((eq (first expr) '/) `((movl ,(second expr) %eax) (cltd) (movl ,(third expr) %ebx) (idivl %ebx)))))(defun stringify (asm) "根据jjcc2产生的S表达式生成汇编代码字符串" (format t " .section __TEXT,__text,regular,pure_instructions~%") (format t " .globl _main~%") (format t "_main:~%") (dolist (ins asm) (cond ((= (length ins) 3) (format t " ~A ~A, ~A~%" (first ins) (if (numberp (second ins)) (format nil "$~A" (second ins)) (second ins)) (if (numberp (third ins)) (format nil "$~A" (third ins)) (third ins)))) ((= (length ins) 2) (format t " ~A ~A~%" (first ins) (if (numberp (second ins)) (format nil "$~A" (second ins)) (second ins)))) ((= (length ins) 1) (format t " ~A~%" (first ins))))) (format t " movl %eax, %edi~%") (format t " movl $0x2000001, %eax~%") (format t " syscall~%"))全文完。 ...

May 23, 2019 · 1 min · jiezi

一个简陋的四则运算编译器实现

本文很水。 有一天,我心血来潮想要写一个将Common Lisp编译成汇编(x64那种)的编译器。我喜欢Common Lisp这门语言,它非常好玩,有许多有趣的特性(宏、condition system等),并且它的生态很贫瘠,有很多造轮子的机会。因为我懂的还不够多,所以没法从源代码一步到位生成可执行文件,只好先输出汇编代码,再利用现成的汇编器(比如as、nasm)从这些输出内容生成可执行文件。至于这东西是不是真的算是编译器,我也不是很在意。 好了,我要开始表演了。 你可能看过龙书,或者其它比较经典的编译原理和实践方面的书。那你应该会知道,编译器还蛮复杂的。但我水平有限,把持不住工业级的产品那么精妙的结构和代码,所以我的编译器很简陋——简陋到起码这个版本一眼就看到尽头了。 尽管简陋,但身为一名业余爱好者,尝试开发这么一个玩具还是很excited的。由于编译器本身也是用Common Lisp写的,所以就偷个懒不写front end的部分了,聚焦于从CL代码到汇编代码的实现。 先从最简单的一种情况——二元整数的加法入手,比如下面这段代码 (+ 1 2)对于加法,可以输出ADDL指令,两个参数则随便找两个寄存器放进去就好了。一段简单得不能再简单的代码一下子就写出来了 (defun jjcc2 (expr) "支持两个数的四则运算的编译器" (cond ((eq (first expr) '+) `((movl ,(second expr) %eax) (movl ,(third expr) %ebx) (addl %eax %ebx)))))(defun stringify (asm) "根据jjcc2产生的S表达式生成汇编代码字符串" (format t " .section __TEXT,__text,regular,pure_instructions~%") (format t " .globl _main~%") (format t "_main:~%") (dolist (ins asm) (format t " ~A ~A, ~A~%" (first ins) (if (numberp (second ins)) (format nil "$~A" (second ins)) (second ins)) (if (numberp (third ins)) (format nil "$~A" (third ins)) (third ins)))) (format t " movl %ebx, %edi~%") (format t " movl $0x2000001, %eax~%") (format t " syscall~%"))在 REPL 中像下面这样运行 ...

May 18, 2019 · 1 min · jiezi

更过程式的let——vertical-let

作为一名自诩的non-trivial的Common Lisp程序员,在编码的时候经常会遇到令人不愉快的地方,其中一个便是LET。一段典型的LET的示例代码如下(let ((a 1)) a)大多数时候,LET不会只有一个绑定。并且,也不会只是绑定一个常量这么简单,而应当是下面这样的(let ((a (foo x y)) (b (bar z))) (function1 a b) (function2 a b))有时候我会想看看某一个绑定的值——最好是在它计算完毕后立即查看。如果要查看foo函数的返回值,可以这样写(let ((a (foo x y)) (b (bar z))) (print a) (function1 a b) (function2 a b))如果调用foo和bar都顺利的话上面的代码也就够了。比较棘手的情况是,如果a的值不符合预期,会导致b的计算过程出状况(尽管在上面的代码中看似不会)。这种情况多出现在LET的使用中,如下面所示(let ((a (foo x y)) (b (bar a))) (function1 a b) (function2 a b))如果错误的a会导致bar的调用出错,那么在调用function1之前才调用print打印a已经为时过晚了——毕竟调用bar的时候就抛出condition往调用链的上游走了。一种方法是写成下面这样子(let* ((a (let ((tmp (foo x y))) (print tmp) tmp)) (b (bar a))) (function1 a b) (function2 a b))这也太丑了!要不然写成下面这样子?(let ((a (foo x y))) (print a) (let ((b (bar a))) (function1 a b) (function2 a b)))本来一个LET就可以做到的事情,这下子用了两个,还导致缩进更深了一级。如果有十个变量需要打印,就会增加十个LET和十层缩进。如果心血来潮想查看一个变量的值,还要大幅调整代码。问题大概就出在LET和LET*的语法上。以LET为例,它由截然分开的bindings和forms组成,两者不能互相穿插。因此,如果想在bindings中求值一段表达式,只能将bindings切开,写成两个LET的形式。好像写一个新的宏可以解决这个问题?是的,vertical-let就是。vertical-let是一个我自己写的宏,源代码在此。其用法如下(vertical-let :with a = 1 a)它借鉴了LOOP中绑定变量的方式(即:with和=),绑定变量和用于求值的代码还可以交织在一起,如下(vertical-let :with a = 1 (print a) :with b = 2 (+ a b))vertical-let最终会展开为LET,比如上面的代码,会展开为如下的代码(LET ((A 1)) (PRINT A) (LET ((B 2)) (+ A B)))vertical-let的算法很简单。它遍历表达式列表,当遇到:with时就把接下来的三个元素分别视为变量名、等号,以及待求值的表达式,将三者打包进一个列表中,再压栈;当遇到其它值时,就视为待求值的表达式(将会组成LET的forms部分),也放进列表中再压栈(具体方法参见源代码)。将所有值都遍历并压栈后,接下来要遍历这个栈中的元素。先准备两个空的栈——一个存放bindings,一个存放forms。接着,对于每一个从栈中弹出的元素,分为如下两种情况:如果表示binding,则直接压入存放bindings的栈,否则;如果是待求值的表达式,并且上一个出栈的元素是binding,则说明已经有一段完整的LET的内容被集齐。因此,将目前在两个栈中的内容全部弹出,组合为一个LET表达式再压入存放forms的栈中。然后将方才弹出的表达式也压入forms。重复上述过程直至所有元素都被处理,最后将还在两个栈中的内容也组合为一个LET表达式便结束了。全文完 ...

March 14, 2019 · 1 min · jiezi

拿Emacs对接我的cuckoo

cuckoo是一个我自己开发的类似待办事项的工具,运行在我本地的电脑上。它有如下两个接口:传入一个UNIX Epoch时间戳创建提醒传入一个标题以及提醒的ID来创建任务这样一来,便能在设定的时刻调用alerter在屏幕右上角弹出提醒。我喜欢用Emacs的org-mode来安排任务,但可惜的是,org-mode没有定点提醒的功能(如果有的话希望来个人打我的脸XD)。开发了cuckoo后,忽然灵机一动——何不给Emacs添砖加瓦,让它可以把org-mode中的条目内容(所谓的heading)当做任务丢给cuckoo,以此来实现定点提醒呢。感觉是个好主意,马上着手写这么些Elisp函数。PS:读者朋友们就不用执着于我的cuckoo究竟是怎样的接口定义了。为了实现所需要的功能,让我从结果反过来推导一番。首先,需要提炼一个TODO条目的标题和时间戳(用来创建提醒获取ID),才能调用cuckoo的接口。标题就是org-mode中一个TODO条目的heading text,在Emacs中用下面的代码获取(nth 4 (org-heading-components))org-headline-components在光标位于TODO条目上的时候,会返回许多信息(参见下图)其中下标为4的component就是我所需要的内容。接着便是要获取一个提醒的ID。ID当然是从cuckoo的接口中返回的,这就需要能够解析JSON格式的文本。在Emacs中解析JSON序列化后的文本可以用json这个库,示例代码如下(let ((s “{"remind":{"create_at":"2019-01-11T14:53:59.000Z","duration":null,"id":41,"restricted_hours":null,"timestamp":1547216100,"update_at":"2019-01-11T14:53:59.000Z"}}”)) (cdr (assoc ‘id (cdr (car (json-read-from-string s))))))既然知道如何解析(同时还知道如何提取解析后的内容),那么接下来便是要能够获取上述示例代码中的s。s来自于HTTP响应的body,为了发出HTTP请求,可以用Emacs的request库,示例代码如下(let* ((this-request (request “http://localhost:7001/remind” :data “{"timestamp":1547216100}” :headers ‘((“Content-Type” . “application/json”)) :parser ‘buffer-string :type “POST” :success (cl-function (lambda (&key data &allow-other-keys) (message “data: %S” data))) :sync t)) (data (request-response-data this-request))) data)此处的:sync参数花了我好长的时间才捣鼓出来——看了一下request函数的docstring后才发现,原来需要传递:sync为t才可以让request函数阻塞地调用,否则一调用request就立马返回了nil。现在需要的就是构造:data的值了,其中的关键是生成秒级的UNIX Epoch时间戳,这个时间戳可以通过TODO条目的SCHEDULED属性转换而来。比如,一个条目的SCHEDULED属性的值可能是<2019-01-11 Fri 22:15>,将这个字符串传递给date-to-time函数可以解析成代表着秒数的几个数字(date-to-time “<2019-01-11 Fri 22:15>")时间戳字符串要怎么拿到?答案是使用org-mode的org-entry-get函数(org-entry-get nil “SCHEDULED”)PS:需要先将光标定位在一个TODO条目上。至此,所有的原件都准备齐全了,最终我的Elisp代码如下(defun scheduled-to-time (scheduled) “将TODO条目的SCHEDULED属性转换为UNIX时间戳” (let ((lst (date-to-time scheduled))) (+ (* (car lst) (expt 2 16)) (cadr lst))))(defun create-remind-in-cuckoo (timestamp) “往cuckoo中创建一个定时提醒并返回这个刚创建的提醒的ID” (let (remind-id) (request “http://localhost:7001/remind” :data (json-encode-alist (list (cons “timestamp” timestamp))) :headers ‘((“Content-Type” . “application/json”)) :parser ‘buffer-string :type “POST” :success (cl-function (lambda (&key data &allow-other-keys) (message “返回内容为:%S” data) (let ((remind (json-read-from-string data))) (setq remind-id (cdr (assoc ‘id (cdr (car remind)))))))) :sync t) remind-id))(defun create-task-in-cuckoo () (interactive) (let ((brief) (remind-id)) (setq brief (nth 4 (org-heading-components))) (let* ((scheduled (org-entry-get nil “SCHEDULED”)) (timestamp (scheduled-to-time scheduled))) (setq remind-id (create-remind-in-cuckoo timestamp))) (request “http://localhost:7001/task” :data (concat “brief=” (url-encode-url brief) “&detail=&remind_id=” (format “%S” remind-id)) :type “POST” :success (cl-function (lambda (&key data &allow-other-keys) (message “任务创建完毕”))))))在create-task-in-cuckoo中,之所以没有再传递application/json形式的数据给cuckoo,是因为不管我怎么测试,始终无法避免中文字符在传递到接口的时候变成了\u编码的形式,不得已而为之,只好把中文先做一遍url encoding,然后再通过表单的形式(form/x-www-urlencode)发送给接口了。全文完。 ...

February 5, 2019 · 1 min · jiezi