dc (程序)

dc
原作者Robert Morris
(于AT&T贝尔实验室
Lorinda Cherry
開發者各种开源商业开发者
编程语言B, C[1]
操作系统Unix, 类Unix, Plan 9
平台跨平台
类型命令
dc
编程范型多范型: 面向堆栈, 串接式
型態系統无类型(所有数据都是任意精度数值
受影响于
RPN

dcdesk calculator:桌面计算器),是采用逆波兰表示法跨平台可编程计算器,它支持任意精度算术[2]。它是Robert Morris贝尔实验室工作期间书写的[3],作为最老的Unix实用工具,先于C语言的发明。像那个年代的其他实用工具一样,它有着一组强力的特征和简洁的语法[4][5]。传统上,采用中缀表示法bc计算器程序是在dc之上实现的。

历史

dc是幸存的最老的Unix语言[3]。在贝尔实验室收到第一台PDP-11的时候,用B语言写成的dc是在这个新机器上运行的第一个语言,甚至在汇编器之前[6]

基本运算

在dc中要做4和5的乘法:

$ dc
4 5 *
p
20
q

这可转译为:“把4和5压入栈顶,通过乘法算符,从栈中弹出两个元素,将二者相乘并把结果压回栈顶”。接着使用p命令打印栈顶的元素。使用q命令退出此次调用的dc实例。注意数值相互间必须以空白分隔,但某些算符可以不必如此。 还可以用如下命令得到这个结果:

$ dc -e '4 5 * p'
20
$ echo "4 5 * p" | dc
20
$ dc -
4 5 *pq
20
$ cat <<EOF > cal.dc
4 5 *
p 
EOF
$ dc cal.dc
20

使用命令k来变更算术精度,它设置算术运算的小数位数。缺省精度是0,例如:

$ dc -e '10946 _6765 / p'
-1

dc将输入数值的前导_识别为负号

通过使用命令k调整精度,可以产生任意数目的小数数位,例如:

$ dc -e '7k 10946 6765 / p'
1.6180339

dc中除法采用截尾函数模除得出余数的正负号同于被除数,例如:

$ dc -e '1k 10946 _6765 % p'
122.0

dc有科学计算器的基本运算功能:命令^计算整数次幂,命令v计算平方根。例如求黄金分割率的值:

$ dc -e '7k 1 5 v + 2 / p'
1.6180339

d命令用于复制栈顶元素。r命令用于对栈顶和仅次栈顶的两个元素进行对换。z命令用于压入当前栈深度,即执行z命令前栈中元素的数目。

输入/输出

使用?命令,从stdin读取一行并执行它。这允许从宏中向用户要求输入,故而此输入必须是语法上正确的,并且这有潜在的安全问题,因为dc的!命令可以执行任意系统命令。

前面提及过,p命令打印栈顶元素,带有随后的一个换行。n命令弹出栈顶元素并输出它,没有尾随换行。f命令打印整个,从栈顶到栈底并且一项一行。

dc还支持控制输入和输出的底数o命令设置输出底数,输出底数必须大于等于2。i命令弹出栈顶元素并将它用作输入底数,输入底数必须在2和16之间,十六进制数字必须大写以避免和dc命令冲突。要记住输入底数将影响对后面的所有数值的分析,所以通常建议在设置输入底数之前先设置输出底数。例如将二进制先转换成十六进制再转换成十进制

$ echo '10011010101111001101111011110000' | dc -e '16o2i ? p 1010i10o p'
9ABCDEF0
2596069104

要读取设置的这些数值,KIO命令将压入当前精度、输入基数和输出基数到栈顶。

语言特征

除了上述的基本算术和操作,dc包括了对、条件和存储结果用于以后检索的支持。

寄存器

寄存器在dc中是有着单一字符名字的存贮位置,它可以分别通过命令sl来存储和检索,它是宏和条件的底层机制:sc弹出栈顶元素并将它存储入寄存器c,而lc将寄存器c的值压入栈顶。例如:

3 sc 4 lc * p

寄存器还被当作次要栈,可以使用SL命令在它们和主要栈之间压入和弹出数值。存储栈顶元素到寄存器中并把这个元素留在栈顶,需要联合使用ds命令。

字符串

字符串是包围在[]之中的字符,可以被压入栈顶和存入寄存器。使用x命令从栈顶弹出字符串并执行它,使用P命令从栈顶弹出并打印字符串,无尾随换行。a命令可以把数值的低位字节转换成ASCII字符,或者在栈顶是字符串时把它替换为这个字符串的第一个字符。此外没有方法去建造字符串或进行字符串操纵。

#字符开始一个注释直到此行结束。

通过允许寄存器和栈项目像数值一样存储字符串,从而实现了。一个字符串可以被打印,也可以被执行,就是说作为dc命令的序列而传递。例如可以把一个宏“加1并接着乘以2”存储到一个寄存器M中:

[1+ 2*]sM

下面的命令将一个数值3压入栈顶,使用x命令执行存储在寄存器M中的宏,并打印留在栈顶的结果:

3 lMx p

条件

最后提供了有条件执行宏的机制。命令=M将从栈顶弹出两个值,如果二者相等,则执行存储在寄存器M中的宏。如下命令序列将在原栈顶元素等于5的条件下打印字符串equal

[[equal]p]sM d 5= M

这里使用了d命令保留原栈顶元素。条件包括:=><!=!>!<,如果栈顶元素分别等于、大于、小于、不等于、不大于(小于等于)、不小于(大于等于)仅次于栈顶的元素,则执行指定寄存器中的宏。注意不同于ForthPostScriptFactor,在不等式比较中的运算元的次序同在算术中的次序相反,53-等价于中缀表示法的5-3,然而53<R3<5时运行寄存器R的内容。下面是有条件执行宏的示例:

$ echo 5 | dc -e '? [[equal]p]sM d5=M'
equal
$ echo 4 | dc -e '? [[equal]p]sM d5=M'
$

这里的宏之中有嵌入的字符串,这个命令在栈顶元素等于5之时有相应的输出,在它不等于5之时没有输出。

如果要实现一般编程语言中表达有两个分支控制流程条件运算符?:条件语句,则需要两个宏构造,例如:

$ echo 5 | dc -e '? [[equal]p q]sM [d5=M [not equal]p]x'
equal
$ echo 4 | dc -e '? [[equal]p q]sM [d5=M [not equal]p]x'
not equal

这里的两个宏之中都有嵌入的字符串,这个命令在栈顶元素等于5和不等于5之时都有相应的输出。

q命令退出2层宏,如果宏少于2层则退出dc。Q命令从栈顶弹出一个值作为退出宏的层数,比如2Q命令退出2层宏,它永不导致退出dc。

递归示例

通过定义进行有条件的递归调用的宏,可以实现递归过程和迭代运算。

阶乘

下面是对栈顶元素计算阶乘递归过程,这个过程在x > 0时有效,它可直接转写为互递归形式:

# F(x):
#   1 < x ? G(x) : x
# G(x):
#   x * F(x-1)
# F()
[d 1- lFx *]sG [d1<G]dsFx

在dc中条件算符所执行的宏必须事先存储在变量中,不支持通过嵌套的宏定义实现控制结构,故而本文中的伪代码采用了条件运算符?:而不采用条件语句。采用邱奇布尔值编码if = λp.λa.λb.p a b,定义guard = λc.λP.λF.λx.if (P c x) (F x) x;这里用伪代码表示为1 < x ? G(x) : x[d1<G],相当于guard 1 < G,它是高阶函数guard已经接受了实际参数1函数参数<G部份应用,在测试条件不成立时相当于恒等函数。这里的F(x):是过程定义,过程F消费在栈顶的命名为x的一个值;这里的F(x-1)是过程调用,在调用过程F之前先将x-1的结果值压入栈顶;这里的F()也是过程调用,在调用过程F之前不需要向栈顶压入一个值。

计算阶乘还可以采用这个条件表达式的等价形式:

# F(x):
#   1 >= x ? Q(x) : x * F(x-1)
# Q(x):
#   q()
# F()
[q]sQ [d1!<Q d 1- lFx *]dsFx

这个递归过程可以进一步采用尾调用写为:

# F(x):
#   x
#   1 < x ? G(x) : x
# G(x):
#   F(x-1)
# H(x, acc):
#   x := x * acc
#   1 < z() ? H() : x
# H(F())
[1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx

这里的伪代码中的H(x, acc):是过程定义,过程H消费在栈顶的命名为acc的一个值,和紧邻其下的命名为x的另一个值;这里的H()是过程调用,在调用过程H之前不需要向栈顶压入一个值。

这里的运算由两个步骤串接而成,首先将递减的整数压入堆栈形成整数集偏序关系区间,然后在这个数列上进行初始值为的应用乘法运算的右侧归约,最终得出一个结果值。这里的命令z,将当前的堆栈深度即堆栈中元素数目压入栈顶。还可以进一步增加针对初始的栈顶元素xx ≤ 0情况的预处理,即执行[d-1+]sAd0!<A来实现0 >= x ? x-x+1 : x。下面是其执行示例:

$ echo 0 | dc -e '? [d-1+]sAd0!<A [1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx p'
1
$ echo 9 | dc -e '? [d-1+]sAd0!<A [1- lFx]sG [d d1<G]dsFx [* z1<H]dsHx p'
362880

下面的例子采用迭代法打印出阶乘的前n项的数列

# n := x
# i := 1
# F(x):
#   x := x * i 
#   p(x)
#   i := i + 1
#   n >= i ? F() : x
# F(1)
sn 1si 1 [lid1+si *p liln!<F]dsFx

这里n := x中的x指称初始的栈顶元素。这里的迭代过程F(x)不同于一般的递归过程之处,是在调用自身之时仍采用当前栈顶元素为参数而不向堆栈压入新元素。这里所迭代的是需要初始化的堆栈中的自动变量x,它是,其初始值1i既是迭代的递增计数器,也是迭代二元运算运算元,其递增形成整数集偏序关系区间,同步地在这个数列上进行输出中间值的初始值为的应用乘法运算的左侧归约,逐项输出中间结果形成数列,这里的x所起到的作用也被称为累加器(accumulator),其含义为“累计”或“累积”而不必然采用加法或乘法。 下面是其执行示例:

$ echo 6 | dc -e '? sn 1si 1 [lid1+si *p liln!<F]dsFx'
1
2
6
24
120
720

可以将它改为计算单个的阶乘:

$ echo 0 | dc -e '? sn 1si 1 [lid1+si * liln!<F]dsFx p'
1
$ echo 9 | dc -e '? sn 1si 1 [lid1+si * liln!<F]dsFx p'
362880

斐波那契数

下面是对栈顶元素计算斐波那契数递归过程,这个过程在x ≥ 0时有效,它可直接转写为互递归形式:

# F(x):
#   1 < x ? G(x) : x
# G(x):
#   F(x-1) + F(x-2)
# F()
[d 2- lFx r 1- lFx +]sG [d1<G]dsFx

这里的r命令反转(交换)栈顶元素和仅次于栈顶的元素二者的次序,此外dc还有循环移位运算R命令,其参数为正数时是循环左移一位,参数为负数时是循环右移一位,参数的绝对值是参与循环移位的堆栈中含栈顶的元素数目,比如后文中用到_3R命令会将堆栈顶部的三个元素a b c转变为c a b

下面是其执行示例,并展示了通过给它增加统计代码来观察其递归调用的时间复杂度

$ echo 0 | dc -e '? [d 2- lFx r 1- lFx +]sG [d1<G]dsFx p'
0
$ echo 20 | dc -e '? 0sX [d 2- lFx r 1- lFx +]sG [lX1+sX d1<G]dsFx p [T(n)=]PlXp'
6765
T(n)=21891
$ echo 20 | dc -e '? [[None]pq]sAd3!<A 1+ 7k5v1+2/r^2*5v/ 0k1/ p'
21891

这个过程可以进一步采用记忆化,即基于数组的保存命令:和访问命令;而写为:

# a[0] := 0
# a[1] := 1
# i := 1
# F(x):
#   i >= x ? N(x) :
#     S(x, b)    
#     temp := G(x)
#     x := L(b)
#     i := x
#     a[x] := temp 
#     temp
# N(x):
#   a[x]
#   q()
# G(x):
#   F(x-1) + F(x-2)
# F()
0 0:a 1 1:a 1si [d 2- lFx r 1- lFx +]sG [;a q]sN [dli!<N dSb lGxd Lbdsi:a]dsFx

这里的i是数组a的已存储过的最大下标,它的初始值是1a[0]a[1]已经预先存储了数值。一个过程如果需要在进行过程调用之后仍持有原来的主栈内容,则需要设立并操纵辅助,这里的伪代码中的S(x, b)表示先复制主栈的栈顶元素再将它移入辅助b的栈顶,L(b)表示弹出辅助b的栈顶元素并把它压入主栈的栈顶,它们基于了寄存器关联的保存命令S和装载命令L

递归定义递归调用中,随着的索引n的递减,而递归下降至被计算出来并保存的第一项,然后沿着调用链逐级返回。在递归的每个步骤中都要计算并保存,它所需要的这两项之中:

  • 如果首先进行递归调用并从其返回,则也需要计算并保存,为此需要先后访问已保存的
  • 如果首先进行递归调用并从其返回,则需要访问已保存的

下面是其执行示例,展示了通过给加三个打印断点的代码来观察递归调用的具体步骤,其中连续高亮标示出某个过程从开始被调用直到它在数组中保存自己的数值后结束返回:

$ echo 6 | dc -e '?0 0:a1 1:a1si[d2-lFxr1-lFx+]sG[[   Load F(]Pdn[)]P10aP;aq]sN[[Level ]Pzn[ F(]Pdn[)]P10aPdli!<NdSblGxdLbdsi[   Save F(]Pdn[)]P10aP:a]dsFxp'
Level 1 F(6)
Level 2 F(4)
Level 3 F(2)
Level 4 F(0)
   Load F(0)
Level 4 F(1)
   Load F(1)
   Save F(2)
Level 3 F(3)
Level 4 F(1)
   Load F(1)
Level 4 F(2)
   Load F(2)
   Save F(3)
   Save F(4)
Level 2 F(5)
Level 3 F(3)
   Load F(3)
Level 3 F(4)
   Load F(4)
   Save F(5)
   Save F(6)
8
$ echo 6 | dc -e '?0 0:a1 1:a1si[d1-lFxr2-lFx+]sG[[   Load F(]Pdn[)]P10aP;aq]sN[[Level ]Pzn[ F(]Pdn[)]P10aPdli!<NdSblGxdLbdsi[   Save F(]Pdn[)]P10aP:a]dsFxp'
Level 1 F(6)
Level 2 F(5)
Level 3 F(4)
Level 4 F(3)
Level 5 F(2)
Level 6 F(1)
   Load F(1)
Level 6 F(0)
   Load F(0)
   Save F(2)
Level 5 F(1)
   Load F(1)
   Save F(3)
Level 4 F(2)
   Load F(2)
   Save F(4)
Level 3 F(3)
   Load F(3)
   Save F(5)
Level 2 F(4)
   Load F(4)
   Save F(6)
8

前面的分析和演示,证实了对数组a的存储是按照下标从小至大连续进行的,对这个数组的访问不会出现访问到未曾存储运算结果的空位的异常情况。在递归定义递归调用中,可以直接将的结果存储在a[++i]之中,这里有着i + 1 = n,故而无需设立并操作辅助栈b。递归过程的返回值之间形成了递推关系,每个的值被用到一次或两次,首次是从递归调用直接返回,再次是对其已保存的值进行访问,每个步骤要么访问两项并保存两项,要么访问一项并保存一项。这个过程可以进一步写为:

# i := 1
# F(x):
#   1 < x ? M(x) : x
# M(x):
#   i >= x ? N(x) :
#     temp := G(x)
#     i := i + 1
#     a[i%2] := temp 
#     temp
# N(x):
#   a[x%2]
#   q()
# G(x):
#   F(x-1) + F(x-2)
# F()
1si [d 2- lFx r 1- lFx +]sG [2%;a q]sN [dli!<N lGx dli1+dsi2%:a]sM [d1<M]dsFx

递归定义的上述两种求值次序中,首先进行递归调用的这种形式,其记忆化实现从一开始就持续进行递归调用直到首次递归返回,然后就逐级递归返回而不再进行递归调用,故而它可以进一步写为:

# a := 0
# F(x):
#   1 < x ? G(x) : x
# G(x):
#   temp := F(x-1)
#   prev := a 
#   a := temp
#   temp + prev
# F()
0sa [1- lFx la r dsa +]sG [d1<G]dsFx

这里的a初始化的值0x在步入递归调用之后,除了在被递减至的值1之时作为调用结果来返回之外,没有参与到后续的加法运算之中,而主要起到了递减计数器的作用。

下面的例子采用迭代法打印出斐波那契数列的不含第0项的前n项:

# i := x
# a := 1
# F(x):
#   prev := a
#   a := x
#   x := x + prev
#   p(x)
#   i := i - 1
#   0 < i ? F() : x
# 0 < i ? F(0) : 0
si 1sa [lardsa +p li1-si li0<F]sF 0 li0<F

这里i := x中的x指称初始的栈顶元素。这里的迭代过程F(x)不同于一般的递归过程之处,是在调用自身之时仍采用当前栈顶元素为参数而不向堆栈压入新元素。针对递推关系,它总共进行n次迭代运算得出。这里的i是进行迭代的递减计数器,所迭代的是需要初始化的两个变量xa:在堆栈中的自动变量x,先是被迭代的,再是迭代出来的,其初始值0,它所起到的作用也被称为累加器;作为全局变量a,以所得的1作为初始值,在首次迭代之后它是始于。 下面是其执行示例:

$ echo 6 | dc -e '? si 1sa [lardsa +p li1-si li0<F]sF 0 li0<F'
1
1
2
3
5
8

可以将它改为计算单个的斐波那契数:

$ echo 0 | dc -e '? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'
0
$ echo 21 | dc -e '? si 1sa [lardsa + li1-si li0<F]sF 0 li0<F p'
10946

男人抑或男孩测试

下面的例子通过男人抑或男孩测试,展示在dc中仅采用数组就能支持递归运算的程度,这个测试用Python可以写为:

import sys
sys.setrecursionlimit(2**10 + 9) # 这里设置的递归限制在实测时刚好够用于k=10

def A(k, x1, x2, x3, x4, x5):
    def B():
        nonlocal k
        k -= 1
        return A(k, B, x1, x2, x3, x4)
    return x4() + x5() if k <= 0 else B()

print(A(10, lambda:1, lambda:-1, lambda:-1, lambda:1, lambda:0))

实现这个测试需要解决的问题,是针对这里的x4() + x5(),以同一种方式调用两种闭包匿名函数即这里的lambda:1lambda:-1lambda:0,和头等函数即这里的嵌入到包围函数A()内的嵌套函数B(),此二者的这些实例皆无参数而可称为thunk。原本的ALGOL 60测试代码,对x4 + x5进行有需要时的隐式强制解过程化,现代编程语言一般不提供这种隐式强制特征。

下面是这个测试的dc实现,这里指定了数组M调用栈,其帧指针S栈指针T

# M[0] := S := T := I := 0
# P(): 
#   S := T + 1
#   n := z()
#   T := S + n
#   M := R(n, n)
# R(x, n):
#   M[S+n] := x
#   n := n - 1
#   0 <= n ? R(n) : n
# Q():
#   T := S - 1
#   S := T - M[T]
# U(x):
#   I := I + 1
#   W[I] := x
#   K[I] := S
# V():
#   I := I - 1
# W(i):
#   W[i](i)
0 0:M 0sS 0sT 0sI
[lT1+sS z dlS+sT dlRxsM]sP 
[d_3RlS+:M 1- d0!>R]sR
[lS1-sT lTd;M-sS]sQ
[lI1+sI lI:W lSlI:K]sU
[lI1-sI]sV [d;Wx]sW

# A(k, x1, x2, x3, x4, x5):
#   P(0); F(); Q()
# F():
#   0 >= M[S] ? G() :
#     U('B()'); B(I); V()
# G():
#   M[S+6] := W(M[S+5])
#   W(M[S+4]) + M[S+6]
#   q()
[0lPx lFx lQx]sA
[lS;M0!<G [lBx]lUx lIlBx lVx]sF
[lS5+;M lWx lS6+:M lS4+;M lWx lS6+;M + q]sG

# B(i):
#   L := K[i]
#   J := i
#   M[L] := M[L] - 1
#   A(M[L], J, M[L+1], M[L+2], M[L+3], M[L+4])
[d;KsL sJ lL;M1-lL:M lL;M lJ lL1+;M lL2+;M lL3+;M lL4+;M lAx]sB

# U('(x):1'); U('(x):-1'); U('(x):-1'); U('(x):1'); U('(x):0')
# p(A(?(), 1, 2, 3, 4, 5))
[sM1]lUx [sM_1]lUx [sM_1]lUx [sM1]lUx [sM0]lUx
? 1 2 3 4 5 lAx p

这里的帧指针S指向调用栈顶部栈帧的底部,这个栈帧的内容是当前执行函数的实际参数列表,加上这个函数中存储其运算所用到的中介值或临时值的局部变量初始值,和上述实际项目的数量n。将项目压入栈帧采用了辅助过程R(n, n),它将在主栈中所有的实际参数和局部变量初始值以及项目数量n,压入调用栈M的当前栈帧相对于帧指针S偏移量0n的位置,它能够妥善处理传递给printf样式的可变参数函数可变参数列表。栈指针T指向整个堆栈的顶部,在项目压入新建栈帧之后,它也是这个顶部栈帧的顶部,即T := S + n,其内容就是数量n,即M[T] := n

在这里的调用约定中实际参数由被调用者清理。在进行堆栈回溯之时,被调用者帧指针S1就是调用者栈帧的栈指针T',即T' = S - 1。栈指针T减去其内容M[T]就得到帧指针,即S := T - M[T],这里不需要为复原帧指针而在栈帧中专门存储控制链接。没有实际内容的空栈帧占用调用栈M的一个位置,有着M[T] := 0; S := T

这里采用了传统Unix哲学平行数组策略,将无参数匿名函数实现为常量函数,从而将两种闭包统一存储在以I为最大索引值的数组W之中,并以在这个闭包数组中的位置索引作为其标识符,以这种标识符为参数的函数调用可称为传名调用。另立数组K储存以实际参数形式提供给嵌套函数B(i)进行非局部引用静态链接,也就是包围函数A()帧指针S'

执行闭包采用了辅助过程W(i),它以闭包标识符为索引从数组W中取出闭包,再以这个标识符为参数调用这个闭包;常量函数比如[sM1]会直接忽略参数值返回常量值,而闭包[lBx]执行嵌套函数B(i),它以这个标识符为索引从数组K中取出静态链接存入变量L中用来进行非局部引用,这里的i的作用相当于支持面向对象编程范型Modula-3Python方法的第一个形式参数self

将上述代码保存入manorboy.dc文件中,下面是针对k010它的执行示例:

$ for i in $(seq 0 10); do echo $i | dc manorboy.dc; done
1
0
-2
0
1
0
1
-1
-10
-30
-67

对于k = 10,这个测试中包围函数A(),被嵌套函数B(i)所直接调用,再加上它的顶层调用,总共执行了722次;嵌套函数B(i),要么被包围函数A()所直接调用,要么作为包围函数A()传名形式参数被求值,总共执行了721次。在嵌套函数B(i)每次被直接调用之前,为它创建一个闭包,总共为它创建了416个闭包,它们作为传名形式参数,总共被求值了721 - 416 = 305次。在包围函数A()的顶层调用之前,为常量函数创建了5个闭包,它们总共被执行了66 + 85 + 102 + 54 + 0 = 307次,从而得出了最终的运算结果66 - 85 - 102 + 54 = -67

这里的包围函数A()传值形式参数k小于等于零的情况下,先求值第5传名形式参数再求值第4个传名形式参数,包围函数A()在调用栈中的栈帧数量的最大值即递归深度是323层,嵌套函数B(i)在其存储数组中堆叠数量的最大值是258层。如果在包围函数A()中先求值第4个传名形式参数再求值第5个传名形式参数,包围函数A()的递归深度为512层,嵌套函数B(i)堆叠数量的最大值是384层。

下面的dc实现改变策略,将闭包作为最后项目压入栈帧之中:

# M[0] := S := T := 0
# P(): 
#   S := T + 1
#   n := z()
#   T := S + n
#   M := R(n, n)
# R(x, n):
#   M[S+n] := x
#   n := n - 1
#   0 <= n ? R(n) : n
# Q():
#   T := S - 1
#   S := T - M[T]
# W(i):
#   M[i](i)
0 0:M 0sS 0sT
[lT1+sS z dlS+sT dlRxsM]sP 
[d_3RlS+:M 1- d0!>R]sR
[lS1-sT lTd;M-sS]sQ
[d;Mx]sW

# A(k, x1, x2, x3, x4, x5):
#   P(0, 'B()'); F(); Q()
# F():
#   0 >= M[S] ? G() : B(T-1)
# G():
#   M[S+6] := W(M[S+5])
#   W(M[S+4]) + M[S+6]
#   q()
[0[lBx]lPx lFx lQx]sA
[lS;M0!<G lT1-lBx]sF
[lS5+;M lWx lS6+:M lS4+;M lWx lS6+;M + q]sG

# B(i):
#   L := i+1 - M[i+1]
#   J := i
#   M[L] := M[L] - 1
#   A(M[L], J, M[L+1], M[L+2], M[L+3], M[L+4])
[d1+d;M-sL sJ lL;M1-lL:M lL;M lJ lL1+;M lL2+;M lL3+;M lL4+;M lAx]sB

# P('(x):1', '(x):-1', '(x):-1', '(x):1', '(x):0')
# p(A(?(), 1, 2, 3, 4, 5))
[sM1] [sM_1] [sM_1] [sM1] [sM0] lPx
? 1 2 3 4 5 lAx p

在栈帧中存储执行嵌套函数B(i)闭包项目[lBx]的作用,是让它通过自己在栈帧中的相对位置,找到包围函数A()的栈指针T',从而计算出进行非局部引用所需的包围函数A()的帧指针S'并存入变量L之中。如果要将这里的调用栈机制普遍化,比如在栈帧存入更多其他的嵌套函数,可以将它在栈帧中项目相对于栈指针的偏移量也存储在其旁边。

与这个测试的Python实现相比,这里的嵌套函数B(i)由于没有调用者在主栈中传递给它的实际参数和自己的局部变量,并且结束于尾调用,故而没有创建自己的栈帧。嵌套函数B(i)如果要创建自己的栈帧,可以将闭包调用辅助过程W(i)在主栈中传递给它的其在调用栈的位置参数i转存入自己的栈帧:

# B(i):
#   P()
#   L := M[S]+1 - M[M[S]+1]
#   M[L] := M[L] - 1
#   A(M[L], M[S], M[L+1], M[L+2], M[L+3], M[L+4])
#   Q()
[lPx lS;M1+d;M-sL lL;M1-lL:M lL;M lS;M lL1+;M lL2+;M lL3+;M lL4+;M lAx lQx]sB

嵌套函数B(i)如果创建了自己的栈帧,那么它在调用栈中的递归深度,比包围函数A()的递归深度少1层。

参见

引用

  1. ^ dc.c. 1979-01-20. 
  2. ^ dc(1): an arbitrary precision calculator – Linux用户命令(User Commands)手册页
  3. ^ 3.0 3.1 Brian Kernighan and Ken Thompson. A nerdy delight for any Vintage Computer Fest 2019 attendee: Kernighan interviewing Thompson about Unix. YouTube. 事件发生在 29m45s. [September 3, 2019]. (原始内容存档于2022-02-01). 
  4. ^ The sources for the manual page for 7th Edition Unix dc. [2020-09-25]. (原始内容存档于2019-09-24). 
  5. ^ Ritchie, Dennis M. The Evolution of the Unix Timesharing System. Sep 1979 [2019-05-31]. (原始内容存档于2010-05-06). 
  6. ^ McIlroy, M. D. A Research Unix reader: annotated excerpts from the Programmer's Manual, 1971–1986 (PDF) (技术报告). CSTR. Bell Labs. 1987 [2019-05-31]. 139. (原始内容存档 (PDF)于2019-11-30). The PDP-11 assembler, a desk calculator dc, and B itself were written in B to bootstrap the system to the PDP-11. Because it could run before the disk had arrived, dc — not the assembler — became the first language to run on our PDP-11. Soon revised to handle arbitrary-precision numbers (v1), dc was taken over by Bob Morris and Lorinda Cherry. It now ranks as the senior language on UNIX systems. 

外部链接