astreum 0.2.35__py3-none-any.whl → 0.2.37__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 +9 -1
- astreum/_communication/__init__.py +9 -0
- astreum/_communication/peer.py +11 -0
- astreum/_communication/route.py +25 -0
- astreum/_communication/setup.py +104 -0
- astreum/_lispeum/__init__.py +16 -0
- astreum/_lispeum/environment.py +13 -0
- astreum/_lispeum/expression.py +37 -0
- astreum/_lispeum/high_evaluation.py +177 -0
- astreum/_lispeum/low_evaluation.py +123 -0
- astreum/_lispeum/meter.py +18 -0
- astreum/_lispeum/parser.py +56 -0
- astreum/_lispeum/tokenizer.py +22 -0
- astreum/_node.py +27 -461
- astreum/_storage/__init__.py +5 -0
- astreum/_storage/atom.py +100 -0
- astreum/_validation/__init__.py +12 -0
- astreum/_validation/block.py +296 -0
- astreum/_validation/chain.py +63 -0
- astreum/_validation/fork.py +98 -0
- astreum/_validation/genesis.py +0 -0
- astreum/_validation/setup.py +141 -0
- astreum/models/block.py +18 -9
- {astreum-0.2.35.dist-info → astreum-0.2.37.dist-info}/METADATA +4 -2
- astreum-0.2.37.dist-info/RECORD +54 -0
- astreum-0.2.35.dist-info/RECORD +0 -34
- {astreum-0.2.35.dist-info → astreum-0.2.37.dist-info}/WHEEL +0 -0
- {astreum-0.2.35.dist-info → astreum-0.2.37.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.35.dist-info → astreum-0.2.37.dist-info}/top_level.txt +0 -0
astreum/__init__.py
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"""Lightweight package initializer to avoid circular imports during tests.
|
|
2
|
+
|
|
3
|
+
Exports are intentionally minimal; import submodules directly as needed:
|
|
4
|
+
- Node, Expr, Env, tokenize, parse -> from astreum._node or astreum.lispeum
|
|
5
|
+
- Validation types -> from astreum._validation
|
|
6
|
+
- Storage types -> from astreum._storage
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
class Peer:
|
|
5
|
+
shared_key: bytes
|
|
6
|
+
timestamp: datetime
|
|
7
|
+
latest_block: bytes
|
|
8
|
+
|
|
9
|
+
def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
|
|
10
|
+
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
11
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
3
|
+
|
|
4
|
+
class Route:
|
|
5
|
+
def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
|
|
6
|
+
self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
7
|
+
self.bucket_size = bucket_size
|
|
8
|
+
self.buckets: Dict[int, List[X25519PublicKey]] = {
|
|
9
|
+
i: [] for i in range(len(self.relay_public_key_bytes) * 8)
|
|
10
|
+
}
|
|
11
|
+
self.peers = {}
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _matching_leading_bits(a: bytes, b: bytes) -> int:
|
|
15
|
+
for byte_index, (ba, bb) in enumerate(zip(a, b)):
|
|
16
|
+
diff = ba ^ bb
|
|
17
|
+
if diff:
|
|
18
|
+
return byte_index * 8 + (8 - diff.bit_length())
|
|
19
|
+
return len(a) * 8
|
|
20
|
+
|
|
21
|
+
def add_peer(self, peer_public_key: X25519PublicKey):
|
|
22
|
+
peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
23
|
+
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
24
|
+
if len(self.buckets[bucket_idx]) < self.bucket_size:
|
|
25
|
+
self.buckets[bucket_idx].append(peer_public_key)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import socket, threading
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import (
|
|
6
|
+
X25519PrivateKey,
|
|
7
|
+
X25519PublicKey,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .. import Node
|
|
13
|
+
|
|
14
|
+
from .import Route
|
|
15
|
+
|
|
16
|
+
def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
|
|
17
|
+
"""DH key for relaying (always X25519)."""
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
|
|
21
|
+
"""Signing key for validation (Ed25519), or None if absent."""
|
|
22
|
+
return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
|
|
23
|
+
if hex_key else None
|
|
24
|
+
|
|
25
|
+
def make_routes(
|
|
26
|
+
relay_pk: X25519PublicKey,
|
|
27
|
+
val_sk: Optional[ed25519.Ed25519PrivateKey]
|
|
28
|
+
) -> Tuple[Route, Optional[Route]]:
|
|
29
|
+
"""Peer route (DH pubkey) + optional validation route (ed pubkey)."""
|
|
30
|
+
peer_rt = Route(relay_pk)
|
|
31
|
+
val_rt = Route(val_sk.public_key()) if val_sk else None
|
|
32
|
+
return peer_rt, val_rt
|
|
33
|
+
|
|
34
|
+
def setup_udp(
|
|
35
|
+
bind_port: int,
|
|
36
|
+
use_ipv6: bool
|
|
37
|
+
) -> Tuple[socket.socket, int, Queue, threading.Thread, threading.Thread]:
|
|
38
|
+
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
39
|
+
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
40
|
+
if use_ipv6:
|
|
41
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
42
|
+
sock.bind(("::" if use_ipv6 else "0.0.0.0", bind_port or 0))
|
|
43
|
+
port = sock.getsockname()[1]
|
|
44
|
+
|
|
45
|
+
q = Queue()
|
|
46
|
+
pop = threading.Thread(target=lambda: None, daemon=True)
|
|
47
|
+
proc = threading.Thread(target=lambda: None, daemon=True)
|
|
48
|
+
pop.start(); proc.start()
|
|
49
|
+
return sock, port, q, pop, proc
|
|
50
|
+
|
|
51
|
+
def setup_outgoing(
|
|
52
|
+
use_ipv6: bool
|
|
53
|
+
) -> Tuple[socket.socket, Queue, threading.Thread]:
|
|
54
|
+
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
55
|
+
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
56
|
+
q = Queue()
|
|
57
|
+
thr = threading.Thread(target=lambda: None, daemon=True)
|
|
58
|
+
thr.start()
|
|
59
|
+
return sock, q, thr
|
|
60
|
+
|
|
61
|
+
def make_maps():
|
|
62
|
+
"""Empty lookup maps: peers and addresses."""
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
def communication_setup(node: "Node", config: dict):
|
|
66
|
+
node.use_ipv6 = config.get('use_ipv6', False)
|
|
67
|
+
|
|
68
|
+
# key loading
|
|
69
|
+
node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
|
|
70
|
+
node.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
|
|
71
|
+
|
|
72
|
+
# derive pubs + routes
|
|
73
|
+
node.relay_public_key = node.relay_secret_key.public_key()
|
|
74
|
+
node.peer_route, node.validation_route = make_routes(
|
|
75
|
+
node.relay_public_key,
|
|
76
|
+
node.validation_secret_key
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# sockets + queues + threads
|
|
80
|
+
(node.incoming_socket,
|
|
81
|
+
node.incoming_port,
|
|
82
|
+
node.incoming_queue,
|
|
83
|
+
node.incoming_populate_thread,
|
|
84
|
+
node.incoming_process_thread
|
|
85
|
+
) = setup_udp(config.get('incoming_port', 7373), node.use_ipv6)
|
|
86
|
+
|
|
87
|
+
(node.outgoing_socket,
|
|
88
|
+
node.outgoing_queue,
|
|
89
|
+
node.outgoing_thread
|
|
90
|
+
) = setup_outgoing(node.use_ipv6)
|
|
91
|
+
|
|
92
|
+
# other workers & maps
|
|
93
|
+
node.object_request_queue = Queue()
|
|
94
|
+
node.peer_manager_thread = threading.Thread(
|
|
95
|
+
target=node._relay_peer_manager,
|
|
96
|
+
daemon=True
|
|
97
|
+
)
|
|
98
|
+
node.peer_manager_thread.start()
|
|
99
|
+
|
|
100
|
+
node.peers, node.addresses = {}, {} # peers: Dict[X25519PublicKey,Peer], addresses: Dict[(str,int),X25519PublicKey]
|
|
101
|
+
|
|
102
|
+
# bootstrap pings
|
|
103
|
+
for addr in config.get('bootstrap', []):
|
|
104
|
+
node._send_ping(addr)
|
|
@@ -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, ParseError
|
|
6
|
+
from .tokenizer import tokenize
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Env",
|
|
10
|
+
"Expr",
|
|
11
|
+
"low_eval",
|
|
12
|
+
"Meter",
|
|
13
|
+
"parse",
|
|
14
|
+
"tokenize",
|
|
15
|
+
"ParseError",
|
|
16
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ast import Expr
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Env:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
data: Optional[Dict[str, Expr]] = None,
|
|
10
|
+
parent_id: Optional[uuid.UUID] = None,
|
|
11
|
+
):
|
|
12
|
+
self.data: Dict[bytes, Expr] = {} if data is None else data
|
|
13
|
+
self.parent_id = parent_id
|
|
@@ -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} list)"
|
|
14
|
+
|
|
15
|
+
class Symbol:
|
|
16
|
+
def __init__(self, value: str):
|
|
17
|
+
self.value = value
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return f"({self.value} symbol)"
|
|
21
|
+
|
|
22
|
+
class Bytes:
|
|
23
|
+
def __init__(self, value: bytes):
|
|
24
|
+
self.value = value
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return f"({self.value} bytes)"
|
|
28
|
+
|
|
29
|
+
class Error:
|
|
30
|
+
def __init__(self, topic: str, origin: Optional['Expr'] = None):
|
|
31
|
+
self.topic = topic
|
|
32
|
+
self.origin = origin
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
if self.origin is None:
|
|
36
|
+
return f'({self.topic} error)'
|
|
37
|
+
return f'({self.origin} {self.topic} error)'
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from .environment import Env
|
|
5
|
+
from .expression import Expr
|
|
6
|
+
from .meter import Meter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
|
|
10
|
+
|
|
11
|
+
if meter is None:
|
|
12
|
+
meter = Meter()
|
|
13
|
+
|
|
14
|
+
# ---------- atoms ----------
|
|
15
|
+
if isinstance(expr, Expr.Error):
|
|
16
|
+
return expr
|
|
17
|
+
|
|
18
|
+
if isinstance(expr, Expr.Symbol):
|
|
19
|
+
bound = self.env_get(env_id, expr.value.encode())
|
|
20
|
+
if bound is None:
|
|
21
|
+
return Expr.Error(f"unbound symbol '{expr.value}'", origin=expr)
|
|
22
|
+
return bound
|
|
23
|
+
|
|
24
|
+
if not isinstance(expr, Expr.ListExpr):
|
|
25
|
+
return expr # Expr.Byte or other literals passthrough
|
|
26
|
+
|
|
27
|
+
# ---------- empty / single ----------
|
|
28
|
+
if len(expr.elements) == 0:
|
|
29
|
+
return expr
|
|
30
|
+
if len(expr.elements) == 1:
|
|
31
|
+
return self.high_eval(env_id=env_id, expr=expr.elements[0], meter=meter)
|
|
32
|
+
|
|
33
|
+
tail = expr.elements[-1]
|
|
34
|
+
|
|
35
|
+
# ---------- (value name def) ----------
|
|
36
|
+
if isinstance(tail, Expr.Symbol) and tail.value == "def":
|
|
37
|
+
if len(expr.elements) < 3:
|
|
38
|
+
return Expr.Error("def expects (value name def)", origin=expr)
|
|
39
|
+
name_e = expr.elements[-2]
|
|
40
|
+
if not isinstance(name_e, Expr.Symbol):
|
|
41
|
+
return Expr.Error("def name must be symbol", origin=name_e)
|
|
42
|
+
value_e = expr.elements[-3]
|
|
43
|
+
value_res = self.high_eval(env_id=env_id, expr=value_e, meter=meter)
|
|
44
|
+
if isinstance(value_res, Expr.Error):
|
|
45
|
+
return value_res
|
|
46
|
+
self.env_set(env_id, name_e.value.encode(), value_res)
|
|
47
|
+
return value_res
|
|
48
|
+
|
|
49
|
+
# ---- LOW-LEVEL call: ( arg1 arg2 ... ( (body) sk ) ) ----
|
|
50
|
+
if isinstance(tail, Expr.ListExpr):
|
|
51
|
+
inner = tail.elements
|
|
52
|
+
if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
|
|
53
|
+
body_expr = inner[-2]
|
|
54
|
+
if not isinstance(body_expr, Expr.ListExpr):
|
|
55
|
+
return Expr.Error("sk body must be list", origin=body_expr)
|
|
56
|
+
|
|
57
|
+
# helper: turn an Expr into a contiguous bytes buffer
|
|
58
|
+
def to_bytes(v: Expr) -> Union[bytes, Expr.Error]:
|
|
59
|
+
if isinstance(v, Expr.Byte):
|
|
60
|
+
return bytes([v.value & 0xFF])
|
|
61
|
+
if isinstance(v, Expr.ListExpr):
|
|
62
|
+
# expect a list of Expr.Byte
|
|
63
|
+
out: bytearray = bytearray()
|
|
64
|
+
for el in v.elements:
|
|
65
|
+
if isinstance(el, Expr.Byte):
|
|
66
|
+
out.append(el.value & 0xFF)
|
|
67
|
+
else:
|
|
68
|
+
return Expr.Error("byte list must contain only Byte", origin=el)
|
|
69
|
+
return bytes(out)
|
|
70
|
+
if isinstance(v, Expr.Error):
|
|
71
|
+
return v
|
|
72
|
+
return Expr.Error("argument must resolve to Byte or (Byte ...)", origin=v)
|
|
73
|
+
|
|
74
|
+
# resolve ALL preceding args into bytes (can be Byte or List[Byte])
|
|
75
|
+
args_exprs = expr.elements[:-1]
|
|
76
|
+
arg_bytes: List[bytes] = []
|
|
77
|
+
for a in args_exprs:
|
|
78
|
+
v = self.high_eval(env_id=env_id, expr=a, meter=meter)
|
|
79
|
+
if isinstance(v, Expr.Error):
|
|
80
|
+
return v
|
|
81
|
+
vb = to_bytes(v)
|
|
82
|
+
if isinstance(vb, Expr.Error):
|
|
83
|
+
return vb
|
|
84
|
+
arg_bytes.append(vb)
|
|
85
|
+
|
|
86
|
+
# build low-level code with $0-based placeholders ($0 = first arg)
|
|
87
|
+
code: List[bytes] = []
|
|
88
|
+
|
|
89
|
+
def emit(tok: Expr) -> Union[None, Expr.Error]:
|
|
90
|
+
if isinstance(tok, Expr.Symbol):
|
|
91
|
+
name = tok.value
|
|
92
|
+
if name.startswith("$"):
|
|
93
|
+
idx_s = name[1:]
|
|
94
|
+
if not idx_s.isdigit():
|
|
95
|
+
return Expr.Error("invalid sk placeholder", origin=tok)
|
|
96
|
+
idx = int(idx_s) # $0 is first
|
|
97
|
+
if idx < 0 or idx >= len(arg_bytes):
|
|
98
|
+
return Expr.Error("arity mismatch in sk placeholder", origin=tok)
|
|
99
|
+
code.append(arg_bytes[idx])
|
|
100
|
+
return None
|
|
101
|
+
code.append(name.encode())
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
if isinstance(tok, Expr.Byte):
|
|
105
|
+
code.append(bytes([tok.value & 0xFF]))
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if isinstance(tok, Expr.ListExpr):
|
|
109
|
+
rv = self.high_eval(env_id, tok, meter=meter)
|
|
110
|
+
if isinstance(rv, Expr.Error):
|
|
111
|
+
return rv
|
|
112
|
+
rb = to_bytes(rv)
|
|
113
|
+
if isinstance(rb, Expr.Error):
|
|
114
|
+
return rb
|
|
115
|
+
code.append(rb)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
if isinstance(tok, Expr.Error):
|
|
119
|
+
return tok
|
|
120
|
+
|
|
121
|
+
return Expr.Error("invalid token in sk body", origin=tok)
|
|
122
|
+
|
|
123
|
+
for t in body_expr.elements:
|
|
124
|
+
err = emit(t)
|
|
125
|
+
if isinstance(err, Expr.Error):
|
|
126
|
+
return err
|
|
127
|
+
|
|
128
|
+
# Execute low-level code built from sk-body using the caller's meter
|
|
129
|
+
res = self.low_eval(code, meter=meter)
|
|
130
|
+
return 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,123 @@
|
|
|
1
|
+
from typing import Dict, List, Union
|
|
2
|
+
from .expression import Expr
|
|
3
|
+
from .meter import Meter
|
|
4
|
+
|
|
5
|
+
def tc_to_int(b: bytes) -> int:
|
|
6
|
+
"""bytes -> int using two's complement (width = len(b)*8)."""
|
|
7
|
+
if not b:
|
|
8
|
+
return 0
|
|
9
|
+
return int.from_bytes(b, "big", signed=True)
|
|
10
|
+
|
|
11
|
+
def int_to_tc(n: int, width_bytes: int) -> bytes:
|
|
12
|
+
"""int -> bytes (two's complement, fixed width)."""
|
|
13
|
+
if width_bytes <= 0:
|
|
14
|
+
return b"\x00"
|
|
15
|
+
return n.to_bytes(width_bytes, "big", signed=True)
|
|
16
|
+
|
|
17
|
+
def min_tc_width(n: int) -> int:
|
|
18
|
+
"""minimum bytes to store n in two's complement."""
|
|
19
|
+
if n == 0:
|
|
20
|
+
return 1
|
|
21
|
+
w = 1
|
|
22
|
+
while True:
|
|
23
|
+
try:
|
|
24
|
+
n.to_bytes(w, "big", signed=True)
|
|
25
|
+
return w
|
|
26
|
+
except OverflowError:
|
|
27
|
+
w += 1
|
|
28
|
+
|
|
29
|
+
def nand_bytes(a: bytes, b: bytes) -> bytes:
|
|
30
|
+
"""Bitwise NAND on two byte strings, zero-extending to max width."""
|
|
31
|
+
w = max(len(a), len(b), 1)
|
|
32
|
+
au = int.from_bytes(a.rjust(w, b"\x00"), "big", signed=False)
|
|
33
|
+
bu = int.from_bytes(b.rjust(w, b"\x00"), "big", signed=False)
|
|
34
|
+
mask = (1 << (w * 8)) - 1
|
|
35
|
+
resu = (~(au & bu)) & mask
|
|
36
|
+
return resu.to_bytes(w, "big", signed=False)
|
|
37
|
+
|
|
38
|
+
def low_eval(self, code: List[bytes], meter: Meter) -> Expr:
|
|
39
|
+
|
|
40
|
+
heap: Dict[bytes, bytes] = {}
|
|
41
|
+
|
|
42
|
+
stack: List[bytes] = []
|
|
43
|
+
pc = 0
|
|
44
|
+
|
|
45
|
+
while True:
|
|
46
|
+
if pc >= len(code):
|
|
47
|
+
if len(stack) != 1:
|
|
48
|
+
return Expr.Error("bad stack")
|
|
49
|
+
# wrap successful result as an Expr.Bytes
|
|
50
|
+
return Expr.Bytes(stack.pop())
|
|
51
|
+
|
|
52
|
+
tok = code[pc]
|
|
53
|
+
pc += 1
|
|
54
|
+
|
|
55
|
+
# ---------- ADD ----------
|
|
56
|
+
if tok == b"add":
|
|
57
|
+
if len(stack) < 2:
|
|
58
|
+
return Expr.Error("underflow")
|
|
59
|
+
b_b = stack.pop()
|
|
60
|
+
a_b = stack.pop()
|
|
61
|
+
a_i = tc_to_int(a_b)
|
|
62
|
+
b_i = tc_to_int(b_b)
|
|
63
|
+
res_i = a_i + b_i
|
|
64
|
+
width = max(len(a_b), len(b_b), min_tc_width(res_i))
|
|
65
|
+
res_b = int_to_tc(res_i, width)
|
|
66
|
+
# charge for both operands' byte widths
|
|
67
|
+
if not meter.charge_bytes(len(a_b) + len(b_b)):
|
|
68
|
+
return Expr.Error("meter limit")
|
|
69
|
+
stack.append(res_b)
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# ---------- NAND ----------
|
|
73
|
+
if tok == b"nand":
|
|
74
|
+
if len(stack) < 2:
|
|
75
|
+
return Expr.Error("underflow")
|
|
76
|
+
b_b = stack.pop()
|
|
77
|
+
a_b = stack.pop()
|
|
78
|
+
res_b = nand_bytes(a_b, b_b)
|
|
79
|
+
# bitwise cost: 2 * max(len(a), len(b))
|
|
80
|
+
if not meter.charge_bytes(2 * max(len(a_b), len(b_b), 1)):
|
|
81
|
+
return Expr.Error("meter limit")
|
|
82
|
+
stack.append(res_b)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# ---------- JUMP ----------
|
|
86
|
+
if tok == b"jump":
|
|
87
|
+
if len(stack) < 1:
|
|
88
|
+
return Expr.Error("underflow")
|
|
89
|
+
tgt_b = stack.pop()
|
|
90
|
+
if not meter.charge_bytes(1):
|
|
91
|
+
return Expr.Error("meter limit")
|
|
92
|
+
tgt_i = tc_to_int(tgt_b)
|
|
93
|
+
if tgt_i < 0 or tgt_i >= len(code):
|
|
94
|
+
return Expr.Error("bad jump")
|
|
95
|
+
pc = tgt_i
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# ---------- HEAP GET ----------
|
|
99
|
+
if tok == b"heap_get":
|
|
100
|
+
if len(stack) < 1:
|
|
101
|
+
return Expr.Error("underflow")
|
|
102
|
+
key = stack.pop()
|
|
103
|
+
val = heap.get(key) or b""
|
|
104
|
+
# get cost: 1
|
|
105
|
+
if not meter.charge_bytes(1):
|
|
106
|
+
return Expr.Error("meter limit")
|
|
107
|
+
stack.append(val)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# ---------- HEAP SET ----------
|
|
111
|
+
if tok == b"heap_set":
|
|
112
|
+
if len(stack) < 2:
|
|
113
|
+
return Expr.Error("underflow")
|
|
114
|
+
val = stack.pop()
|
|
115
|
+
key = stack.pop()
|
|
116
|
+
if not meter.charge_bytes(len(val)):
|
|
117
|
+
return Expr.Error("meter limit")
|
|
118
|
+
heap[key] = val
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# if no opcode matched above, treat token as literal
|
|
122
|
+
# not an opcode → literal blob
|
|
123
|
+
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,56 @@
|
|
|
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 → Bytes (variable-length two's complement)
|
|
31
|
+
try:
|
|
32
|
+
n = int(tok)
|
|
33
|
+
# encode as minimal-width signed two's complement, big-endian
|
|
34
|
+
def int_to_min_tc(v: int) -> bytes:
|
|
35
|
+
"""Return the minimal-width signed two's complement big-endian
|
|
36
|
+
byte encoding of integer v. Width expands just enough so that
|
|
37
|
+
decoding with signed=True yields the same value and sign.
|
|
38
|
+
Example: 0 -> b"\x00", 127 -> b"\x7f", 128 -> b"\x00\x80".
|
|
39
|
+
"""
|
|
40
|
+
if v == 0:
|
|
41
|
+
return b"\x00"
|
|
42
|
+
w = 1
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
return v.to_bytes(w, "big", signed=True)
|
|
46
|
+
except OverflowError:
|
|
47
|
+
w += 1
|
|
48
|
+
|
|
49
|
+
return Expr.Bytes(int_to_min_tc(n)), pos + 1
|
|
50
|
+
except ValueError:
|
|
51
|
+
return Expr.Symbol(tok), pos + 1
|
|
52
|
+
|
|
53
|
+
def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
|
|
54
|
+
"""Parse tokens into an Expr and return (expr, remaining_tokens)."""
|
|
55
|
+
expr, next_pos = _parse_one(tokens, 0)
|
|
56
|
+
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
|