astreum 0.2.41__py3-none-any.whl → 0.2.61__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/_communication/message.py +1 -0
- astreum/_communication/peer.py +23 -11
- astreum/_communication/route.py +40 -3
- astreum/_communication/setup.py +72 -4
- astreum/_consensus/account.py +62 -137
- astreum/_consensus/accounts.py +7 -36
- astreum/_consensus/block.py +311 -328
- astreum/_consensus/genesis.py +15 -84
- astreum/_consensus/receipt.py +67 -108
- astreum/_consensus/setup.py +50 -3
- astreum/_consensus/transaction.py +141 -118
- astreum/_consensus/workers/validation.py +5 -2
- astreum/_consensus/workers/verify.py +1 -1
- astreum/_lispeum/expression.py +190 -37
- astreum/_lispeum/high_evaluation.py +232 -173
- astreum/_lispeum/low_evaluation.py +21 -21
- astreum/_lispeum/parser.py +26 -31
- astreum/_node.py +154 -14
- astreum/_storage/__init__.py +7 -5
- astreum/_storage/atom.py +88 -96
- astreum/_storage/patricia.py +51 -16
- astreum/_storage/setup.py +35 -0
- astreum/models/block.py +20 -20
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/METADATA +14 -1
- astreum-0.2.61.dist-info/RECORD +57 -0
- astreum-0.2.41.dist-info/RECORD +0 -53
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
|
@@ -3,43 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, List, Optional, Tuple
|
|
5
5
|
|
|
6
|
-
from .._storage.atom import Atom, ZERO32
|
|
7
|
-
from .
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if value is None:
|
|
12
|
-
return b""
|
|
13
|
-
value = int(value)
|
|
14
|
-
if value == 0:
|
|
15
|
-
return b"\x00"
|
|
16
|
-
size = (value.bit_length() + 7) // 8
|
|
17
|
-
return value.to_bytes(size, "big")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _be_bytes_to_int(data: Optional[bytes]) -> int:
|
|
21
|
-
if not data:
|
|
22
|
-
return 0
|
|
23
|
-
return int.from_bytes(data, "big")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
27
|
-
atoms: List[Atom] = []
|
|
28
|
-
next_hash = ZERO32
|
|
29
|
-
chain: List[Atom] = []
|
|
30
|
-
for child_id in reversed(child_ids):
|
|
31
|
-
elem = Atom.from_data(data=child_id, next_hash=next_hash)
|
|
32
|
-
next_hash = elem.object_id()
|
|
33
|
-
chain.append(elem)
|
|
34
|
-
|
|
35
|
-
chain.reverse()
|
|
36
|
-
list_value = Atom.from_data(data=len(child_ids).to_bytes(8, "little"), next_hash=next_hash)
|
|
37
|
-
list_type = Atom.from_data(data=b"list", next_hash=list_value.object_id())
|
|
38
|
-
atoms.extend(chain)
|
|
39
|
-
atoms.append(list_value)
|
|
40
|
-
atoms.append(list_type)
|
|
41
|
-
return list_type.object_id(), atoms
|
|
42
|
-
|
|
6
|
+
from .._storage.atom import Atom, AtomKind, ZERO32
|
|
7
|
+
from ..utils.integer import bytes_to_int, int_to_bytes
|
|
8
|
+
from .account import Account
|
|
9
|
+
from .genesis import TREASURY_ADDRESS
|
|
10
|
+
from .receipt import STATUS_FAILED, Receipt, STATUS_SUCCESS
|
|
43
11
|
|
|
44
12
|
@dataclass
|
|
45
13
|
class Transaction:
|
|
@@ -57,31 +25,46 @@ class Transaction:
|
|
|
57
25
|
acc: List[Atom] = []
|
|
58
26
|
|
|
59
27
|
def emit(payload: bytes) -> None:
|
|
60
|
-
atom = Atom.from_data(data=payload)
|
|
28
|
+
atom = Atom.from_data(data=payload, kind=AtomKind.BYTES)
|
|
61
29
|
body_child_ids.append(atom.object_id())
|
|
62
30
|
acc.append(atom)
|
|
63
31
|
|
|
64
|
-
emit(
|
|
65
|
-
emit(
|
|
32
|
+
emit(int_to_bytes(self.amount))
|
|
33
|
+
emit(int_to_bytes(self.counter))
|
|
66
34
|
emit(bytes(self.data))
|
|
67
35
|
emit(bytes(self.recipient))
|
|
68
36
|
emit(bytes(self.sender))
|
|
69
37
|
|
|
70
|
-
|
|
38
|
+
# Build the linked list of body entry references.
|
|
39
|
+
body_atoms: List[Atom] = []
|
|
40
|
+
body_head = ZERO32
|
|
41
|
+
for child_id in reversed(body_child_ids):
|
|
42
|
+
node = Atom.from_data(data=child_id, next_hash=body_head, kind=AtomKind.BYTES)
|
|
43
|
+
body_head = node.object_id()
|
|
44
|
+
body_atoms.append(node)
|
|
45
|
+
body_atoms.reverse()
|
|
71
46
|
acc.extend(body_atoms)
|
|
72
47
|
|
|
73
|
-
|
|
74
|
-
|
|
48
|
+
body_list_atom = Atom.from_data(data=body_head, kind=AtomKind.LIST)
|
|
49
|
+
acc.append(body_list_atom)
|
|
50
|
+
body_list_id = body_list_atom.object_id()
|
|
75
51
|
|
|
76
|
-
|
|
77
|
-
|
|
52
|
+
signature_atom = Atom.from_data(
|
|
53
|
+
data=bytes(self.signature),
|
|
54
|
+
next_hash=body_list_id,
|
|
55
|
+
kind=AtomKind.BYTES,
|
|
56
|
+
)
|
|
57
|
+
type_atom = Atom.from_data(
|
|
58
|
+
data=b"transaction",
|
|
59
|
+
next_hash=signature_atom.object_id(),
|
|
60
|
+
kind=AtomKind.SYMBOL,
|
|
78
61
|
)
|
|
79
62
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
63
|
+
acc.append(signature_atom)
|
|
64
|
+
acc.append(type_atom)
|
|
65
|
+
|
|
66
|
+
self.hash = type_atom.object_id()
|
|
67
|
+
return self.hash, acc
|
|
85
68
|
|
|
86
69
|
@classmethod
|
|
87
70
|
def from_atom(
|
|
@@ -89,83 +72,86 @@ class Transaction:
|
|
|
89
72
|
node: Any,
|
|
90
73
|
transaction_id: bytes,
|
|
91
74
|
) -> Transaction:
|
|
92
|
-
storage_get = node
|
|
75
|
+
storage_get = getattr(node, "storage_get", None)
|
|
93
76
|
if not callable(storage_get):
|
|
94
77
|
raise NotImplementedError("node does not expose a storage getter")
|
|
95
78
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
79
|
+
def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
|
|
80
|
+
kind_value = getattr(atom, "kind", None)
|
|
81
|
+
if isinstance(kind_value, AtomKind):
|
|
82
|
+
return kind_value
|
|
83
|
+
if isinstance(kind_value, int):
|
|
84
|
+
try:
|
|
85
|
+
return AtomKind(kind_value)
|
|
86
|
+
except ValueError:
|
|
87
|
+
return None
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def _require_atom(
|
|
91
|
+
atom_id: Optional[bytes],
|
|
92
|
+
context: str,
|
|
93
|
+
expected_kind: Optional[AtomKind] = None,
|
|
94
|
+
) -> Atom:
|
|
95
|
+
if not atom_id or atom_id == ZERO32:
|
|
96
|
+
raise ValueError(f"missing {context}")
|
|
97
|
+
atom = storage_get(atom_id)
|
|
98
|
+
if atom is None:
|
|
99
|
+
raise ValueError(f"missing {context}")
|
|
100
|
+
if expected_kind is not None:
|
|
101
|
+
kind = _atom_kind(atom)
|
|
102
|
+
if kind is not expected_kind:
|
|
103
|
+
raise ValueError(f"malformed {context}")
|
|
104
|
+
return atom
|
|
105
|
+
|
|
106
|
+
def _read_list(head_id: Optional[bytes], context: str) -> List[bytes]:
|
|
115
107
|
entries: List[bytes] = []
|
|
116
|
-
current =
|
|
108
|
+
current = head_id if head_id and head_id != ZERO32 else None
|
|
117
109
|
while current:
|
|
118
|
-
|
|
119
|
-
if
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
110
|
+
node = storage_get(current)
|
|
111
|
+
if node is None:
|
|
112
|
+
raise ValueError(f"missing list node while decoding {context}")
|
|
113
|
+
node_kind = _atom_kind(node)
|
|
114
|
+
if node_kind is not AtomKind.BYTES:
|
|
115
|
+
raise ValueError(f"invalid list node while decoding {context}")
|
|
116
|
+
if len(node.data) != len(ZERO32):
|
|
117
|
+
raise ValueError(f"malformed list entry while decoding {context}")
|
|
118
|
+
entries.append(node.data)
|
|
119
|
+
nxt = node.next
|
|
120
|
+
current = nxt if nxt and nxt != ZERO32 else None
|
|
124
121
|
return entries
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
raise ValueError("malformed transaction (body/signature missing)")
|
|
129
|
-
|
|
130
|
-
body_id, signature_atom_id = remainder_entries[0], remainder_entries[1]
|
|
131
|
-
|
|
132
|
-
body_type_atom = storage_get(body_id)
|
|
133
|
-
if body_type_atom is None or body_type_atom.data != b"list":
|
|
134
|
-
raise ValueError("malformed transaction body (type)")
|
|
135
|
-
|
|
136
|
-
body_value_atom = storage_get(body_type_atom.next)
|
|
137
|
-
if body_value_atom is None:
|
|
138
|
-
raise ValueError("malformed transaction body (value)")
|
|
139
|
-
|
|
140
|
-
body_entries = read_list_entries(body_value_atom.next)
|
|
141
|
-
if len(body_entries) < 5:
|
|
142
|
-
body_entries.extend([ZERO32] * (5 - len(body_entries)))
|
|
143
|
-
|
|
144
|
-
def read_detail_bytes(entry_id: bytes) -> bytes:
|
|
145
|
-
if entry_id == ZERO32:
|
|
146
|
-
return b""
|
|
147
|
-
elem = storage_get(entry_id)
|
|
148
|
-
if elem is None:
|
|
123
|
+
def _read_detail_bytes(entry_id: Optional[bytes]) -> bytes:
|
|
124
|
+
if not entry_id or entry_id == ZERO32:
|
|
149
125
|
return b""
|
|
150
|
-
detail_atom = storage_get(
|
|
126
|
+
detail_atom = storage_get(entry_id)
|
|
151
127
|
return detail_atom.data if detail_atom is not None else b""
|
|
152
128
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
129
|
+
type_atom = _require_atom(transaction_id, "transaction type atom", AtomKind.SYMBOL)
|
|
130
|
+
if type_atom.data != b"transaction":
|
|
131
|
+
raise ValueError("not a transaction (type atom payload)")
|
|
132
|
+
|
|
133
|
+
signature_atom = _require_atom(type_atom.next, "transaction signature atom", AtomKind.BYTES)
|
|
134
|
+
body_list_atom = _require_atom(signature_atom.next, "transaction body list atom", AtomKind.LIST)
|
|
135
|
+
if body_list_atom.next and body_list_atom.next != ZERO32:
|
|
136
|
+
raise ValueError("malformed transaction (body list tail)")
|
|
137
|
+
|
|
138
|
+
body_entry_ids = _read_list(body_list_atom.data, "transaction body")
|
|
139
|
+
if len(body_entry_ids) < 5:
|
|
140
|
+
body_entry_ids.extend([ZERO32] * (5 - len(body_entry_ids)))
|
|
158
141
|
|
|
159
|
-
|
|
160
|
-
|
|
142
|
+
amount_bytes = _read_detail_bytes(body_entry_ids[0])
|
|
143
|
+
counter_bytes = _read_detail_bytes(body_entry_ids[1])
|
|
144
|
+
data_bytes = _read_detail_bytes(body_entry_ids[2])
|
|
145
|
+
recipient_bytes = _read_detail_bytes(body_entry_ids[3])
|
|
146
|
+
sender_bytes = _read_detail_bytes(body_entry_ids[4])
|
|
161
147
|
|
|
162
148
|
return cls(
|
|
163
|
-
amount=
|
|
164
|
-
counter=
|
|
149
|
+
amount=bytes_to_int(amount_bytes),
|
|
150
|
+
counter=bytes_to_int(counter_bytes),
|
|
165
151
|
data=data_bytes,
|
|
166
152
|
recipient=recipient_bytes,
|
|
167
153
|
sender=sender_bytes,
|
|
168
|
-
signature=
|
|
154
|
+
signature=signature_atom.data,
|
|
169
155
|
hash=bytes(transaction_id),
|
|
170
156
|
)
|
|
171
157
|
|
|
@@ -174,9 +160,50 @@ def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None
|
|
|
174
160
|
"""Apply transaction to the candidate block. Override downstream."""
|
|
175
161
|
transaction = Transaction.from_atom(node, transaction_hash)
|
|
176
162
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
163
|
+
accounts = block.accounts
|
|
164
|
+
|
|
165
|
+
sender_account = accounts.get_account(address=transaction.sender, node=node)
|
|
166
|
+
|
|
167
|
+
if sender_account is None:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
tx_cost = 1 + transaction.amount
|
|
171
|
+
|
|
172
|
+
if sender_account.balance < tx_cost:
|
|
173
|
+
low_sender_balance_receipt = Receipt(
|
|
174
|
+
transaction_hash=transaction_hash,
|
|
175
|
+
cost=0,
|
|
176
|
+
logs=b"low sender balance",
|
|
177
|
+
status=STATUS_FAILED
|
|
178
|
+
)
|
|
179
|
+
low_sender_balance_receipt.atomize()
|
|
180
|
+
block.receipts.append(receipt)
|
|
181
|
+
block.transactions.append(transaction)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
recipient_account = accounts.get_account(address=transaction.recipient, node=node)
|
|
185
|
+
|
|
186
|
+
if recipient_account is None:
|
|
187
|
+
recipient_account = Account.create()
|
|
188
|
+
|
|
189
|
+
if transaction.recipient == TREASURY_ADDRESS:
|
|
190
|
+
stake_trie = recipient_account.data
|
|
191
|
+
existing_stake = stake_trie.get(node, transaction.sender)
|
|
192
|
+
current_stake = bytes_to_int(existing_stake)
|
|
193
|
+
new_stake = current_stake + transaction.amount
|
|
194
|
+
stake_trie.put(node, transaction.sender, int_to_bytes(new_stake))
|
|
195
|
+
recipient_account.data_hash = stake_trie.root_hash or ZERO32
|
|
196
|
+
recipient_account.balance += transaction.amount
|
|
197
|
+
else:
|
|
198
|
+
recipient_account.balance += transaction.amount
|
|
199
|
+
|
|
200
|
+
sender_account.balance -= tx_cost
|
|
201
|
+
|
|
202
|
+
block.accounts.set_account(address=sender_account)
|
|
203
|
+
|
|
204
|
+
block.accounts.set_account(address=recipient_account)
|
|
205
|
+
|
|
206
|
+
block.transactions.append(transaction_hash)
|
|
180
207
|
|
|
181
208
|
receipt = Receipt(
|
|
182
209
|
transaction_hash=bytes(transaction_hash),
|
|
@@ -185,8 +212,4 @@ def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None
|
|
|
185
212
|
status=STATUS_SUCCESS,
|
|
186
213
|
)
|
|
187
214
|
receipt.atomize()
|
|
188
|
-
if block.receipts is None:
|
|
189
|
-
block.receipts = []
|
|
190
215
|
block.receipts.append(receipt)
|
|
191
|
-
|
|
192
|
-
# Downstream implementations can extend this to apply state changes.
|
|
@@ -88,7 +88,7 @@ def make_validation_worker(
|
|
|
88
88
|
new_block.timestamp = max(int(now), min_allowed)
|
|
89
89
|
|
|
90
90
|
# atomize block
|
|
91
|
-
new_block_hash,
|
|
91
|
+
new_block_hash, new_block_atoms = new_block.to_atom()
|
|
92
92
|
# put as own latest block hash
|
|
93
93
|
node.latest_block_hash = new_block_hash
|
|
94
94
|
|
|
@@ -117,6 +117,9 @@ def make_validation_worker(
|
|
|
117
117
|
except Exception:
|
|
118
118
|
pass
|
|
119
119
|
|
|
120
|
-
#
|
|
120
|
+
# upload block atoms
|
|
121
|
+
|
|
122
|
+
# upload receipt atoms
|
|
123
|
+
# upload account atoms
|
|
121
124
|
|
|
122
125
|
return _validation_worker
|
|
@@ -17,7 +17,7 @@ def _process_peers_latest_block(
|
|
|
17
17
|
fk.head for fk in node.forks.values() if fk.head != latest_block_hash
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
new_fork.validate(storage_get=node.
|
|
20
|
+
new_fork.validate(storage_get=node.storage_get, stop_heads=current_fork_heads)
|
|
21
21
|
|
|
22
22
|
if new_fork.validated_upto and new_fork.validated_upto in node.forks:
|
|
23
23
|
ref = node.forks[new_fork.validated_upto]
|
astreum/_lispeum/expression.py
CHANGED
|
@@ -1,37 +1,190 @@
|
|
|
1
|
-
from typing import List, Optional
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def
|
|
31
|
-
self
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
from typing import Any, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from .._storage.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 == 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
|
|
106
|
+
if not next_id or 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.from_data(data=value.encode("utf-8"), kind=AtomKind.SYMBOL)
|
|
122
|
+
return atom.object_id(), [atom]
|
|
123
|
+
|
|
124
|
+
def bytes_value(data: bytes) -> Tuple[bytes, List[Atom]]:
|
|
125
|
+
atom = Atom.from_data(data=data, kind=AtomKind.BYTES)
|
|
126
|
+
return atom.object_id(), [atom]
|
|
127
|
+
|
|
128
|
+
def lst(items: List["Expr"]) -> Tuple[bytes, List[Atom]]:
|
|
129
|
+
acc: List[Atom] = []
|
|
130
|
+
child_hashes: List[bytes] = []
|
|
131
|
+
for it in items:
|
|
132
|
+
h, atoms = Expr.to_atoms(it)
|
|
133
|
+
acc.extend(atoms)
|
|
134
|
+
child_hashes.append(h)
|
|
135
|
+
next_hash = ZERO32
|
|
136
|
+
elem_atoms: List[Atom] = []
|
|
137
|
+
for h in reversed(child_hashes):
|
|
138
|
+
a = Atom.from_data(h, next_hash, kind=AtomKind.LIST)
|
|
139
|
+
next_hash = a.object_id()
|
|
140
|
+
elem_atoms.append(a)
|
|
141
|
+
elem_atoms.reverse()
|
|
142
|
+
if elem_atoms:
|
|
143
|
+
head = elem_atoms[0].object_id()
|
|
144
|
+
else:
|
|
145
|
+
empty_atom = Atom.from_data(data=b"", next_hash=ZERO32, kind=AtomKind.LIST)
|
|
146
|
+
elem_atoms = [empty_atom]
|
|
147
|
+
head = empty_atom.object_id()
|
|
148
|
+
return head, acc + elem_atoms
|
|
149
|
+
|
|
150
|
+
if isinstance(e, Expr.Symbol):
|
|
151
|
+
return symbol(e.value)
|
|
152
|
+
if isinstance(e, Expr.Bytes):
|
|
153
|
+
return bytes_value(e.value)
|
|
154
|
+
if isinstance(e, Expr.ListExpr):
|
|
155
|
+
return lst(e.elements)
|
|
156
|
+
raise TypeError("unknown Expr variant")
|
|
157
|
+
|
|
158
|
+
def _expr_generate_id(expr) -> bytes:
|
|
159
|
+
expr_id, _ = Expr.to_atoms(expr)
|
|
160
|
+
return expr_id
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _expr_cached_id(expr) -> bytes:
|
|
164
|
+
cached = getattr(expr, "_cached_id", None)
|
|
165
|
+
if cached is None:
|
|
166
|
+
cached = _expr_generate_id(expr)
|
|
167
|
+
setattr(expr, "_cached_id", cached)
|
|
168
|
+
return cached
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
for _expr_cls in (Expr.ListExpr, Expr.Symbol, Expr.Bytes):
|
|
172
|
+
_expr_cls.generate_id = _expr_generate_id # type: ignore[attr-defined]
|
|
173
|
+
_expr_cls.id = property(_expr_cached_id) # type: ignore[attr-defined]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def error_expr(topic: str, message: str) -> Expr.ListExpr:
|
|
177
|
+
"""Encode an error as (error <topic-bytes> <message-bytes>)."""
|
|
178
|
+
try:
|
|
179
|
+
topic_bytes = topic.encode("utf-8")
|
|
180
|
+
except UnicodeEncodeError as exc:
|
|
181
|
+
raise ValueError("error topic must be valid utf-8") from exc
|
|
182
|
+
try:
|
|
183
|
+
message_bytes = message.encode("utf-8")
|
|
184
|
+
except UnicodeEncodeError as exc:
|
|
185
|
+
raise ValueError("error message must be valid utf-8") from exc
|
|
186
|
+
return Expr.ListExpr([
|
|
187
|
+
Expr.Symbol(ERROR_SYMBOL),
|
|
188
|
+
Expr.Bytes(topic_bytes),
|
|
189
|
+
Expr.Bytes(message_bytes),
|
|
190
|
+
])
|