支持减、乘,以及除

上一篇文章中,初步搭建了一个输入Common Lisp代码,输出汇编代码的编译器的骨架,实现了二元整数的加法运算。在这个基础上,要想实现减法、乘法,以及除法就是手到擒来的事情了。只需依葫芦画瓢,补充更多的分支情况即可。

我自己模仿着x64的调用约定,规定四则运算的结果始终放在EAX这个寄存器中。在稍后给出的代码中,对于减法和除法运算,都是把运算符的左操作数放到EAX寄存器中,再从EAX中减去或者除掉右操作数。

在摸索除法的汇编代码怎么生成时,遇到了个费解的问题,最后才知道,原来需要把EAX寄存器的符号扩展到高位的EDX寄存器中去。对于as这个汇编器来说,需要用到CLTD指令。

最后,jjcc2stringify两个函数被修改为如下的样子

Read More

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

本文很水。

有一天,我心血来潮想要写一个将Common Lisp编译成汇编(x64那种)的编译器。我喜欢Common Lisp这门语言,它非常好玩,有许多有趣的特性(宏、condition system等),并且它的生态很贫瘠,有很多造轮子的机会。因为我懂的还不够多,所以没法从源代码一步到位生成可执行文件,只好先输出汇编代码,再利用现成的汇编器(比如as、nasm)从这些输出内容生成可执行文件。至于这东西是不是真的算是编译器,我也不是很在意。

好了,我要开始表演了。

Read More

Boostnote及对记笔记的思考

这篇文章就是在Boostnote 中写成的XD

来龙去脉

有一阵子,我沉迷于“笔记软件狩猎”中——就是不停寻找各种各样的笔记软件,再一个个试用,企图从中选出一个最强大的。回想起来,我尝试过有道云笔记、印象笔记、Quiver、Boostnote、OneNote、Yu Writer、Leanote(在本地搭建),等等。大部分都是浅尝辄止,例如OneNote,当我发现它不支持代码块语法高亮时,就放弃了它。目前仍然在使用的是Boostnote,并且也是最令我满意的。

Read More

更过程式的let——vertical-let

作为一名自诩的non-trivial的Common Lisp程序员,在编码的时候经常会遇到令人不愉快的地方,其中一个便是LET

一段典型的LET的示例代码如下

1
2
(let ((a 1))
a)

大多数时候,LET不会只有一个绑定。并且,也不会只是绑定一个常量这么简单,而应当是下面这样的

1
2
3
4
(let ((a (foo x y))
(b (bar z)))
(function1 a b)
(function2 a b))

有时候我会想看看某一个绑定的值——最好是在它计算完毕后立即查看。如果要查看foo函数的返回值,可以这样写

1
2
3
4
5
(let ((a (foo x y))
(b (bar z)))
(print a)
(function1 a b)
(function2 a b))

如果调用foobar都顺利的话上面的代码也就够了。比较棘手的情况是,如果a的值不符合预期,会导致b的计算过程出状况(尽管在上面的代码中看似不会)。这种情况多出现在LET*的使用中,如下面所示

1
2
3
4
(let* ((a (foo x y))
(b (bar a)))
(function1 a b)
(function2 a b))

如果错误的a会导致bar的调用出错,那么在调用function1之前才调用print打印a已经为时过晚了——毕竟调用bar的时候就抛出condition往调用链的上游走了。一种方法是写成下面这样子

1
2
3
4
5
6
(let* ((a (let ((tmp (foo x y)))
(print tmp)
tmp))
(b (bar a)))
(function1 a b)
(function2 a b))

这也太丑了!要不然写成下面这样子?

1
2
3
4
5
(let ((a (foo x y)))
(print a)
(let ((b (bar a)))
(function1 a b)
(function2 a b)))

本来一个LET就可以做到的事情,这下子用了两个,还导致缩进更深了一级。如果有十个变量需要打印,就会增加十个LET和十层缩进。如果心血来潮想查看一个变量的值,还要大幅调整代码。

问题大概就出在LETLET*的语法上。以LET为例,它由截然分开的bindingsforms组成,两者不能互相穿插。因此,如果想在bindings中求值一段表达式,只能将bindings切开,写成两个LET的形式。好像写一个新的宏可以解决这个问题?是的,vertical-let就是。

vertical-let是一个我自己写的宏,源代码在此。其用法如下

1
2
3
(vertical-let
:with a = 1
a)

它借鉴了LOOP中绑定变量的方式(即:with=),绑定变量和用于求值的代码还可以交织在一起,如下

1
2
3
4
5
(vertical-let
:with a = 1
(print a)
:with b = 2
(+ a b))

vertical-let最终会展开为LET,比如上面的代码,会展开为如下的代码

1
2
3
4
(LET ((A 1))
(PRINT A)
(LET ((B 2))
(+ A B)))

vertical-let的算法很简单。它遍历表达式列表,当遇到:with时就把接下来的三个元素分别视为变量名、等号,以及待求值的表达式,将三者打包进一个列表中,再压栈;当遇到其它值时,就视为待求值的表达式(将会组成LETforms部分),也放进列表中再压栈(具体方法参见源代码)。

将所有值都遍历并压栈后,接下来要遍历这个栈中的元素。先准备两个空的栈——一个存放bindings,一个存放forms。接着,对于每一个从栈中弹出的元素,分为如下两种情况:

  1. 如果表示binding,则直接压入存放bindings的栈,否则;
  2. 如果是待求值的表达式,并且上一个出栈的元素是binding,则说明已经有一段完整的LET的内容被集齐。因此,将目前在两个栈中的内容全部弹出,组合为一个LET表达式再压入存放forms的栈中。然后将方才弹出的表达式也压入forms。重复上述过程直至所有元素都被处理,最后将还在两个栈中的内容也组合为一个LET表达式便结束了。

全文完

不同工具查看代码分支diff的差异

本文只是简单地讲述我自己在使用GNU EmacsFork,以及Visual Studio Code查看Git仓库的不同分支的diff上的经历。

Emacs

当使用Emacs时,我更喜欢用M-x package-install安装的magit提供的功能——magit-diff,而不是它自带的vc-dir。按下M-x,输入magit-diff并敲下回车后,Emacs会在minibuffer中等待用户输入要比较的分支。就像在shell中使用git-diff一样,只需要输入两个以..连接的分支名并敲下回车,就可以列出它们间的差异。如下图所示

上图是master与re两个分支间的差异。magit-diff会列出两个分支间不一致的文件,与直接使用git-diff命令没有不同。往下滚动跨过文件清单后,还可以查看单个文件的差异。如下图所示

Fork

第一次知道Fork是在知乎闲逛的时候,好像是在浏览“Mac下有什么值得推荐的软件”这类问题时看到的。某一次,为了能直观地查看两个commit间的差异,便试用了一番,效果确实不错。Fork的界面如下图所示

还记得,当时为了能够比较两个commit间的差异,我还在Fork的菜单栏中翻了很久——虽然是毫无收获。结果发现,原来只需要选中两个commit就可以了。如下图所示

图片两行蓝色的就是我选中的两个commit——先用鼠标点击其一,按住control键后再选中另一个即可。图片下方的部分与magit-diff差不多,应该也算是一目了然了。

Visual Studio Code

原本我觉得Fork已经足够好了,某一天在用VSCode时才忽然发现,在Fork中显示的代码差异,是没有语法高亮的。通常来说,即使没有语法高亮,查看短小的diff也不成问题。但如果差异的内容很多,或是diff位于一个很长的函数内部,这时光靠diff来做代码审查已经不太够了——因为不好确定在这片diff中出现的变量和函数,到底是不是正确地定义了的。

后来我发现,VSCode自带的“源代码管理”,即便是在查看diff时也是有语法高亮的——不仅有语法高亮,对于Node.js代码而言,还有ESLint的提示和“跳转到定义”的功能,awesome!不愧是全宇宙最好用的编辑器(笑

为了可以用VSCode来查看两个分支间的差异,放狗搜了一下,找到了神器GitLens

安装GitLens后,VSCode的最左侧会多出一个菜单项,在其中可以方便地选择两个分支来进行比较。首先,找到一个要比较的分支,选择“Select for Compare”。如下图所示

再选中另一个要比较的分支,右键单击选择“Compare with Selected”,然后便可以在下方看到VSCode罗列出不一致的文件清单了。如下图所示

VSCode是最吼的!

全文完。

拿Emacs对接我的cuckoo

cuckoo是一个我自己开发的类似待办事项的工具,运行在我本地的电脑上。它有如下两个接口:

  1. 传入一个UNIX Epoch时间戳创建提醒
  2. 传入一个标题以及提醒的ID来创建任务

这样一来,便能在设定的时刻调用alerter在屏幕右上角弹出提醒。

我喜欢用Emacsorg-mode来安排任务,但可惜的是,org-mode没有定点提醒的功能(如果有的话希望来个人打我的脸XD)。开发了cuckoo后,忽然灵机一动——何不给Emacs添砖加瓦,让它可以把org-mode中的条目内容(所谓的heading)当做任务丢给cuckoo,以此来实现定点提醒呢。感觉是个好主意,马上着手写这么些Elisp函数。

PS:读者朋友们就不用执着于我的cuckoo究竟是怎样的接口定义了。

为了实现所需要的功能,让我从结果反过来推导一番。首先,需要提炼一个TODO条目的标题和时间戳(用来创建提醒获取ID),才能调用cuckoo的接口。标题就是org-mode中一个TODO条目的heading text,在Emacs中用下面的代码获取

1
(nth 4 (org-heading-components))

org-headline-components在光标位于TODO条目上的时候,会返回许多信息(参见下图)

其中下标为4的component就是我所需要的内容。

接着便是要获取一个提醒的ID。ID当然是从cuckoo的接口中返回的,这就需要能够解析JSON格式的文本。在Emacs中解析JSON序列化后的文本可以用json这个库,示例代码如下

1
2
(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))))))

既然知道如何解析(同时还知道如何提取解析后的内容),那么接下来便是要能够获取上述示例代码中的ss来自于HTTP响应的body,为了发出HTTP请求,可以用Emacs的request库,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
(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后才发现,原来需要传递:synct才可以让request函数阻塞地调用,否则一调用request就立马返回了nil

现在需要的就是构造:data的值了,其中的关键是生成秒级的UNIX Epoch时间戳,这个时间戳可以通过TODO条目的SCHEDULED属性转换而来。比如,一个条目的SCHEDULED属性的值可能是<2019-01-11 Fri 22:15>,将这个字符串传递给date-to-time函数可以解析成代表着秒数的几个数字

1
(date-to-time "<2019-01-11 Fri 22:15>")

时间戳字符串要怎么拿到?答案是使用org-mode的org-entry-get函数

1
(org-entry-get nil "SCHEDULED")

PS:需要先将光标定位在一个TODO条目上。

至此,所有的原件都准备齐全了,最终我的Elisp代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
42
(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)发送给接口了。

全文完。

Project-Euler第69题

大学的时候挺喜欢解Project Euler上的题目的,尤其是它不在乎答题者使用哪一门编程语言,甚至还有很多参与者是使用pen&paper来解题的。去年开始重新开始做Project Euler上的题目,而第69题则是最近刚刚解决的一题。惭愧的是,因为不晓得欧拉函数的计算公式(甚至都没有想过欧拉函数有没有可以用来计算的公式),所以这一题我是用暴力计算的方法来解决的。尽管花了40分钟左右才找出了问题的答案,但欧拉函数的计算方法本身还是让我觉得挺有意思的,下面我就来讲讲我在计算欧拉函数方面做的一些尝试。

69题本身很容易读懂,就是要找到一个不大于一百万的正整数n,这个n与以它为参的欧拉函数值的比值能够达到最大。欧拉函数的介绍可以看维基百科,简而言之,就是在不大于n的正整数中与n互质的数的个数,具体的例子可以参见69题描述中给出的表格。

网上可以找到别人的解法,基本的思路是:按从小到大的顺序,对于一百万以内的每个素数,都计算出它们的倍数的欧拉函数值的一部分——即对于素数p计算出1-1/p并与这个位置上原来的值相乘。当遍历完一百万以内的所有素数后,也就计算出每一个位置上的欧拉函数值,再遍历一次就可以计算出比值最大的数字了。

但我今天要讲的是笨方法。

笨方法的关键就是乖巧地计算出每一个数字的欧拉函数值。而其中最笨的,当属挑出每一个不大于n的因数,计算它们与n的最大公约数,并根据这个最大公约数是否为1,来决定是否给某一个计数器加一。示例代码如下

1
2
3
4
5
(defun phi (n)
(let ((count 0))
(dotimes (i n count)
(when (= (gcd (1+ i) n) 1)
(incf count)))))

这个phi可以稍微改进一下。例如,如果一个数a与n不是互质的,那么a的倍数(小于n的那一些)也一定不会与n互质。因此,每当遇到这么一个因数a,就知道后续的2a3a等等,都不再需要计算其与n的最大公约数了。改进后的算法代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(defun phi (n)
"通过将不互质的比特设置为1并计算为0的比特的个数来计算phi函数"
(let ((bits (make-array n :element-type 'bit :initial-element 0))
(count 0))
(dotimes (i n)
(cond ((= (bit bits i) 1)
;; 该比特已经为1,说明已经在比它小的倍数被处理时一并被标记了
)
((= i (1- n))
;; 只处理比上界要小的数字
)
((/= (gcd (1+ i) n) 1)
;; 除了当前这个不互质的数字之外,还需要将这个数字的倍数也一并处理
(dotimes (j (floor n (1+ i)))
(let* ((j (1+ j))
(m (* (1+ i) j)))
(setf (bit bits (1- m)) 1))))
(t (incf count))))
count))

为了节省内存空间,这里用了一个bitmap来标记小于n的每一个数字是否与n互质——1表示不互质,0表示互质。

其实并不需要遍历所有比n小的数字,只要遍历所有n的素因子即可。比如,将8分解为素因子,就是3个2,那么对于小于8的所有2的倍数(4和6)都是与8不互质的。基于这个方法,将所有的素因子的倍数所对应的位置为1,再数一下总共有多少个0比特即可。

对每个n都进行质因数分解效率不高,先生成一个一百万以内的素数表吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defun primep (n)
(cond ((< n 2) nil)
((= 2 n) t)
(t
(let ((bnd (truncate (sqrt n))))
(labels ((rec (test)
(cond ((> test bnd) t)
((= 0 (rem n test)) nil)
(t (rec (1+ test))))))
(rec 2))))))

(defvar *primes-in-1000000* nil)
(defun generate-primes-in-1000000 ()
(dotimes (i 1000000)
(when (primep (1+ i))
(push (1+ i) *primes-in-1000000*)))
(setf *primes-in-1000000* (nreverse *primes-in-1000000*)))

然后对于一个给定的n,遍历所有小于它的素数并对相应的倍数所在的比特置一就可以了,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defun phi3 (n)
"直接用素数表来做筛法"
(prog
((bits (make-array n :element-type 'bit :initial-element 0)))
(dolist (num *primes-in-1000000*)
(cond ((> num n)
(go :return))
((zerop (mod n num))
(labels ((aux (i)
(when (< i n)
(setf (bit bits i) 1)
(aux (+ i num)))))
(aux (1- num))))))
:return
(return-from phi3
(count-if (lambda (bit) (zerop bit)) bits))))

PS:写这个phi3的时候发现Common Lisp提供了一个prog宏,这个宏倒是真的挺好用。

改进了两轮,其实这仍然是笨方法。即便是用phi3,用来计算题目的答案也花了40多分钟。

全文完

一些在Emacs中搜索文本的方法

在Emacs中写代码的时候,常常需要查找一个函数、方法,或者变量的定义。如果是正在写Common Lisp,那么SLIME已经配置好了相应的快捷键M-.,只需要将光标移动到要查看的函数、方法,或者变量的名字上,按下M-.便可以跳转过去——再按一下M-,还能回到原来的位置。

如果是写其它语言的代码,很多时候都没办法方便地跳转过去,这时候就需要依赖于文本搜索了,这也是本篇所要讲述的主题。

通常情况下,用C-sC-r就足够了——一个负责“往下”搜索一个负责“往上”搜索。尤其在安装了Emacs的插件swiper之后,只需使用C-s便可以同时查看到上下两个方向的匹配文本。

C-s也有其局限性。例如,它不能跨文件搜索,如果要查看的函数、方法,或者变量的定义不在当前buffer中,就不得不手动在多个buffer间切换并频繁按下C-s了。

有多种办法可以解决上面这种问题。例如,可以用Emacs的projectile-ag。通常,如果代码散布在多个源文件中,那么它们多半是放在一个项目中——比如一个Git仓库。打开位于项目中的文件时,Emacs的projectile-mode就会启动。此时,按下C-c C-p s s这套组合键,会调用projectile-ag函数。projectile-ag会在minibuffer中等候输入要搜索的内容,按下回车后,Emacs会调用命令行工具ag来搜索这个项目下的所有文件,找出匹配关键字的行并显示。

projectile-ag函数会打开另一个buffer来展示搜索结果,一个示例如下

1
2
3
4
5
6
ag --literal --group --line-number --column --color --color-match 30\;43 --color-path 1\;32 --smart-case --stats -- emacs .
0 matches
0 files contained matches
36 files searched
111365 bytes searched
0.007795 seconds

使用projectile-ag的前提是要搜索的文件都在同一个一个项目中,但并非所有时候都满足这个要求。这时,可以用Emacs的find-grep函数。

find-grep函数调起后同样要求使用者在minibuffer输入内容,但它更原始一点

光标会定位在-e选项之后,需要填补交给grep的正则表达式。由于minibuffer中给出的是完整的、将会被运行的命令,因此可以也给find命令添加一些选项和参数,来改变搜索行为。

如果是在一个Node.js项目中搜索,一般还要让find忽略一些文件,如node_modules目录下的大量依赖,或者构建产生出来的.css和.js文件。这些文件中的行不仅很可能会命中输入的正则表达式,还极可能成片地出现,占据搜索结果中的半壁江山。

除了grep之外,还有许多命令行的文本搜索工具,例如ackrg,并且它们都称自己更快。要在Emacs中使用它们也很简单,尤其是后者还有相应的插件rg.el可以方便调起。

如果经常要控制find来忽略node_modules,可以考虑用git-grepman git-grep中说到,它只会搜索tracked的文件

git-grep的man文档

node_modules一般都不会被git跟踪,自然也就不会被搜索。

全文完

Emacs的org-mode实现自动的internal archive

缘由

org-mode是一个Emacs内置的major mode,当打开一个后缀为.org的文件时就会被启用。在官网的介绍中提到,它可以用于管理待办事项,而这也正是我目前使用org-mode最多的场合。比如,我用它来记录漫画的阅读进度,每一话或每一章就是一个标记了TODO关键字的条目,读完那一话或那一章后就会将对应的条目标记为DONE。一般我会一周一次地归档自己标记为DONE的条目,但由于一次要处理的条目可能很多,逐一将它们归档比较繁琐,因此,便打算二次开发实现一个自动归档的功能。

需要事先声明的是,本文不是org-mode的入门教程,也不会讲解如何配置org-mode。对这方面有兴趣的读者,可以自己搜索一番,资料还是相当丰富的。

菜谱

对于每一个被完成的单独的阅读任务,我的做法是将其internal archive。等到一整本漫画都读完之后,再将整个以漫画名命名的条目归档到别的文件中去。要实现自动的internal archive,最简单直接的办法是借助于org-mode提供的各种hook。

org-mode提供了许多的hook,在官方的文档中有一一列举 。其中,名为org-after-todo-state-change-hook的便是我所需要的钩子。只需往这个变量所绑定的列表中添加一个函数,那么这个函数便会在条目切换状态时(比如从TODO切换到DONE)被org-mode调用。

最终的ELisp代码如下

1
2
3
4
5
6
7
8
(defun lt-archive-if-manga ()
(let ((state org-state))
(when (string= state "DONE")
(let ((tags (org-get-tags-at)))
(when (member "漫画" tags)
(org-toggle-archive-tag))))))

(add-to-list 'org-after-todo-state-change-hook 'lt-archive-if-manga t)

稍微解释一下。从C-h v org-after-todo-state-change-hook RET的文档可以得知,条目的新状态可以通过变量org-state获取。取得新状态(是个字符串)后,首先检查其是否为"DONE"。如果是,再检查这个条目是否为一个阅读漫画的任务。

在我的用法中,凡是漫画条目,都打上了名为"漫画"的标签。因此,使用函数org-get-tags-at取得一个条目的所有标签(包括从父级条目继承下来的),再用member函数判断这些标签中是否包含字符串"漫画"。如果有,就调用org-toggle-archive-tag将该条目internal archive。

传给函数add-to-list的第三个参数t的作用,是让这个新加入钩子的函数最后被调用。

全文完