astreum 0.2.34__py3-none-any.whl → 0.2.36__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.
Potentially problematic release.
This version of astreum might be problematic. Click here for more details.
- astreum/__init__.py +1 -1
- astreum/_lispeum/__init__.py +16 -0
- astreum/_lispeum/environment.py +10 -0
- astreum/_lispeum/expression.py +37 -0
- astreum/_lispeum/high_evaluation.py +177 -0
- astreum/_lispeum/low_evaluation.py +121 -0
- astreum/_lispeum/meter.py +18 -0
- astreum/_lispeum/parser.py +40 -0
- astreum/_lispeum/tokenizer.py +22 -0
- astreum/_node.py +108 -453
- astreum/lispeum/__init__.py +0 -2
- {astreum-0.2.34.dist-info → astreum-0.2.36.dist-info}/METADATA +1 -1
- {astreum-0.2.34.dist-info → astreum-0.2.36.dist-info}/RECORD +16 -8
- {astreum-0.2.34.dist-info → astreum-0.2.36.dist-info}/WHEEL +0 -0
- {astreum-0.2.34.dist-info → astreum-0.2.36.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.34.dist-info → astreum-0.2.36.dist-info}/top_level.txt +0 -0
astreum/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from ._node import Node, Expr, Env, tokenize, parse
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .expression import Expr
|
|
2
|
+
from .environment import Env
|
|
3
|
+
from .low_evaluation import low_eval
|
|
4
|
+
from .meter import Meter
|
|
5
|
+
from .parser import parse
|
|
6
|
+
from .tokenizer import tokenize
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Env",
|
|
10
|
+
"Expr",
|
|
11
|
+
"low_eval",
|
|
12
|
+
"Meter",
|
|
13
|
+
"parse",
|
|
14
|
+
"tokenize"
|
|
15
|
+
]
|
|
16
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Expr:
|
|
5
|
+
class ListExpr:
|
|
6
|
+
def __init__(self, elements: List['Expr']):
|
|
7
|
+
self.elements = elements
|
|
8
|
+
|
|
9
|
+
def __repr__(self):
|
|
10
|
+
if not self.elements:
|
|
11
|
+
return "()"
|
|
12
|
+
inner = " ".join(str(e) for e in self.elements)
|
|
13
|
+
return f"({inner})"
|
|
14
|
+
|
|
15
|
+
class Symbol:
|
|
16
|
+
def __init__(self, value: str):
|
|
17
|
+
self.value = value
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return self.value
|
|
21
|
+
|
|
22
|
+
class Byte:
|
|
23
|
+
def __init__(self, value: int):
|
|
24
|
+
self.value = value
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return self.value
|
|
28
|
+
|
|
29
|
+
class Error:
|
|
30
|
+
def __init__(self, topic: str, origin: Optional['Expr'] = None):
|
|
31
|
+
self.topic = topic
|
|
32
|
+
self.origin = origin
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
if self.origin is None:
|
|
36
|
+
return f'({self.topic} err)'
|
|
37
|
+
return f'({self.origin} {self.topic} err)'
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from src.astreum._lispeum import Env, Expr, Meter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
|
|
8
|
+
|
|
9
|
+
if meter is None:
|
|
10
|
+
meter = Meter()
|
|
11
|
+
|
|
12
|
+
# ---------- atoms ----------
|
|
13
|
+
if isinstance(expr, Expr.Error):
|
|
14
|
+
return expr
|
|
15
|
+
|
|
16
|
+
if isinstance(expr, Expr.Symbol):
|
|
17
|
+
bound = self.env_get(env_id, expr.value.encode())
|
|
18
|
+
if bound is None:
|
|
19
|
+
return Expr.Error(f"unbound symbol '{expr.value}'", origin=expr)
|
|
20
|
+
return bound
|
|
21
|
+
|
|
22
|
+
if not isinstance(expr, Expr.ListExpr):
|
|
23
|
+
return expr # Expr.Byte or other literals passthrough
|
|
24
|
+
|
|
25
|
+
# ---------- empty / single ----------
|
|
26
|
+
if len(expr.elements) == 0:
|
|
27
|
+
return expr
|
|
28
|
+
if len(expr.elements) == 1:
|
|
29
|
+
return self.high_eval(env_id=env_id, expr=expr.elements[0], meter=meter)
|
|
30
|
+
|
|
31
|
+
tail = expr.elements[-1]
|
|
32
|
+
|
|
33
|
+
# ---------- (value name def) ----------
|
|
34
|
+
if isinstance(tail, Expr.Symbol) and tail.value == "def":
|
|
35
|
+
if len(expr.elements) < 3:
|
|
36
|
+
return Expr.Error("def expects (value name def)", origin=expr)
|
|
37
|
+
name_e = expr.elements[-2]
|
|
38
|
+
if not isinstance(name_e, Expr.Symbol):
|
|
39
|
+
return Expr.Error("def name must be symbol", origin=name_e)
|
|
40
|
+
value_e = expr.elements[-3]
|
|
41
|
+
value_res = self.high_eval(env_id=env_id, expr=value_e, meter=meter)
|
|
42
|
+
if isinstance(value_res, Expr.Error):
|
|
43
|
+
return value_res
|
|
44
|
+
self.env_set(env_id, name_e.value.encode(), value_res)
|
|
45
|
+
return value_res
|
|
46
|
+
|
|
47
|
+
# ---- LOW-LEVEL call: ( arg1 arg2 ... ( (body) sk ) ) ----
|
|
48
|
+
if isinstance(tail, Expr.ListExpr):
|
|
49
|
+
inner = tail.elements
|
|
50
|
+
if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
|
|
51
|
+
body_expr = inner[-2]
|
|
52
|
+
if not isinstance(body_expr, Expr.ListExpr):
|
|
53
|
+
return Expr.Error("sk body must be list", origin=body_expr)
|
|
54
|
+
|
|
55
|
+
# helper: turn an Expr into a contiguous bytes buffer
|
|
56
|
+
def to_bytes(v: Expr) -> Union[bytes, Expr.Error]:
|
|
57
|
+
if isinstance(v, Expr.Byte):
|
|
58
|
+
return bytes([v.value & 0xFF])
|
|
59
|
+
if isinstance(v, Expr.ListExpr):
|
|
60
|
+
# expect a list of Expr.Byte
|
|
61
|
+
out: bytearray = bytearray()
|
|
62
|
+
for el in v.elements:
|
|
63
|
+
if isinstance(el, Expr.Byte):
|
|
64
|
+
out.append(el.value & 0xFF)
|
|
65
|
+
else:
|
|
66
|
+
return Expr.Error("byte list must contain only Byte", origin=el)
|
|
67
|
+
return bytes(out)
|
|
68
|
+
if isinstance(v, Expr.Error):
|
|
69
|
+
return v
|
|
70
|
+
return Expr.Error("argument must resolve to Byte or (Byte ...)", origin=v)
|
|
71
|
+
|
|
72
|
+
# resolve ALL preceding args into bytes (can be Byte or List[Byte])
|
|
73
|
+
args_exprs = expr.elements[:-1]
|
|
74
|
+
arg_bytes: List[bytes] = []
|
|
75
|
+
for a in args_exprs:
|
|
76
|
+
v = self.high_eval(env_id=env_id, expr=a, meter=meter)
|
|
77
|
+
if isinstance(v, Expr.Error):
|
|
78
|
+
return v
|
|
79
|
+
vb = to_bytes(v)
|
|
80
|
+
if isinstance(vb, Expr.Error):
|
|
81
|
+
return vb
|
|
82
|
+
arg_bytes.append(vb)
|
|
83
|
+
|
|
84
|
+
# build low-level code with $0-based placeholders ($0 = first arg)
|
|
85
|
+
code: List[bytes] = []
|
|
86
|
+
|
|
87
|
+
def emit(tok: Expr) -> Union[None, Expr.Error]:
|
|
88
|
+
if isinstance(tok, Expr.Symbol):
|
|
89
|
+
name = tok.value
|
|
90
|
+
if name.startswith("$"):
|
|
91
|
+
idx_s = name[1:]
|
|
92
|
+
if not idx_s.isdigit():
|
|
93
|
+
return Expr.Error("invalid sk placeholder", origin=tok)
|
|
94
|
+
idx = int(idx_s) # $0 is first
|
|
95
|
+
if idx < 0 or idx >= len(arg_bytes):
|
|
96
|
+
return Expr.Error("arity mismatch in sk placeholder", origin=tok)
|
|
97
|
+
code.append(arg_bytes[idx])
|
|
98
|
+
return None
|
|
99
|
+
code.append(name.encode())
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
if isinstance(tok, Expr.Byte):
|
|
103
|
+
code.append(bytes([tok.value & 0xFF]))
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
if isinstance(tok, Expr.ListExpr):
|
|
107
|
+
rv = self.high_eval(env_id, tok, meter=meter)
|
|
108
|
+
if isinstance(rv, Expr.Error):
|
|
109
|
+
return rv
|
|
110
|
+
rb = to_bytes(rv)
|
|
111
|
+
if isinstance(rb, Expr.Error):
|
|
112
|
+
return rb
|
|
113
|
+
code.append(rb)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
if isinstance(tok, Expr.Error):
|
|
117
|
+
return tok
|
|
118
|
+
|
|
119
|
+
return Expr.Error("invalid token in sk body", origin=tok)
|
|
120
|
+
|
|
121
|
+
for t in body_expr.elements:
|
|
122
|
+
err = emit(t)
|
|
123
|
+
if isinstance(err, Expr.Error):
|
|
124
|
+
return err
|
|
125
|
+
|
|
126
|
+
# Execute low-level code built from sk-body using the caller's meter
|
|
127
|
+
res = self.low_eval(code, meter=meter)
|
|
128
|
+
if isinstance(res, Expr.Error):
|
|
129
|
+
return res
|
|
130
|
+
return Expr.ListExpr([Expr.Byte(b) for b in res])
|
|
131
|
+
|
|
132
|
+
# ---------- (... (body params fn)) HIGH-LEVEL CALL ----------
|
|
133
|
+
if isinstance(tail, Expr.ListExpr):
|
|
134
|
+
fn_form = tail
|
|
135
|
+
if (len(fn_form.elements) >= 3
|
|
136
|
+
and isinstance(fn_form.elements[-1], Expr.Symbol)
|
|
137
|
+
and fn_form.elements[-1].value == "fn"):
|
|
138
|
+
|
|
139
|
+
body_expr = fn_form.elements[-3]
|
|
140
|
+
params_expr = fn_form.elements[-2]
|
|
141
|
+
|
|
142
|
+
if not isinstance(body_expr, Expr.ListExpr):
|
|
143
|
+
return Expr.Error("fn body must be list", origin=body_expr)
|
|
144
|
+
if not isinstance(params_expr, Expr.ListExpr):
|
|
145
|
+
return Expr.Error("fn params must be list", origin=params_expr)
|
|
146
|
+
|
|
147
|
+
params: List[bytes] = []
|
|
148
|
+
for p in params_expr.elements:
|
|
149
|
+
if not isinstance(p, Expr.Symbol):
|
|
150
|
+
return Expr.Error("fn param must be symbol", origin=p)
|
|
151
|
+
params.append(p.value.encode())
|
|
152
|
+
|
|
153
|
+
args_exprs = expr.elements[:-1]
|
|
154
|
+
if len(args_exprs) != len(params):
|
|
155
|
+
return Expr.Error("arity mismatch", origin=expr)
|
|
156
|
+
|
|
157
|
+
arg_bytes: List[bytes] = []
|
|
158
|
+
for a in args_exprs:
|
|
159
|
+
v = self.high_eval(env_id, a, meter=meter)
|
|
160
|
+
if isinstance(v, Expr.Error):
|
|
161
|
+
return v
|
|
162
|
+
if not isinstance(v, Expr.Byte):
|
|
163
|
+
return Expr.Error("argument must resolve to Byte", origin=a)
|
|
164
|
+
arg_bytes.append(bytes([v.value & 0xFF]))
|
|
165
|
+
|
|
166
|
+
# child env, bind params -> Expr.Byte
|
|
167
|
+
child_env = uuid.uuid4()
|
|
168
|
+
self.environments[child_env] = Env(parent_id=env_id)
|
|
169
|
+
for name_b, val_b in zip(params, arg_bytes):
|
|
170
|
+
self.env_set(child_env, name_b, Expr.Byte(val_b[0]))
|
|
171
|
+
|
|
172
|
+
# evaluate HL body, metered from the top
|
|
173
|
+
return self.high_eval(child_env, body_expr, meter=meter)
|
|
174
|
+
|
|
175
|
+
# ---------- default: resolve each element and return list ----------
|
|
176
|
+
resolved: List[Expr] = [self.high_eval(env_id, e, meter=meter) for e in expr.elements]
|
|
177
|
+
return Expr.ListExpr(resolved)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Dict, List, Union
|
|
2
|
+
from src.astreum._lispeum import Expr, Meter
|
|
3
|
+
|
|
4
|
+
def tc_to_int(b: bytes) -> int:
|
|
5
|
+
"""bytes -> int using two's complement (width = len(b)*8)."""
|
|
6
|
+
if not b:
|
|
7
|
+
return 0
|
|
8
|
+
return int.from_bytes(b, "big", signed=True)
|
|
9
|
+
|
|
10
|
+
def int_to_tc(n: int, width_bytes: int) -> bytes:
|
|
11
|
+
"""int -> bytes (two's complement, fixed width)."""
|
|
12
|
+
if width_bytes <= 0:
|
|
13
|
+
return b"\x00"
|
|
14
|
+
return n.to_bytes(width_bytes, "big", signed=True)
|
|
15
|
+
|
|
16
|
+
def min_tc_width(n: int) -> int:
|
|
17
|
+
"""minimum bytes to store n in two's complement."""
|
|
18
|
+
if n == 0:
|
|
19
|
+
return 1
|
|
20
|
+
w = 1
|
|
21
|
+
while True:
|
|
22
|
+
try:
|
|
23
|
+
n.to_bytes(w, "big", signed=True)
|
|
24
|
+
return w
|
|
25
|
+
except OverflowError:
|
|
26
|
+
w += 1
|
|
27
|
+
|
|
28
|
+
def nand_bytes(a: bytes, b: bytes) -> bytes:
|
|
29
|
+
"""Bitwise NAND on two byte strings, zero-extending to max width."""
|
|
30
|
+
w = max(len(a), len(b), 1)
|
|
31
|
+
au = int.from_bytes(a.rjust(w, b"\x00"), "big", signed=False)
|
|
32
|
+
bu = int.from_bytes(b.rjust(w, b"\x00"), "big", signed=False)
|
|
33
|
+
mask = (1 << (w * 8)) - 1
|
|
34
|
+
resu = (~(au & bu)) & mask
|
|
35
|
+
return resu.to_bytes(w, "big", signed=False)
|
|
36
|
+
|
|
37
|
+
def low_eval(self, code: List[bytes], meter: Meter) -> Union[bytes, Expr.Error]:
|
|
38
|
+
|
|
39
|
+
heap: Dict[bytes, bytes] = {}
|
|
40
|
+
|
|
41
|
+
stack: List[bytes] = []
|
|
42
|
+
pc = 0
|
|
43
|
+
|
|
44
|
+
while True:
|
|
45
|
+
if pc >= len(code):
|
|
46
|
+
if len(stack) != 1:
|
|
47
|
+
return Expr.Error("bad stack")
|
|
48
|
+
return stack.pop()
|
|
49
|
+
|
|
50
|
+
tok = code[pc]
|
|
51
|
+
pc += 1
|
|
52
|
+
|
|
53
|
+
# ---------- ADD ----------
|
|
54
|
+
if tok == b"add":
|
|
55
|
+
if len(stack) < 2:
|
|
56
|
+
return Expr.Error("underflow")
|
|
57
|
+
b_b = stack.pop()
|
|
58
|
+
a_b = stack.pop()
|
|
59
|
+
a_i = tc_to_int(a_b)
|
|
60
|
+
b_i = tc_to_int(b_b)
|
|
61
|
+
res_i = a_i + b_i
|
|
62
|
+
width = max(len(a_b), len(b_b), min_tc_width(res_i))
|
|
63
|
+
res_b = int_to_tc(res_i, width)
|
|
64
|
+
# charge for both operands' byte widths
|
|
65
|
+
if not meter.charge_bytes(len(a_b) + len(b_b)):
|
|
66
|
+
return Expr.Error("meter limit")
|
|
67
|
+
stack.append(res_b)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# ---------- NAND ----------
|
|
71
|
+
if tok == b"nand":
|
|
72
|
+
if len(stack) < 2:
|
|
73
|
+
return Expr.Error("underflow")
|
|
74
|
+
b_b = stack.pop()
|
|
75
|
+
a_b = stack.pop()
|
|
76
|
+
res_b = nand_bytes(a_b, b_b)
|
|
77
|
+
# bitwise cost: 2 * max(len(a), len(b))
|
|
78
|
+
if not meter.charge_bytes(2 * max(len(a_b), len(b_b), 1)):
|
|
79
|
+
return Expr.Error("meter limit")
|
|
80
|
+
stack.append(res_b)
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# ---------- JUMP ----------
|
|
84
|
+
if tok == b"jump":
|
|
85
|
+
if len(stack) < 1:
|
|
86
|
+
return Expr.Error("underflow")
|
|
87
|
+
tgt_b = stack.pop()
|
|
88
|
+
if not meter.charge_bytes(1):
|
|
89
|
+
return Expr.Error("meter limit")
|
|
90
|
+
tgt_i = tc_to_int(tgt_b)
|
|
91
|
+
if tgt_i < 0 or tgt_i >= len(code):
|
|
92
|
+
return Expr.Error("bad jump")
|
|
93
|
+
pc = tgt_i
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# ---------- HEAP GET ----------
|
|
97
|
+
if tok == b"heap_get":
|
|
98
|
+
if len(stack) < 1:
|
|
99
|
+
return Expr.Error("underflow")
|
|
100
|
+
key = stack.pop()
|
|
101
|
+
val = heap.get(key) or b""
|
|
102
|
+
# get cost: 1
|
|
103
|
+
if not meter.charge_bytes(1):
|
|
104
|
+
return Expr.Error("meter limit")
|
|
105
|
+
stack.append(val)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# ---------- HEAP SET ----------
|
|
109
|
+
if tok == b"heap_set":
|
|
110
|
+
if len(stack) < 2:
|
|
111
|
+
return Expr.Error("underflow")
|
|
112
|
+
val = stack.pop()
|
|
113
|
+
key = stack.pop()
|
|
114
|
+
if not meter.charge_bytes(len(val)):
|
|
115
|
+
return Expr.Error("meter limit")
|
|
116
|
+
heap[key] = val
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# if no opcode matched above, treat token as literal
|
|
120
|
+
# not an opcode → literal blob
|
|
121
|
+
stack.append(tok)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Meter:
|
|
5
|
+
def __init__(self, enabled: bool = True, limit: Optional[int] = None):
|
|
6
|
+
self.enabled = enabled
|
|
7
|
+
self.limit: Optional[int] = limit
|
|
8
|
+
self.used: int = 0
|
|
9
|
+
|
|
10
|
+
def charge_bytes(self, n: int) -> bool:
|
|
11
|
+
if not self.enabled:
|
|
12
|
+
return True
|
|
13
|
+
if n < 0:
|
|
14
|
+
n = 0
|
|
15
|
+
if self.limit is not None and (self.used + n) >= self.limit:
|
|
16
|
+
return False
|
|
17
|
+
self.used += n
|
|
18
|
+
return True
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
from src.astreum._lispeum import Expr
|
|
3
|
+
|
|
4
|
+
class ParseError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
def _parse_one(tokens: List[str], pos: int = 0) -> Tuple[Expr, int]:
|
|
8
|
+
if pos >= len(tokens):
|
|
9
|
+
raise ParseError("unexpected end")
|
|
10
|
+
tok = tokens[pos]
|
|
11
|
+
|
|
12
|
+
if tok == '(': # list
|
|
13
|
+
items: List[Expr] = []
|
|
14
|
+
i = pos + 1
|
|
15
|
+
while i < len(tokens):
|
|
16
|
+
if tokens[i] == ')':
|
|
17
|
+
# special-case error form at close: (origin topic err) or (topic err)
|
|
18
|
+
if len(items) >= 3 and isinstance(items[-1], Expr.Symbol) and items[-1].value == 'err' and isinstance(items[-2], Expr.Symbol):
|
|
19
|
+
return Expr.Error(items[-2].value, origin=items[-3]), i + 1
|
|
20
|
+
if len(items) == 2 and isinstance(items[-1], Expr.Symbol) and items[-1].value == 'err' and isinstance(items[-2], Expr.Symbol):
|
|
21
|
+
return Expr.Error(items[-2].value), i + 1
|
|
22
|
+
return Expr.ListExpr(items), i + 1
|
|
23
|
+
expr, i = _parse_one(tokens, i)
|
|
24
|
+
items.append(expr)
|
|
25
|
+
raise ParseError("expected ')'")
|
|
26
|
+
|
|
27
|
+
if tok == ')':
|
|
28
|
+
raise ParseError("unexpected ')'")
|
|
29
|
+
|
|
30
|
+
# try integer → Byte
|
|
31
|
+
try:
|
|
32
|
+
n = int(tok)
|
|
33
|
+
return Expr.Byte(n), pos + 1
|
|
34
|
+
except ValueError:
|
|
35
|
+
return Expr.Symbol(tok), pos + 1
|
|
36
|
+
|
|
37
|
+
def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
|
|
38
|
+
"""Parse tokens into an Expr and return (expr, remaining_tokens)."""
|
|
39
|
+
expr, next_pos = _parse_one(tokens, 0)
|
|
40
|
+
return expr, tokens[next_pos:]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def tokenize(source: str) -> List[str]:
|
|
5
|
+
tokens: List[str] = []
|
|
6
|
+
cur: List[str] = []
|
|
7
|
+
for ch in source:
|
|
8
|
+
if ch.isspace():
|
|
9
|
+
if cur:
|
|
10
|
+
tokens.append("".join(cur))
|
|
11
|
+
cur = []
|
|
12
|
+
continue
|
|
13
|
+
if ch in ("(", ")"):
|
|
14
|
+
if cur:
|
|
15
|
+
tokens.append("".join(cur))
|
|
16
|
+
cur = []
|
|
17
|
+
tokens.append(ch)
|
|
18
|
+
continue
|
|
19
|
+
cur.append(ch)
|
|
20
|
+
if cur:
|
|
21
|
+
tokens.append("".join(cur))
|
|
22
|
+
return tokens
|
astreum/_node.py
CHANGED
|
@@ -3,116 +3,124 @@ from typing import Dict, List, Optional, Tuple, Union
|
|
|
3
3
|
import uuid
|
|
4
4
|
import threading
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
# 1. Helpers (no decoding, two's complement)
|
|
8
|
-
# ===============================
|
|
9
|
-
|
|
10
|
-
def tc_to_int(b: bytes) -> int:
|
|
11
|
-
"""bytes -> int using two's complement (width = len(b)*8)."""
|
|
12
|
-
if not b:
|
|
13
|
-
return 0
|
|
14
|
-
return int.from_bytes(b, "big", signed=True)
|
|
15
|
-
|
|
16
|
-
def int_to_tc(n: int, width_bytes: int) -> bytes:
|
|
17
|
-
"""int -> bytes (two's complement, fixed width)."""
|
|
18
|
-
if width_bytes <= 0:
|
|
19
|
-
return b"\x00"
|
|
20
|
-
return n.to_bytes(width_bytes, "big", signed=True)
|
|
21
|
-
|
|
22
|
-
def min_tc_width(n: int) -> int:
|
|
23
|
-
"""minimum bytes to store n in two's complement."""
|
|
24
|
-
if n == 0:
|
|
25
|
-
return 1
|
|
26
|
-
w = 1
|
|
27
|
-
while True:
|
|
28
|
-
try:
|
|
29
|
-
n.to_bytes(w, "big", signed=True)
|
|
30
|
-
return w
|
|
31
|
-
except OverflowError:
|
|
32
|
-
w += 1
|
|
33
|
-
|
|
34
|
-
def nand_bytes(a: bytes, b: bytes) -> bytes:
|
|
35
|
-
"""Bitwise NAND on two byte strings, zero-extending to max width."""
|
|
36
|
-
w = max(len(a), len(b), 1)
|
|
37
|
-
au = int.from_bytes(a.rjust(w, b"\x00"), "big", signed=False)
|
|
38
|
-
bu = int.from_bytes(b.rjust(w, b"\x00"), "big", signed=False)
|
|
39
|
-
mask = (1 << (w * 8)) - 1
|
|
40
|
-
resu = (~(au & bu)) & mask
|
|
41
|
-
return resu.to_bytes(w, "big", signed=False)
|
|
6
|
+
from src.astreum._lispeum import Env, Expr, Meter, low_eval
|
|
42
7
|
|
|
43
8
|
def bytes_touched(*vals: bytes) -> int:
|
|
44
9
|
"""For metering: how many bytes were manipulated (max of operands)."""
|
|
45
10
|
return max((len(v) for v in vals), default=1)
|
|
46
11
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
self
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
):
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
12
|
+
from blake3 import blake3
|
|
13
|
+
|
|
14
|
+
ZERO32 = b"\x00"*32
|
|
15
|
+
|
|
16
|
+
def u64_le(n: int) -> bytes:
|
|
17
|
+
return int(n).to_bytes(8, "little", signed=False)
|
|
18
|
+
|
|
19
|
+
def hash_bytes(b: bytes) -> bytes:
|
|
20
|
+
return blake3(b).digest()
|
|
21
|
+
|
|
22
|
+
class Atom:
|
|
23
|
+
data: bytes
|
|
24
|
+
next: bytes
|
|
25
|
+
size: int
|
|
26
|
+
|
|
27
|
+
def __init__(self, data: bytes, next: bytes = ZERO32, size: Optional[int] = None):
|
|
28
|
+
self.data = data
|
|
29
|
+
self.next = next
|
|
30
|
+
self.size = len(data) if size is None else size
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def from_data(data: bytes, next_hash: bytes = ZERO32) -> "Atom":
|
|
34
|
+
return Atom(data=data, next=next_hash, size=len(data))
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def object_id_from_parts(data_hash: bytes, next_hash: bytes, size: int) -> bytes:
|
|
38
|
+
return blake3(data_hash + next_hash + u64_le(size)).digest()
|
|
39
|
+
|
|
40
|
+
def data_hash(self) -> bytes:
|
|
41
|
+
return hash_bytes(self.data)
|
|
42
|
+
|
|
43
|
+
def object_id(self) -> bytes:
|
|
44
|
+
return self.object_id_from_parts(self.data_hash(), self.next, self.size)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def verify_metadata(object_id: bytes, size: int, next_hash: bytes, data_hash: bytes) -> bool:
|
|
48
|
+
return object_id == blake3(data_hash + next_hash + u64_le(size)).digest()
|
|
49
|
+
|
|
50
|
+
def to_bytes(self) -> bytes:
|
|
51
|
+
if len(self.next) != len(ZERO32):
|
|
52
|
+
raise ValueError("next must be 32 bytes")
|
|
53
|
+
return self.next + self.data
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def from_bytes(buf: bytes) -> "Atom":
|
|
57
|
+
if len(buf) < len(ZERO32):
|
|
58
|
+
raise ValueError("buffer too short for Atom header")
|
|
59
|
+
next_hash = buf[:len(ZERO32)]
|
|
60
|
+
data = buf[len(ZERO32):]
|
|
61
|
+
return Atom(data=data, next=next_hash, size=len(data))
|
|
62
|
+
|
|
63
|
+
def u32_le(n: int) -> bytes:
|
|
64
|
+
return int(n).to_bytes(4, "little", signed=False)
|
|
65
|
+
|
|
66
|
+
def expr_to_atoms(e: Expr) -> Tuple[bytes, List[Atom]]:
|
|
67
|
+
def sym(v: str) -> Tuple[bytes, List[Atom]]:
|
|
68
|
+
val = v.encode("utf-8")
|
|
69
|
+
val_atom = Atom.from_data(u32_le(len(val)) + val)
|
|
70
|
+
typ_atom = Atom.from_data(b"symbol", val_atom.object_id())
|
|
71
|
+
return typ_atom.object_id(), [val_atom, typ_atom]
|
|
72
|
+
|
|
73
|
+
def byt(v: int) -> Tuple[bytes, List[Atom]]:
|
|
74
|
+
val_atom = Atom.from_data(bytes([v & 0xFF]))
|
|
75
|
+
typ_atom = Atom.from_data(b"byte", val_atom.object_id())
|
|
76
|
+
return typ_atom.object_id(), [val_atom, typ_atom]
|
|
77
|
+
|
|
78
|
+
def err(topic: str, origin: Optional[Expr]) -> Tuple[bytes, List[Atom]]:
|
|
79
|
+
t = topic.encode("utf-8")
|
|
80
|
+
origin_hash, acc = (ZERO32, [])
|
|
81
|
+
if origin is not None:
|
|
82
|
+
origin_hash, acc = expr_to_atoms(origin)
|
|
83
|
+
val_atom = Atom.from_data(u32_le(len(t)) + t + origin_hash)
|
|
84
|
+
typ_atom = Atom.from_data(b"error", val_atom.object_id())
|
|
85
|
+
return typ_atom.object_id(), acc + [val_atom, typ_atom]
|
|
86
|
+
|
|
87
|
+
def lst(items: List[Expr]) -> Tuple[bytes, List[Atom]]:
|
|
88
|
+
acc: List[Atom] = []
|
|
89
|
+
child_hashes: List[bytes] = []
|
|
90
|
+
for it in items:
|
|
91
|
+
h, atoms = expr_to_atoms(it)
|
|
92
|
+
acc.extend(atoms)
|
|
93
|
+
child_hashes.append(h)
|
|
94
|
+
next_hash = ZERO32
|
|
95
|
+
elem_atoms: List[Atom] = []
|
|
96
|
+
for h in reversed(child_hashes):
|
|
97
|
+
a = Atom.from_data(h, next_hash)
|
|
98
|
+
next_hash = a.object_id()
|
|
99
|
+
elem_atoms.append(a)
|
|
100
|
+
elem_atoms.reverse()
|
|
101
|
+
head = next_hash
|
|
102
|
+
val_atom = Atom.from_data(u64_le(len(items)), head)
|
|
103
|
+
typ_atom = Atom.from_data(b"list", val_atom.object_id())
|
|
104
|
+
return typ_atom.object_id(), acc + elem_atoms + [val_atom, typ_atom]
|
|
105
|
+
|
|
106
|
+
if isinstance(e, Expr.Symbol):
|
|
107
|
+
return sym(e.value)
|
|
108
|
+
if isinstance(e, Expr.Byte):
|
|
109
|
+
return byt(e.value)
|
|
110
|
+
if isinstance(e, Expr.Error):
|
|
111
|
+
return err(e.topic, e.origin)
|
|
112
|
+
if isinstance(e, Expr.ListExpr):
|
|
113
|
+
return lst(e.elements)
|
|
114
|
+
raise TypeError("unknown Expr variant")
|
|
110
115
|
|
|
111
116
|
class Node:
|
|
112
117
|
def __init__(self):
|
|
113
|
-
|
|
118
|
+
# Storage Setup
|
|
114
119
|
self.in_memory_storage: Dict[bytes, bytes] = {}
|
|
120
|
+
# Lispeum Setup
|
|
121
|
+
self.environments: Dict[uuid.UUID, Env] = {}
|
|
115
122
|
self.machine_environments_lock = threading.RLock()
|
|
123
|
+
self.low_eval = low_eval
|
|
116
124
|
|
|
117
125
|
# ---- Env helpers ----
|
|
118
126
|
def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
|
|
@@ -137,356 +145,3 @@ class Node:
|
|
|
137
145
|
|
|
138
146
|
def _local_set(self, key: bytes, value: bytes) -> None:
|
|
139
147
|
self.in_memory_storage[key] = value
|
|
140
|
-
|
|
141
|
-
# ---- Eval ----
|
|
142
|
-
def low_eval(self, code: List[bytes], meter: Meter) -> Union[bytes, Expr.Error]:
|
|
143
|
-
|
|
144
|
-
heap: Dict[bytes, bytes] = {}
|
|
145
|
-
|
|
146
|
-
stack: List[bytes] = []
|
|
147
|
-
pc = 0
|
|
148
|
-
|
|
149
|
-
while True:
|
|
150
|
-
if pc >= len(code):
|
|
151
|
-
if len(stack) != 1:
|
|
152
|
-
return Expr.Error("bad stack")
|
|
153
|
-
return stack.pop()
|
|
154
|
-
|
|
155
|
-
tok = code[pc]
|
|
156
|
-
pc += 1
|
|
157
|
-
|
|
158
|
-
# ---------- ADD ----------
|
|
159
|
-
if tok == b"add":
|
|
160
|
-
if len(stack) < 2:
|
|
161
|
-
return Expr.Error("underflow")
|
|
162
|
-
b_b = stack.pop()
|
|
163
|
-
a_b = stack.pop()
|
|
164
|
-
a_i = tc_to_int(a_b)
|
|
165
|
-
b_i = tc_to_int(b_b)
|
|
166
|
-
res_i = a_i + b_i
|
|
167
|
-
width = max(len(a_b), len(b_b), min_tc_width(res_i))
|
|
168
|
-
res_b = int_to_tc(res_i, width)
|
|
169
|
-
# charge for both operands' byte widths
|
|
170
|
-
if not meter.charge_bytes(len(a_b) + len(b_b)):
|
|
171
|
-
return Expr.Error("meter limit")
|
|
172
|
-
stack.append(res_b)
|
|
173
|
-
continue
|
|
174
|
-
|
|
175
|
-
# ---------- NAND ----------
|
|
176
|
-
if tok == b"nand":
|
|
177
|
-
if len(stack) < 2:
|
|
178
|
-
return Expr.Error("underflow")
|
|
179
|
-
b_b = stack.pop()
|
|
180
|
-
a_b = stack.pop()
|
|
181
|
-
res_b = nand_bytes(a_b, b_b)
|
|
182
|
-
# bitwise cost: 2 * max(len(a), len(b))
|
|
183
|
-
if not meter.charge_bytes(2 * max(len(a_b), len(b_b), 1)):
|
|
184
|
-
return Expr.Error("meter limit")
|
|
185
|
-
stack.append(res_b)
|
|
186
|
-
continue
|
|
187
|
-
|
|
188
|
-
# ---------- JUMP ----------
|
|
189
|
-
if tok == b"jump":
|
|
190
|
-
if len(stack) < 1:
|
|
191
|
-
return Expr.Error("underflow")
|
|
192
|
-
tgt_b = stack.pop()
|
|
193
|
-
if not meter.charge_bytes(1):
|
|
194
|
-
return Expr.Error("meter limit")
|
|
195
|
-
tgt_i = tc_to_int(tgt_b)
|
|
196
|
-
if tgt_i < 0 or tgt_i >= len(code):
|
|
197
|
-
return Expr.Error("bad jump")
|
|
198
|
-
pc = tgt_i
|
|
199
|
-
continue
|
|
200
|
-
|
|
201
|
-
# ---------- HEAP GET ----------
|
|
202
|
-
if tok == b"heap_get":
|
|
203
|
-
if len(stack) < 1:
|
|
204
|
-
return Expr.Error("underflow")
|
|
205
|
-
key = stack.pop()
|
|
206
|
-
val = heap.get(key) or b""
|
|
207
|
-
# get cost: 1
|
|
208
|
-
if not meter.charge_bytes(1):
|
|
209
|
-
return Expr.Error("meter limit")
|
|
210
|
-
stack.append(val)
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
# ---------- HEAP SET ----------
|
|
214
|
-
if tok == b"heap_set":
|
|
215
|
-
if len(stack) < 2:
|
|
216
|
-
return Expr.Error("underflow")
|
|
217
|
-
val = stack.pop()
|
|
218
|
-
key = stack.pop()
|
|
219
|
-
if not meter.charge_bytes(len(val)):
|
|
220
|
-
return Expr.Error("meter limit")
|
|
221
|
-
heap[key] = val
|
|
222
|
-
continue
|
|
223
|
-
|
|
224
|
-
# ---------- STORAGE GET ----------
|
|
225
|
-
if tok == b"storage_get":
|
|
226
|
-
if len(stack) < 1:
|
|
227
|
-
return Expr.Error("underflow")
|
|
228
|
-
key = stack.pop()
|
|
229
|
-
val = self._local_get(key) or b""
|
|
230
|
-
if not meter.charge_bytes(1):
|
|
231
|
-
return Expr.Error("meter limit")
|
|
232
|
-
stack.append(val)
|
|
233
|
-
continue
|
|
234
|
-
|
|
235
|
-
# ---------- STORAGE SET ----------
|
|
236
|
-
if tok == b"storage_set":
|
|
237
|
-
if len(stack) < 2:
|
|
238
|
-
return Expr.Error("underflow")
|
|
239
|
-
val = stack.pop()
|
|
240
|
-
key = stack.pop()
|
|
241
|
-
if not meter.charge_bytes(len(val)):
|
|
242
|
-
return Expr.Error("meter limit")
|
|
243
|
-
self._local_set(key, val)
|
|
244
|
-
continue
|
|
245
|
-
|
|
246
|
-
# if no opcode matched above, treat token as literal
|
|
247
|
-
|
|
248
|
-
# not an opcode → literal blob
|
|
249
|
-
stack.append(tok)
|
|
250
|
-
|
|
251
|
-
def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
|
|
252
|
-
|
|
253
|
-
if meter is None:
|
|
254
|
-
meter = Meter()
|
|
255
|
-
|
|
256
|
-
# ---------- atoms ----------
|
|
257
|
-
if isinstance(expr, Expr.Error):
|
|
258
|
-
return expr
|
|
259
|
-
|
|
260
|
-
if isinstance(expr, Expr.Symbol):
|
|
261
|
-
bound = self.env_get(env_id, expr.value.encode())
|
|
262
|
-
if bound is None:
|
|
263
|
-
return Expr.Error(f"unbound symbol '{expr.value}'", origin=expr)
|
|
264
|
-
return bound
|
|
265
|
-
|
|
266
|
-
if not isinstance(expr, Expr.ListExpr):
|
|
267
|
-
return expr # Expr.Byte or other literals passthrough
|
|
268
|
-
|
|
269
|
-
# ---------- empty / single ----------
|
|
270
|
-
if len(expr.elements) == 0:
|
|
271
|
-
return expr
|
|
272
|
-
if len(expr.elements) == 1:
|
|
273
|
-
return self.high_eval(env_id=env_id, expr=expr.elements[0], meter=meter)
|
|
274
|
-
|
|
275
|
-
tail = expr.elements[-1]
|
|
276
|
-
|
|
277
|
-
# ---------- (value name def) ----------
|
|
278
|
-
if isinstance(tail, Expr.Symbol) and tail.value == "def":
|
|
279
|
-
if len(expr.elements) < 3:
|
|
280
|
-
return Expr.Error("def expects (value name def)", origin=expr)
|
|
281
|
-
name_e = expr.elements[-2]
|
|
282
|
-
if not isinstance(name_e, Expr.Symbol):
|
|
283
|
-
return Expr.Error("def name must be symbol", origin=name_e)
|
|
284
|
-
value_e = expr.elements[-3]
|
|
285
|
-
value_res = self.high_eval(env_id=env_id, expr=value_e, meter=meter)
|
|
286
|
-
if isinstance(value_res, Expr.Error):
|
|
287
|
-
return value_res
|
|
288
|
-
self.env_set(env_id, name_e.value.encode(), value_res)
|
|
289
|
-
return value_res
|
|
290
|
-
|
|
291
|
-
# ---- LOW-LEVEL call: ( arg1 arg2 ... ( (body) sk ) ) ----
|
|
292
|
-
if isinstance(tail, Expr.ListExpr):
|
|
293
|
-
inner = tail.elements
|
|
294
|
-
if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
|
|
295
|
-
body_expr = inner[-2]
|
|
296
|
-
if not isinstance(body_expr, Expr.ListExpr):
|
|
297
|
-
return Expr.Error("sk body must be list", origin=body_expr)
|
|
298
|
-
|
|
299
|
-
# helper: turn an Expr into a contiguous bytes buffer
|
|
300
|
-
def to_bytes(v: Expr) -> Union[bytes, Expr.Error]:
|
|
301
|
-
if isinstance(v, Expr.Byte):
|
|
302
|
-
return bytes([v.value & 0xFF])
|
|
303
|
-
if isinstance(v, Expr.ListExpr):
|
|
304
|
-
# expect a list of Expr.Byte
|
|
305
|
-
out: bytearray = bytearray()
|
|
306
|
-
for el in v.elements:
|
|
307
|
-
if isinstance(el, Expr.Byte):
|
|
308
|
-
out.append(el.value & 0xFF)
|
|
309
|
-
else:
|
|
310
|
-
return Expr.Error("byte list must contain only Byte", origin=el)
|
|
311
|
-
return bytes(out)
|
|
312
|
-
if isinstance(v, Expr.Error):
|
|
313
|
-
return v
|
|
314
|
-
return Expr.Error("argument must resolve to Byte or (Byte ...)", origin=v)
|
|
315
|
-
|
|
316
|
-
# resolve ALL preceding args into bytes (can be Byte or List[Byte])
|
|
317
|
-
args_exprs = expr.elements[:-1]
|
|
318
|
-
arg_bytes: List[bytes] = []
|
|
319
|
-
for a in args_exprs:
|
|
320
|
-
v = self.high_eval(env_id=env_id, expr=a, meter=meter)
|
|
321
|
-
if isinstance(v, Expr.Error):
|
|
322
|
-
return v
|
|
323
|
-
vb = to_bytes(v)
|
|
324
|
-
if isinstance(vb, Expr.Error):
|
|
325
|
-
return vb
|
|
326
|
-
arg_bytes.append(vb)
|
|
327
|
-
|
|
328
|
-
# build low-level code with $0-based placeholders ($0 = first arg)
|
|
329
|
-
code: List[bytes] = []
|
|
330
|
-
|
|
331
|
-
def emit(tok: Expr) -> Union[None, Expr.Error]:
|
|
332
|
-
if isinstance(tok, Expr.Symbol):
|
|
333
|
-
name = tok.value
|
|
334
|
-
if name.startswith("$"):
|
|
335
|
-
idx_s = name[1:]
|
|
336
|
-
if not idx_s.isdigit():
|
|
337
|
-
return Expr.Error("invalid sk placeholder", origin=tok)
|
|
338
|
-
idx = int(idx_s) # $0 is first
|
|
339
|
-
if idx < 0 or idx >= len(arg_bytes):
|
|
340
|
-
return Expr.Error("arity mismatch in sk placeholder", origin=tok)
|
|
341
|
-
code.append(arg_bytes[idx])
|
|
342
|
-
return None
|
|
343
|
-
code.append(name.encode())
|
|
344
|
-
return None
|
|
345
|
-
|
|
346
|
-
if isinstance(tok, Expr.Byte):
|
|
347
|
-
code.append(bytes([tok.value & 0xFF]))
|
|
348
|
-
return None
|
|
349
|
-
|
|
350
|
-
if isinstance(tok, Expr.ListExpr):
|
|
351
|
-
rv = self.high_eval(env_id, tok, meter=meter)
|
|
352
|
-
if isinstance(rv, Expr.Error):
|
|
353
|
-
return rv
|
|
354
|
-
rb = to_bytes(rv)
|
|
355
|
-
if isinstance(rb, Expr.Error):
|
|
356
|
-
return rb
|
|
357
|
-
code.append(rb)
|
|
358
|
-
return None
|
|
359
|
-
|
|
360
|
-
if isinstance(tok, Expr.Error):
|
|
361
|
-
return tok
|
|
362
|
-
|
|
363
|
-
return Expr.Error("invalid token in sk body", origin=tok)
|
|
364
|
-
|
|
365
|
-
for t in body_expr.elements:
|
|
366
|
-
err = emit(t)
|
|
367
|
-
if isinstance(err, Expr.Error):
|
|
368
|
-
return err
|
|
369
|
-
|
|
370
|
-
# Execute low-level code built from sk-body using the caller's meter
|
|
371
|
-
res = self.low_eval(code, meter=meter)
|
|
372
|
-
if isinstance(res, Expr.Error):
|
|
373
|
-
return res
|
|
374
|
-
return Expr.ListExpr([Expr.Byte(b) for b in res])
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# ---------- (... (body params fn)) HIGH-LEVEL CALL ----------
|
|
379
|
-
if isinstance(tail, Expr.ListExpr):
|
|
380
|
-
fn_form = tail
|
|
381
|
-
if (len(fn_form.elements) >= 3
|
|
382
|
-
and isinstance(fn_form.elements[-1], Expr.Symbol)
|
|
383
|
-
and fn_form.elements[-1].value == "fn"):
|
|
384
|
-
|
|
385
|
-
body_expr = fn_form.elements[-3]
|
|
386
|
-
params_expr = fn_form.elements[-2]
|
|
387
|
-
|
|
388
|
-
if not isinstance(body_expr, Expr.ListExpr):
|
|
389
|
-
return Expr.Error("fn body must be list", origin=body_expr)
|
|
390
|
-
if not isinstance(params_expr, Expr.ListExpr):
|
|
391
|
-
return Expr.Error("fn params must be list", origin=params_expr)
|
|
392
|
-
|
|
393
|
-
params: List[bytes] = []
|
|
394
|
-
for p in params_expr.elements:
|
|
395
|
-
if not isinstance(p, Expr.Symbol):
|
|
396
|
-
return Expr.Error("fn param must be symbol", origin=p)
|
|
397
|
-
params.append(p.value.encode())
|
|
398
|
-
|
|
399
|
-
args_exprs = expr.elements[:-1]
|
|
400
|
-
if len(args_exprs) != len(params):
|
|
401
|
-
return Expr.Error("arity mismatch", origin=expr)
|
|
402
|
-
|
|
403
|
-
arg_bytes: List[bytes] = []
|
|
404
|
-
for a in args_exprs:
|
|
405
|
-
v = self.high_eval(env_id, a, meter=meter)
|
|
406
|
-
if isinstance(v, Expr.Error):
|
|
407
|
-
return v
|
|
408
|
-
if not isinstance(v, Expr.Byte):
|
|
409
|
-
return Expr.Error("argument must resolve to Byte", origin=a)
|
|
410
|
-
arg_bytes.append(bytes([v.value & 0xFF]))
|
|
411
|
-
|
|
412
|
-
# child env, bind params -> Expr.Byte
|
|
413
|
-
child_env = uuid.uuid4()
|
|
414
|
-
self.environments[child_env] = Env(parent_id=env_id)
|
|
415
|
-
for name_b, val_b in zip(params, arg_bytes):
|
|
416
|
-
self.env_set(child_env, name_b, Expr.Byte(val_b[0]))
|
|
417
|
-
|
|
418
|
-
# evaluate HL body, metered from the top
|
|
419
|
-
return self.high_eval(child_env, body_expr, meter=meter)
|
|
420
|
-
|
|
421
|
-
# ---------- default: resolve each element and return list ----------
|
|
422
|
-
resolved: List[Expr] = [self.high_eval(env_id, e, meter=meter) for e in expr.elements]
|
|
423
|
-
return Expr.ListExpr(resolved)
|
|
424
|
-
|
|
425
|
-
# ===============================
|
|
426
|
-
# 3. Lightweight Parser for Expr (postfix, limited forms)
|
|
427
|
-
# ===============================
|
|
428
|
-
|
|
429
|
-
class ParseError(Exception):
|
|
430
|
-
pass
|
|
431
|
-
|
|
432
|
-
def tokenize(source: str) -> List[str]:
|
|
433
|
-
"""Tokenize a minimal Lispeum subset for this module.
|
|
434
|
-
|
|
435
|
-
Supports:
|
|
436
|
-
- Integers (e.g., -1, 0, 255) → Byte
|
|
437
|
-
- Symbols (e.g., add, nand, def, fn, sk, int.sub, $0)
|
|
438
|
-
- Lists using parentheses
|
|
439
|
-
"""
|
|
440
|
-
tokens: List[str] = []
|
|
441
|
-
cur: List[str] = []
|
|
442
|
-
for ch in source:
|
|
443
|
-
if ch.isspace():
|
|
444
|
-
if cur:
|
|
445
|
-
tokens.append("".join(cur))
|
|
446
|
-
cur = []
|
|
447
|
-
continue
|
|
448
|
-
if ch in ("(", ")"):
|
|
449
|
-
if cur:
|
|
450
|
-
tokens.append("".join(cur))
|
|
451
|
-
cur = []
|
|
452
|
-
tokens.append(ch)
|
|
453
|
-
continue
|
|
454
|
-
cur.append(ch)
|
|
455
|
-
if cur:
|
|
456
|
-
tokens.append("".join(cur))
|
|
457
|
-
return tokens
|
|
458
|
-
|
|
459
|
-
def _parse_one(tokens: List[str], pos: int = 0) -> Tuple[Expr, int]:
|
|
460
|
-
if pos >= len(tokens):
|
|
461
|
-
raise ParseError("unexpected end")
|
|
462
|
-
tok = tokens[pos]
|
|
463
|
-
|
|
464
|
-
if tok == '(': # list
|
|
465
|
-
items: List[Expr] = []
|
|
466
|
-
i = pos + 1
|
|
467
|
-
while i < len(tokens):
|
|
468
|
-
if tokens[i] == ')':
|
|
469
|
-
# special-case error form at close: (origin topic err) or (topic err)
|
|
470
|
-
if len(items) >= 3 and isinstance(items[-1], Expr.Symbol) and items[-1].value == 'err' and isinstance(items[-2], Expr.Symbol):
|
|
471
|
-
return Expr.Error(items[-2].value, origin=items[-3]), i + 1
|
|
472
|
-
if len(items) == 2 and isinstance(items[-1], Expr.Symbol) and items[-1].value == 'err' and isinstance(items[-2], Expr.Symbol):
|
|
473
|
-
return Expr.Error(items[-2].value), i + 1
|
|
474
|
-
return Expr.ListExpr(items), i + 1
|
|
475
|
-
expr, i = _parse_one(tokens, i)
|
|
476
|
-
items.append(expr)
|
|
477
|
-
raise ParseError("expected ')'")
|
|
478
|
-
|
|
479
|
-
if tok == ')':
|
|
480
|
-
raise ParseError("unexpected ')'")
|
|
481
|
-
|
|
482
|
-
# try integer → Byte
|
|
483
|
-
try:
|
|
484
|
-
n = int(tok)
|
|
485
|
-
return Expr.Byte(n), pos + 1
|
|
486
|
-
except ValueError:
|
|
487
|
-
return Expr.Symbol(tok), pos + 1
|
|
488
|
-
|
|
489
|
-
def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
|
|
490
|
-
"""Parse tokens into an Expr and return (expr, remaining_tokens)."""
|
|
491
|
-
expr, next_pos = _parse_one(tokens, 0)
|
|
492
|
-
return expr, tokens[next_pos:]
|
astreum/lispeum/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.36
|
|
4
4
|
Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
|
|
5
5
|
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
6
|
Project-URL: Homepage, https://github.com/astreum/lib
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
astreum/__init__.py,sha256=
|
|
2
|
-
astreum/_node.py,sha256=
|
|
1
|
+
astreum/__init__.py,sha256=oIrOJ0DZZByGtiIaupDBLhIOLL2nm0FOI7uWhDcaIsk,51
|
|
2
|
+
astreum/_node.py,sha256=YWz7RASGM9qCHJEQoldehzcnWBhS3jLbRfT6_VlWZcM,5391
|
|
3
3
|
astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
|
|
4
4
|
astreum/node.py,sha256=SuVm1b0QWl1FpDUaLRH1fiFYnXCrPs6qYeUQlPDae8w,38358
|
|
5
|
+
astreum/_lispeum/__init__.py,sha256=HzJp03YmP0b8IS2NtM893katuUooro_C701H08uf4i4,274
|
|
6
|
+
astreum/_lispeum/environment.py,sha256=5SAEUgEzRlDGqtrmr-g6Za-fuuhX55GXGnZKVKxfkCQ,230
|
|
7
|
+
astreum/_lispeum/expression.py,sha256=PS8RAahwduhnJy7-6jYaILAc0uFu1XLhbnHUJhvVwo8,1024
|
|
8
|
+
astreum/_lispeum/high_evaluation.py,sha256=s6DJSEWLc_-05Tpk8TOaSJm0TKrC3H_YEym5pnXfPWw,8041
|
|
9
|
+
astreum/_lispeum/low_evaluation.py,sha256=mbeA0KMeWQruAPctYJAP-L6QmU1btU0l5ScJWFF_nxA,4322
|
|
10
|
+
astreum/_lispeum/meter.py,sha256=5q2PFW7_jmgKVM1-vwE4RRjMfPEthUA4iu1CwR-Axws,505
|
|
11
|
+
astreum/_lispeum/parser.py,sha256=_J-KeIZlK-PP0Wc9mAl82hyQsPG2wsgshqAc3fCNVk8,1562
|
|
12
|
+
astreum/_lispeum/tokenizer.py,sha256=P68uIj4aPKzjuCJ85jfzRi67QztpuXIOC1vvLQueBI4,552
|
|
5
13
|
astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
14
|
astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
|
|
7
15
|
astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
|
|
8
16
|
astreum/crypto/wesolowski.py,sha256=SUgGXW3Id07dJtWzDcs4dluIhjqbRWQ8YWjn_mK78AQ,4092
|
|
9
17
|
astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
|
|
10
|
-
astreum/lispeum/__init__.py,sha256=
|
|
18
|
+
astreum/lispeum/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
19
|
astreum/lispeum/environment.py,sha256=wolwt9psDl62scgjaVG0G59xlBs1AM4NPgryUbxzG_4,1220
|
|
12
20
|
astreum/lispeum/expression.py,sha256=K8gFifDaHu394bs9qnpvP8tjeiymFGQpnDC_iW9nU4E,2379
|
|
13
21
|
astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
|
|
@@ -27,8 +35,8 @@ astreum/relay/setup.py,sha256=ynvGaJdlDtw_f5LLiow2Wo7IRzUjvgk8eSr1Sv4_zTg,2090
|
|
|
27
35
|
astreum/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
36
|
astreum/storage/object.py,sha256=knFlvw_tpcC4twSu1DGNpHX31wlANN8E5dgEqIfU--Q,2041
|
|
29
37
|
astreum/storage/setup.py,sha256=1-9ztEFI_BvRDvAA0lAn4mFya8iq65THTArlj--M3Hg,626
|
|
30
|
-
astreum-0.2.
|
|
31
|
-
astreum-0.2.
|
|
32
|
-
astreum-0.2.
|
|
33
|
-
astreum-0.2.
|
|
34
|
-
astreum-0.2.
|
|
38
|
+
astreum-0.2.36.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
39
|
+
astreum-0.2.36.dist-info/METADATA,sha256=P3P6HjaFqg6F1e9EVnr9OtMapOJMvtaVB0hqw486D6I,6149
|
|
40
|
+
astreum-0.2.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
+
astreum-0.2.36.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
42
|
+
astreum-0.2.36.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|