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.
Files changed (82) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +83 -0
  4. astreum/communication/handlers/ping.py +48 -0
  5. astreum/communication/handlers/storage_request.py +81 -0
  6. astreum/communication/models/__init__.py +0 -0
  7. astreum/{_communication → communication/models}/message.py +1 -0
  8. astreum/communication/models/peer.py +23 -0
  9. astreum/{_communication → communication/models}/route.py +45 -8
  10. astreum/{_communication → communication}/setup.py +46 -95
  11. astreum/communication/start.py +38 -0
  12. astreum/consensus/__init__.py +20 -0
  13. astreum/consensus/genesis.py +66 -0
  14. astreum/consensus/models/__init__.py +0 -0
  15. astreum/consensus/models/account.py +84 -0
  16. astreum/consensus/models/accounts.py +72 -0
  17. astreum/consensus/models/block.py +364 -0
  18. astreum/{_consensus → consensus/models}/chain.py +7 -7
  19. astreum/{_consensus → consensus/models}/fork.py +8 -8
  20. astreum/consensus/models/receipt.py +98 -0
  21. astreum/consensus/models/transaction.py +213 -0
  22. astreum/{_consensus → consensus}/setup.py +26 -11
  23. astreum/consensus/start.py +68 -0
  24. astreum/consensus/validator.py +95 -0
  25. astreum/{_consensus → consensus}/workers/discovery.py +20 -1
  26. astreum/consensus/workers/validation.py +291 -0
  27. astreum/{_consensus → consensus}/workers/verify.py +32 -3
  28. astreum/machine/__init__.py +20 -0
  29. astreum/machine/evaluations/__init__.py +0 -0
  30. astreum/machine/evaluations/high_evaluation.py +237 -0
  31. astreum/machine/evaluations/low_evaluation.py +281 -0
  32. astreum/machine/evaluations/script_evaluation.py +27 -0
  33. astreum/machine/models/__init__.py +0 -0
  34. astreum/machine/models/environment.py +31 -0
  35. astreum/machine/models/expression.py +218 -0
  36. astreum/{_lispeum → machine}/parser.py +26 -31
  37. astreum/machine/tokenizer.py +90 -0
  38. astreum/node.py +73 -781
  39. astreum/storage/__init__.py +7 -0
  40. astreum/storage/actions/get.py +69 -0
  41. astreum/storage/actions/set.py +132 -0
  42. astreum/storage/models/atom.py +107 -0
  43. astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
  44. astreum/storage/setup.py +44 -15
  45. astreum/utils/bytes.py +24 -0
  46. astreum/utils/integer.py +25 -0
  47. astreum/utils/logging.py +219 -0
  48. astreum-0.3.1.dist-info/METADATA +160 -0
  49. astreum-0.3.1.dist-info/RECORD +62 -0
  50. astreum/_communication/peer.py +0 -11
  51. astreum/_consensus/__init__.py +0 -20
  52. astreum/_consensus/account.py +0 -170
  53. astreum/_consensus/accounts.py +0 -67
  54. astreum/_consensus/block.py +0 -328
  55. astreum/_consensus/genesis.py +0 -141
  56. astreum/_consensus/receipt.py +0 -177
  57. astreum/_consensus/transaction.py +0 -192
  58. astreum/_consensus/workers/validation.py +0 -122
  59. astreum/_lispeum/__init__.py +0 -16
  60. astreum/_lispeum/environment.py +0 -13
  61. astreum/_lispeum/expression.py +0 -37
  62. astreum/_lispeum/high_evaluation.py +0 -177
  63. astreum/_lispeum/low_evaluation.py +0 -123
  64. astreum/_lispeum/tokenizer.py +0 -22
  65. astreum/_node.py +0 -58
  66. astreum/_storage/__init__.py +0 -5
  67. astreum/_storage/atom.py +0 -117
  68. astreum/format.py +0 -75
  69. astreum/models/block.py +0 -441
  70. astreum/models/merkle.py +0 -205
  71. astreum/models/patricia.py +0 -393
  72. astreum/storage/object.py +0 -68
  73. astreum-0.2.41.dist-info/METADATA +0 -146
  74. astreum-0.2.41.dist-info/RECORD +0 -53
  75. /astreum/{models → communication/handlers}/__init__.py +0 -0
  76. /astreum/{_communication → communication/models}/ping.py +0 -0
  77. /astreum/{_communication → communication}/util.py +0 -0
  78. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  79. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  80. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
  81. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
  82. {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 src.astreum._lispeum import Expr
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
- # 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
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:]