8 个版本
0.2.3 | 2023年1月4日 |
---|---|
0.2.2 | 2021年10月6日 |
0.2.1 | 2021年7月30日 |
0.2.0 | 2021年4月24日 |
0.1.3 | 2021年3月29日 |
#325 在 数学 中
50 每月下载量
62KB
949 行
逆波兰表示法计算器
一个简单的程序,允许您以逆波兰表示法输入数学表达式(甚至多行),并对其进行评估。程序保留一个全局变量表,以便您可以存储用于以后使用的值。所有数字都存储为 RAMP 库提供的多位精度有理数,因此您的计算仅受限于您的内存(和有理数)。
这个小型项目始于必要性(我需要一个用于从终端快速编写表达式的程序,并且我希望它能够计算大数),并且为了尝试使用简单的词法分析器和简单的栈机(仅用于版本 0.1.1)。最初我只是想让它计算简单的算术,但在中途我开始添加一些提升用户体验的功能,如变量和其他命令,我计划添加一些其他功能。
从 crates.io 获取
cargo install rpn-c
Hello, World!
; "Hello, World!" -> "!dlroW ,olleH" reverse the string
; "!dlorW ,olleH" -> "21 64 6c 72 6f 57 20 2c 6f 6c 6c 65 48" convert each character into a byte
; "0x21646c726f57202c6f6c6c6548" -> "2645608968345021733469237830984" convert it single integer
; use '&' to print
2645608968345021733469237830984 &
; or, with the new sintax
"Hello, World!" &
构建说明
构建需要 nightly Rust 工具链,因为 RAMP
使用 nightly 特性以获得更好的性能(特别是:lazy_statics,内联汇编)。此外,RAMP
不支持交叉编译,但这只是一个小不便。此外,这个 crate 假定您正在为本机编译,并使用 target-cpu=native
标志自动启用与 CPU 相关的功能,例如向量化,以提高性能。这不允许交叉编译,如果您想为不同的架构进行交叉编译,您必须选择不同的目标 CPU。请注意,交叉编译尚未经过测试。
文件
将在未来添加对输入和输出文件的支持。
提示历史和配置文件可以根据操作系统相关标准在您的本地数据目录中找到。
语法(rpn-l)
rpn-l是rpn-c使用的语言(并为rpn-c开发)。它并不非常用户友好,但它能工作,并允许您为快速计算需求编写自己的脚本和函数。每个表达式(以及大多数命令)都使用固定词法来定义,因此不需要括号。只有两个例外(>
和@
),仍然具有已知的词法。
rpn-l语句由两种类型的标记组成:表达式,可以与其他表达式组合形成新的表达式;命令,不能组合,但有时需要由表达式先导。所有表达式标记都从左到右推入栈顶,但不进行评估。当rpn-c遇到命令时(始终从左到右),这可能导致最近推入栈的表达式的评估,或产生其他副作用。命令引起动作,它们不会推入栈中,这意味着它们不能在函数内部调用。
rpn-c维护一个包含所有标识符及其含义的表,只有命令可以更改此表,使得表达式和函数对其不可变。这似乎是一个限制,但不可变性允许评估树并行执行。
- 表达式
(+|-)<some_decimal_number>(/<another_number>)
标识一个数值常数(一个分数)- 符号是可选的
- 分母是可选的(不能没有分母留下悬空的
/
)
"<some_string>"
标识一个字符串并将其转换为整数\n
换行符转义序列\r
回车符转义序列\t
制表符转义序列\\
反斜杠转义序列\"
双引号转义序列<hex>
任意字节转义序列(必须是两个十六进制数字)
<variable_name>
标识一个变量<exp0> <exp1> (+|-|*|/)
执行一个算术二元操作- 操作具有固定词法,因此不需要括号
<exp0> <exp1> ~
执行正减法- 如果结果小于
0
,则返回0
- 否则返回结果
- 如果结果小于
<exp0> <exp1> \
执行欧几里得(或整数)除法- 执行除法并取整结果
- 始终返回一个整数
<exp0> <exp1> ^
执行指数运算- 为了保持在有理数范围内,使用
<exp1>
的向下取整绝对值作为指数
- 为了保持在有理数范围内,使用
<exp0> <exp1> <exp2> _
在模<exp2>
下进行指数运算- 为了保持在有理数范围内,使用
<exp1>
和<exp2>
的向下取整绝对值
- 为了保持在有理数范围内,使用
<exp0> <exp1> <exp2> ?
if-then 构造- 如果
<exp2>
不等于0
,则丢弃<exp1>
,计算并返回<exp0>
- 如果
<exp2>
等于0
,则丢弃<exp0>
,计算并返回<exp1>
- 如果
$<some_number>
识别一个参数- 参数只能在函数内部使用
<exp0> <exp1> ... <function_name>
调用函数- 每个
<expN>
对应于参数$N
- 每个
- 命令
<exp0> <function_name>|<arity>
声明一个<arity>
的函数为<exp1>
- 函数在执行时被评估,如果标识符改变了其意义,引用它的函数将改变行为,请记得更新它们
- 有时你可能想定义一个函数,从另一个函数中引用它,然后更改第一个函数
- 这使函数之间可以相互递归
- 请记住保持相同的arity,否则这会破坏其他函数
<exp0> <exp1> ... <expN-1> <expN> <expN+1> <function_name>@<arity>
声明了一个具有<arity>
N
的迭代函数- 等同于
<exp0> <exp1> ... <expN-1> <function_name> <expN> <expN+1> ? <function_name|<arity>
,但效率略高 - 在之前版本的
rpn-c
中需要这样做,当时没有实现 TCO (尾部调用优化),现在只为了向后兼容
- 等同于
<exp0> =<variable_name>
评估栈顶的表达式并将其值赋给一个变量<exp0> =
评估栈顶的表达式并将其打印出来<exp0> #
评估栈顶的表达式并将其打印出来,同时 将结果推回栈中<exp0> :
打印当前栈>
评估并打印栈上的所有表达式(从栈顶开始)<exp0> <
评估并复制栈顶的表达式<exp0> &
评估<exp0>
并将其作为字符串打印- 按字节读取分子,从最低有效位开始,并将其写入 stdout
- 如果分母不是 1,则将其打印在新的一行上
<exp0> []
评估<exp0>
并打印一个近似值- 近似值是通过将数字转换为双精度浮点数来计算的
RAMP
在这个转换中使用了朴素的方法,因此近似值可能不准确- 未来将考虑使用 GMP 算法的转换
<exp0> !
删除栈顶的表达式- 删除整个表达式,而不仅仅是最后一个令牌
%
删除整个栈;<some_comment>
注释整行的其余部分
std_lib
rpn-c
包含一个自动加载的标准库,这个库包含了一些常用的数学运算,主要用于自然数。
- 函数
n floor
将n
四舍五入到小于或等于n
的最大整数n abs
计算n
的绝对值n fib
计算n
的第 n 个斐波那契数n tfib
是fib
的另一种实现(主要用于测试目的)n m mod
计算n 除以
m
的余数n phi
使用斐波那契数近似 phi,n
越大,结果越准确n fact
计算n!
n k bin
计算组合数n
取k
n gsum
计算前n
个整数的和a b sift
计算介于 a 和 b(包括)之间的所有整数的和n m ack
计算Ackermann函数n
和m
;很可能会在有用的时间范围内无法成功c s cons
将字符c
插入到字符串s
的前面s1 s2 cat
连接字符串s1
和字符串s2
s reverse
反转字符串s
x to_string
将正整数x
转换为字符串s str_len
找到s
的长度
- 变量
lf
换行符cr
回车符chara
字符'a'
charA
字符'A'
char0
字符'0'
hello
字符串"Hello, World!"
null
空字符串(0)lipsum
一段 2000 个字符的 Lorem Ipsum
完整性
从版本 0.1.1 开始,rpn-l 是图灵完备的,因此从理论上讲,它可以计算任何可计算的事物,但仍然需要做更多的工作。该语言还需要更多功能以简化用户的工作。肯定会添加幂、整数除法和余数;而根和对数将需要更多时间(如果它们将被实现),因为它们会导致精度损失(由于无理性)。无限精度(有理数)是程序的关键元素,因此任何导致回退到浮点数(即使是多精度浮点数)的东西都将暂时被忽略。
关于脚本。目前,用户对脚本的运用能力仅限于:将某些数字和一个脚本文件连接起来,然后将它们管道输入到 rpn-c。这比普通的4操作计算器能做的事情多,但还不够。未来还将添加更多面向脚本和与库一起工作的功能。
图灵完备性和等价的证明
语言完备性将通过使用实际 rpn-l 语言的子集模拟 μ-递归函数构造所需的原始函数和操作符的行为来证明。该子集包括操作符 +
和 ~
、N-元函数的定义以及整型字面量 0
和 1
;此证明不需要 =
命令,但需要它来运行以这种方式定义的函数。语言的其他功能对于完备性不是必需的,但使语言更易于使用。
原始函数
零函数
一个名为 zero
的函数,接收 N 个参数并返回 0。
0 zero|N
后继函数
一个名为 S
的函数,将其一个参数加 1。
$0 1 + S|1
投影函数(恒等函数)
一个名为 P
的函数,接收 N 个参数并返回第 I 个参数。
$I P|N
操作符
组合操作符
可以通过组合 K 个 N-元函数 fk
和一个 K-元函数 h
来定义一个 K-元函数 g
。
$0 ... $N-1 f1
...
$0 ... $N-1 fK
h g|N
原始递归操作符
可以定义一个 K+1-元原始递归函数 f
,其中 g
是基例的 K-元函数,而 h
是递归情况的 K+2-元函数。
$0 1 ~
$0 1 ~ $1 ... $K f ; Recursive call
$1 ... $K
h ; Recursive case
$1 ... $K g ; Base case
$0
? f|K+1
μ-操作符
给定一个 K+1-元函数 f
,可以编写一个 K-元函数 mu-f
,该函数接收 K 个参数并找到从 0 开始的最小值(如果有的话),该值(连同其他 K 个参数一起)使 f
返回 0。
$0 1 + $1 ... $K mu-f_rec ; Recursive case
$0 ; Found minimum
$0 ... $k f ; Test for zero
? mu-f_rec|K+1 ; Auxiliary function
0 $0 ... $K-1 mu-f_rec mu-f|K ; μ-function
或者
$0 1 + $1 ... $K ; Calculate next arguments
$0 ; Found minimum
$0 ... $K f ; Test for 0
mu-f_aux@K+1 ; Auxiliary function
0 $0 ... $k-1 mu-f_aux mu-f|K ; μ-function
结论
结果
- μ-递归函数已被证明是图灵等价的
- 能够模拟它们使得 rpn-l 完备
- 计算机能够模拟 rpn-l(rpn-c 在计算机上运行)
- 每个 rpn-l 函数也是图灵可计算的
- 从上述陈述可以得出结论,rpn-l 是图灵等价的
为什么是 RAMP
而不是 GMP
?
截至版本 0.1.4,rpn-c
依赖于 GMP
,使用 rug
包作为 C++ 库的安全接口。GMP 在某些情况下确实提供了更好的性能,可能总体上也是如此(由于其成熟的声誉)。但 RAMP 提供与 GMP 相当的性能(至少在 Linux x86_64 上,性能更好),并且更易于使用(更重要的是),不需要 GNU 环境来构建。
如果性能成为问题,将来将考虑切换回 GMP。在这种情况下,将在 GitHub 发布作为发布版本的预构建可执行文件。在此之前,RAMP 将是此项目的首选库。
对于您的 Rust 项目,如果您不介意需要 GNU 环境来构建,并且 rug 的易用性不是问题,这可能是最好的选择,因为它无疑具有更好的性能。
未来发展
近期
- 注释(暂停)
- 一些基本操作(暂停)
- 幂
- 整数除法(0.2.0版本所需)
- 余数
- 用户自定义函数
-
if-else
- 递归
-
- 图灵完备性证明
- 第一次发布到crates.io
- 解决栈溢出问题
- 允许编写迭代函数(0.2.0版本所需)
- 允许编写在数千次迭代中不会溢出的函数
- 通过实现TCO(尾调用优化)解决
- 允许编写迭代函数(0.2.0版本所需)
- 添加并行化
- 由于它造成了过多的开销,严重降低了性能,因此在0.2.2版本中删除
- 切换到非GMP依赖的crate
- 一个不错的提示(带有历史记录)
- 从配置文件获取配置
- 从多个文件输入
- 支持shebang
- 输出到文件(静默模式)
也许有一天
- 一些无理操作的近似
- 例如pi、phi、e、log_2(10)等无理常数的近似
- 加速尾递归
- 添加一些不使用递归的迭代形式
- 升级到真正的LALR(这似乎违背了整个目的)
- 编写实际的编译器
许可证
许可方式任选以下之一
- Apache许可证,版本2.0(LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT许可证(LICENSE-MIT 或 https://open-source.org.cn/licenses/MIT)
任选。
贡献
除非你明确声明,否则你提交给作品以供包含在内的任何贡献,根据Apache-2.0许可证的定义,将按照上述方式双许可,不附加任何额外条款或条件。
依赖关系
约11-22MB
约295K SLoC