sel-lang 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sel/__init__.py +132 -0
- sel/__main__.py +6 -0
- sel/_cli.py +85 -0
- sel/builtins/__init__.py +15 -0
- sel/builtins/aggregate.py +110 -0
- sel/builtins/binary.py +149 -0
- sel/builtins/control.py +43 -0
- sel/builtins/number.py +52 -0
- sel/builtins/regex.py +462 -0
- sel/builtins/structure.py +9 -0
- sel/builtins/text.py +171 -0
- sel/decimal.py +226 -0
- sel/errors.py +40 -0
- sel/eval.py +408 -0
- sel/lexer.py +333 -0
- sel/parser.py +388 -0
- sel/py.typed +0 -0
- sel/registry.py +50 -0
- sel/utf8.py +126 -0
- sel/value.py +336 -0
- sel_lang-0.3.0.dist-info/METADATA +735 -0
- sel_lang-0.3.0.dist-info/RECORD +25 -0
- sel_lang-0.3.0.dist-info/WHEEL +4 -0
- sel_lang-0.3.0.dist-info/entry_points.txt +2 -0
- sel_lang-0.3.0.dist-info/licenses/LICENSE +36 -0
sel/__init__.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""SEL — a small expression language for validation rules.
|
|
2
|
+
|
|
3
|
+
One rule file evaluates identically on JavaScript, PHP, C++23, Common Lisp and
|
|
4
|
+
Python. Exact decimal arithmetic, no floating point, no truthiness.
|
|
5
|
+
|
|
6
|
+
from sel import compile, evaluate, Value, SelError
|
|
7
|
+
|
|
8
|
+
program = compile('TOTAL > CREDIT_LIMIT')
|
|
9
|
+
program.dependencies() # ['CREDIT_LIMIT', 'TOTAL']
|
|
10
|
+
evaluate('1 + 2').as_text() # '3'
|
|
11
|
+
|
|
12
|
+
Public host interface. See spec/SPEC.md §8.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from . import builtins as _builtins # noqa: F401 registers the function table
|
|
20
|
+
from .errors import Pos, SelError
|
|
21
|
+
from .eval import Context, eval_node
|
|
22
|
+
from .parser import Node, parse
|
|
23
|
+
from .registry import names as _names
|
|
24
|
+
from .value import BIN, BOOL, NONE, TEXT, Value
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
'compile', 'evaluate', 'Program', 'Value', 'SelError', 'Context', 'Pos',
|
|
28
|
+
'function_names', 'NONE', 'TEXT', 'BIN', 'BOOL', '__version__',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
__version__ = '0.3.0'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Program:
|
|
35
|
+
__slots__ = ('source', 'ast')
|
|
36
|
+
|
|
37
|
+
def __init__(self, source: str, ast: Node) -> None:
|
|
38
|
+
self.source = source
|
|
39
|
+
self.ast = ast
|
|
40
|
+
|
|
41
|
+
def run(self, context: Any = None) -> Value:
|
|
42
|
+
"""`context` may be a Value, a plain dict, or omitted. Returns a Value;
|
|
43
|
+
the context is mutated in place by any assignments the program performs.
|
|
44
|
+
"""
|
|
45
|
+
root = context if isinstance(context, Value) else Value.from_native(context or {})
|
|
46
|
+
return eval_node(self.ast, Context(root))
|
|
47
|
+
|
|
48
|
+
def dependencies(self) -> list[str]:
|
|
49
|
+
"""Every variable the program reads without having assigned it first,
|
|
50
|
+
found statically. Only possible because SEL has no dynamic symbol
|
|
51
|
+
operator; this is what tells a frontend which inputs should re-trigger
|
|
52
|
+
which rule.
|
|
53
|
+
"""
|
|
54
|
+
reads: set[str] = set()
|
|
55
|
+
assigned: set[str] = set()
|
|
56
|
+
_collect(self.ast, frozenset(), reads, assigned)
|
|
57
|
+
return sorted(n for n in reads if n not in assigned)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _collect(node: Node | None, bound: frozenset, reads: set, assigned: set) -> None:
|
|
61
|
+
if node is None:
|
|
62
|
+
return
|
|
63
|
+
t = node.t
|
|
64
|
+
|
|
65
|
+
if t == 'var':
|
|
66
|
+
if node.name not in bound:
|
|
67
|
+
reads.add(node.name)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if t == 'assign':
|
|
71
|
+
target = node.target
|
|
72
|
+
while target.t == 'index':
|
|
73
|
+
_collect(target.idx, bound, reads, assigned)
|
|
74
|
+
target = target.obj
|
|
75
|
+
# `A = x` defines A; `A[k] = x` and `A += x` also read it.
|
|
76
|
+
if node.target.t != 'var' or node.op != '=':
|
|
77
|
+
if target.name not in bound:
|
|
78
|
+
reads.add(target.name)
|
|
79
|
+
assigned.add(target.name)
|
|
80
|
+
_collect(node.value, bound, reads, assigned)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if t == 'call':
|
|
84
|
+
# An aggregate's three-argument form binds its second argument as a name
|
|
85
|
+
# for the duration of the third.
|
|
86
|
+
if node.spec and node.spec.binds and len(node.args) == 3 and node.args[1].t == 'var':
|
|
87
|
+
_collect(node.args[0], bound, reads, assigned)
|
|
88
|
+
inner = bound | {node.args[1].name, '_K'}
|
|
89
|
+
_collect(node.args[2], inner, reads, assigned)
|
|
90
|
+
return
|
|
91
|
+
if node.spec and node.spec.binds and len(node.args) == 2:
|
|
92
|
+
_collect(node.args[0], bound, reads, assigned)
|
|
93
|
+
inner = bound | {'_', '_K'}
|
|
94
|
+
_collect(node.args[1], inner, reads, assigned)
|
|
95
|
+
return
|
|
96
|
+
for a in node.args:
|
|
97
|
+
_collect(a, bound, reads, assigned)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
if t in ('seq', 'list'):
|
|
101
|
+
for item in node.items:
|
|
102
|
+
_collect(item, bound, reads, assigned)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if t == 'index':
|
|
106
|
+
_collect(node.obj, bound, reads, assigned)
|
|
107
|
+
_collect(node.idx, bound, reads, assigned)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if t == 'bin':
|
|
111
|
+
_collect(node.l, bound, reads, assigned)
|
|
112
|
+
_collect(node.r, bound, reads, assigned)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if t == 'un':
|
|
116
|
+
_collect(node.x, bound, reads, assigned)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compile(source: str) -> Program: # noqa: A001 - mirrors compile() in every host
|
|
121
|
+
"""Parse and check `source`. Raises SelError on a syntax error, an unknown
|
|
122
|
+
function name or a wrong argument count — all three are compile time.
|
|
123
|
+
"""
|
|
124
|
+
return Program(source, parse(source))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def evaluate(source: str, context: Any = None) -> Value:
|
|
128
|
+
return compile(source).run(context)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def function_names() -> list[str]:
|
|
132
|
+
return _names()
|
sel/__main__.py
ADDED
sel/_cli.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""SEL command line: evaluate an expression, a file, or start a REPL.
|
|
2
|
+
|
|
3
|
+
sel -e 'EXPR' evaluate and print
|
|
4
|
+
sel file.sel evaluate a file
|
|
5
|
+
sel --deps -e 'EXPR' print the variables the expression reads
|
|
6
|
+
sel --functions list the function table
|
|
7
|
+
sel REPL, keeping one context across lines
|
|
8
|
+
|
|
9
|
+
Also reachable as `python -m sel`, which is the spelling to prefer when the
|
|
10
|
+
`sel` console script collides with another package's — the JS package installs
|
|
11
|
+
a `sel` command too.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from . import SelError, Value, compile as sel_compile, function_names
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def show(v: Value) -> str:
|
|
22
|
+
if v.size() == 0:
|
|
23
|
+
if v.kind == 'TEXT':
|
|
24
|
+
return v.scalar
|
|
25
|
+
if v.kind == 'BOOL':
|
|
26
|
+
return 'TRUE' if v.scalar else 'FALSE'
|
|
27
|
+
if v.kind == 'BIN':
|
|
28
|
+
return f'bin:{v.dump()[1:]}'
|
|
29
|
+
return v.dump()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _report(e: SelError) -> None:
|
|
33
|
+
sys.stderr.write(f'{e.code} at line {e.line} column {e.col}: {e.message}\n')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv: list[str] | None = None) -> int:
|
|
37
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
38
|
+
want_deps = '--deps' in argv
|
|
39
|
+
args = [a for a in argv if a != '--deps']
|
|
40
|
+
|
|
41
|
+
if args and args[0] == '--functions':
|
|
42
|
+
print('\n'.join(function_names()))
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
source = None
|
|
46
|
+
if args and args[0] == '-e':
|
|
47
|
+
if len(args) < 2:
|
|
48
|
+
sys.stderr.write('sel: -e needs an expression\n')
|
|
49
|
+
return 2
|
|
50
|
+
source = args[1]
|
|
51
|
+
elif args:
|
|
52
|
+
try:
|
|
53
|
+
with open(args[0], encoding='utf-8') as fh:
|
|
54
|
+
source = fh.read()
|
|
55
|
+
except OSError as e:
|
|
56
|
+
sys.stderr.write(f'sel: {e}\n')
|
|
57
|
+
return 2
|
|
58
|
+
|
|
59
|
+
if source is not None:
|
|
60
|
+
try:
|
|
61
|
+
program = sel_compile(source)
|
|
62
|
+
if want_deps:
|
|
63
|
+
print('\n'.join(program.dependencies()))
|
|
64
|
+
else:
|
|
65
|
+
print(show(program.run(Value.none())))
|
|
66
|
+
except SelError as e:
|
|
67
|
+
_report(e)
|
|
68
|
+
return 1
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
# REPL: one context for the whole session, so assignments persist.
|
|
72
|
+
root = Value.none()
|
|
73
|
+
while True:
|
|
74
|
+
try:
|
|
75
|
+
line = input('sel> ')
|
|
76
|
+
except (EOFError, KeyboardInterrupt):
|
|
77
|
+
print()
|
|
78
|
+
return 0
|
|
79
|
+
if not line.strip():
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
82
|
+
print(show(sel_compile(line).run(root)))
|
|
83
|
+
except SelError as e:
|
|
84
|
+
_report(e)
|
|
85
|
+
return 0
|
sel/builtins/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Every built-in module is imported here for its side effect: each registers
|
|
2
|
+
its functions in the table in sel/registry.py.
|
|
3
|
+
|
|
4
|
+
The table must be complete before any source is parsed, because an unknown
|
|
5
|
+
function name is a compile-time error. sel/__init__.py imports this module
|
|
6
|
+
first, for exactly that reason.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from . import control # noqa: F401
|
|
10
|
+
from . import structure # noqa: F401
|
|
11
|
+
from . import aggregate # noqa: F401
|
|
12
|
+
from . import text # noqa: F401
|
|
13
|
+
from . import number # noqa: F401
|
|
14
|
+
from . import binary # noqa: F401
|
|
15
|
+
from . import regex # noqa: F401
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Aggregates. These are why SEL needs no loop: each evaluates one argument node
|
|
2
|
+
once per element, which is the same move IF makes, repeated.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .. import decimal as D
|
|
6
|
+
from ..registry import define
|
|
7
|
+
from ..value import NONE, Value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def shape(args):
|
|
11
|
+
"""Two-argument form binds `_`; three-argument form takes a bare identifier
|
|
12
|
+
as the binder, checked by inspecting the AST node the caller handed us.
|
|
13
|
+
"""
|
|
14
|
+
if args.count() == 3:
|
|
15
|
+
return args.symbol(1), args.node(2)
|
|
16
|
+
return '_', args.node(1)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def elements(value):
|
|
20
|
+
"""A scalar with no children behaves as a one-element list containing itself,
|
|
21
|
+
consistent with scalar context (§3.2). A NONE with no children is genuinely
|
|
22
|
+
empty — that is what FILTER returns when nothing matched, and ALL over it
|
|
23
|
+
must be TRUE rather than a scalar-context failure.
|
|
24
|
+
"""
|
|
25
|
+
if value.size() > 0:
|
|
26
|
+
return value.entries()
|
|
27
|
+
return [] if value.kind == NONE else [('1', value)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def walk(args, ctx, visit):
|
|
31
|
+
"""Runs `visit` per element with the binder and _K in scope. Returning a
|
|
32
|
+
value from `visit` stops the walk and becomes the result.
|
|
33
|
+
|
|
34
|
+
The frame is popped in a finally, so a body that raises does not leave the
|
|
35
|
+
binder in scope for whatever runs next.
|
|
36
|
+
"""
|
|
37
|
+
binder, body = shape(args)
|
|
38
|
+
for key, item in elements(args.val(0)):
|
|
39
|
+
ctx.push_frame({binder: item, '_K': Value.text(key)})
|
|
40
|
+
try:
|
|
41
|
+
result = visit(args.eval_node(body), key, item, body)
|
|
42
|
+
finally:
|
|
43
|
+
ctx.pop_frame()
|
|
44
|
+
if result is not None:
|
|
45
|
+
return result
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _all(args, ctx):
|
|
50
|
+
short = walk(args, ctx,
|
|
51
|
+
lambda r, k, i, body: None if r.as_bool(body.pos) else Value.bool(False))
|
|
52
|
+
return short if short is not None else Value.bool(True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _any(args, ctx):
|
|
56
|
+
short = walk(args, ctx,
|
|
57
|
+
lambda r, k, i, body: Value.bool(True) if r.as_bool(body.pos) else None)
|
|
58
|
+
return short if short is not None else Value.bool(False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _map(args, ctx):
|
|
62
|
+
out = []
|
|
63
|
+
|
|
64
|
+
def visit(r, k, i, body):
|
|
65
|
+
out.append(r.clone())
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
walk(args, ctx, visit)
|
|
69
|
+
return Value.list(out)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _filter(args, ctx):
|
|
73
|
+
"""The one aggregate that preserves keys — a filtered list should still be
|
|
74
|
+
addressable the way the original was.
|
|
75
|
+
"""
|
|
76
|
+
out = Value.none()
|
|
77
|
+
|
|
78
|
+
def visit(r, key, item, body):
|
|
79
|
+
if r.as_bool(body.pos):
|
|
80
|
+
out.set(key, item.clone())
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
walk(args, ctx, visit)
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _sum(args, ctx):
|
|
88
|
+
total = [D.ZERO]
|
|
89
|
+
|
|
90
|
+
def visit(r, k, i, body):
|
|
91
|
+
total[0] = D.add(total[0], r.as_decimal(body.pos))
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
walk(args, ctx, visit)
|
|
95
|
+
return Value.num(total[0])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _join(args, ctx):
|
|
99
|
+
"""Strict, not an aggregate: its second argument is a separator, not a body."""
|
|
100
|
+
sep = args.text(1)
|
|
101
|
+
parts = [item.as_text(args.pos_of(0)) for _, item in elements(args.val(0))]
|
|
102
|
+
return Value.text(sep.join(parts))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
define('ALL', 2, 3, lazy=True, binds=True, fn=_all)
|
|
106
|
+
define('ANY', 2, 3, lazy=True, binds=True, fn=_any)
|
|
107
|
+
define('MAP', 2, 3, lazy=True, binds=True, fn=_map)
|
|
108
|
+
define('FILTER', 2, 3, lazy=True, binds=True, fn=_filter)
|
|
109
|
+
define('SUM', 2, 3, lazy=True, binds=True, fn=_sum)
|
|
110
|
+
define('JOIN', 2, 2, fn=_join)
|
sel/builtins/binary.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Binary built-ins.
|
|
2
|
+
|
|
3
|
+
base64 and CRC32 are hand-written rather than taken from `base64` and `zlib`.
|
|
4
|
+
The stdlib versions are lenient in ways the spec is not — `base64.b64decode`
|
|
5
|
+
ignores characters outside the alphabet unless validate=True, and even then
|
|
6
|
+
accepts some padding the other hosts reject — and agreeing with three other
|
|
7
|
+
implementations on exactly which inputs are E_BAD_ARG matters more here than
|
|
8
|
+
saving twenty lines. `zlib.crc32` would in fact agree, but is left alone so that
|
|
9
|
+
the four cores can be read against each other.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
from ..errors import fail
|
|
15
|
+
from ..registry import define
|
|
16
|
+
from ..utf8 import bytes_to_hex, decode_utf8
|
|
17
|
+
from ..value import Value
|
|
18
|
+
|
|
19
|
+
define('BLEN', 1, 1, fn=lambda a, ctx: Value.int(len(a.bytes(0))))
|
|
20
|
+
define('TO_UTF8', 1, 1, fn=lambda a, ctx: Value.bin(a.bytes(0)))
|
|
21
|
+
define('FROM_UTF8', 1, 1,
|
|
22
|
+
fn=lambda a, ctx: Value.text(decode_utf8(a.bytes(0), a.pos_of(0))))
|
|
23
|
+
define('TO_HEX', 1, 1, fn=lambda a, ctx: Value.text(bytes_to_hex(a.bytes(0))))
|
|
24
|
+
|
|
25
|
+
# .fullmatch() for the reason in sel/decimal.py. This one happens to be safe
|
|
26
|
+
# with .match() because the slice is always exactly two characters, but the
|
|
27
|
+
# safety is accidental and the next edit would lose it.
|
|
28
|
+
_HEX_PAIR = re.compile(r'^[0-9a-fA-F]{2}$')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _from_hex(a, ctx):
|
|
32
|
+
s = a.text(0)
|
|
33
|
+
if len(s) % 2 != 0:
|
|
34
|
+
fail('E_BAD_ARG', 'FROM_HEX needs an even number of digits', a.pos_of(0))
|
|
35
|
+
out = bytearray(len(s) // 2)
|
|
36
|
+
for i in range(len(out)):
|
|
37
|
+
pair = s[i * 2:i * 2 + 2]
|
|
38
|
+
# Checked against an explicit ASCII class, not int(pair, 16): int()
|
|
39
|
+
# accepts Unicode digits, so "٣٣" would decode rather than fail.
|
|
40
|
+
if not _HEX_PAIR.fullmatch(pair):
|
|
41
|
+
fail('E_BAD_ARG', f'FROM_HEX: "{pair}" is not hex', a.pos_of(0))
|
|
42
|
+
out[i] = int(pair, 16)
|
|
43
|
+
return Value.bin(bytes(out))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
define('FROM_HEX', 1, 1, fn=_from_hex)
|
|
47
|
+
|
|
48
|
+
_B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
49
|
+
_B64_INDEX = {ch: i for i, ch in enumerate(_B64)}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _encode_base64(a, ctx):
|
|
53
|
+
b = a.bytes(0)
|
|
54
|
+
out = []
|
|
55
|
+
for i in range(0, len(b), 3):
|
|
56
|
+
n = (b[i] << 16) \
|
|
57
|
+
| ((b[i + 1] if i + 1 < len(b) else 0) << 8) \
|
|
58
|
+
| (b[i + 2] if i + 2 < len(b) else 0)
|
|
59
|
+
out.append(_B64[(n >> 18) & 63])
|
|
60
|
+
out.append(_B64[(n >> 12) & 63])
|
|
61
|
+
out.append(_B64[(n >> 6) & 63] if i + 1 < len(b) else '=')
|
|
62
|
+
out.append(_B64[n & 63] if i + 2 < len(b) else '=')
|
|
63
|
+
return Value.text(''.join(out))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
define('ENCODE_BASE64', 1, 1, fn=_encode_base64)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _decode_base64(a, ctx):
|
|
70
|
+
"""Strict: padding is required and any character outside the alphabet fails."""
|
|
71
|
+
s = a.text(0)
|
|
72
|
+
pos = a.pos_of(0)
|
|
73
|
+
if len(s) % 4 != 0:
|
|
74
|
+
fail('E_BAD_ARG', 'DECODE_BASE64 needs a length that is a multiple of 4', pos)
|
|
75
|
+
out = bytearray()
|
|
76
|
+
for i in range(0, len(s), 4):
|
|
77
|
+
quad = []
|
|
78
|
+
padding = 0
|
|
79
|
+
for k in range(4):
|
|
80
|
+
ch = s[i + k]
|
|
81
|
+
if ch == '=':
|
|
82
|
+
if i + 4 < len(s) or k < 2:
|
|
83
|
+
fail('E_BAD_ARG', 'misplaced base64 padding', pos)
|
|
84
|
+
padding += 1
|
|
85
|
+
quad.append(0)
|
|
86
|
+
continue
|
|
87
|
+
if padding > 0:
|
|
88
|
+
fail('E_BAD_ARG', 'misplaced base64 padding', pos)
|
|
89
|
+
v = _B64_INDEX.get(ch)
|
|
90
|
+
if v is None:
|
|
91
|
+
fail('E_BAD_ARG', f'invalid base64 character "{ch}"', pos)
|
|
92
|
+
quad.append(v)
|
|
93
|
+
n = (quad[0] << 18) | (quad[1] << 12) | (quad[2] << 6) | quad[3]
|
|
94
|
+
out.append((n >> 16) & 255)
|
|
95
|
+
if padding < 2:
|
|
96
|
+
out.append((n >> 8) & 255)
|
|
97
|
+
if padding < 1:
|
|
98
|
+
out.append(n & 255)
|
|
99
|
+
return Value.bin(bytes(out))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
define('DECODE_BASE64', 1, 1, fn=_decode_base64)
|
|
103
|
+
|
|
104
|
+
# CRC-32/ISO-HDLC: reflected, polynomial 0xEDB88320, init and final xor all ones.
|
|
105
|
+
_CRC_TABLE = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _crc_table():
|
|
109
|
+
global _CRC_TABLE
|
|
110
|
+
if _CRC_TABLE is None:
|
|
111
|
+
table = []
|
|
112
|
+
for i in range(256):
|
|
113
|
+
c = i
|
|
114
|
+
for _ in range(8):
|
|
115
|
+
c = (0xEDB88320 ^ (c >> 1)) if (c & 1) else (c >> 1)
|
|
116
|
+
table.append(c)
|
|
117
|
+
_CRC_TABLE = table
|
|
118
|
+
return _CRC_TABLE
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _crc32(a, ctx):
|
|
122
|
+
t = _crc_table()
|
|
123
|
+
b = a.bytes(0)
|
|
124
|
+
crc = 0xFFFFFFFF
|
|
125
|
+
for byte in b:
|
|
126
|
+
crc = t[(crc ^ byte) & 255] ^ (crc >> 8)
|
|
127
|
+
return Value.text(f'{crc ^ 0xFFFFFFFF:08x}')
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
define('CRC32', 1, 1, fn=_crc32)
|
|
131
|
+
|
|
132
|
+
define('BTL', 1, 1,
|
|
133
|
+
fn=lambda a, ctx: Value.list([Value.int(b) for b in a.bytes(0)]))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _ltb(a, ctx):
|
|
137
|
+
v = a.val(0)
|
|
138
|
+
items = v.values() if v.size() > 0 else [v]
|
|
139
|
+
out = bytearray(len(items))
|
|
140
|
+
for i, item in enumerate(items):
|
|
141
|
+
d = item.as_decimal(a.pos_of(0))
|
|
142
|
+
n = -d.digits if d.neg else d.digits
|
|
143
|
+
if d.scale != 0 or n < 0 or n > 255:
|
|
144
|
+
fail('E_RANGE', f'LTB element {i + 1} is not a byte value', a.pos_of(0))
|
|
145
|
+
out[i] = n
|
|
146
|
+
return Value.bin(bytes(out))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
define('LTB', 1, 1, fn=_ltb)
|
sel/builtins/control.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from ..errors import fail
|
|
2
|
+
from ..registry import INF, define
|
|
3
|
+
from ..value import Value
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# The whole of SEL's control flow. Lazy, so only the taken branch is evaluated —
|
|
7
|
+
# exactly the property the AST calling convention exists to provide.
|
|
8
|
+
def _if(args, ctx):
|
|
9
|
+
if args.bool(0):
|
|
10
|
+
return args.val(1)
|
|
11
|
+
if args.count() == 3:
|
|
12
|
+
return args.val(2)
|
|
13
|
+
return Value.text('')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
define('IF', 2, 3, lazy=True, fn=_if)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Flat multi-branch selection — sugar for a nested IF ladder, with exactly the
|
|
20
|
+
# same laziness: conditions are evaluated in order, and only the result that
|
|
21
|
+
# matches is evaluated at all.
|
|
22
|
+
#
|
|
23
|
+
# The argument count must be odd: condition/result pairs plus a mandatory
|
|
24
|
+
# default. IF can safely let its two-argument form default to "" because there
|
|
25
|
+
# is one branch and nothing to mis-pair, but with an even count here a single
|
|
26
|
+
# miscounted comma would shift every pair by one and still compile. Requiring
|
|
27
|
+
# the default turns that into a compile-time E_ARITY instead of a wrong answer.
|
|
28
|
+
def _cond(args, ctx):
|
|
29
|
+
last = args.count() - 1
|
|
30
|
+
for i in range(0, last, 2):
|
|
31
|
+
if args.bool(i):
|
|
32
|
+
return args.val(i + 1)
|
|
33
|
+
return args.val(last)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
define('COND', 3, INF, lazy=True, fn=_cond,
|
|
37
|
+
arity_error=lambda n: (
|
|
38
|
+
f'COND takes condition/result pairs and a final default '
|
|
39
|
+
f'(an odd number of arguments), got {n}') if n % 2 == 0 else None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# The one error a rule author raises deliberately.
|
|
43
|
+
define('ABORT', 1, 1, fn=lambda args, ctx: fail('E_ABORT', args.text(0), args.pos_of(0)))
|
sel/builtins/number.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from .. import decimal as D
|
|
2
|
+
from ..errors import fail
|
|
3
|
+
from ..registry import INF, define
|
|
4
|
+
from ..value import Value
|
|
5
|
+
|
|
6
|
+
# spec/SPEC.md §6.4. Without these, a size argument nobody meant to write takes
|
|
7
|
+
# down the host instead of failing as a rule error.
|
|
8
|
+
MAX_SCALE = 1000000
|
|
9
|
+
MAX_POWER = 100000
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sized(args, i, limit, what):
|
|
13
|
+
n = args.non_neg_int(i)
|
|
14
|
+
if n > limit:
|
|
15
|
+
fail('E_RANGE', f'{what} {n} exceeds the maximum of {limit}', args.pos_of(i))
|
|
16
|
+
return n
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
define('ABS', 1, 1, fn=lambda a, ctx: Value.num(D.abs_(a.dec(0))))
|
|
20
|
+
define('SIGN', 1, 1, fn=lambda a, ctx: Value.int(D.sign(a.dec(0))))
|
|
21
|
+
define('CEIL', 1, 1, fn=lambda a, ctx: Value.num(D.ceil(a.dec(0))))
|
|
22
|
+
define('FLOOR', 1, 1, fn=lambda a, ctx: Value.num(D.floor(a.dec(0))))
|
|
23
|
+
define('TRUNC', 1, 1, fn=lambda a, ctx: Value.num(D.trunc(a.dec(0))))
|
|
24
|
+
|
|
25
|
+
define('ROUND', 2, 2,
|
|
26
|
+
fn=lambda a, ctx: Value.num(D.round(a.dec(0), _sized(a, 1, MAX_SCALE, 'ROUND scale'))))
|
|
27
|
+
define('POWER', 2, 2,
|
|
28
|
+
fn=lambda a, ctx: Value.num(D.power(a.dec(0), _sized(a, 1, MAX_POWER, 'POWER exponent'))))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _min(a, ctx):
|
|
32
|
+
best = a.dec(0)
|
|
33
|
+
for i in range(1, a.count()):
|
|
34
|
+
d = a.dec(i)
|
|
35
|
+
if D.cmp(d, best) < 0:
|
|
36
|
+
best = d
|
|
37
|
+
return Value.num(best)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _max(a, ctx):
|
|
41
|
+
best = a.dec(0)
|
|
42
|
+
for i in range(1, a.count()):
|
|
43
|
+
d = a.dec(i)
|
|
44
|
+
if D.cmp(d, best) > 0:
|
|
45
|
+
best = d
|
|
46
|
+
return Value.num(best)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
define('MIN', 1, INF, fn=_min)
|
|
50
|
+
define('MAX', 1, INF, fn=_max)
|
|
51
|
+
|
|
52
|
+
define('ISNUM', 1, 1, fn=lambda a, ctx: Value.bool(a.val(0).looks_numeric()))
|