astreum 0.2.61__py3-none-any.whl → 0.3.9__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 (86) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +89 -0
  4. astreum/communication/handlers/object_request.py +176 -0
  5. astreum/communication/handlers/object_response.py +115 -0
  6. astreum/communication/handlers/ping.py +34 -0
  7. astreum/communication/handlers/route_request.py +76 -0
  8. astreum/communication/handlers/route_response.py +53 -0
  9. astreum/communication/models/__init__.py +0 -0
  10. astreum/communication/models/message.py +124 -0
  11. astreum/communication/models/peer.py +51 -0
  12. astreum/{_communication → communication/models}/route.py +7 -12
  13. astreum/communication/processors/__init__.py +0 -0
  14. astreum/communication/processors/incoming.py +98 -0
  15. astreum/communication/processors/outgoing.py +20 -0
  16. astreum/communication/setup.py +166 -0
  17. astreum/communication/start.py +37 -0
  18. astreum/{_communication → communication}/util.py +7 -0
  19. astreum/consensus/__init__.py +20 -0
  20. astreum/consensus/genesis.py +66 -0
  21. astreum/consensus/models/__init__.py +0 -0
  22. astreum/consensus/models/account.py +84 -0
  23. astreum/consensus/models/accounts.py +72 -0
  24. astreum/consensus/models/block.py +364 -0
  25. astreum/{_consensus → consensus/models}/chain.py +7 -7
  26. astreum/{_consensus → consensus/models}/fork.py +8 -8
  27. astreum/consensus/models/receipt.py +98 -0
  28. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  29. astreum/{_consensus → consensus}/setup.py +18 -50
  30. astreum/consensus/start.py +67 -0
  31. astreum/consensus/validator.py +95 -0
  32. astreum/{_consensus → consensus}/workers/discovery.py +19 -1
  33. astreum/consensus/workers/validation.py +307 -0
  34. astreum/{_consensus → consensus}/workers/verify.py +29 -2
  35. astreum/crypto/chacha20poly1305.py +74 -0
  36. astreum/machine/__init__.py +20 -0
  37. astreum/machine/evaluations/__init__.py +0 -0
  38. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
  39. astreum/machine/evaluations/low_evaluation.py +281 -0
  40. astreum/machine/evaluations/script_evaluation.py +27 -0
  41. astreum/machine/models/__init__.py +0 -0
  42. astreum/machine/models/environment.py +31 -0
  43. astreum/{_lispeum → machine/models}/expression.py +36 -8
  44. astreum/machine/tokenizer.py +90 -0
  45. astreum/node.py +78 -767
  46. astreum/storage/__init__.py +7 -0
  47. astreum/storage/actions/get.py +183 -0
  48. astreum/storage/actions/set.py +178 -0
  49. astreum/{_storage → storage/models}/atom.py +55 -57
  50. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  51. astreum/storage/requests.py +28 -0
  52. astreum/storage/setup.py +22 -15
  53. astreum/utils/config.py +48 -0
  54. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
  55. astreum-0.3.9.dist-info/RECORD +71 -0
  56. astreum/_communication/message.py +0 -101
  57. astreum/_communication/peer.py +0 -23
  58. astreum/_communication/setup.py +0 -322
  59. astreum/_consensus/__init__.py +0 -20
  60. astreum/_consensus/account.py +0 -95
  61. astreum/_consensus/accounts.py +0 -38
  62. astreum/_consensus/block.py +0 -311
  63. astreum/_consensus/genesis.py +0 -72
  64. astreum/_consensus/receipt.py +0 -136
  65. astreum/_consensus/workers/validation.py +0 -125
  66. astreum/_lispeum/__init__.py +0 -16
  67. astreum/_lispeum/environment.py +0 -13
  68. astreum/_lispeum/low_evaluation.py +0 -123
  69. astreum/_lispeum/tokenizer.py +0 -22
  70. astreum/_node.py +0 -198
  71. astreum/_storage/__init__.py +0 -7
  72. astreum/_storage/setup.py +0 -35
  73. astreum/format.py +0 -75
  74. astreum/models/block.py +0 -441
  75. astreum/models/merkle.py +0 -205
  76. astreum/models/patricia.py +0 -393
  77. astreum/storage/object.py +0 -68
  78. astreum-0.2.61.dist-info/RECORD +0 -57
  79. /astreum/{models → communication/handlers}/__init__.py +0 -0
  80. /astreum/{_communication → communication/models}/ping.py +0 -0
  81. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  82. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  83. /astreum/{_lispeum → machine}/parser.py +0 -0
  84. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
  85. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
  86. {astreum-0.2.61.dist-info → astreum-0.3.9.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
@@ -1,6 +1,6 @@
1
1
  from typing import Any, List, Optional, Tuple
2
2
 
3
- from .._storage.atom import Atom, AtomKind
3
+ from ...storage.models.atom import Atom, AtomKind
4
4
 
5
5
  ZERO32 = b"\x00" * 32
6
6
  ERROR_SYMBOL = "error"
@@ -88,7 +88,7 @@ class Expr:
88
88
 
89
89
  if kind_enum is AtomKind.LIST:
90
90
  # Empty list sentinel: zero-length payload and no next pointer.
91
- if len(type_atom.data) == 0 and type_atom.next == ZERO32:
91
+ if len(type_atom.data) == 0 and type_atom.next_id == ZERO32:
92
92
  return cls.ListExpr([])
93
93
 
94
94
  elements: List[Expr] = []
@@ -102,8 +102,8 @@ class Expr:
102
102
  raise ValueError("list element hash has unexpected length")
103
103
  child_expr = cls.from_atoms(node, child_hash)
104
104
  elements.append(child_expr)
105
- next_id = current_atom.next
106
- if not next_id or next_id == ZERO32:
105
+ next_id = current_atom.next_id
106
+ if next_id == ZERO32:
107
107
  break
108
108
  next_atom = _require(next_id, f"list element {idx}")
109
109
  next_kind = _atom_kind(next_atom)
@@ -118,11 +118,17 @@ class Expr:
118
118
  @staticmethod
119
119
  def to_atoms(e: "Expr") -> Tuple[bytes, List[Atom]]:
120
120
  def symbol(value: str) -> Tuple[bytes, List[Atom]]:
121
- atom = Atom.from_data(data=value.encode("utf-8"), kind=AtomKind.SYMBOL)
121
+ atom = Atom(
122
+ data=value.encode("utf-8"),
123
+ kind=AtomKind.SYMBOL,
124
+ )
122
125
  return atom.object_id(), [atom]
123
126
 
124
127
  def bytes_value(data: bytes) -> Tuple[bytes, List[Atom]]:
125
- atom = Atom.from_data(data=data, kind=AtomKind.BYTES)
128
+ atom = Atom(
129
+ data=data,
130
+ kind=AtomKind.BYTES,
131
+ )
126
132
  return atom.object_id(), [atom]
127
133
 
128
134
  def lst(items: List["Expr"]) -> Tuple[bytes, List[Atom]]:
@@ -135,14 +141,14 @@ class Expr:
135
141
  next_hash = ZERO32
136
142
  elem_atoms: List[Atom] = []
137
143
  for h in reversed(child_hashes):
138
- a = Atom.from_data(h, next_hash, kind=AtomKind.LIST)
144
+ a = Atom(data=h, next_id=next_hash, kind=AtomKind.LIST)
139
145
  next_hash = a.object_id()
140
146
  elem_atoms.append(a)
141
147
  elem_atoms.reverse()
142
148
  if elem_atoms:
143
149
  head = elem_atoms[0].object_id()
144
150
  else:
145
- empty_atom = Atom.from_data(data=b"", next_hash=ZERO32, kind=AtomKind.LIST)
151
+ empty_atom = Atom(data=b"", kind=AtomKind.LIST)
146
152
  elem_atoms = [empty_atom]
147
153
  head = empty_atom.object_id()
148
154
  return head, acc + elem_atoms
@@ -188,3 +194,25 @@ def error_expr(topic: str, message: str) -> Expr.ListExpr:
188
194
  Expr.Bytes(topic_bytes),
189
195
  Expr.Bytes(message_bytes),
190
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)
@@ -0,0 +1,90 @@
1
+ from typing import List
2
+
3
+
4
+ def tokenize(source: str) -> List[str]:
5
+ tokens: List[str] = []
6
+ cur: List[str] = []
7
+ n = len(source)
8
+ i = 0
9
+
10
+ def flush_cur() -> None:
11
+ if cur:
12
+ tokens.append("".join(cur))
13
+ cur.clear()
14
+
15
+ def skip_line_comment(idx: int) -> int:
16
+ while idx < n and source[idx] != "\n":
17
+ idx += 1
18
+ return idx
19
+
20
+ def skip_ws_and_comments(idx: int) -> int:
21
+ while idx < n:
22
+ ch = source[idx]
23
+ if ch.isspace():
24
+ flush_cur()
25
+ idx += 1
26
+ continue
27
+ if ch == ";":
28
+ flush_cur()
29
+ idx = skip_line_comment(idx + 1)
30
+ continue
31
+ break
32
+ return idx
33
+
34
+ def skip_expression(idx: int) -> int:
35
+ idx = skip_ws_and_comments(idx)
36
+ if idx >= n:
37
+ return n
38
+ ch = source[idx]
39
+ if ch == "(":
40
+ depth = 0
41
+ while idx < n:
42
+ ch = source[idx]
43
+ if ch == "(":
44
+ depth += 1
45
+ idx += 1
46
+ continue
47
+ if ch == ")":
48
+ depth -= 1
49
+ idx += 1
50
+ if depth == 0:
51
+ break
52
+ continue
53
+ if ch == ";":
54
+ idx = skip_line_comment(idx + 1)
55
+ continue
56
+ if ch == "#" and idx + 1 < n and source[idx + 1] == ";":
57
+ idx = skip_expression(idx + 2)
58
+ continue
59
+ idx += 1
60
+ return idx
61
+ if ch == ")":
62
+ return idx + 1
63
+ while idx < n:
64
+ ch = source[idx]
65
+ if ch.isspace() or ch in ("(", ")", ";"):
66
+ break
67
+ if ch == "#" and idx + 1 < n and source[idx + 1] == ";":
68
+ break
69
+ idx += 1
70
+ return idx
71
+
72
+ while i < n:
73
+ i = skip_ws_and_comments(i)
74
+ if i >= n:
75
+ break
76
+ ch = source[i]
77
+ if ch == "#" and i + 1 < n and source[i + 1] == ";":
78
+ flush_cur()
79
+ i = skip_expression(i + 2)
80
+ continue
81
+ if ch in ("(", ")"):
82
+ flush_cur()
83
+ tokens.append(ch)
84
+ i += 1
85
+ continue
86
+ cur.append(ch)
87
+ i += 1
88
+
89
+ flush_cur()
90
+ return tokens