糟糕的(令人震惊的基本但 somehow 大部分足够)语言
项目描述
糟糕的表示“令人震惊的基本但 somehow 大部分足够”。
糟糕的是一个编程语言,旨在允许非程序员实现简单的业务逻辑,用于计算价格、排名或其他类型的数值,而不会承担非专业程序员在生产代码中通常会产生安全性和稳定性风险。换句话说,它是一个沙箱,让商人可以随心所欲地摆弄他们的业务逻辑,而不涉及您的开发人员或破坏任何东西。
功能
支持Python 3.3及以上
依赖关系
python3-dev 包含Python C头文件的本地库
libmpdec-dev 用于十进制算术的本地库
语言参考
糟糕的程序旨在由商人编写,因此该语言放弃了编程语言中程序员想要的几乎所有功能,以模仿商人理解的东西:流程图。
商人在糟糕的程序中“崩溃”的唯一方式是通过除以零,因为
它不是图灵完备的
它不能分配内存
它不能访问宿主进程或环境
它只操作一种类型:任意精度十进制数
它的唯一控制流程结构是GOTO
它甚至不允许循环!
示例程序
# input variables: # # flavor: VANILLA, CHOCOLATE, or STRAWBERRY # scoops: 1, 2, etc. # cone: SUGAR or WAFFLE # sprinkles: 0 or 1 # weekday: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, or SUNDAY # output variables: # # price: total price, including tax let TAX_RATE = 5.3% let WEEKDAY_DISCOUNT = 25% let GIVEAWAY_RATE = 1% @start: random! <= GIVEAWAY_RATE => @giveaway_winner price = scoops * (flavor == STRAWBERRY ? 1.25 : 1.00) price = price + (cone == WAFFLE ? 1.00 : 0.00) price = price + (sprinkles * 0.25) weekday not in {SATURDAY, SUNDAY} => @apply_weekday_discount => @compute_total @apply_weekday_discount: price = price * (1 - WEEKDAY_DISCOUNT) => @compute_total @giveaway_winner: price = 0.00 @compute_total: price = price * (1 + TAX_RATE)
控制流
Abysmal程序模拟包含一个或多个步骤或状态的流程图。程序执行从第一个状态的开始处开始,一直持续到达到死胡同。在这个过程中,变量可以分配新的值,并且执行可以跳转到其他状态。仅此而已。
每个状态都有一个以@开头的名称。状态声明如下
@start:
状态声明之后跟着一系列的动作。每个动作单独占一行,可以是以下之一
将值赋给变量的赋值,如下所示
price = scoops * flavor == STRAWBERRY ? 1.25 : 1.00
跳转到另一个状态的条件跳转,如下所示
weekday not in {SATURDAY, SUNDAY} => @apply_weekday_discount
跳转到另一个状态的无条件跳转,如下所示
=> @compute_total
当执行到达一个状态时,该状态的动作将按顺序执行。如果执行到达状态末尾而没有跳转到新状态,程序将退出。
不允许程序包含循环或其他执行循环。任何包含循环的程序都将无法编译。
动作通常缩进以使状态标签更容易看到,但这仅是一种风格约定,并且语言不强制执行。
行续
行尾的\表示下一行是上一行的续行。这使得将长行分割成多个更短的行以便于阅读变得容易。注意,注释可以出现在\之后。
数字
Abysmal支持整数和定点小数文字,如123、3.14159等。此外,数字可以有以下后缀
后缀 |
含义 |
---|---|
% |
百分比(12.5% 等同于 0.125) |
k或K |
千(50k 等同于 50000) |
m或M |
百万(1.2m 等同于 1200000) |
b或B |
十亿(0.5b 等同于 500000000) |
不支持科学记数法。
布尔值
Abysmal使用1和0来表示任何产生逻辑真/假值的操作的 结果。在评估条件跳转或?表达式中的条件时,零被视为假,任何非零值被视为真。
表达式
程序可以评估包含以下操作符的表达式
操作符 |
优先级 |
含义 |
示例 |
---|---|---|---|
( exp ) |
0(最高) |
分组 |
(x + 1) * y |
! |
1 |
逻辑非 |
!x |
+ |
1 |
一元加(无效果) |
+x |
- |
1 |
一元减 |
-x |
^ |
2 |
指数(右结合) |
x ^ 3 |
* |
3 |
乘法 |
x * 100 |
/ |
3 |
除法 |
x / 2 |
+ |
4 |
加法 |
x + 5 |
- |
4 |
减法 |
x - 3 |
in { exp, … } |
5 |
是集合的成员 |
x in {0, y, -z} |
not in { exp, … } |
5 |
不是集合的成员 |
x not in {0, y, -z} |
in [ low , high ] |
5 |
位于区间内(见区间) |
x in [-3, 7] |
not in [ low , high ] |
5 |
不在区间内 |
x not in [-3, 7] |
< |
6 |
小于 |
x < y |
<= |
6 |
小于等于 |
x <= y |
> |
6 |
大于 |
x > y |
>= |
6 |
大于等于 |
x >= y |
== |
7 |
等于 |
x == y |
!= |
7 |
不等于 |
x != y |
&& |
8 |
逻辑与(短路) |
x && (y / x > 0.8) |
|| |
9 |
逻辑或(短路) |
x > 3 || y > 7 |
exp ? exp : exp |
10(最低) |
if-then-else |
x < 0 ? -x : x |
区间
区间支持包含端点(用方括号指定)和排除端点(用圆括号指定),两者可以自由混合。例如,以下都是有效的检查
x in (0, 1)
x in (0, 1]
x in [0, 1)
x in [0, 1]
请注意,“反向”区间(第一个端点大于第二个端点)被视为病态,并被视为空集。因此 2 in (1, 3) 的结果为 1(即 true),但 2 in (3, 1) 的结果为 0(即 false)。
函数
表达式可以利用以下内置函数
function |
返回 |
---|---|
ABS(exp) |
指定值的绝对值 |
CEILING(exp) |
大于或等于指定值的最小整数 |
FLOOR(exp) |
小于或等于指定值的最小整数 |
MAX(exp1, exp2, …) |
指定值的最大值 |
MIN(exp1, exp2, …) |
指定值的最小值 |
ROUND(exp) |
指定值,四舍五入到最接近的整数 |
变量
Abymal程序可以读取和写入在编译程序时定义的变量。其中一些将是输入,其值在运行程序之前设置。其他将是输出,程序将计算这些值,以便在程序终止后检查这些值。
Abymal不区分输入和输出变量。
所有变量和常量值都是十进制数。Abymal没有字符串、布尔值、null或任何其他类型的概念。
如果没有显式设置,变量默认为0。
random! 是一个特殊的只读变量,每次引用时都会产生一个新的随机值。
您还可以在编译程序时为程序提供命名常量。常量不能修改。
程序也可以声明自定义变量,它在运行模型时可以用来存储中间结果,或者简单地为模型内部使用的值定义更友好的名称。自定义变量必须在声明第一个状态之前声明。
每个自定义变量都单独声明一行,如下所示
let PI = 3.14159 let area = PI * r * r
用法
Abymal程序在运行之前必须进行编译。编译器需要知道程序应有权访问的变量名称以及要定义的任何常量的名称和值
ICE_CREAM_VARIABLES = {
# inputs
'flavor',
'scoops',
'cone',
'sprinkles',
'weekday',
# outputs
'price',
}
ICE_CREAM_CONSTANTS = {
# flavors
'VANILLA': 1,
'CHOCOLATE': 2,
'STRAWBERRY': 3,
# cones
'SUGAR': 1,
'WAFFLE': 2,
# weekdays
'MONDAY': 1,
'TUESDAY': 2,
'WEDNESDAY': 3,
'THURSDAY': 4,
'FRIDAY': 5,
'SATURDAY': 6,
'SUNDAY': 7,
}
compiled_program, source_map = abysmal.compile(source_code, ICE_CREAM_VARIABLES, ICE_CREAM_CONSTANTS)
现在忽略 abysmal.compile() 返回的第二个值(请参阅测量覆盖率部分以了解其用途)。
接下来,我们需要为编译后的程序创建一个虚拟机来运行
machine = compiled_program.machine()
接下来,我们可以根据需要设置任何变量
# Variables can be set in bulk during reset()...
machine.reset(
flavor=ICE_CREAM_CONSTANTS['CHOCOLATE'],
scoops=2,
cone=ICE_CREAM_CONSTANTS['WAFFLE']
)
# ... or one at a time (though this is less efficient)
machine['sprinkles'] = True # automatically converted to '1'
最后,我们可以运行机器并检查最终变量值
price = Decimal('0.00')
try:
machine.run()
price = round(Decimal(machine['price']), 2)
except abysmal.ExecutionError as ex:
print('The ice cream pricing algorithm is broken: ' + str(ex))
else:
print('Two scoops of chocolate ice cream in a waffle cone with sprinkles costs: ${0}'.format(price))
请注意,虚拟机将变量值作为字符串暴露,这些值可能以科学或定点表示法格式化。
变量可以从 int、float、bool、Decimal 和 string 值设置,但在分配时转换为字符串。在运行机器后检查变量时,需要将值转换回 Decimal、float 或您感兴趣的任何数值类型。
随机数
默认情况下,random! 生成介于0和1之间,精度为9位小数的数字,并使用默认的Python PRNG(random.randrange)。
如果您需要更安全的PRNG、不同的精度或您想为了测试目的强制产生某些值,您可以在运行机器之前提供自己的随机数迭代器
# force random! to yield 0, 1, 0, 1, ...
machine.random_number_iterator = itertools.cycle([0, 1])
您返回的值不需要落在任何特定的范围内,但[0, 1]是推荐的,以确保与默认行为的一致性。
限制
十进制值按照IEEE 754 decimal128格式进行约束。这提供了34位精度和-6143到+6144的指数范围。
无穷大、负无穷大和NaN(非数字)是不允许的。如果计算结果会导致这些值之一,则会引发错误。
此外,如果计算结果太大或太小而无法适应decimal128范围,则计算可能导致溢出或下溢。
错误
- abysmal.CompilationError
由 abysmal.compile() 抛出,如果源代码无法编译
- abysmal.ExecutionError
由 machine.run() 和 machine.run_with_coverage() 抛出,如果在运行程序时遇到错误;这包括以下条件:除以零、无效的指数运算、堆栈溢出、浮点溢出、浮点下溢、空间不足和无法生成随机数
- abysmal.InstructionLimitExceededError
由 machine.run() 和 machine.run_with_coverage() 抛出,如果程序超出其允许的指令计数并被终止;这个错误是 abysmal.ExecutionError 的子类
性能提示
一旦编译,Abysmal程序运行非常快,并且虚拟机已优化以使重复运行不同输入尽可能便宜。
一如既往,确定您的性能目标并在优化之前进行测量。
为了获得最佳性能,请遵循以下提示
避免重新编译
编译程序比实际运行它要慢得多。
保存编译后的程序并重用它,而不是每次都重新编译。编译后的程序是可序列化的,因此易于缓存。
使用基线图像
创建机器时,您可以通过传递关键字参数来设置机器的变量为初始值。变量在此时的状态称为基线图像。当您重置机器时,它将非常有效地将所有变量恢复到基线图像。因此,如果您将重复运行特定程序并具有所有运行中相同值的某些输入,则应在基线中指定这些输入值。
例如
def compute_shipping_costs(product, weight, zip_codes, compiled_program):
shipping_costs = {}
machine = compiled_program.machine(product=product, weight=weight)
for zip_code in zip_codes:
machine.reset(zip=zip_code).run()
shipping_costs[zip_code] = round(Decimal(machine['shippingCost']), 2)
return shipping_costs
同时设置多个变量
通过传递关键字到 machine.reset() 来覆盖基线变量值,而不是逐个分配变量。如果您的场景需要性能,则多次调用Python函数的开销是非平凡的!
仅读取和写入您需要的变量
在程序运行之前初始化变量并在之后读取变量可以很容易地增加实际运行典型程序所需的时间。如果性能对您的场景至关重要,您可以通过仅检查您真正需要的变量的值来节省时间。
限制指令执行
由于Abysmal不支持循环,因此很难创建运行时间非常长的程序。但是,您可以通过设置机器的 instruction_limit 属性来对程序可以执行的指令数量施加额外限制
machine.instruction_limit = 5000
如果程序超出其指令限制,它将引发 abysmal.InstructionLimitExceededError。
默认指令限制为10000。
run() 方法返回程序退出前执行的指令数量。
测量覆盖率
除了 run() 之外,虚拟机还公开了一个 run_with_coverage() 方法,它可以与 abysmal.compile() 返回的源映射一起使用,以生成Abysmal程序的覆盖率报告。
coverage_tuples = [
machine.reset(**test_case_inputs).run_with_coverage()
for test_case_inputs in test_cases
]
coverage_report = abysmal.get_uncovered_lines(source_map, coverage_tuples)
print('Partially covered lines: ' + ', '.join(map(str, coverage_report.partially_covered_line_numbers)))
print('Totally uncovered lines: ' + ', '.join(map(str, coverage_report.uncovered_line_numbers))
覆盖率如何工作
run_with_coverage() 函数返回一个与编译程序中指令数量相等的 覆盖率元组。覆盖率元组中索引 i 的值将根据指令 i 在程序运行过程中是否被执行而返回 True 或 False。
源映射 是另一个与覆盖率元组长度相同的元组。源映射中索引 i 的值指示生成编译程序指令 i 的源代码中的哪一行或几行。有三种可能性:
None - 指令不是由任何源代码行直接生成的
int - 指令由单个源代码行生成
(int, int, …) - 指令由多行源代码生成(由于使用了行续行符)
安装
注意,在安装 abysmal 库之前,必须安装原生库依赖项。
pip install abysmal
开发
# Install system-level dependencies on Debian/Ubuntu
make setup
# Run unit tests
make test
# Check code cleanliness
make pylint
# Check code coverage
make cover
# Create sdist package
make package
项目详情
abysmal-1.2.0.tar.gz 的散列值
算法 | 散列摘要 | |
---|---|---|
SHA256 | 85eebbbc4ca3024bf7496e9941b5cfbe26c728c1b1ef3bcde837e83e0b964a31 |
|
MD5 | 1f92de134166d3e00fdc97bcd4d51fa4 |
|
BLAKE2b-256 | 6d51612d8c883658b758e54db4ef041a0c2d1eb1b6e1770b737d3c8d99122bc0 |
注释
行上的#之后的所有内容都被视为注释,并会被忽略。