astreum 0.2.41__py3-none-any.whl → 0.3.1__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.
- astreum/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +83 -0
- astreum/communication/handlers/ping.py +48 -0
- astreum/communication/handlers/storage_request.py +81 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/{_communication → communication/models}/message.py +1 -0
- astreum/communication/models/peer.py +23 -0
- astreum/{_communication → communication/models}/route.py +45 -8
- astreum/{_communication → communication}/setup.py +46 -95
- astreum/communication/start.py +38 -0
- astreum/consensus/__init__.py +20 -0
- astreum/consensus/genesis.py +66 -0
- astreum/consensus/models/__init__.py +0 -0
- astreum/consensus/models/account.py +84 -0
- astreum/consensus/models/accounts.py +72 -0
- astreum/consensus/models/block.py +364 -0
- astreum/{_consensus → consensus/models}/chain.py +7 -7
- astreum/{_consensus → consensus/models}/fork.py +8 -8
- astreum/consensus/models/receipt.py +98 -0
- astreum/consensus/models/transaction.py +213 -0
- astreum/{_consensus → consensus}/setup.py +26 -11
- astreum/consensus/start.py +68 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +20 -1
- astreum/consensus/workers/validation.py +291 -0
- astreum/{_consensus → consensus}/workers/verify.py +32 -3
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/machine/evaluations/high_evaluation.py +237 -0
- astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum/machine/models/__init__.py +0 -0
- astreum/machine/models/environment.py +31 -0
- astreum/machine/models/expression.py +218 -0
- astreum/{_lispeum → machine}/parser.py +26 -31
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +73 -781
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +69 -0
- astreum/storage/actions/set.py +132 -0
- astreum/storage/models/atom.py +107 -0
- astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
- astreum/storage/setup.py +44 -15
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- astreum-0.3.1.dist-info/METADATA +160 -0
- astreum-0.3.1.dist-info/RECORD +62 -0
- astreum/_communication/peer.py +0 -11
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -170
- astreum/_consensus/accounts.py +0 -67
- astreum/_consensus/block.py +0 -328
- astreum/_consensus/genesis.py +0 -141
- astreum/_consensus/receipt.py +0 -177
- astreum/_consensus/transaction.py +0 -192
- astreum/_consensus/workers/validation.py +0 -122
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/expression.py +0 -37
- astreum/_lispeum/high_evaluation.py +0 -177
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -58
- astreum/_storage/__init__.py +0 -5
- astreum/_storage/atom.py +0 -117
- astreum/format.py +0 -75
- astreum/models/block.py +0 -441
- astreum/models/merkle.py +0 -205
- astreum/models/patricia.py +0 -393
- astreum/storage/object.py +0 -68
- astreum-0.2.41.dist-info/METADATA +0 -146
- astreum-0.2.41.dist-info/RECORD +0 -53
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_communication → communication}/util.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from typing import Dict, List, Union
|
|
2
|
+
|
|
3
|
+
from astreum.storage.models.atom import ZERO32, Atom, AtomKind
|
|
4
|
+
from ..models.expression import Expr, error_expr
|
|
5
|
+
from ..models.meter import Meter
|
|
6
|
+
|
|
7
|
+
def tc_to_int(b: bytes) -> int:
|
|
8
|
+
"""bytes -> int using two's complement (width = len(b)*8)."""
|
|
9
|
+
if not b:
|
|
10
|
+
return 0
|
|
11
|
+
return int.from_bytes(b, "big", signed=True)
|
|
12
|
+
|
|
13
|
+
def int_to_tc(n: int, width_bytes: int) -> bytes:
|
|
14
|
+
"""int -> bytes (two's complement, fixed width)."""
|
|
15
|
+
if width_bytes <= 0:
|
|
16
|
+
return b"\x00"
|
|
17
|
+
return n.to_bytes(width_bytes, "big", signed=True)
|
|
18
|
+
|
|
19
|
+
def min_tc_width(n: int) -> int:
|
|
20
|
+
"""minimum bytes to store n in two's complement."""
|
|
21
|
+
if n == 0:
|
|
22
|
+
return 1
|
|
23
|
+
w = 1
|
|
24
|
+
while True:
|
|
25
|
+
try:
|
|
26
|
+
n.to_bytes(w, "big", signed=True)
|
|
27
|
+
return w
|
|
28
|
+
except OverflowError:
|
|
29
|
+
w += 1
|
|
30
|
+
|
|
31
|
+
def nand_bytes(a: bytes, b: bytes) -> bytes:
|
|
32
|
+
"""Bitwise NAND (NOT-AND) on two byte strings, zero-extending to max width.
|
|
33
|
+
|
|
34
|
+
The NAND gate yields 0 only when all inputs are 1; otherwise it outputs 1.
|
|
35
|
+
Truth table:
|
|
36
|
+
A | B | NAND
|
|
37
|
+
0 | 0 | 1
|
|
38
|
+
0 | 1 | 1
|
|
39
|
+
1 | 0 | 1
|
|
40
|
+
1 | 1 | 0
|
|
41
|
+
"""
|
|
42
|
+
w = max(len(a), len(b), 1)
|
|
43
|
+
au = int.from_bytes(a.rjust(w, b"\x00"), "big", signed=False)
|
|
44
|
+
bu = int.from_bytes(b.rjust(w, b"\x00"), "big", signed=False)
|
|
45
|
+
mask = (1 << (w * 8)) - 1
|
|
46
|
+
resu = (~(au & bu)) & mask
|
|
47
|
+
return resu.to_bytes(w, "big", signed=False)
|
|
48
|
+
|
|
49
|
+
def low_eval(self, code: List[bytes], meter: Meter) -> Expr:
|
|
50
|
+
"""Execute low-level bytecode with stack/heap semantics under metering."""
|
|
51
|
+
heap: Dict[bytes, bytes] = {}
|
|
52
|
+
|
|
53
|
+
stack: List[bytes] = []
|
|
54
|
+
pc = 0
|
|
55
|
+
|
|
56
|
+
while True:
|
|
57
|
+
if pc >= len(code):
|
|
58
|
+
if len(stack) != 1:
|
|
59
|
+
return error_expr("low_eval", "bad stack")
|
|
60
|
+
# wrap successful result as an Expr.Bytes
|
|
61
|
+
return Expr.Bytes(stack.pop())
|
|
62
|
+
|
|
63
|
+
tok = code[pc]
|
|
64
|
+
pc += 1
|
|
65
|
+
|
|
66
|
+
# ---------- ADD ----------
|
|
67
|
+
if tok == b"add":
|
|
68
|
+
if len(stack) < 2:
|
|
69
|
+
return error_expr("low_eval", "underflow")
|
|
70
|
+
b_b = stack.pop()
|
|
71
|
+
a_b = stack.pop()
|
|
72
|
+
a_i = tc_to_int(a_b)
|
|
73
|
+
b_i = tc_to_int(b_b)
|
|
74
|
+
res_i = a_i + b_i
|
|
75
|
+
width = max(len(a_b), len(b_b), min_tc_width(res_i))
|
|
76
|
+
res_b = int_to_tc(res_i, width)
|
|
77
|
+
# charge for both operands' byte widths
|
|
78
|
+
if not meter.charge_bytes(len(a_b) + len(b_b)):
|
|
79
|
+
return error_expr("low_eval", "meter limit")
|
|
80
|
+
stack.append(res_b)
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# ---------- NAND ----------
|
|
84
|
+
|
|
85
|
+
if tok == b"nand":
|
|
86
|
+
if len(stack) < 2:
|
|
87
|
+
return error_expr("low_eval", "underflow")
|
|
88
|
+
b_b = stack.pop()
|
|
89
|
+
a_b = stack.pop()
|
|
90
|
+
res_b = nand_bytes(a_b, b_b)
|
|
91
|
+
# bitwise cost: 2 * max(len(a), len(b))
|
|
92
|
+
if not meter.charge_bytes(2 * max(len(a_b), len(b_b), 1)):
|
|
93
|
+
return error_expr("low_eval", "meter limit")
|
|
94
|
+
stack.append(res_b)
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# ---------- JUMP ----------
|
|
98
|
+
if tok == b"jump":
|
|
99
|
+
if len(stack) < 1:
|
|
100
|
+
return error_expr("low_eval", "underflow")
|
|
101
|
+
tgt_b = stack.pop()
|
|
102
|
+
if not meter.charge_bytes(1):
|
|
103
|
+
return error_expr("low_eval", "meter limit")
|
|
104
|
+
tgt_i = tc_to_int(tgt_b)
|
|
105
|
+
if tgt_i < 0 or tgt_i >= len(code):
|
|
106
|
+
return error_expr("low_eval", "bad jump")
|
|
107
|
+
pc = tgt_i
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# ---------- HEAP GET ----------
|
|
111
|
+
if tok == b"heap_get":
|
|
112
|
+
if len(stack) < 1:
|
|
113
|
+
return error_expr("low_eval", "underflow")
|
|
114
|
+
key = stack.pop()
|
|
115
|
+
val = heap.get(key) or b""
|
|
116
|
+
# get cost: 1
|
|
117
|
+
if not meter.charge_bytes(1):
|
|
118
|
+
return error_expr("low_eval", "meter limit")
|
|
119
|
+
stack.append(val)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# ---------- HEAP SET ----------
|
|
123
|
+
if tok == b"heap_set":
|
|
124
|
+
if len(stack) < 2:
|
|
125
|
+
return error_expr("low_eval", "underflow")
|
|
126
|
+
val = stack.pop()
|
|
127
|
+
key = stack.pop()
|
|
128
|
+
if not meter.charge_bytes(len(val)):
|
|
129
|
+
return error_expr("low_eval", "meter limit")
|
|
130
|
+
heap[key] = val
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# ---------- ATOM SLICE ----------
|
|
134
|
+
if tok == b"atom_slice":
|
|
135
|
+
if len(stack) < 3:
|
|
136
|
+
return error_expr("low_eval", "underflow")
|
|
137
|
+
len_b = stack.pop()
|
|
138
|
+
idx_b = stack.pop()
|
|
139
|
+
id_b = stack.pop()
|
|
140
|
+
|
|
141
|
+
idx = tc_to_int(idx_b)
|
|
142
|
+
length = tc_to_int(len_b)
|
|
143
|
+
if idx < 0 or length < 0:
|
|
144
|
+
return error_expr("low_eval", "bad slice")
|
|
145
|
+
|
|
146
|
+
atom = self.storage_get(key=id_b)
|
|
147
|
+
if atom is None:
|
|
148
|
+
return error_expr("low_eval", "unknown atom")
|
|
149
|
+
|
|
150
|
+
data = atom.data
|
|
151
|
+
slice_bytes = data[idx : idx + length]
|
|
152
|
+
|
|
153
|
+
if not meter.charge_bytes(len(slice_bytes)):
|
|
154
|
+
return error_expr("low_eval", "meter limit")
|
|
155
|
+
|
|
156
|
+
new_atom = Atom(data=slice_bytes, kind=atom.kind)
|
|
157
|
+
new_id = new_atom.object_id()
|
|
158
|
+
try:
|
|
159
|
+
self._hot_storage_set(key=new_id, value=new_atom)
|
|
160
|
+
except RuntimeError:
|
|
161
|
+
return error_expr("low_eval", "storage error")
|
|
162
|
+
|
|
163
|
+
stack.append(new_id)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# ---------- ATOM LINK ----------
|
|
167
|
+
if tok == b"atom_link":
|
|
168
|
+
if len(stack) < 2:
|
|
169
|
+
return error_expr("low_eval", "underflow")
|
|
170
|
+
id2_b = stack.pop()
|
|
171
|
+
id1_b = stack.pop()
|
|
172
|
+
|
|
173
|
+
if not meter.charge_bytes(len(id1_b) + len(id2_b)):
|
|
174
|
+
return error_expr("low_eval", "meter limit")
|
|
175
|
+
|
|
176
|
+
atom = self.storage_get(key=id_b)
|
|
177
|
+
if atom is None:
|
|
178
|
+
return error_expr("low_eval", "unknown atom")
|
|
179
|
+
|
|
180
|
+
new_atom = Atom(data=id1_b, next_id=id2_b, kind=atom.kind)
|
|
181
|
+
new_id = new_atom.object_id()
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
self._hot_storage_set(key=new_id, value=new_atom)
|
|
185
|
+
except RuntimeError:
|
|
186
|
+
return error_expr("low_eval", "storage error")
|
|
187
|
+
|
|
188
|
+
stack.append(new_id)
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# ---------- ATOM CONCAT ----------
|
|
192
|
+
if tok == b"atom_concat":
|
|
193
|
+
if len(stack) < 2:
|
|
194
|
+
return error_expr("low_eval", "underflow")
|
|
195
|
+
id2_b = stack.pop()
|
|
196
|
+
id1_b = stack.pop()
|
|
197
|
+
|
|
198
|
+
atom1 = self.storage_get(key=id1_b)
|
|
199
|
+
atom2 = self.storage_get(key=id2_b)
|
|
200
|
+
if atom1 is None or atom2 is None:
|
|
201
|
+
return error_expr("low_eval", "unknown atom")
|
|
202
|
+
|
|
203
|
+
joined = atom1.data + atom2.data
|
|
204
|
+
|
|
205
|
+
if not meter.charge_bytes(len(joined)):
|
|
206
|
+
return error_expr("low_eval", "meter limit")
|
|
207
|
+
|
|
208
|
+
new_atom = Atom(data=joined, kind=AtomKind.BYTES)
|
|
209
|
+
new_id = new_atom.object_id()
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
self._hot_storage_set(key=new_id, value=new_atom)
|
|
213
|
+
except RuntimeError:
|
|
214
|
+
return error_expr("low_eval", "storage error")
|
|
215
|
+
|
|
216
|
+
stack.append(new_id)
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# ---------- ATOM NEW ----------
|
|
220
|
+
|
|
221
|
+
if tok == b"atom_new":
|
|
222
|
+
if len(stack) < 2:
|
|
223
|
+
return error_expr("low_eval", "underflow")
|
|
224
|
+
data_b = stack.pop()
|
|
225
|
+
kind_b = stack.pop()
|
|
226
|
+
|
|
227
|
+
if len(kind_b) != 1:
|
|
228
|
+
return error_expr("low_eval", "bad atom kind")
|
|
229
|
+
|
|
230
|
+
kind_value = kind_b[0]
|
|
231
|
+
try:
|
|
232
|
+
kind = AtomKind(kind_value)
|
|
233
|
+
except ValueError:
|
|
234
|
+
return error_expr("low_eval", "unknown atom kind")
|
|
235
|
+
|
|
236
|
+
if not meter.charge_bytes(len(data_b)):
|
|
237
|
+
return error_expr("low_eval", "meter limit")
|
|
238
|
+
|
|
239
|
+
new_atom = Atom(data=data_b, kind=kind)
|
|
240
|
+
new_id = new_atom.object_id()
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
self._hot_storage_set(key=new_id, value=new_atom)
|
|
244
|
+
except RuntimeError:
|
|
245
|
+
return error_expr("low_eval", "storage error")
|
|
246
|
+
|
|
247
|
+
stack.append(new_id)
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
# ---------- ATOM LOAD ----------
|
|
251
|
+
|
|
252
|
+
if tok == b"atom_load":
|
|
253
|
+
if len(stack) < 3:
|
|
254
|
+
return error_expr("low_eval", "underflow")
|
|
255
|
+
len_b = stack.pop()
|
|
256
|
+
idx_b = stack.pop()
|
|
257
|
+
id_b = stack.pop()
|
|
258
|
+
|
|
259
|
+
idx = tc_to_int(idx_b)
|
|
260
|
+
length = tc_to_int(len_b)
|
|
261
|
+
if idx < 0 or length < 0:
|
|
262
|
+
return error_expr("low_eval", "bad load")
|
|
263
|
+
if length > 32:
|
|
264
|
+
return error_expr("low_eval", "load too wide")
|
|
265
|
+
|
|
266
|
+
atom = self.storage_get(key=id_b)
|
|
267
|
+
if atom is None:
|
|
268
|
+
return error_expr("low_eval", "unknown atom")
|
|
269
|
+
|
|
270
|
+
data = atom.data
|
|
271
|
+
chunk = data[idx : idx + length]
|
|
272
|
+
|
|
273
|
+
if not meter.charge_bytes(len(chunk)):
|
|
274
|
+
return error_expr("low_eval", "meter limit")
|
|
275
|
+
|
|
276
|
+
stack.append(chunk)
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# if no opcode matched above, treat token as literal
|
|
280
|
+
# not an opcode → literal blob
|
|
281
|
+
stack.append(tok)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""High level eval helper that reuses the existing tokenizer, parser and evaluator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..models.expression import Expr, error_expr
|
|
9
|
+
from ..parser import ParseError, parse
|
|
10
|
+
from ..tokenizer import tokenize
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def script_eval(self, source: str, env_id: Optional[uuid.UUID] = None, meter=None) -> Expr:
|
|
14
|
+
"""Evaluate textual expressions by tokenizing, parsing and forwarding to high_eval."""
|
|
15
|
+
tokens = tokenize(source)
|
|
16
|
+
if not tokens:
|
|
17
|
+
return error_expr("eval", "no expression provided")
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
expr, rest = parse(tokens)
|
|
21
|
+
except ParseError as exc:
|
|
22
|
+
return error_expr("eval", f"parse error: {exc}")
|
|
23
|
+
|
|
24
|
+
if rest:
|
|
25
|
+
return error_expr("eval", "unexpected tokens after expression")
|
|
26
|
+
|
|
27
|
+
return self.high_eval(expr=expr, env_id=env_id, meter=meter)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ast import Expr
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Env:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
data: Optional[Dict[str, Expr]] = None,
|
|
10
|
+
parent_id: Optional[uuid.UUID] = None,
|
|
11
|
+
):
|
|
12
|
+
self.data: Dict[str, Expr] = {} if data is None else data
|
|
13
|
+
self.parent_id = parent_id
|
|
14
|
+
|
|
15
|
+
def env_get(self, env_id: uuid.UUID, key: str) -> Optional[Expr]:
|
|
16
|
+
"""Resolve a value by walking the environment chain starting at env_id."""
|
|
17
|
+
cur = self.environments.get(env_id)
|
|
18
|
+
while cur is not None:
|
|
19
|
+
if key in cur.data:
|
|
20
|
+
return cur.data[key]
|
|
21
|
+
cur = self.environments.get(cur.parent_id) if cur.parent_id else None
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
def env_set(self, env_id: uuid.UUID, key: str, value: Expr) -> bool:
|
|
25
|
+
"""Bind a value to key within the specified environment if it exists."""
|
|
26
|
+
with self.machine_environments_lock:
|
|
27
|
+
env = self.environments.get(env_id)
|
|
28
|
+
if env is None:
|
|
29
|
+
return False
|
|
30
|
+
env.data[key] = value
|
|
31
|
+
return True
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from typing import Any, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from ...storage.models.atom import Atom, AtomKind
|
|
4
|
+
|
|
5
|
+
ZERO32 = b"\x00" * 32
|
|
6
|
+
ERROR_SYMBOL = "error"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Expr:
|
|
10
|
+
class ListExpr:
|
|
11
|
+
def __init__(self, elements: List['Expr']):
|
|
12
|
+
self.elements = elements
|
|
13
|
+
|
|
14
|
+
def __repr__(self):
|
|
15
|
+
if not self.elements:
|
|
16
|
+
return "()"
|
|
17
|
+
inner = " ".join(str(e) for e in self.elements)
|
|
18
|
+
return f"({inner})"
|
|
19
|
+
|
|
20
|
+
def to_atoms(self):
|
|
21
|
+
return Expr.to_atoms(self)
|
|
22
|
+
|
|
23
|
+
class Symbol:
|
|
24
|
+
def __init__(self, value: str):
|
|
25
|
+
self.value = value
|
|
26
|
+
|
|
27
|
+
def __repr__(self):
|
|
28
|
+
return f"{self.value}"
|
|
29
|
+
|
|
30
|
+
def to_atoms(self):
|
|
31
|
+
return Expr.to_atoms(self)
|
|
32
|
+
|
|
33
|
+
class Bytes:
|
|
34
|
+
def __init__(self, value: bytes):
|
|
35
|
+
self.value = value
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
int_value = int.from_bytes(self.value, "big") if self.value else 0
|
|
39
|
+
return f"{int_value}"
|
|
40
|
+
|
|
41
|
+
def to_atoms(self):
|
|
42
|
+
return Expr.to_atoms(self)
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_atoms(cls, node: Any, root_hash: bytes) -> "Expr":
|
|
45
|
+
"""Rebuild an expression tree from stored atoms."""
|
|
46
|
+
if not isinstance(root_hash, (bytes, bytearray)):
|
|
47
|
+
raise TypeError("root hash must be bytes-like")
|
|
48
|
+
|
|
49
|
+
storage_get = getattr(node, "storage_get", None)
|
|
50
|
+
if not callable(storage_get):
|
|
51
|
+
raise TypeError("node must provide a callable 'storage_get'")
|
|
52
|
+
|
|
53
|
+
expr_id = bytes(root_hash)
|
|
54
|
+
|
|
55
|
+
def _require(atom_id: Optional[bytes], context: str):
|
|
56
|
+
if not atom_id:
|
|
57
|
+
raise ValueError(f"missing atom id while decoding {context}")
|
|
58
|
+
atom = storage_get(atom_id)
|
|
59
|
+
if atom is None:
|
|
60
|
+
raise ValueError(f"missing atom data while decoding {context}")
|
|
61
|
+
return atom
|
|
62
|
+
|
|
63
|
+
def _atom_kind(atom: Any) -> Optional[AtomKind]:
|
|
64
|
+
kind_value = getattr(atom, "kind", None)
|
|
65
|
+
if isinstance(kind_value, AtomKind):
|
|
66
|
+
return kind_value
|
|
67
|
+
if isinstance(kind_value, int):
|
|
68
|
+
try:
|
|
69
|
+
return AtomKind(kind_value)
|
|
70
|
+
except ValueError:
|
|
71
|
+
return None
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
type_atom = _require(expr_id, "expression atom")
|
|
75
|
+
|
|
76
|
+
kind_enum = _atom_kind(type_atom)
|
|
77
|
+
if kind_enum is None:
|
|
78
|
+
raise ValueError("expression atom missing kind")
|
|
79
|
+
|
|
80
|
+
if kind_enum is AtomKind.SYMBOL:
|
|
81
|
+
try:
|
|
82
|
+
return cls.Symbol(type_atom.data.decode("utf-8"))
|
|
83
|
+
except UnicodeDecodeError as exc:
|
|
84
|
+
raise ValueError("symbol atom is not valid utf-8") from exc
|
|
85
|
+
|
|
86
|
+
if kind_enum is AtomKind.BYTES:
|
|
87
|
+
return cls.Bytes(type_atom.data)
|
|
88
|
+
|
|
89
|
+
if kind_enum is AtomKind.LIST:
|
|
90
|
+
# Empty list sentinel: zero-length payload and no next pointer.
|
|
91
|
+
if len(type_atom.data) == 0 and type_atom.next_id == ZERO32:
|
|
92
|
+
return cls.ListExpr([])
|
|
93
|
+
|
|
94
|
+
elements: List[Expr] = []
|
|
95
|
+
current_atom = type_atom
|
|
96
|
+
idx = 0
|
|
97
|
+
while True:
|
|
98
|
+
child_hash = current_atom.data
|
|
99
|
+
if not child_hash:
|
|
100
|
+
raise ValueError("list element missing child hash")
|
|
101
|
+
if len(child_hash) != len(ZERO32):
|
|
102
|
+
raise ValueError("list element hash has unexpected length")
|
|
103
|
+
child_expr = cls.from_atoms(node, child_hash)
|
|
104
|
+
elements.append(child_expr)
|
|
105
|
+
next_id = current_atom.next_id
|
|
106
|
+
if next_id == ZERO32:
|
|
107
|
+
break
|
|
108
|
+
next_atom = _require(next_id, f"list element {idx}")
|
|
109
|
+
next_kind = _atom_kind(next_atom)
|
|
110
|
+
if next_kind is not AtomKind.LIST:
|
|
111
|
+
raise ValueError("list chain contains non-list atom")
|
|
112
|
+
current_atom = next_atom
|
|
113
|
+
idx += 1
|
|
114
|
+
return cls.ListExpr(elements)
|
|
115
|
+
|
|
116
|
+
raise ValueError(f"unknown expression kind: {kind_enum}")
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def to_atoms(e: "Expr") -> Tuple[bytes, List[Atom]]:
|
|
120
|
+
def symbol(value: str) -> Tuple[bytes, List[Atom]]:
|
|
121
|
+
atom = Atom(
|
|
122
|
+
data=value.encode("utf-8"),
|
|
123
|
+
kind=AtomKind.SYMBOL,
|
|
124
|
+
)
|
|
125
|
+
return atom.object_id(), [atom]
|
|
126
|
+
|
|
127
|
+
def bytes_value(data: bytes) -> Tuple[bytes, List[Atom]]:
|
|
128
|
+
atom = Atom(
|
|
129
|
+
data=data,
|
|
130
|
+
kind=AtomKind.BYTES,
|
|
131
|
+
)
|
|
132
|
+
return atom.object_id(), [atom]
|
|
133
|
+
|
|
134
|
+
def lst(items: List["Expr"]) -> Tuple[bytes, List[Atom]]:
|
|
135
|
+
acc: List[Atom] = []
|
|
136
|
+
child_hashes: List[bytes] = []
|
|
137
|
+
for it in items:
|
|
138
|
+
h, atoms = Expr.to_atoms(it)
|
|
139
|
+
acc.extend(atoms)
|
|
140
|
+
child_hashes.append(h)
|
|
141
|
+
next_hash = ZERO32
|
|
142
|
+
elem_atoms: List[Atom] = []
|
|
143
|
+
for h in reversed(child_hashes):
|
|
144
|
+
a = Atom(data=h, next_id=next_hash, kind=AtomKind.LIST)
|
|
145
|
+
next_hash = a.object_id()
|
|
146
|
+
elem_atoms.append(a)
|
|
147
|
+
elem_atoms.reverse()
|
|
148
|
+
if elem_atoms:
|
|
149
|
+
head = elem_atoms[0].object_id()
|
|
150
|
+
else:
|
|
151
|
+
empty_atom = Atom(data=b"", kind=AtomKind.LIST)
|
|
152
|
+
elem_atoms = [empty_atom]
|
|
153
|
+
head = empty_atom.object_id()
|
|
154
|
+
return head, acc + elem_atoms
|
|
155
|
+
|
|
156
|
+
if isinstance(e, Expr.Symbol):
|
|
157
|
+
return symbol(e.value)
|
|
158
|
+
if isinstance(e, Expr.Bytes):
|
|
159
|
+
return bytes_value(e.value)
|
|
160
|
+
if isinstance(e, Expr.ListExpr):
|
|
161
|
+
return lst(e.elements)
|
|
162
|
+
raise TypeError("unknown Expr variant")
|
|
163
|
+
|
|
164
|
+
def _expr_generate_id(expr) -> bytes:
|
|
165
|
+
expr_id, _ = Expr.to_atoms(expr)
|
|
166
|
+
return expr_id
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _expr_cached_id(expr) -> bytes:
|
|
170
|
+
cached = getattr(expr, "_cached_id", None)
|
|
171
|
+
if cached is None:
|
|
172
|
+
cached = _expr_generate_id(expr)
|
|
173
|
+
setattr(expr, "_cached_id", cached)
|
|
174
|
+
return cached
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
for _expr_cls in (Expr.ListExpr, Expr.Symbol, Expr.Bytes):
|
|
178
|
+
_expr_cls.generate_id = _expr_generate_id # type: ignore[attr-defined]
|
|
179
|
+
_expr_cls.id = property(_expr_cached_id) # type: ignore[attr-defined]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def error_expr(topic: str, message: str) -> Expr.ListExpr:
|
|
183
|
+
"""Encode an error as (error <topic-bytes> <message-bytes>)."""
|
|
184
|
+
try:
|
|
185
|
+
topic_bytes = topic.encode("utf-8")
|
|
186
|
+
except UnicodeEncodeError as exc:
|
|
187
|
+
raise ValueError("error topic must be valid utf-8") from exc
|
|
188
|
+
try:
|
|
189
|
+
message_bytes = message.encode("utf-8")
|
|
190
|
+
except UnicodeEncodeError as exc:
|
|
191
|
+
raise ValueError("error message must be valid utf-8") from exc
|
|
192
|
+
return Expr.ListExpr([
|
|
193
|
+
Expr.Symbol(ERROR_SYMBOL),
|
|
194
|
+
Expr.Bytes(topic_bytes),
|
|
195
|
+
Expr.Bytes(message_bytes),
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
def get_expr_list_from_storage(self, key: bytes) -> Optional["ListExpr"]:
|
|
199
|
+
"""Load a list expression from storage using the given atom list root hash."""
|
|
200
|
+
atoms = self.get_atom_list_from_storage(root_hash=key)
|
|
201
|
+
if atoms is None:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
expr_list = []
|
|
205
|
+
for atom in atoms:
|
|
206
|
+
match atom.kind:
|
|
207
|
+
case AtomKind.SYMBOL:
|
|
208
|
+
expr_list.append(Expr.Symbol(atom.data))
|
|
209
|
+
case AtomKind.BYTES:
|
|
210
|
+
expr_list.append(Expr.Bytes(atom.data))
|
|
211
|
+
case AtomKind.LIST:
|
|
212
|
+
expr_list.append(Expr.ListExpr([
|
|
213
|
+
Expr.Bytes(atom.data),
|
|
214
|
+
Expr.Symbol("ref")
|
|
215
|
+
]))
|
|
216
|
+
|
|
217
|
+
expr_list.reverse()
|
|
218
|
+
return Expr.ListExpr(expr_list)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from typing import List, Tuple
|
|
2
|
-
from
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
from . import Expr
|
|
3
3
|
|
|
4
4
|
class ParseError(Exception):
|
|
5
5
|
pass
|
|
@@ -14,12 +14,7 @@ def _parse_one(tokens: List[str], pos: int = 0) -> Tuple[Expr, int]:
|
|
|
14
14
|
i = pos + 1
|
|
15
15
|
while i < len(tokens):
|
|
16
16
|
if tokens[i] == ')':
|
|
17
|
-
|
|
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
|
|
17
|
+
return Expr.ListExpr(items), i + 1
|
|
23
18
|
expr, i = _parse_one(tokens, i)
|
|
24
19
|
items.append(expr)
|
|
25
20
|
raise ParseError("expected ')'")
|
|
@@ -27,30 +22,30 @@ def _parse_one(tokens: List[str], pos: int = 0) -> Tuple[Expr, int]:
|
|
|
27
22
|
if tok == ')':
|
|
28
23
|
raise ParseError("unexpected ')'")
|
|
29
24
|
|
|
30
|
-
# try integer → Bytes (variable-length two's complement)
|
|
31
|
-
try:
|
|
32
|
-
n = int(tok)
|
|
33
|
-
# encode as minimal-width signed two's complement, big-endian
|
|
34
|
-
def int_to_min_tc(v: int) -> bytes:
|
|
35
|
-
"""Return the minimal-width signed two's complement big-endian
|
|
36
|
-
byte encoding of integer v. Width expands just enough so that
|
|
37
|
-
decoding with signed=True yields the same value and sign.
|
|
38
|
-
Example: 0 -> b"\x00", 127 -> b"\x7f", 128 -> b"\x00\x80".
|
|
39
|
-
"""
|
|
40
|
-
if v == 0:
|
|
41
|
-
return b"\x00"
|
|
42
|
-
w = 1
|
|
43
|
-
while True:
|
|
44
|
-
try:
|
|
45
|
-
return v.to_bytes(w, "big", signed=True)
|
|
46
|
-
except OverflowError:
|
|
47
|
-
w += 1
|
|
48
|
-
|
|
49
|
-
return Expr.Bytes(int_to_min_tc(n)), pos + 1
|
|
50
|
-
except ValueError:
|
|
51
|
-
return Expr.Symbol(tok), pos + 1
|
|
25
|
+
# try integer → Bytes (variable-length two's complement)
|
|
26
|
+
try:
|
|
27
|
+
n = int(tok)
|
|
28
|
+
# encode as minimal-width signed two's complement, big-endian
|
|
29
|
+
def int_to_min_tc(v: int) -> bytes:
|
|
30
|
+
"""Return the minimal-width signed two's complement big-endian
|
|
31
|
+
byte encoding of integer v. Width expands just enough so that
|
|
32
|
+
decoding with signed=True yields the same value and sign.
|
|
33
|
+
Example: 0 -> b"\x00", 127 -> b"\x7f", 128 -> b"\x00\x80".
|
|
34
|
+
"""
|
|
35
|
+
if v == 0:
|
|
36
|
+
return b"\x00"
|
|
37
|
+
w = 1
|
|
38
|
+
while True:
|
|
39
|
+
try:
|
|
40
|
+
return v.to_bytes(w, "big", signed=True)
|
|
41
|
+
except OverflowError:
|
|
42
|
+
w += 1
|
|
43
|
+
|
|
44
|
+
return Expr.Bytes(int_to_min_tc(n)), pos + 1
|
|
45
|
+
except ValueError:
|
|
46
|
+
return Expr.Symbol(tok), pos + 1
|
|
52
47
|
|
|
53
48
|
def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
|
|
54
49
|
"""Parse tokens into an Expr and return (expr, remaining_tokens)."""
|
|
55
50
|
expr, next_pos = _parse_one(tokens, 0)
|
|
56
|
-
return expr, tokens[next_pos:]
|
|
51
|
+
return expr, tokens[next_pos:]
|