astreum 0.2.34__py3-none-any.whl → 0.2.36__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.

Potentially problematic release.


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

astreum/__init__.py CHANGED
@@ -1 +1 @@
1
- from .node import Node, Expr
1
+ from ._node import Node, Expr, Env, tokenize, parse
@@ -0,0 +1,16 @@
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
6
+ from .tokenizer import tokenize
7
+
8
+ __all__ = [
9
+ "Env",
10
+ "Expr",
11
+ "low_eval",
12
+ "Meter",
13
+ "parse",
14
+ "tokenize"
15
+ ]
16
+
@@ -0,0 +1,10 @@
1
+ from ast import Expr
2
+ from typing import Dict, Optional
3
+
4
+
5
+ class Env:
6
+ def __init__(
7
+ self,
8
+ data: Optional[Dict[str, Expr]] = None
9
+ ):
10
+ self.data: Dict[bytes, Expr] = {} if data is None else data
@@ -0,0 +1,37 @@
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})"
14
+
15
+ class Symbol:
16
+ def __init__(self, value: str):
17
+ self.value = value
18
+
19
+ def __repr__(self):
20
+ return self.value
21
+
22
+ class Byte:
23
+ def __init__(self, value: int):
24
+ self.value = value
25
+
26
+ def __repr__(self):
27
+ return self.value
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} err)'
37
+ return f'({self.origin} {self.topic} err)'
@@ -0,0 +1,177 @@
1
+ from typing import List, Union
2
+ import uuid
3
+
4
+ from src.astreum._lispeum import Env, Expr, Meter
5
+
6
+
7
+ def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
8
+
9
+ if meter is None:
10
+ meter = Meter()
11
+
12
+ # ---------- atoms ----------
13
+ if isinstance(expr, Expr.Error):
14
+ return expr
15
+
16
+ if isinstance(expr, Expr.Symbol):
17
+ bound = self.env_get(env_id, expr.value.encode())
18
+ if bound is None:
19
+ return Expr.Error(f"unbound symbol '{expr.value}'", origin=expr)
20
+ return bound
21
+
22
+ if not isinstance(expr, Expr.ListExpr):
23
+ return expr # Expr.Byte or other literals passthrough
24
+
25
+ # ---------- empty / single ----------
26
+ if len(expr.elements) == 0:
27
+ return expr
28
+ if len(expr.elements) == 1:
29
+ return self.high_eval(env_id=env_id, expr=expr.elements[0], meter=meter)
30
+
31
+ tail = expr.elements[-1]
32
+
33
+ # ---------- (value name def) ----------
34
+ if isinstance(tail, Expr.Symbol) and tail.value == "def":
35
+ if len(expr.elements) < 3:
36
+ return Expr.Error("def expects (value name def)", origin=expr)
37
+ name_e = expr.elements[-2]
38
+ if not isinstance(name_e, Expr.Symbol):
39
+ return Expr.Error("def name must be symbol", origin=name_e)
40
+ value_e = expr.elements[-3]
41
+ value_res = self.high_eval(env_id=env_id, expr=value_e, meter=meter)
42
+ if isinstance(value_res, Expr.Error):
43
+ return value_res
44
+ self.env_set(env_id, name_e.value.encode(), value_res)
45
+ return value_res
46
+
47
+ # ---- LOW-LEVEL call: ( arg1 arg2 ... ( (body) sk ) ) ----
48
+ if isinstance(tail, Expr.ListExpr):
49
+ inner = tail.elements
50
+ if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
51
+ body_expr = inner[-2]
52
+ if not isinstance(body_expr, Expr.ListExpr):
53
+ return Expr.Error("sk body must be list", origin=body_expr)
54
+
55
+ # helper: turn an Expr into a contiguous bytes buffer
56
+ def to_bytes(v: Expr) -> Union[bytes, Expr.Error]:
57
+ if isinstance(v, Expr.Byte):
58
+ return bytes([v.value & 0xFF])
59
+ if isinstance(v, Expr.ListExpr):
60
+ # expect a list of Expr.Byte
61
+ out: bytearray = bytearray()
62
+ for el in v.elements:
63
+ if isinstance(el, Expr.Byte):
64
+ out.append(el.value & 0xFF)
65
+ else:
66
+ return Expr.Error("byte list must contain only Byte", origin=el)
67
+ return bytes(out)
68
+ if isinstance(v, Expr.Error):
69
+ return v
70
+ return Expr.Error("argument must resolve to Byte or (Byte ...)", origin=v)
71
+
72
+ # resolve ALL preceding args into bytes (can be Byte or List[Byte])
73
+ args_exprs = expr.elements[:-1]
74
+ arg_bytes: List[bytes] = []
75
+ for a in args_exprs:
76
+ v = self.high_eval(env_id=env_id, expr=a, meter=meter)
77
+ if isinstance(v, Expr.Error):
78
+ return v
79
+ vb = to_bytes(v)
80
+ if isinstance(vb, Expr.Error):
81
+ return vb
82
+ arg_bytes.append(vb)
83
+
84
+ # build low-level code with $0-based placeholders ($0 = first arg)
85
+ code: List[bytes] = []
86
+
87
+ def emit(tok: Expr) -> Union[None, Expr.Error]:
88
+ if isinstance(tok, Expr.Symbol):
89
+ name = tok.value
90
+ if name.startswith("$"):
91
+ idx_s = name[1:]
92
+ if not idx_s.isdigit():
93
+ return Expr.Error("invalid sk placeholder", origin=tok)
94
+ idx = int(idx_s) # $0 is first
95
+ if idx < 0 or idx >= len(arg_bytes):
96
+ return Expr.Error("arity mismatch in sk placeholder", origin=tok)
97
+ code.append(arg_bytes[idx])
98
+ return None
99
+ code.append(name.encode())
100
+ return None
101
+
102
+ if isinstance(tok, Expr.Byte):
103
+ code.append(bytes([tok.value & 0xFF]))
104
+ return None
105
+
106
+ if isinstance(tok, Expr.ListExpr):
107
+ rv = self.high_eval(env_id, tok, meter=meter)
108
+ if isinstance(rv, Expr.Error):
109
+ return rv
110
+ rb = to_bytes(rv)
111
+ if isinstance(rb, Expr.Error):
112
+ return rb
113
+ code.append(rb)
114
+ return None
115
+
116
+ if isinstance(tok, Expr.Error):
117
+ return tok
118
+
119
+ return Expr.Error("invalid token in sk body", origin=tok)
120
+
121
+ for t in body_expr.elements:
122
+ err = emit(t)
123
+ if isinstance(err, Expr.Error):
124
+ return err
125
+
126
+ # Execute low-level code built from sk-body using the caller's meter
127
+ res = self.low_eval(code, meter=meter)
128
+ if isinstance(res, Expr.Error):
129
+ return res
130
+ return Expr.ListExpr([Expr.Byte(b) for b in res])
131
+
132
+ # ---------- (... (body params fn)) HIGH-LEVEL CALL ----------
133
+ if isinstance(tail, Expr.ListExpr):
134
+ fn_form = tail
135
+ if (len(fn_form.elements) >= 3
136
+ and isinstance(fn_form.elements[-1], Expr.Symbol)
137
+ and fn_form.elements[-1].value == "fn"):
138
+
139
+ body_expr = fn_form.elements[-3]
140
+ params_expr = fn_form.elements[-2]
141
+
142
+ if not isinstance(body_expr, Expr.ListExpr):
143
+ return Expr.Error("fn body must be list", origin=body_expr)
144
+ if not isinstance(params_expr, Expr.ListExpr):
145
+ return Expr.Error("fn params must be list", origin=params_expr)
146
+
147
+ params: List[bytes] = []
148
+ for p in params_expr.elements:
149
+ if not isinstance(p, Expr.Symbol):
150
+ return Expr.Error("fn param must be symbol", origin=p)
151
+ params.append(p.value.encode())
152
+
153
+ args_exprs = expr.elements[:-1]
154
+ if len(args_exprs) != len(params):
155
+ return Expr.Error("arity mismatch", origin=expr)
156
+
157
+ arg_bytes: List[bytes] = []
158
+ for a in args_exprs:
159
+ v = self.high_eval(env_id, a, meter=meter)
160
+ if isinstance(v, Expr.Error):
161
+ return v
162
+ if not isinstance(v, Expr.Byte):
163
+ return Expr.Error("argument must resolve to Byte", origin=a)
164
+ arg_bytes.append(bytes([v.value & 0xFF]))
165
+
166
+ # child env, bind params -> Expr.Byte
167
+ child_env = uuid.uuid4()
168
+ self.environments[child_env] = Env(parent_id=env_id)
169
+ for name_b, val_b in zip(params, arg_bytes):
170
+ self.env_set(child_env, name_b, Expr.Byte(val_b[0]))
171
+
172
+ # evaluate HL body, metered from the top
173
+ return self.high_eval(child_env, body_expr, meter=meter)
174
+
175
+ # ---------- default: resolve each element and return list ----------
176
+ resolved: List[Expr] = [self.high_eval(env_id, e, meter=meter) for e in expr.elements]
177
+ return Expr.ListExpr(resolved)
@@ -0,0 +1,121 @@
1
+ from typing import Dict, List, Union
2
+ from src.astreum._lispeum import Expr, Meter
3
+
4
+ def tc_to_int(b: bytes) -> int:
5
+ """bytes -> int using two's complement (width = len(b)*8)."""
6
+ if not b:
7
+ return 0
8
+ return int.from_bytes(b, "big", signed=True)
9
+
10
+ def int_to_tc(n: int, width_bytes: int) -> bytes:
11
+ """int -> bytes (two's complement, fixed width)."""
12
+ if width_bytes <= 0:
13
+ return b"\x00"
14
+ return n.to_bytes(width_bytes, "big", signed=True)
15
+
16
+ def min_tc_width(n: int) -> int:
17
+ """minimum bytes to store n in two's complement."""
18
+ if n == 0:
19
+ return 1
20
+ w = 1
21
+ while True:
22
+ try:
23
+ n.to_bytes(w, "big", signed=True)
24
+ return w
25
+ except OverflowError:
26
+ w += 1
27
+
28
+ def nand_bytes(a: bytes, b: bytes) -> bytes:
29
+ """Bitwise NAND on two byte strings, zero-extending to max width."""
30
+ w = max(len(a), len(b), 1)
31
+ au = int.from_bytes(a.rjust(w, b"\x00"), "big", signed=False)
32
+ bu = int.from_bytes(b.rjust(w, b"\x00"), "big", signed=False)
33
+ mask = (1 << (w * 8)) - 1
34
+ resu = (~(au & bu)) & mask
35
+ return resu.to_bytes(w, "big", signed=False)
36
+
37
+ def low_eval(self, code: List[bytes], meter: Meter) -> Union[bytes, Expr.Error]:
38
+
39
+ heap: Dict[bytes, bytes] = {}
40
+
41
+ stack: List[bytes] = []
42
+ pc = 0
43
+
44
+ while True:
45
+ if pc >= len(code):
46
+ if len(stack) != 1:
47
+ return Expr.Error("bad stack")
48
+ return stack.pop()
49
+
50
+ tok = code[pc]
51
+ pc += 1
52
+
53
+ # ---------- ADD ----------
54
+ if tok == b"add":
55
+ if len(stack) < 2:
56
+ return Expr.Error("underflow")
57
+ b_b = stack.pop()
58
+ a_b = stack.pop()
59
+ a_i = tc_to_int(a_b)
60
+ b_i = tc_to_int(b_b)
61
+ res_i = a_i + b_i
62
+ width = max(len(a_b), len(b_b), min_tc_width(res_i))
63
+ res_b = int_to_tc(res_i, width)
64
+ # charge for both operands' byte widths
65
+ if not meter.charge_bytes(len(a_b) + len(b_b)):
66
+ return Expr.Error("meter limit")
67
+ stack.append(res_b)
68
+ continue
69
+
70
+ # ---------- NAND ----------
71
+ if tok == b"nand":
72
+ if len(stack) < 2:
73
+ return Expr.Error("underflow")
74
+ b_b = stack.pop()
75
+ a_b = stack.pop()
76
+ res_b = nand_bytes(a_b, b_b)
77
+ # bitwise cost: 2 * max(len(a), len(b))
78
+ if not meter.charge_bytes(2 * max(len(a_b), len(b_b), 1)):
79
+ return Expr.Error("meter limit")
80
+ stack.append(res_b)
81
+ continue
82
+
83
+ # ---------- JUMP ----------
84
+ if tok == b"jump":
85
+ if len(stack) < 1:
86
+ return Expr.Error("underflow")
87
+ tgt_b = stack.pop()
88
+ if not meter.charge_bytes(1):
89
+ return Expr.Error("meter limit")
90
+ tgt_i = tc_to_int(tgt_b)
91
+ if tgt_i < 0 or tgt_i >= len(code):
92
+ return Expr.Error("bad jump")
93
+ pc = tgt_i
94
+ continue
95
+
96
+ # ---------- HEAP GET ----------
97
+ if tok == b"heap_get":
98
+ if len(stack) < 1:
99
+ return Expr.Error("underflow")
100
+ key = stack.pop()
101
+ val = heap.get(key) or b""
102
+ # get cost: 1
103
+ if not meter.charge_bytes(1):
104
+ return Expr.Error("meter limit")
105
+ stack.append(val)
106
+ continue
107
+
108
+ # ---------- HEAP SET ----------
109
+ if tok == b"heap_set":
110
+ if len(stack) < 2:
111
+ return Expr.Error("underflow")
112
+ val = stack.pop()
113
+ key = stack.pop()
114
+ if not meter.charge_bytes(len(val)):
115
+ return Expr.Error("meter limit")
116
+ heap[key] = val
117
+ continue
118
+
119
+ # if no opcode matched above, treat token as literal
120
+ # not an opcode → literal blob
121
+ stack.append(tok)
@@ -0,0 +1,18 @@
1
+ from typing import Optional
2
+
3
+
4
+ class Meter:
5
+ def __init__(self, enabled: bool = True, limit: Optional[int] = None):
6
+ self.enabled = enabled
7
+ self.limit: Optional[int] = limit
8
+ self.used: int = 0
9
+
10
+ def charge_bytes(self, n: int) -> bool:
11
+ if not self.enabled:
12
+ return True
13
+ if n < 0:
14
+ n = 0
15
+ if self.limit is not None and (self.used + n) >= self.limit:
16
+ return False
17
+ self.used += n
18
+ return True
@@ -0,0 +1,40 @@
1
+ from typing import List, Tuple
2
+ from src.astreum._lispeum import Expr
3
+
4
+ class ParseError(Exception):
5
+ pass
6
+
7
+ def _parse_one(tokens: List[str], pos: int = 0) -> Tuple[Expr, int]:
8
+ if pos >= len(tokens):
9
+ raise ParseError("unexpected end")
10
+ tok = tokens[pos]
11
+
12
+ if tok == '(': # list
13
+ items: List[Expr] = []
14
+ i = pos + 1
15
+ while i < len(tokens):
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
23
+ expr, i = _parse_one(tokens, i)
24
+ items.append(expr)
25
+ raise ParseError("expected ')'")
26
+
27
+ if tok == ')':
28
+ raise ParseError("unexpected ')'")
29
+
30
+ # try integer → Byte
31
+ try:
32
+ n = int(tok)
33
+ return Expr.Byte(n), pos + 1
34
+ except ValueError:
35
+ return Expr.Symbol(tok), pos + 1
36
+
37
+ def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
38
+ """Parse tokens into an Expr and return (expr, remaining_tokens)."""
39
+ expr, next_pos = _parse_one(tokens, 0)
40
+ return expr, tokens[next_pos:]
@@ -0,0 +1,22 @@
1
+ from typing import List
2
+
3
+
4
+ def tokenize(source: str) -> List[str]:
5
+ tokens: List[str] = []
6
+ cur: List[str] = []
7
+ for ch in source:
8
+ if ch.isspace():
9
+ if cur:
10
+ tokens.append("".join(cur))
11
+ cur = []
12
+ continue
13
+ if ch in ("(", ")"):
14
+ if cur:
15
+ tokens.append("".join(cur))
16
+ cur = []
17
+ tokens.append(ch)
18
+ continue
19
+ cur.append(ch)
20
+ if cur:
21
+ tokens.append("".join(cur))
22
+ return tokens
astreum/_node.py CHANGED
@@ -3,116 +3,124 @@ from typing import Dict, List, Optional, Tuple, Union
3
3
  import uuid
4
4
  import threading
5
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)
6
+ from src.astreum._lispeum import Env, Expr, Meter, low_eval
42
7
 
43
8
  def bytes_touched(*vals: bytes) -> int:
44
9
  """For metering: how many bytes were manipulated (max of operands)."""
45
10
  return max((len(v) for v in vals), default=1)
46
11
 
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
12
+ from blake3 import blake3
13
+
14
+ ZERO32 = b"\x00"*32
15
+
16
+ def u64_le(n: int) -> bytes:
17
+ return int(n).to_bytes(8, "little", signed=False)
18
+
19
+ def hash_bytes(b: bytes) -> bytes:
20
+ return blake3(b).digest()
21
+
22
+ class Atom:
23
+ data: bytes
24
+ next: bytes
25
+ size: int
26
+
27
+ def __init__(self, data: bytes, next: bytes = ZERO32, size: Optional[int] = None):
28
+ self.data = data
29
+ self.next = next
30
+ self.size = len(data) if size is None else size
31
+
32
+ @staticmethod
33
+ def from_data(data: bytes, next_hash: bytes = ZERO32) -> "Atom":
34
+ return Atom(data=data, next=next_hash, size=len(data))
35
+
36
+ @staticmethod
37
+ def object_id_from_parts(data_hash: bytes, next_hash: bytes, size: int) -> bytes:
38
+ return blake3(data_hash + next_hash + u64_le(size)).digest()
39
+
40
+ def data_hash(self) -> bytes:
41
+ return hash_bytes(self.data)
42
+
43
+ def object_id(self) -> bytes:
44
+ return self.object_id_from_parts(self.data_hash(), self.next, self.size)
45
+
46
+ @staticmethod
47
+ def verify_metadata(object_id: bytes, size: int, next_hash: bytes, data_hash: bytes) -> bool:
48
+ return object_id == blake3(data_hash + next_hash + u64_le(size)).digest()
49
+
50
+ def to_bytes(self) -> bytes:
51
+ if len(self.next) != len(ZERO32):
52
+ raise ValueError("next must be 32 bytes")
53
+ return self.next + self.data
54
+
55
+ @staticmethod
56
+ def from_bytes(buf: bytes) -> "Atom":
57
+ if len(buf) < len(ZERO32):
58
+ raise ValueError("buffer too short for Atom header")
59
+ next_hash = buf[:len(ZERO32)]
60
+ data = buf[len(ZERO32):]
61
+ return Atom(data=data, next=next_hash, size=len(data))
62
+
63
+ def u32_le(n: int) -> bytes:
64
+ return int(n).to_bytes(4, "little", signed=False)
65
+
66
+ def expr_to_atoms(e: Expr) -> Tuple[bytes, List[Atom]]:
67
+ def sym(v: str) -> Tuple[bytes, List[Atom]]:
68
+ val = v.encode("utf-8")
69
+ val_atom = Atom.from_data(u32_le(len(val)) + val)
70
+ typ_atom = Atom.from_data(b"symbol", val_atom.object_id())
71
+ return typ_atom.object_id(), [val_atom, typ_atom]
72
+
73
+ def byt(v: int) -> Tuple[bytes, List[Atom]]:
74
+ val_atom = Atom.from_data(bytes([v & 0xFF]))
75
+ typ_atom = Atom.from_data(b"byte", val_atom.object_id())
76
+ return typ_atom.object_id(), [val_atom, typ_atom]
77
+
78
+ def err(topic: str, origin: Optional[Expr]) -> Tuple[bytes, List[Atom]]:
79
+ t = topic.encode("utf-8")
80
+ origin_hash, acc = (ZERO32, [])
81
+ if origin is not None:
82
+ origin_hash, acc = expr_to_atoms(origin)
83
+ val_atom = Atom.from_data(u32_le(len(t)) + t + origin_hash)
84
+ typ_atom = Atom.from_data(b"error", val_atom.object_id())
85
+ return typ_atom.object_id(), acc + [val_atom, typ_atom]
86
+
87
+ def lst(items: List[Expr]) -> Tuple[bytes, List[Atom]]:
88
+ acc: List[Atom] = []
89
+ child_hashes: List[bytes] = []
90
+ for it in items:
91
+ h, atoms = expr_to_atoms(it)
92
+ acc.extend(atoms)
93
+ child_hashes.append(h)
94
+ next_hash = ZERO32
95
+ elem_atoms: List[Atom] = []
96
+ for h in reversed(child_hashes):
97
+ a = Atom.from_data(h, next_hash)
98
+ next_hash = a.object_id()
99
+ elem_atoms.append(a)
100
+ elem_atoms.reverse()
101
+ head = next_hash
102
+ val_atom = Atom.from_data(u64_le(len(items)), head)
103
+ typ_atom = Atom.from_data(b"list", val_atom.object_id())
104
+ return typ_atom.object_id(), acc + elem_atoms + [val_atom, typ_atom]
105
+
106
+ if isinstance(e, Expr.Symbol):
107
+ return sym(e.value)
108
+ if isinstance(e, Expr.Byte):
109
+ return byt(e.value)
110
+ if isinstance(e, Expr.Error):
111
+ return err(e.topic, e.origin)
112
+ if isinstance(e, Expr.ListExpr):
113
+ return lst(e.elements)
114
+ raise TypeError("unknown Expr variant")
110
115
 
111
116
  class Node:
112
117
  def __init__(self):
113
- self.environments: Dict[uuid.UUID, Env] = {}
118
+ # Storage Setup
114
119
  self.in_memory_storage: Dict[bytes, bytes] = {}
120
+ # Lispeum Setup
121
+ self.environments: Dict[uuid.UUID, Env] = {}
115
122
  self.machine_environments_lock = threading.RLock()
123
+ self.low_eval = low_eval
116
124
 
117
125
  # ---- Env helpers ----
118
126
  def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
@@ -137,356 +145,3 @@ class Node:
137
145
 
138
146
  def _local_set(self, key: bytes, value: bytes) -> None:
139
147
  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,2 +0,0 @@
1
- from .parser import parse
2
- from .tokenizer import tokenize
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.34
3
+ Version: 0.2.36
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
@@ -1,13 +1,21 @@
1
- astreum/__init__.py,sha256=y2Ok3EY_FstcmlVASr80lGR_0w-dH-SXDCCQFmL6uwA,28
2
- astreum/_node.py,sha256=cJSzj7N4unkOH5T_nDa7bKy_jBD-p0oJTDlAXjuFFYw,18773
1
+ astreum/__init__.py,sha256=oIrOJ0DZZByGtiIaupDBLhIOLL2nm0FOI7uWhDcaIsk,51
2
+ astreum/_node.py,sha256=YWz7RASGM9qCHJEQoldehzcnWBhS3jLbRfT6_VlWZcM,5391
3
3
  astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
4
4
  astreum/node.py,sha256=SuVm1b0QWl1FpDUaLRH1fiFYnXCrPs6qYeUQlPDae8w,38358
5
+ astreum/_lispeum/__init__.py,sha256=HzJp03YmP0b8IS2NtM893katuUooro_C701H08uf4i4,274
6
+ astreum/_lispeum/environment.py,sha256=5SAEUgEzRlDGqtrmr-g6Za-fuuhX55GXGnZKVKxfkCQ,230
7
+ astreum/_lispeum/expression.py,sha256=PS8RAahwduhnJy7-6jYaILAc0uFu1XLhbnHUJhvVwo8,1024
8
+ astreum/_lispeum/high_evaluation.py,sha256=s6DJSEWLc_-05Tpk8TOaSJm0TKrC3H_YEym5pnXfPWw,8041
9
+ astreum/_lispeum/low_evaluation.py,sha256=mbeA0KMeWQruAPctYJAP-L6QmU1btU0l5ScJWFF_nxA,4322
10
+ astreum/_lispeum/meter.py,sha256=5q2PFW7_jmgKVM1-vwE4RRjMfPEthUA4iu1CwR-Axws,505
11
+ astreum/_lispeum/parser.py,sha256=_J-KeIZlK-PP0Wc9mAl82hyQsPG2wsgshqAc3fCNVk8,1562
12
+ astreum/_lispeum/tokenizer.py,sha256=P68uIj4aPKzjuCJ85jfzRi67QztpuXIOC1vvLQueBI4,552
5
13
  astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
14
  astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
7
15
  astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
8
16
  astreum/crypto/wesolowski.py,sha256=SUgGXW3Id07dJtWzDcs4dluIhjqbRWQ8YWjn_mK78AQ,4092
9
17
  astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
10
- astreum/lispeum/__init__.py,sha256=K-NDzIjtIsXzC9X7lnYvlvIaVxjFcY7WNsgLIE3DH3U,58
18
+ astreum/lispeum/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
19
  astreum/lispeum/environment.py,sha256=wolwt9psDl62scgjaVG0G59xlBs1AM4NPgryUbxzG_4,1220
12
20
  astreum/lispeum/expression.py,sha256=K8gFifDaHu394bs9qnpvP8tjeiymFGQpnDC_iW9nU4E,2379
13
21
  astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
@@ -27,8 +35,8 @@ astreum/relay/setup.py,sha256=ynvGaJdlDtw_f5LLiow2Wo7IRzUjvgk8eSr1Sv4_zTg,2090
27
35
  astreum/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
36
  astreum/storage/object.py,sha256=knFlvw_tpcC4twSu1DGNpHX31wlANN8E5dgEqIfU--Q,2041
29
37
  astreum/storage/setup.py,sha256=1-9ztEFI_BvRDvAA0lAn4mFya8iq65THTArlj--M3Hg,626
30
- astreum-0.2.34.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
31
- astreum-0.2.34.dist-info/METADATA,sha256=5EXXn-HyFQ6cCd34YuK0A0g1xDZA0VOZaudtxHdqimA,6149
32
- astreum-0.2.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- astreum-0.2.34.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
34
- astreum-0.2.34.dist-info/RECORD,,
38
+ astreum-0.2.36.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
39
+ astreum-0.2.36.dist-info/METADATA,sha256=P3P6HjaFqg6F1e9EVnr9OtMapOJMvtaVB0hqw486D6I,6149
40
+ astreum-0.2.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
+ astreum-0.2.36.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
42
+ astreum-0.2.36.dist-info/RECORD,,