astreum 0.2.31__tar.gz → 0.2.33__tar.gz

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.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

Files changed (41) hide show
  1. {astreum-0.2.31/src/astreum.egg-info → astreum-0.2.33}/PKG-INFO +35 -14
  2. {astreum-0.2.31 → astreum-0.2.33}/README.md +34 -13
  3. {astreum-0.2.31 → astreum-0.2.33}/pyproject.toml +1 -1
  4. astreum-0.2.33/src/astreum/_node.py +492 -0
  5. {astreum-0.2.31 → astreum-0.2.33/src/astreum.egg-info}/PKG-INFO +35 -14
  6. astreum-0.2.31/src/astreum/_node.py +0 -382
  7. {astreum-0.2.31 → astreum-0.2.33}/LICENSE +0 -0
  8. {astreum-0.2.31 → astreum-0.2.33}/setup.cfg +0 -0
  9. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/__init__.py +0 -0
  10. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/crypto/__init__.py +0 -0
  11. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/crypto/ed25519.py +0 -0
  12. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/crypto/quadratic_form.py +0 -0
  13. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/crypto/wesolowski.py +0 -0
  14. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/crypto/x25519.py +0 -0
  15. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/format.py +0 -0
  16. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/lispeum/__init__.py +0 -0
  17. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/lispeum/environment.py +0 -0
  18. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/lispeum/expression.py +0 -0
  19. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/lispeum/parser.py +0 -0
  20. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/lispeum/tokenizer.py +0 -0
  21. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/__init__.py +0 -0
  22. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/account.py +0 -0
  23. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/accounts.py +0 -0
  24. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/block.py +0 -0
  25. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/merkle.py +0 -0
  26. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/message.py +0 -0
  27. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/patricia.py +0 -0
  28. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/models/transaction.py +0 -0
  29. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/node.py +0 -0
  30. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/relay/__init__.py +0 -0
  31. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/relay/peer.py +0 -0
  32. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/relay/route.py +0 -0
  33. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/relay/setup.py +0 -0
  34. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/storage/__init__.py +0 -0
  35. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/storage/object.py +0 -0
  36. {astreum-0.2.31 → astreum-0.2.33}/src/astreum/storage/setup.py +0 -0
  37. {astreum-0.2.31 → astreum-0.2.33}/src/astreum.egg-info/SOURCES.txt +0 -0
  38. {astreum-0.2.31 → astreum-0.2.33}/src/astreum.egg-info/dependency_links.txt +0 -0
  39. {astreum-0.2.31 → astreum-0.2.33}/src/astreum.egg-info/requires.txt +0 -0
  40. {astreum-0.2.31 → astreum-0.2.33}/src/astreum.egg-info/top_level.txt +0 -0
  41. {astreum-0.2.31 → astreum-0.2.33}/tests/test_node_machine.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.31
3
+ Version: 0.2.33
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -76,24 +76,45 @@ node = Node(config)
76
76
  The Lispeum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Lispeum source text, and the node tokenizes, parses, and **evaluates** the resulting AST inside an isolated environment.
77
77
 
78
78
  ```python
79
- from astreum.node import Node
80
- from astreum.machine.tokenizer import tokenize
81
- from astreum.machine.parser import parse
79
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
80
+
81
+ import uuid
82
+ from astreum._node import Node, Env, Expr
83
+
84
+ # 1) Spin‑up a stand‑alone VM
85
+ node = Node()
86
+
87
+ # 2) Create an environment (simple manual setup)
88
+ env_id = uuid.uuid4()
89
+ node.environments[env_id] = Env()
90
+
91
+ # 3) Build a function value using a low‑level stack body via `sk`.
92
+ # Body does: $0 $1 add (i.e., a + b)
93
+ low_body = Expr.ListExpr([
94
+ Expr.Symbol("$0"), # a (first arg)
95
+ Expr.Symbol("$1"), # b (second arg)
96
+ Expr.Symbol("add"),
97
+ ])
82
98
 
83
- # 1. Spin‑up a stand‑alone VM (machine‑only node).
84
- node = Node({"machine-only": True})
99
+ fn_body = Expr.ListExpr([
100
+ Expr.Symbol("a"),
101
+ Expr.Symbol("b"),
102
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
103
+ ])
85
104
 
86
- # 2. Create an environment.
87
- env_id = node.machine_create_environment()
105
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
106
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
88
107
 
89
- # 3. Convert Lispeum source Expr AST.
90
- source = '(+ 1 (* 2 3))'
91
- expr, _ = parse(tokenize(source))
108
+ # 4) Store under the name "int.add"
109
+ node.env_set(env_id, b"int.add", int_add_fn)
92
110
 
93
- # 4. Evaluate
94
- result = node.machine_expr_eval(env_id=env_id, expr=expr) # -> Expr.Integer(7)
111
+ # 5) Retrieve the function and call it with bytes 1 and 2
112
+ bound = node.env_get(env_id, b"int.add")
113
+ call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
114
+ res = node.high_eval(env_id, call)
95
115
 
96
- print(result.value) # 7
116
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
117
+ print([b.value for b in res.elements]) # [3]
97
118
  ```
98
119
 
99
120
  ### Handling errors
@@ -58,24 +58,45 @@ node = Node(config)
58
58
  The Lispeum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Lispeum source text, and the node tokenizes, parses, and **evaluates** the resulting AST inside an isolated environment.
59
59
 
60
60
  ```python
61
- from astreum.node import Node
62
- from astreum.machine.tokenizer import tokenize
63
- from astreum.machine.parser import parse
61
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
62
+
63
+ import uuid
64
+ from astreum._node import Node, Env, Expr
65
+
66
+ # 1) Spin‑up a stand‑alone VM
67
+ node = Node()
68
+
69
+ # 2) Create an environment (simple manual setup)
70
+ env_id = uuid.uuid4()
71
+ node.environments[env_id] = Env()
72
+
73
+ # 3) Build a function value using a low‑level stack body via `sk`.
74
+ # Body does: $0 $1 add (i.e., a + b)
75
+ low_body = Expr.ListExpr([
76
+ Expr.Symbol("$0"), # a (first arg)
77
+ Expr.Symbol("$1"), # b (second arg)
78
+ Expr.Symbol("add"),
79
+ ])
64
80
 
65
- # 1. Spin‑up a stand‑alone VM (machine‑only node).
66
- node = Node({"machine-only": True})
81
+ fn_body = Expr.ListExpr([
82
+ Expr.Symbol("a"),
83
+ Expr.Symbol("b"),
84
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
85
+ ])
67
86
 
68
- # 2. Create an environment.
69
- env_id = node.machine_create_environment()
87
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
88
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
70
89
 
71
- # 3. Convert Lispeum source Expr AST.
72
- source = '(+ 1 (* 2 3))'
73
- expr, _ = parse(tokenize(source))
90
+ # 4) Store under the name "int.add"
91
+ node.env_set(env_id, b"int.add", int_add_fn)
74
92
 
75
- # 4. Evaluate
76
- result = node.machine_expr_eval(env_id=env_id, expr=expr) # -> Expr.Integer(7)
93
+ # 5) Retrieve the function and call it with bytes 1 and 2
94
+ bound = node.env_get(env_id, b"int.add")
95
+ call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
96
+ res = node.high_eval(env_id, call)
77
97
 
78
- print(result.value) # 7
98
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
99
+ print([b.value for b in res.elements]) # [3]
79
100
  ```
80
101
 
81
102
  ### Handling errors
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.2.31"
3
+ version = "0.2.33"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -0,0 +1,492 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, List, Optional, Tuple, Union
3
+ import uuid
4
+ import threading
5
+
6
+ # ===============================
7
+ # 1. Helpers (no decoding, two's complement)
8
+ # ===============================
9
+
10
+ def tc_to_int(b: bytes) -> int:
11
+ """bytes -> int using two's complement (width = len(b)*8)."""
12
+ if not b:
13
+ return 0
14
+ return int.from_bytes(b, "big", signed=True)
15
+
16
+ def int_to_tc(n: int, width_bytes: int) -> bytes:
17
+ """int -> bytes (two's complement, fixed width)."""
18
+ if width_bytes <= 0:
19
+ return b"\x00"
20
+ return n.to_bytes(width_bytes, "big", signed=True)
21
+
22
+ def min_tc_width(n: int) -> int:
23
+ """minimum bytes to store n in two's complement."""
24
+ if n == 0:
25
+ return 1
26
+ w = 1
27
+ while True:
28
+ try:
29
+ n.to_bytes(w, "big", signed=True)
30
+ return w
31
+ except OverflowError:
32
+ w += 1
33
+
34
+ def nand_bytes(a: bytes, b: bytes) -> bytes:
35
+ """Bitwise NAND on two byte strings, zero-extending to max width."""
36
+ w = max(len(a), len(b), 1)
37
+ au = int.from_bytes(a.rjust(w, b"\x00"), "big", signed=False)
38
+ bu = int.from_bytes(b.rjust(w, b"\x00"), "big", signed=False)
39
+ mask = (1 << (w * 8)) - 1
40
+ resu = (~(au & bu)) & mask
41
+ return resu.to_bytes(w, "big", signed=False)
42
+
43
+ def bytes_touched(*vals: bytes) -> int:
44
+ """For metering: how many bytes were manipulated (max of operands)."""
45
+ return max((len(v) for v in vals), default=1)
46
+
47
+ # ===============================
48
+ # 2. Structures
49
+ # ===============================
50
+
51
+ class Expr:
52
+ class ListExpr:
53
+ def __init__(self, elements: List['Expr']):
54
+ self.elements = elements
55
+
56
+ def __repr__(self):
57
+ if not self.elements:
58
+ return "()"
59
+ inner = " ".join(str(e) for e in self.elements)
60
+ return f"({inner})"
61
+
62
+ class Symbol:
63
+ def __init__(self, value: str):
64
+ self.value = value
65
+
66
+ def __repr__(self):
67
+ return self.value
68
+
69
+ class Byte:
70
+ def __init__(self, value: int):
71
+ self.value = value
72
+
73
+ def __repr__(self):
74
+ return self.value
75
+
76
+ class Error:
77
+ def __init__(self, topic: str, origin: Optional['Expr'] = None):
78
+ self.topic = topic
79
+ self.origin = origin
80
+
81
+ def __repr__(self):
82
+ if self.origin is None:
83
+ return f'({self.topic} err)'
84
+ return f'({self.origin} {self.topic} err)'
85
+
86
+ class Env:
87
+ def __init__(
88
+ self,
89
+ data: Optional[Dict[str, Expr]] = None,
90
+ parent_id: Optional[uuid.UUID] = None,
91
+ ):
92
+ self.data: Dict[bytes, Expr] = {} if data is None else data
93
+ self.parent_id = parent_id
94
+
95
+ class Meter:
96
+ def __init__(self, enabled: bool = True, limit: Optional[int] = None):
97
+ self.enabled = enabled
98
+ self.limit: Optional[int] = limit
99
+ self.used: int = 0
100
+
101
+ def charge_bytes(self, n: int) -> bool:
102
+ if not self.enabled:
103
+ return True
104
+ if n < 0:
105
+ n = 0
106
+ if self.limit is not None and (self.used + n) >= self.limit:
107
+ return False
108
+ self.used += n
109
+ return True
110
+
111
+ class Node:
112
+ def __init__(self):
113
+ self.environments: Dict[uuid.UUID, Env] = {}
114
+ self.in_memory_storage: Dict[bytes, bytes] = {}
115
+ self.machine_environments_lock = threading.RLock()
116
+
117
+ # ---- Env helpers ----
118
+ def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
119
+ cur = self.environments.get(env_id)
120
+ while cur is not None:
121
+ if key in cur.data:
122
+ return cur.data[key]
123
+ cur = self.environments.get(cur.parent_id) if cur.parent_id else None
124
+ return None
125
+
126
+ def env_set(self, env_id: uuid.UUID, key: bytes, value: Expr) -> bool:
127
+ with self.machine_environments_lock:
128
+ env = self.environments.get(env_id)
129
+ if env is None:
130
+ return False
131
+ env.data[key] = value
132
+ return True
133
+
134
+ # ---- Storage (persistent) ----
135
+ def _local_get(self, key: bytes) -> Optional[bytes]:
136
+ return self.in_memory_storage.get(key)
137
+
138
+ def _local_set(self, key: bytes, value: bytes) -> None:
139
+ self.in_memory_storage[key] = value
140
+
141
+ # ---- Eval ----
142
+ def low_eval(self, code: List[bytes], meter: Meter) -> Union[bytes, Expr.Error]:
143
+
144
+ heap: Dict[bytes, bytes] = {}
145
+
146
+ stack: List[bytes] = []
147
+ pc = 0
148
+
149
+ while True:
150
+ if pc >= len(code):
151
+ if len(stack) != 1:
152
+ return Expr.Error("bad stack")
153
+ return stack.pop()
154
+
155
+ tok = code[pc]
156
+ pc += 1
157
+
158
+ # ---------- ADD ----------
159
+ if tok == b"add":
160
+ if len(stack) < 2:
161
+ return Expr.Error("underflow")
162
+ b_b = stack.pop()
163
+ a_b = stack.pop()
164
+ a_i = tc_to_int(a_b)
165
+ b_i = tc_to_int(b_b)
166
+ res_i = a_i + b_i
167
+ width = max(len(a_b), len(b_b), min_tc_width(res_i))
168
+ res_b = int_to_tc(res_i, width)
169
+ # charge for both operands' byte widths
170
+ if not meter.charge_bytes(len(a_b) + len(b_b)):
171
+ return Expr.Error("meter limit")
172
+ stack.append(res_b)
173
+ continue
174
+
175
+ # ---------- NAND ----------
176
+ if tok == b"nand":
177
+ if len(stack) < 2:
178
+ return Expr.Error("underflow")
179
+ b_b = stack.pop()
180
+ a_b = stack.pop()
181
+ res_b = nand_bytes(a_b, b_b)
182
+ # bitwise cost: 2 * max(len(a), len(b))
183
+ if not meter.charge_bytes(2 * max(len(a_b), len(b_b), 1)):
184
+ return Expr.Error("meter limit")
185
+ stack.append(res_b)
186
+ continue
187
+
188
+ # ---------- JUMP ----------
189
+ if tok == b"jump":
190
+ if len(stack) < 1:
191
+ return Expr.Error("underflow")
192
+ tgt_b = stack.pop()
193
+ if not meter.charge_bytes(1):
194
+ return Expr.Error("meter limit")
195
+ tgt_i = tc_to_int(tgt_b)
196
+ if tgt_i < 0 or tgt_i >= len(code):
197
+ return Expr.Error("bad jump")
198
+ pc = tgt_i
199
+ continue
200
+
201
+ # ---------- HEAP GET ----------
202
+ if tok == b"heap_get":
203
+ if len(stack) < 1:
204
+ return Expr.Error("underflow")
205
+ key = stack.pop()
206
+ val = heap.get(key) or b""
207
+ # get cost: 1
208
+ if not meter.charge_bytes(1):
209
+ return Expr.Error("meter limit")
210
+ stack.append(val)
211
+ continue
212
+
213
+ # ---------- HEAP SET ----------
214
+ if tok == b"heap_set":
215
+ if len(stack) < 2:
216
+ return Expr.Error("underflow")
217
+ val = stack.pop()
218
+ key = stack.pop()
219
+ if not meter.charge_bytes(len(val)):
220
+ return Expr.Error("meter limit")
221
+ heap[key] = val
222
+ continue
223
+
224
+ # ---------- STORAGE GET ----------
225
+ if tok == b"storage_get":
226
+ if len(stack) < 1:
227
+ return Expr.Error("underflow")
228
+ key = stack.pop()
229
+ val = self._local_get(key) or b""
230
+ if not meter.charge_bytes(1):
231
+ return Expr.Error("meter limit")
232
+ stack.append(val)
233
+ continue
234
+
235
+ # ---------- STORAGE SET ----------
236
+ if tok == b"storage_set":
237
+ if len(stack) < 2:
238
+ return Expr.Error("underflow")
239
+ val = stack.pop()
240
+ key = stack.pop()
241
+ if not meter.charge_bytes(len(val)):
242
+ return Expr.Error("meter limit")
243
+ self._local_set(key, val)
244
+ continue
245
+
246
+ # if no opcode matched above, treat token as literal
247
+
248
+ # not an opcode → literal blob
249
+ stack.append(tok)
250
+
251
+ def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
252
+
253
+ if meter is None:
254
+ meter = Meter()
255
+
256
+ # ---------- atoms ----------
257
+ if isinstance(expr, Expr.Error):
258
+ return expr
259
+
260
+ if isinstance(expr, Expr.Symbol):
261
+ bound = self.env_get(env_id, expr.value.encode())
262
+ if bound is None:
263
+ return Expr.Error(f"unbound symbol '{expr.value}'", origin=expr)
264
+ return bound
265
+
266
+ if not isinstance(expr, Expr.ListExpr):
267
+ return expr # Expr.Byte or other literals passthrough
268
+
269
+ # ---------- empty / single ----------
270
+ if len(expr.elements) == 0:
271
+ return expr
272
+ if len(expr.elements) == 1:
273
+ return self.high_eval(env_id=env_id, expr=expr.elements[0], meter=meter)
274
+
275
+ tail = expr.elements[-1]
276
+
277
+ # ---------- (value name def) ----------
278
+ if isinstance(tail, Expr.Symbol) and tail.value == "def":
279
+ if len(expr.elements) < 3:
280
+ return Expr.Error("def expects (value name def)", origin=expr)
281
+ name_e = expr.elements[-2]
282
+ if not isinstance(name_e, Expr.Symbol):
283
+ return Expr.Error("def name must be symbol", origin=name_e)
284
+ value_e = expr.elements[-3]
285
+ value_res = self.high_eval(env_id=env_id, expr=value_e, meter=meter)
286
+ if isinstance(value_res, Expr.Error):
287
+ return value_res
288
+ self.env_set(env_id, name_e.value.encode(), value_res)
289
+ return value_res
290
+
291
+ # ---- LOW-LEVEL call: ( arg1 arg2 ... ( (body) sk ) ) ----
292
+ if isinstance(tail, Expr.ListExpr):
293
+ inner = tail.elements
294
+ if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
295
+ body_expr = inner[-2]
296
+ if not isinstance(body_expr, Expr.ListExpr):
297
+ return Expr.Error("sk body must be list", origin=body_expr)
298
+
299
+ # helper: turn an Expr into a contiguous bytes buffer
300
+ def to_bytes(v: Expr) -> Union[bytes, Expr.Error]:
301
+ if isinstance(v, Expr.Byte):
302
+ return bytes([v.value & 0xFF])
303
+ if isinstance(v, Expr.ListExpr):
304
+ # expect a list of Expr.Byte
305
+ out: bytearray = bytearray()
306
+ for el in v.elements:
307
+ if isinstance(el, Expr.Byte):
308
+ out.append(el.value & 0xFF)
309
+ else:
310
+ return Expr.Error("byte list must contain only Byte", origin=el)
311
+ return bytes(out)
312
+ if isinstance(v, Expr.Error):
313
+ return v
314
+ return Expr.Error("argument must resolve to Byte or (Byte ...)", origin=v)
315
+
316
+ # resolve ALL preceding args into bytes (can be Byte or List[Byte])
317
+ args_exprs = expr.elements[:-1]
318
+ arg_bytes: List[bytes] = []
319
+ for a in args_exprs:
320
+ v = self.high_eval(env_id=env_id, expr=a, meter=meter)
321
+ if isinstance(v, Expr.Error):
322
+ return v
323
+ vb = to_bytes(v)
324
+ if isinstance(vb, Expr.Error):
325
+ return vb
326
+ arg_bytes.append(vb)
327
+
328
+ # build low-level code with $0-based placeholders ($0 = first arg)
329
+ code: List[bytes] = []
330
+
331
+ def emit(tok: Expr) -> Union[None, Expr.Error]:
332
+ if isinstance(tok, Expr.Symbol):
333
+ name = tok.value
334
+ if name.startswith("$"):
335
+ idx_s = name[1:]
336
+ if not idx_s.isdigit():
337
+ return Expr.Error("invalid sk placeholder", origin=tok)
338
+ idx = int(idx_s) # $0 is first
339
+ if idx < 0 or idx >= len(arg_bytes):
340
+ return Expr.Error("arity mismatch in sk placeholder", origin=tok)
341
+ code.append(arg_bytes[idx])
342
+ return None
343
+ code.append(name.encode())
344
+ return None
345
+
346
+ if isinstance(tok, Expr.Byte):
347
+ code.append(bytes([tok.value & 0xFF]))
348
+ return None
349
+
350
+ if isinstance(tok, Expr.ListExpr):
351
+ rv = self.high_eval(env_id, tok, meter=meter)
352
+ if isinstance(rv, Expr.Error):
353
+ return rv
354
+ rb = to_bytes(rv)
355
+ if isinstance(rb, Expr.Error):
356
+ return rb
357
+ code.append(rb)
358
+ return None
359
+
360
+ if isinstance(tok, Expr.Error):
361
+ return tok
362
+
363
+ return Expr.Error("invalid token in sk body", origin=tok)
364
+
365
+ for t in body_expr.elements:
366
+ err = emit(t)
367
+ if isinstance(err, Expr.Error):
368
+ return err
369
+
370
+ # Execute low-level code built from sk-body using the caller's meter
371
+ res = self.low_eval(code, meter=meter)
372
+ if isinstance(res, Expr.Error):
373
+ return res
374
+ return Expr.ListExpr([Expr.Byte(b) for b in res])
375
+
376
+
377
+
378
+ # ---------- (... (body params fn)) HIGH-LEVEL CALL ----------
379
+ if isinstance(tail, Expr.ListExpr):
380
+ fn_form = tail
381
+ if (len(fn_form.elements) >= 3
382
+ and isinstance(fn_form.elements[-1], Expr.Symbol)
383
+ and fn_form.elements[-1].value == "fn"):
384
+
385
+ body_expr = fn_form.elements[-3]
386
+ params_expr = fn_form.elements[-2]
387
+
388
+ if not isinstance(body_expr, Expr.ListExpr):
389
+ return Expr.Error("fn body must be list", origin=body_expr)
390
+ if not isinstance(params_expr, Expr.ListExpr):
391
+ return Expr.Error("fn params must be list", origin=params_expr)
392
+
393
+ params: List[bytes] = []
394
+ for p in params_expr.elements:
395
+ if not isinstance(p, Expr.Symbol):
396
+ return Expr.Error("fn param must be symbol", origin=p)
397
+ params.append(p.value.encode())
398
+
399
+ args_exprs = expr.elements[:-1]
400
+ if len(args_exprs) != len(params):
401
+ return Expr.Error("arity mismatch", origin=expr)
402
+
403
+ arg_bytes: List[bytes] = []
404
+ for a in args_exprs:
405
+ v = self.high_eval(env_id, a, meter=meter)
406
+ if isinstance(v, Expr.Error):
407
+ return v
408
+ if not isinstance(v, Expr.Byte):
409
+ return Expr.Error("argument must resolve to Byte", origin=a)
410
+ arg_bytes.append(bytes([v.value & 0xFF]))
411
+
412
+ # child env, bind params -> Expr.Byte
413
+ child_env = uuid.uuid4()
414
+ self.environments[child_env] = Env(parent_id=env_id)
415
+ for name_b, val_b in zip(params, arg_bytes):
416
+ self.env_set(child_env, name_b, Expr.Byte(val_b[0]))
417
+
418
+ # evaluate HL body, metered from the top
419
+ return self.high_eval(child_env, body_expr, meter=meter)
420
+
421
+ # ---------- default: resolve each element and return list ----------
422
+ resolved: List[Expr] = [self.high_eval(env_id, e, meter=meter) for e in expr.elements]
423
+ return Expr.ListExpr(resolved)
424
+
425
+ # ===============================
426
+ # 3. Lightweight Parser for Expr (postfix, limited forms)
427
+ # ===============================
428
+
429
+ class ParseError(Exception):
430
+ pass
431
+
432
+ def tokenize(source: str) -> List[str]:
433
+ """Tokenize a minimal Lispeum subset for this module.
434
+
435
+ Supports:
436
+ - Integers (e.g., -1, 0, 255) → Byte
437
+ - Symbols (e.g., add, nand, def, fn, sk, int.sub, $0)
438
+ - Lists using parentheses
439
+ """
440
+ tokens: List[str] = []
441
+ cur: List[str] = []
442
+ for ch in source:
443
+ if ch.isspace():
444
+ if cur:
445
+ tokens.append("".join(cur))
446
+ cur = []
447
+ continue
448
+ if ch in ("(", ")"):
449
+ if cur:
450
+ tokens.append("".join(cur))
451
+ cur = []
452
+ tokens.append(ch)
453
+ continue
454
+ cur.append(ch)
455
+ if cur:
456
+ tokens.append("".join(cur))
457
+ return tokens
458
+
459
+ def _parse_one(tokens: List[str], pos: int = 0) -> Tuple[Expr, int]:
460
+ if pos >= len(tokens):
461
+ raise ParseError("unexpected end")
462
+ tok = tokens[pos]
463
+
464
+ if tok == '(': # list
465
+ items: List[Expr] = []
466
+ i = pos + 1
467
+ while i < len(tokens):
468
+ if tokens[i] == ')':
469
+ # special-case error form at close: (origin topic err) or (topic err)
470
+ if len(items) >= 3 and isinstance(items[-1], Expr.Symbol) and items[-1].value == 'err' and isinstance(items[-2], Expr.Symbol):
471
+ return Expr.Error(items[-2].value, origin=items[-3]), i + 1
472
+ if len(items) == 2 and isinstance(items[-1], Expr.Symbol) and items[-1].value == 'err' and isinstance(items[-2], Expr.Symbol):
473
+ return Expr.Error(items[-2].value), i + 1
474
+ return Expr.ListExpr(items), i + 1
475
+ expr, i = _parse_one(tokens, i)
476
+ items.append(expr)
477
+ raise ParseError("expected ')'")
478
+
479
+ if tok == ')':
480
+ raise ParseError("unexpected ')'")
481
+
482
+ # try integer → Byte
483
+ try:
484
+ n = int(tok)
485
+ return Expr.Byte(n), pos + 1
486
+ except ValueError:
487
+ return Expr.Symbol(tok), pos + 1
488
+
489
+ def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
490
+ """Parse tokens into an Expr and return (expr, remaining_tokens)."""
491
+ expr, next_pos = _parse_one(tokens, 0)
492
+ return expr, tokens[next_pos:]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.31
3
+ Version: 0.2.33
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -76,24 +76,45 @@ node = Node(config)
76
76
  The Lispeum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Lispeum source text, and the node tokenizes, parses, and **evaluates** the resulting AST inside an isolated environment.
77
77
 
78
78
  ```python
79
- from astreum.node import Node
80
- from astreum.machine.tokenizer import tokenize
81
- from astreum.machine.parser import parse
79
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
80
+
81
+ import uuid
82
+ from astreum._node import Node, Env, Expr
83
+
84
+ # 1) Spin‑up a stand‑alone VM
85
+ node = Node()
86
+
87
+ # 2) Create an environment (simple manual setup)
88
+ env_id = uuid.uuid4()
89
+ node.environments[env_id] = Env()
90
+
91
+ # 3) Build a function value using a low‑level stack body via `sk`.
92
+ # Body does: $0 $1 add (i.e., a + b)
93
+ low_body = Expr.ListExpr([
94
+ Expr.Symbol("$0"), # a (first arg)
95
+ Expr.Symbol("$1"), # b (second arg)
96
+ Expr.Symbol("add"),
97
+ ])
82
98
 
83
- # 1. Spin‑up a stand‑alone VM (machine‑only node).
84
- node = Node({"machine-only": True})
99
+ fn_body = Expr.ListExpr([
100
+ Expr.Symbol("a"),
101
+ Expr.Symbol("b"),
102
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
103
+ ])
85
104
 
86
- # 2. Create an environment.
87
- env_id = node.machine_create_environment()
105
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
106
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
88
107
 
89
- # 3. Convert Lispeum source Expr AST.
90
- source = '(+ 1 (* 2 3))'
91
- expr, _ = parse(tokenize(source))
108
+ # 4) Store under the name "int.add"
109
+ node.env_set(env_id, b"int.add", int_add_fn)
92
110
 
93
- # 4. Evaluate
94
- result = node.machine_expr_eval(env_id=env_id, expr=expr) # -> Expr.Integer(7)
111
+ # 5) Retrieve the function and call it with bytes 1 and 2
112
+ bound = node.env_get(env_id, b"int.add")
113
+ call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
114
+ res = node.high_eval(env_id, call)
95
115
 
96
- print(result.value) # 7
116
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
117
+ print([b.value for b in res.elements]) # [3]
97
118
  ```
98
119
 
99
120
  ### Handling errors
@@ -1,382 +0,0 @@
1
- from __future__ import annotations
2
- from typing import Dict, List, Optional
3
- import uuid
4
- import threading
5
-
6
- # ===============================
7
- # 1. Helpers (no decoding, two's complement)
8
- # ===============================
9
-
10
- def tc_to_int(b: bytes) -> int:
11
- """bytes -> int using two's complement (width = len(b)*8)."""
12
- if not b:
13
- return 0
14
- return int.from_bytes(b, "big", signed=True)
15
-
16
- def int_to_tc(n: int, width_bytes: int) -> bytes:
17
- """int -> bytes (two's complement, fixed width)."""
18
- if width_bytes <= 0:
19
- return b"\x00"
20
- return n.to_bytes(width_bytes, "big", signed=True)
21
-
22
- def min_tc_width(n: int) -> int:
23
- """minimum bytes to store n in two's complement."""
24
- if n == 0:
25
- return 1
26
- w = 1
27
- while True:
28
- try:
29
- n.to_bytes(w, "big", signed=True)
30
- return w
31
- except OverflowError:
32
- w += 1
33
-
34
- def nand_bytes(a: bytes, b: bytes) -> bytes:
35
- """Bitwise NAND on two byte strings, zero-extending to max width."""
36
- w = max(len(a), len(b), 1)
37
- au = int.from_bytes(a.rjust(w, b"\x00"), "big", signed=False)
38
- bu = int.from_bytes(b.rjust(w, b"\x00"), "big", signed=False)
39
- mask = (1 << (w * 8)) - 1
40
- resu = (~(au & bu)) & mask
41
- return resu.to_bytes(w, "big", signed=False)
42
-
43
- def bytes_touched(*vals: bytes) -> int:
44
- """For metering: how many bytes were manipulated (max of operands)."""
45
- return max((len(v) for v in vals), default=1)
46
-
47
- # ===============================
48
- # 2. Structures
49
- # ===============================
50
-
51
- class Expr:
52
- class ListExpr:
53
- def __init__(self, elements: List['Expr']):
54
- self.elements = elements
55
-
56
- def __repr__(self):
57
- if not self.elements:
58
- return "()"
59
- inner = " ".join(str(e) for e in self.elements)
60
- return f"({inner})"
61
-
62
- class Symbol:
63
- def __init__(self, value: str):
64
- self.value = value
65
-
66
- def __repr__(self):
67
- return self.value
68
-
69
- class Byte:
70
- def __init__(self, value: int):
71
- self.value = value
72
-
73
- def __repr__(self):
74
- return self.value
75
-
76
- class Error:
77
- def __init__(self, message: str, origin: Optional['Expr'] = None):
78
- self.message = message
79
- self.origin = origin
80
-
81
- def __repr__(self):
82
- if self.origin is None:
83
- return f'(error "{self.message}")'
84
- return f'(error "{self.message}" in {self.origin})'
85
-
86
- class Env:
87
- def __init__(
88
- self,
89
- data: Optional[Dict[str, Expr]] = None,
90
- parent_id: Optional[uuid.UUID] = None,
91
- ):
92
- self.data: Dict[bytes, Expr] = {} if data is None else data
93
- self.parent_id = parent_id
94
-
95
- class Meter:
96
- def __init__(self, prices: Dict[bytes, int], enabled: bool):
97
- self.enabled = enabled
98
- self.prices = prices
99
- self.total = 0
100
-
101
- def charge(self, op: bytes, size: int):
102
- if not self.enabled:
103
- return
104
- self.total += self.prices.get(op, 0) * max(1, size)
105
-
106
- class Node:
107
- OPS = {
108
- b"add", b"nand", b"jump",
109
- b"heap_get", b"heap_set",
110
- b"storage_get", b"storage_set",
111
- }
112
-
113
- def __init__(self):
114
- self.environments: Dict[uuid.UUID, Env] = {}
115
- self.machine_costs: Dict[bytes, int] = {
116
- b"add": 1,
117
- b"nand": 1,
118
- b"jump": 1,
119
- b"heap_set": 1,
120
- b"heap_get": 1,
121
- b"storage_set": 1,
122
- b"storage_get": 1,
123
- }
124
- self.in_memory_storage: Dict[bytes, bytes] = {}
125
- self.machine_environments_lock = threading.RLock()
126
-
127
- # ---- Env helpers ----
128
- def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
129
- cur = self.environments.get(env_id)
130
- while cur is not None:
131
- if key in cur.data:
132
- return cur.data[key]
133
- cur = self.environments.get(cur.parent_id) if cur.parent_id else None
134
- return None
135
-
136
- def env_set(self, env_id: uuid.UUID, key: bytes, value: Expr) -> bool:
137
- with self.machine_environments_lock:
138
- env = self.environments.get(env_id)
139
- if env is None:
140
- return False
141
- env.data[key] = value
142
- return True
143
-
144
- # ---- Storage (persistent) ----
145
- def _local_get(self, key: bytes) -> Optional[bytes]:
146
- return self.in_memory_storage.get(key)
147
-
148
- def _local_set(self, key: bytes, value: bytes) -> None:
149
- self.in_memory_storage[key] = value
150
-
151
- # ---- Eval ----
152
- def low_eval(self, code: List[bytes], metered: bool = False) -> bytes:
153
-
154
- heap: Dict[int, bytes] = []
155
-
156
- meter = Meter(self.machine_costs, enabled=metered)
157
-
158
- stack: List[bytes] = []
159
- pc = 0
160
-
161
- while True:
162
- if pc >= len(code):
163
- if len(stack) != 1:
164
- raise RuntimeError(f"bad stack state at end: {stack}")
165
- return stack.pop()
166
-
167
- tok = code[pc]
168
- pc += 1
169
-
170
- # opcode?
171
- if tok in self.OPS:
172
- # ---------- ADD ----------
173
- if tok == b"add":
174
- b_b = stack.pop()
175
- a_b = stack.pop()
176
- a_i = tc_to_int(a_b)
177
- b_i = tc_to_int(b_b)
178
- res_i = a_i + b_i
179
- width = max(len(a_b), len(b_b), min_tc_width(res_i))
180
- res_b = int_to_tc(res_i, width)
181
- meter.charge(b"add", bytes_touched(a_b, b_b, res_b))
182
- stack.append(res_b)
183
- continue
184
-
185
- # ---------- NAND ----------
186
- if tok == b"nand":
187
- b_b = stack.pop()
188
- a_b = stack.pop()
189
- res_b = nand_bytes(a_b, b_b)
190
- meter.charge(b"nand", bytes_touched(a_b, b_b, res_b))
191
- stack.append(res_b)
192
- continue
193
-
194
- # ---------- JUMP ----------
195
- if tok == b"jump":
196
- tgt_b = stack.pop()
197
- tgt_i = tc_to_int(tgt_b)
198
- meter.charge(b"jump", 0)
199
- pc = tgt_i
200
- continue
201
-
202
- # ---------- HEAP GET ----------
203
- if tok == b"heap_get":
204
- key = stack.pop()
205
- val = heap.get(key) or b""
206
- meter.charge(b"heap_get", len(val))
207
- stack.append(val)
208
- continue
209
-
210
- # ---------- HEAP SET ----------
211
- if tok == b"heap_set":
212
- val = stack.pop()
213
- key = stack.pop()
214
- ok = heap[key] = val
215
- if not ok:
216
- raise RuntimeError("heap_set failed (env missing)")
217
- meter.charge(b"heap_set", len(val))
218
- continue
219
-
220
- # ---------- STORAGE GET ----------
221
- if tok == b"storage_get":
222
- key = stack.pop()
223
- val = self._local_get(key) or b""
224
- meter.charge(b"storage_get", len(val))
225
- stack.append(val)
226
- continue
227
-
228
- # ---------- STORAGE SET ----------
229
- if tok == b"storage_set":
230
- val = stack.pop()
231
- key = stack.pop()
232
- self._local_set(key, val)
233
- meter.charge(b"storage_set", len(val))
234
- continue
235
-
236
- # unreachable
237
- continue
238
-
239
- # not an opcode → literal blob
240
- stack.append(tok)
241
-
242
- def high_eval(self, env_id: uuid.UUID, expr: Expr) -> Expr:
243
- # ---------- atoms ----------
244
- if isinstance(expr, Expr.Error):
245
- return expr
246
-
247
- if isinstance(expr, Expr.Symbol):
248
- bound = self.env_get(env_id, expr.value.encode())
249
- if bound is None:
250
- return Expr.Error(f"unbound symbol '{expr.value}'", origin=expr)
251
- return bound
252
-
253
- if not isinstance(expr, Expr.ListExpr):
254
- return expr # Expr.Byte or other literals passthrough
255
-
256
- # ---------- empty / single ----------
257
- if len(expr.elements) == 0:
258
- return expr
259
- if len(expr.elements) == 1:
260
- return self.high_eval(env_id, expr.elements[0])
261
-
262
- tail = expr.elements[-1]
263
-
264
- # ---------- (value name def) ----------
265
- if isinstance(tail, Expr.Symbol) and tail.value == "def":
266
- if len(expr.elements) < 3:
267
- return Expr.Error("def expects (value name def)", origin=expr)
268
- name_e = expr.elements[-2]
269
- if not isinstance(name_e, Expr.Symbol):
270
- return Expr.Error("def name must be symbol", origin=name_e)
271
- value_e = expr.elements[-3]
272
- value_res = self.high_eval(env_id=env_id, expr=value_e)
273
- if isinstance(value_res, Expr.Error):
274
- return value_res
275
- self.env_set(env_id, name_e.value.encode(), value_res)
276
- return value_res
277
-
278
- # ---------- (... (body params sk)) LOW-LEVEL CALL ----------
279
- if isinstance(tail, Expr.ListExpr):
280
- fn_form = tail
281
- if (len(fn_form.elements) >= 3
282
- and isinstance(fn_form.elements[-1], Expr.Symbol)
283
- and fn_form.elements[-1].value == "sk"):
284
-
285
- body_expr = fn_form.elements[-3]
286
- params_expr = fn_form.elements[-2]
287
-
288
- if not isinstance(body_expr, Expr.ListExpr):
289
- return Expr.Error("sk body must be list", origin=body_expr)
290
- if not isinstance(params_expr, Expr.ListExpr):
291
- return Expr.Error("sk params must be list", origin=params_expr)
292
-
293
- # params → bytes keys
294
- params: List[bytes] = []
295
- for p in params_expr.elements:
296
- if not isinstance(p, Expr.Symbol):
297
- return Expr.Error("sk param must be symbol", origin=p)
298
- params.append(p.value.encode())
299
-
300
- # args: preceding items; MUST resolve to Expr.Byte
301
- args_exprs = expr.elements[:-1]
302
- if len(args_exprs) != len(params):
303
- return Expr.Error("arity mismatch", origin=expr)
304
-
305
- arg_bytes: List[bytes] = []
306
- for a in args_exprs:
307
- v = self.high_eval(env_id, a)
308
- if isinstance(v, Expr.Error):
309
- return v
310
- if not isinstance(v, Expr.Byte):
311
- return Expr.Error("argument must resolve to Byte", origin=a)
312
- arg_bytes.append(bytes([v.value & 0xFF]))
313
-
314
- subst: Dict[bytes, bytes] = dict(zip(params, arg_bytes))
315
-
316
- # build low-level code with param substitution
317
- code: List[bytes] = []
318
- for tok in body_expr.elements:
319
- if isinstance(tok, Expr.Symbol):
320
- sb = tok.value.encode()
321
- code.append(subst.get(sb, sb))
322
- elif isinstance(tok, Expr.Byte):
323
- code.append(bytes([tok.value & 0xFF]))
324
- elif isinstance(tok, Expr.ListExpr):
325
- rv = self.high_eval(env_id, tok)
326
- if isinstance(rv, Expr.Error):
327
- return rv
328
- if not isinstance(rv, Expr.Byte):
329
- return Expr.Error("nested list must resolve to Byte", origin=tok)
330
- code.append(bytes([rv.value & 0xFF]))
331
- else:
332
- return Expr.Error("invalid token in sk body", origin=tok)
333
-
334
- res_bytes = self.low_eval(code, metered=False)
335
- return Expr.ListExpr([Expr.Byte(b) for b in res_bytes])
336
-
337
- # ---------- (... (body params fn)) HIGH-LEVEL CALL ----------
338
- if isinstance(tail, Expr.ListExpr):
339
- fn_form = tail
340
- if (len(fn_form.elements) >= 3
341
- and isinstance(fn_form.elements[-1], Expr.Symbol)
342
- and fn_form.elements[-1].value == "fn"):
343
-
344
- body_expr = fn_form.elements[-3]
345
- params_expr = fn_form.elements[-2]
346
-
347
- if not isinstance(body_expr, Expr.ListExpr):
348
- return Expr.Error("fn body must be list", origin=body_expr)
349
- if not isinstance(params_expr, Expr.ListExpr):
350
- return Expr.Error("fn params must be list", origin=params_expr)
351
-
352
- params: List[bytes] = []
353
- for p in params_expr.elements:
354
- if not isinstance(p, Expr.Symbol):
355
- return Expr.Error("fn param must be symbol", origin=p)
356
- params.append(p.value.encode())
357
-
358
- args_exprs = expr.elements[:-1]
359
- if len(args_exprs) != len(params):
360
- return Expr.Error("arity mismatch", origin=expr)
361
-
362
- arg_bytes: List[bytes] = []
363
- for a in args_exprs:
364
- v = self.high_eval(env_id, a)
365
- if isinstance(v, Expr.Error):
366
- return v
367
- if not isinstance(v, Expr.Byte):
368
- return Expr.Error("argument must resolve to Byte", origin=a)
369
- arg_bytes.append(bytes([v.value & 0xFF]))
370
-
371
- # child env, bind params -> Expr.Byte
372
- child_env = uuid.uuid4()
373
- self.environments[child_env] = Env(parent_id=env_id)
374
- for name_b, val_b in zip(params, arg_bytes):
375
- self.env_set(child_env, name_b, Expr.Byte(val_b[0]))
376
-
377
- # evaluate HL body
378
- return self.high_eval(child_env, body_expr)
379
-
380
- # ---------- default: resolve each element and return list ----------
381
- resolved: List[Expr] = [self.high_eval(env_id, e) for e in expr.elements]
382
- return Expr.ListExpr(resolved)
File without changes
File without changes
File without changes
File without changes