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
@@ -1,177 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import Callable, List, Optional, Tuple
5
-
6
- from .._storage.atom import Atom, ZERO32
7
-
8
- STATUS_SUCCESS = 0
9
- STATUS_FAILED = 1
10
-
11
-
12
- def _int_to_be_bytes(value: Optional[int]) -> bytes:
13
- if value is None:
14
- return b""
15
- value = int(value)
16
- if value == 0:
17
- return b"\x00"
18
- size = (value.bit_length() + 7) // 8
19
- return value.to_bytes(size, "big")
20
-
21
-
22
- def _be_bytes_to_int(data: Optional[bytes]) -> int:
23
- if not data:
24
- return 0
25
- return int.from_bytes(data, "big")
26
-
27
-
28
- def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
29
- atoms: List[Atom] = []
30
- next_hash = ZERO32
31
- elements: List[Atom] = []
32
- for child_id in reversed(child_ids):
33
- elem = Atom.from_data(data=child_id, next_hash=next_hash)
34
- next_hash = elem.object_id()
35
- elements.append(elem)
36
- elements.reverse()
37
- list_value = Atom.from_data(data=len(child_ids).to_bytes(8, "little"), next_hash=next_hash)
38
- list_type = Atom.from_data(data=b"list", next_hash=list_value.object_id())
39
- atoms.extend(elements)
40
- atoms.append(list_value)
41
- atoms.append(list_type)
42
- return list_type.object_id(), atoms
43
-
44
-
45
- def _read_list_entries(
46
- storage_get: Callable[[bytes], Optional[Atom]], start: bytes
47
- ) -> List[bytes]:
48
- entries: List[bytes] = []
49
- current = start if start and start != ZERO32 else b""
50
- while current:
51
- elem = storage_get(current)
52
- if elem is None:
53
- break
54
- entries.append(elem.data)
55
- nxt = elem.next
56
- current = nxt if nxt and nxt != ZERO32 else b""
57
- return entries
58
-
59
-
60
- def _read_payload_bytes(
61
- storage_get: Callable[[bytes], Optional[Atom]], object_id: bytes
62
- ) -> bytes:
63
- if not object_id or object_id == ZERO32:
64
- return b""
65
- atom = storage_get(object_id)
66
- if atom is None:
67
- return b""
68
- if atom.data == b"bytes":
69
- value_atom = storage_get(atom.next)
70
- return value_atom.data if value_atom is not None else b""
71
- return atom.data
72
-
73
-
74
- @dataclass
75
- class Receipt:
76
- transaction_hash: bytes = ZERO32
77
- cost: int = 0
78
- logs: bytes = b""
79
- status: int = 0
80
- hash: bytes = ZERO32
81
- atoms: List[Atom] = field(default_factory=list)
82
-
83
- def to_atom(self) -> Tuple[bytes, List[Atom]]:
84
- """Serialise the receipt into Atom storage."""
85
- if self.status not in (STATUS_SUCCESS, STATUS_FAILED):
86
- raise ValueError("unsupported receipt status")
87
-
88
- atoms: List[Atom] = []
89
-
90
- tx_atom = Atom.from_data(data=bytes(self.transaction_hash))
91
- status_atom = Atom.from_data(data=_int_to_be_bytes(self.status))
92
- cost_atom = Atom.from_data(data=_int_to_be_bytes(self.cost))
93
- logs_atom = Atom.from_data(data=bytes(self.logs))
94
-
95
- atoms.extend([tx_atom, status_atom, cost_atom, logs_atom])
96
-
97
- body_child_ids = [
98
- tx_atom.object_id(),
99
- status_atom.object_id(),
100
- cost_atom.object_id(),
101
- logs_atom.object_id(),
102
- ]
103
- body_id, body_atoms = _make_list(body_child_ids)
104
- atoms.extend(body_atoms)
105
-
106
- type_atom = Atom.from_data(data=b"receipt", next_hash=body_id)
107
- atoms.append(type_atom)
108
-
109
- top_list_id, top_atoms = _make_list([type_atom.object_id(), body_id])
110
- atoms.extend(top_atoms)
111
-
112
- return top_list_id, atoms
113
-
114
- def atomize(self) -> Tuple[bytes, List[Atom]]:
115
- """Generate atoms for this receipt and cache them."""
116
- receipt_id, atoms = self.to_atom()
117
- self.hash = receipt_id
118
- self.atoms = atoms
119
- return receipt_id, atoms
120
-
121
- @classmethod
122
- def from_atom(
123
- cls,
124
- storage_get: Callable[[bytes], Optional[Atom]],
125
- receipt_id: bytes,
126
- ) -> Receipt:
127
- """Materialise a Receipt from Atom storage."""
128
- top_type_atom = storage_get(receipt_id)
129
- if top_type_atom is None or top_type_atom.data != b"list":
130
- raise ValueError("not a receipt (outer list missing)")
131
-
132
- top_value_atom = storage_get(top_type_atom.next)
133
- if top_value_atom is None:
134
- raise ValueError("malformed receipt (outer value missing)")
135
-
136
- head = top_value_atom.next
137
- first_elem = storage_get(head) if head else None
138
- if first_elem is None:
139
- raise ValueError("malformed receipt (type element missing)")
140
-
141
- type_atom_id = first_elem.data
142
- type_atom = storage_get(type_atom_id)
143
- if type_atom is None or type_atom.data != b"receipt":
144
- raise ValueError("not a receipt (type mismatch)")
145
-
146
- remainder_entries = _read_list_entries(storage_get, first_elem.next)
147
- if not remainder_entries:
148
- raise ValueError("malformed receipt (body missing)")
149
- body_id = remainder_entries[0]
150
-
151
- body_type_atom = storage_get(body_id)
152
- if body_type_atom is None or body_type_atom.data != b"list":
153
- raise ValueError("malformed receipt body (type)")
154
-
155
- body_value_atom = storage_get(body_type_atom.next)
156
- if body_value_atom is None:
157
- raise ValueError("malformed receipt body (value)")
158
-
159
- body_entries = _read_list_entries(storage_get, body_value_atom.next)
160
- if len(body_entries) < 4:
161
- body_entries.extend([ZERO32] * (4 - len(body_entries)))
162
-
163
- transaction_hash_bytes = _read_payload_bytes(storage_get, body_entries[0])
164
- status_bytes = _read_payload_bytes(storage_get, body_entries[1])
165
- cost_bytes = _read_payload_bytes(storage_get, body_entries[2])
166
- logs_bytes = _read_payload_bytes(storage_get, body_entries[3])
167
- status_value = _be_bytes_to_int(status_bytes)
168
- if status_value not in (STATUS_SUCCESS, STATUS_FAILED):
169
- raise ValueError("unsupported receipt status")
170
-
171
- return cls(
172
- transaction_hash=transaction_hash_bytes or ZERO32,
173
- cost=_be_bytes_to_int(cost_bytes),
174
- logs=logs_bytes,
175
- status=status_value,
176
- hash=bytes(receipt_id),
177
- )
@@ -1,192 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Any, List, Optional, Tuple
5
-
6
- from .._storage.atom import Atom, ZERO32
7
- from .receipt import Receipt, STATUS_SUCCESS
8
-
9
-
10
- def _int_to_be_bytes(value: Optional[int]) -> bytes:
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
-
43
-
44
- @dataclass
45
- class Transaction:
46
- amount: int
47
- counter: int
48
- data: bytes = b""
49
- recipient: bytes = b""
50
- sender: bytes = b""
51
- signature: bytes = b""
52
- hash: bytes = ZERO32
53
-
54
- def to_atom(self) -> Tuple[bytes, List[Atom]]:
55
- """Serialise the transaction, returning (object_id, atoms)."""
56
- body_child_ids: List[bytes] = []
57
- acc: List[Atom] = []
58
-
59
- def emit(payload: bytes) -> None:
60
- atom = Atom.from_data(data=payload)
61
- body_child_ids.append(atom.object_id())
62
- acc.append(atom)
63
-
64
- emit(_int_to_be_bytes(self.amount))
65
- emit(_int_to_be_bytes(self.counter))
66
- emit(bytes(self.data))
67
- emit(bytes(self.recipient))
68
- emit(bytes(self.sender))
69
-
70
- body_id, body_atoms = _make_list(body_child_ids)
71
- acc.extend(body_atoms)
72
-
73
- type_atom = Atom.from_data(data=b"transaction", next_hash=body_id)
74
- signature_atom = Atom.from_data(data=bytes(self.signature))
75
-
76
- top_list_id, top_atoms = _make_list(
77
- [type_atom.object_id(), body_id, signature_atom.object_id()]
78
- )
79
-
80
- atoms: List[Atom] = acc
81
- atoms.append(type_atom)
82
- atoms.append(signature_atom)
83
- atoms.extend(top_atoms)
84
- return top_list_id, atoms
85
-
86
- @classmethod
87
- def from_atom(
88
- cls,
89
- node: Any,
90
- transaction_id: bytes,
91
- ) -> Transaction:
92
- storage_get = node._local_get
93
- if not callable(storage_get):
94
- raise NotImplementedError("node does not expose a storage getter")
95
-
96
- top_type_atom = storage_get(transaction_id)
97
- if top_type_atom is None or top_type_atom.data != b"list":
98
- raise ValueError("not a transaction (outer list missing)")
99
-
100
- top_value_atom = storage_get(top_type_atom.next)
101
- if top_value_atom is None:
102
- raise ValueError("malformed transaction (outer value missing)")
103
-
104
- head = top_value_atom.next
105
- first_elem = storage_get(head)
106
- if first_elem is None:
107
- raise ValueError("malformed transaction (type element missing)")
108
-
109
- type_atom_id = first_elem.data
110
- type_atom = storage_get(type_atom_id)
111
- if type_atom is None or type_atom.data != b"transaction":
112
- raise ValueError("not a transaction (type mismatch)")
113
-
114
- def read_list_entries(start: bytes) -> List[bytes]:
115
- entries: List[bytes] = []
116
- current = start if start != ZERO32 else b""
117
- while current:
118
- elem = storage_get(current)
119
- if elem is None:
120
- break
121
- entries.append(elem.data)
122
- nxt = elem.next
123
- current = nxt if nxt != ZERO32 else b""
124
- return entries
125
-
126
- remainder_entries = read_list_entries(first_elem.next)
127
- if len(remainder_entries) < 2:
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:
149
- return b""
150
- detail_atom = storage_get(elem.data)
151
- return detail_atom.data if detail_atom is not None else b""
152
-
153
- amount_bytes = read_detail_bytes(body_entries[0])
154
- counter_bytes = read_detail_bytes(body_entries[1])
155
- data_bytes = read_detail_bytes(body_entries[2])
156
- recipient_bytes = read_detail_bytes(body_entries[3])
157
- sender_bytes = read_detail_bytes(body_entries[4])
158
-
159
- signature_atom = storage_get(signature_atom_id)
160
- signature_bytes = signature_atom.data if signature_atom is not None else b""
161
-
162
- return cls(
163
- amount=_be_bytes_to_int(amount_bytes),
164
- counter=_be_bytes_to_int(counter_bytes),
165
- data=data_bytes,
166
- recipient=recipient_bytes,
167
- sender=sender_bytes,
168
- signature=signature_bytes,
169
- hash=bytes(transaction_id),
170
- )
171
-
172
-
173
- def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None:
174
- """Apply transaction to the candidate block. Override downstream."""
175
- transaction = Transaction.from_atom(node, transaction_hash)
176
-
177
- if block.transactions is None:
178
- block.transactions = []
179
- block.transactions.append(transaction)
180
-
181
- receipt = Receipt(
182
- transaction_hash=bytes(transaction_hash),
183
- cost=0,
184
- logs=b"",
185
- status=STATUS_SUCCESS,
186
- )
187
- receipt.atomize()
188
- if block.receipts is None:
189
- block.receipts = []
190
- block.receipts.append(receipt)
191
-
192
- # Downstream implementations can extend this to apply state changes.
@@ -1,122 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- from queue import Empty
5
- from typing import Any, Callable
6
-
7
- from ..block import Block
8
- from ..transaction import apply_transaction
9
- from ..._storage.atom import bytes_list_to_atoms
10
- from ..._storage.patricia import PatriciaTrie
11
- from ..._communication.message import Message, MessageTopic
12
- from ..._communication.ping import Ping
13
-
14
-
15
- def make_validation_worker(
16
- node: Any,
17
- *,
18
- current_validator: Callable[[Any], bytes],
19
- ) -> Callable[[], None]:
20
- """Build the validation worker bound to the given node."""
21
-
22
- def _validation_worker() -> None:
23
- stop = node._validation_stop_event
24
- while not stop.is_set():
25
- validation_public_key = getattr(node, "validation_public_key", None)
26
- if not validation_public_key:
27
- time.sleep(0.5)
28
- continue
29
-
30
- scheduled_validator = current_validator(node)
31
-
32
- if scheduled_validator != validation_public_key:
33
- time.sleep(0.5)
34
- continue
35
-
36
- try:
37
- current_hash = node._validation_transaction_queue.get_nowait()
38
- except Empty:
39
- time.sleep(0.1)
40
- continue
41
-
42
- # create thread to perform vdf
43
-
44
- new_block = Block()
45
- new_block.validator_public_key = validation_public_key
46
- new_block.previous_block_hash = node.latest_block_hash
47
- try:
48
- new_block.previous_block = Block.from_atom(node, new_block.previous_block_hash)
49
- except Exception:
50
- continue
51
- new_block.accounts = PatriciaTrie(root_hash=new_block.previous_block.accounts_hash)
52
-
53
- # we may want to add a timer to process part of the txs only on a slow computer
54
- while True:
55
- try:
56
- apply_transaction(node, new_block, current_hash)
57
- except NotImplementedError:
58
- node._validation_transaction_queue.put(current_hash)
59
- time.sleep(0.5)
60
- break
61
- except Exception:
62
- pass
63
-
64
- try:
65
- current_hash = node._validation_transaction_queue.get_nowait()
66
- except Empty:
67
- break
68
-
69
- # create an atom list of transactions, save the list head hash as the block's transactions_hash
70
- transactions = new_block.transactions or []
71
- tx_hashes = [bytes(tx.hash) for tx in transactions if tx.hash]
72
- head_hash, _ = bytes_list_to_atoms(tx_hashes)
73
- new_block.transactions_hash = head_hash
74
-
75
- receipts = new_block.receipts or []
76
- receipt_hashes = [bytes(rcpt.hash) for rcpt in receipts if rcpt.hash]
77
- receipts_head, _ = bytes_list_to_atoms(receipt_hashes)
78
- new_block.receipts_hash = receipts_head
79
-
80
- # get vdf result, default to 0 for now
81
-
82
- # get timestamp or wait for a the next second from the previous block, rule is the next block must be atleast 1 second after the previous
83
- now = time.time()
84
- min_allowed = new_block.previous_block.timestamp + 1
85
- if now < min_allowed:
86
- time.sleep(max(0.0, min_allowed - now))
87
- now = time.time()
88
- new_block.timestamp = max(int(now), min_allowed)
89
-
90
- # atomize block
91
- new_block_hash, _ = new_block.to_atom()
92
- # put as own latest block hash
93
- node.latest_block_hash = new_block_hash
94
-
95
- # ping peers in the validation route to update there records
96
- if node.validation_route and node.outgoing_queue and node.addresses:
97
- route_peers = {
98
- peer_key
99
- for bucket in getattr(node.validation_route, "buckets", {}).values()
100
- for peer_key in bucket
101
- }
102
- if route_peers:
103
- ping_payload = Ping(
104
- is_validator=True,
105
- latest_block=new_block_hash,
106
- ).to_bytes()
107
-
108
- message_bytes = Message(
109
- topic=MessageTopic.PING,
110
- content=ping_payload,
111
- ).to_bytes()
112
-
113
- for address, peer_key in node.addresses.items():
114
- if peer_key in route_peers:
115
- try:
116
- node.outgoing_queue.put((message_bytes, address))
117
- except Exception:
118
- pass
119
-
120
- # store the new block and receipts
121
-
122
- return _validation_worker
@@ -1,16 +0,0 @@
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, ParseError
6
- from .tokenizer import tokenize
7
-
8
- __all__ = [
9
- "Env",
10
- "Expr",
11
- "low_eval",
12
- "Meter",
13
- "parse",
14
- "tokenize",
15
- "ParseError",
16
- ]
@@ -1,13 +0,0 @@
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[bytes, Expr] = {} if data is None else data
13
- self.parent_id = parent_id
@@ -1,37 +0,0 @@
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} list)"
14
-
15
- class Symbol:
16
- def __init__(self, value: str):
17
- self.value = value
18
-
19
- def __repr__(self):
20
- return f"({self.value} symbol)"
21
-
22
- class Bytes:
23
- def __init__(self, value: bytes):
24
- self.value = value
25
-
26
- def __repr__(self):
27
- return f"({self.value} bytes)"
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} error)'
37
- return f'({self.origin} {self.topic} error)'