astreum 0.2.1__tar.gz → 0.2.4__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.
Files changed (66) hide show
  1. {astreum-0.2.1/src/astreum.egg-info → astreum-0.2.4}/PKG-INFO +7 -7
  2. {astreum-0.2.1 → astreum-0.2.4}/README.md +6 -6
  3. {astreum-0.2.1 → astreum-0.2.4}/pyproject.toml +1 -1
  4. astreum-0.2.4/src/astreum/__init__.py +1 -0
  5. astreum-0.2.4/src/astreum/lispeum/__init__.py +2 -0
  6. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/parser.py +3 -2
  7. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/storage.py +1 -1
  8. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/tokenizer.py +2 -2
  9. astreum-0.2.4/src/astreum/machine/error.py +0 -0
  10. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/node.py +75 -58
  11. {astreum-0.2.1 → astreum-0.2.4/src/astreum.egg-info}/PKG-INFO +7 -7
  12. {astreum-0.2.1 → astreum-0.2.4}/src/astreum.egg-info/SOURCES.txt +2 -2
  13. astreum-0.2.4/tests/test_node_machine.py +56 -0
  14. astreum-0.2.1/src/astreum/__init__.py +0 -1
  15. astreum-0.2.1/src/astreum/lispeum/__init__.py +0 -2
  16. astreum-0.2.1/src/astreum/lispeum/utils.py +0 -17
  17. astreum-0.2.1/src/astreum/machine/error.py +0 -2
  18. {astreum-0.2.1 → astreum-0.2.4}/LICENSE +0 -0
  19. {astreum-0.2.1 → astreum-0.2.4}/setup.cfg +0 -0
  20. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/__init__.py +0 -0
  21. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/relay/__init__.py +0 -0
  22. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/relay/bucket.py +0 -0
  23. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/relay/envelope.py +0 -0
  24. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/relay/message.py +0 -0
  25. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/relay/peer.py +0 -0
  26. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/relay/route.py +0 -0
  27. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/storage/__init__.py +0 -0
  28. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/storage/merkle.py +0 -0
  29. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/storage/patricia.py +0 -0
  30. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/storage/storage.py +0 -0
  31. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/storage/utils.py +0 -0
  32. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/utils.py +0 -0
  33. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/__init__.py +0 -0
  34. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/_block/__init__.py +0 -0
  35. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/_block/create.py +0 -0
  36. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/_block/model.py +0 -0
  37. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/_block/validate.py +0 -0
  38. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/account.py +0 -0
  39. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/block.py +0 -0
  40. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/constants.py +0 -0
  41. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/stake.py +0 -0
  42. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/transaction.py +0 -0
  43. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/_node/validation/vdf.py +0 -0
  44. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/crypto/__init__.py +0 -0
  45. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/crypto/ed25519.py +0 -0
  46. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/crypto/x25519.py +0 -0
  47. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/format.py +0 -0
  48. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/expression.py +0 -0
  49. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/__init__.py +0 -0
  50. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/definition.py +0 -0
  51. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/__init__.py +0 -0
  52. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/all.py +0 -0
  53. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/any.py +0 -0
  54. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/fold.py +0 -0
  55. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/get.py +0 -0
  56. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/insert.py +0 -0
  57. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/map.py +0 -0
  58. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/position.py +0 -0
  59. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/list/remove.py +0 -0
  60. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/number/__init__.py +0 -0
  61. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/lispeum/special/number/addition.py +0 -0
  62. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/machine/__init__.py +0 -0
  63. {astreum-0.2.1 → astreum-0.2.4}/src/astreum/machine/environment.py +0 -0
  64. {astreum-0.2.1 → astreum-0.2.4}/src/astreum.egg-info/dependency_links.txt +0 -0
  65. {astreum-0.2.1 → astreum-0.2.4}/src/astreum.egg-info/requires.txt +0 -0
  66. {astreum-0.2.1 → astreum-0.2.4}/src/astreum.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.1
3
+ Version: 0.2.4
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
@@ -73,7 +73,7 @@ node = Node(config)
73
73
 
74
74
  ## Lispeum Machine Quickstart
75
75
 
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 *session* (lexical environment).
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
79
  from astreum.node import Node
@@ -83,16 +83,15 @@ from astreum.machine.parser import parse
83
83
  # 1. Spin‑up a stand‑alone VM (machine‑only node).
84
84
  node = Node({"machine-only": True})
85
85
 
86
- # 2. Create a fresh session (environment).
87
- session_id = node.machine_session_create()
86
+ # 2. Create an environment.
87
+ env_id = node.machine_create_environment()
88
88
 
89
89
  # 3. Convert Lispeum source → Expr AST.
90
90
  source = '(+ 1 (* 2 3))'
91
91
  expr, _ = parse(tokenize(source))
92
92
 
93
- # 4. Evaluate inside that session.
94
- env = node.sessions[session_id] # fetch the Env
95
- result = node.machine_expr_eval(env, expr) # -> Expr.Integer(7)
93
+ # 4. Evaluate
94
+ result = node.machine_expr_eval(env_id=env_id, expr=expr) # -> Expr.Integer(7)
96
95
 
97
96
  print(result.value) # 7
98
97
  ```
@@ -119,5 +118,6 @@ except ParseError as e:
119
118
  ## Testing
120
119
 
121
120
  ```bash
121
+
122
122
  python3 -m unittest discover -s tests
123
123
  ```
@@ -55,7 +55,7 @@ node = Node(config)
55
55
 
56
56
  ## Lispeum Machine Quickstart
57
57
 
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 *session* (lexical environment).
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
61
  from astreum.node import Node
@@ -65,16 +65,15 @@ from astreum.machine.parser import parse
65
65
  # 1. Spin‑up a stand‑alone VM (machine‑only node).
66
66
  node = Node({"machine-only": True})
67
67
 
68
- # 2. Create a fresh session (environment).
69
- session_id = node.machine_session_create()
68
+ # 2. Create an environment.
69
+ env_id = node.machine_create_environment()
70
70
 
71
71
  # 3. Convert Lispeum source → Expr AST.
72
72
  source = '(+ 1 (* 2 3))'
73
73
  expr, _ = parse(tokenize(source))
74
74
 
75
- # 4. Evaluate inside that session.
76
- env = node.sessions[session_id] # fetch the Env
77
- result = node.machine_expr_eval(env, expr) # -> Expr.Integer(7)
75
+ # 4. Evaluate
76
+ result = node.machine_expr_eval(env_id=env_id, expr=expr) # -> Expr.Integer(7)
78
77
 
79
78
  print(result.value) # 7
80
79
  ```
@@ -101,5 +100,6 @@ except ParseError as e:
101
100
  ## Testing
102
101
 
103
102
  ```bash
103
+
104
104
  python3 -m unittest discover -s tests
105
105
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.2.1"
3
+ version = "0.2.4"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -0,0 +1 @@
1
+ from .node import Node, Expr
@@ -0,0 +1,2 @@
1
+ from .parser import parse
2
+ from .tokenizer import tokenize
@@ -1,7 +1,8 @@
1
1
  from typing import List, Tuple
2
- from astreum.machine.error import ParseError
3
- from astreum.node import Expr
2
+ from ..node import Expr
4
3
 
4
+ class ParseError(Exception):
5
+ pass
5
6
 
6
7
  def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
7
8
  if not tokens:
@@ -9,7 +9,7 @@ import struct
9
9
  from typing import Dict, Tuple, Any, List, Optional
10
10
 
11
11
  from astreum.lispeum.expression import Expr
12
- from .utils import hash_data
12
+ from ..crypto.blake30 import hash_data
13
13
 
14
14
 
15
15
  def expr_to_objects(expr: Any) -> Tuple[bytes, Dict[bytes, bytes]]:
@@ -3,8 +3,8 @@
3
3
  # Tokenizer function
4
4
  from typing import List
5
5
 
6
- from astreum.machine.error import ParseError
7
-
6
+ class ParseError(Exception):
7
+ pass
8
8
 
9
9
  def tokenize(input: str) -> List[str]:
10
10
  tokens = []
File without changes
@@ -6,10 +6,10 @@ from pathlib import Path
6
6
  from typing import Tuple, Dict, Union, Optional, List
7
7
  from datetime import datetime, timedelta, timezone
8
8
  import uuid
9
- from astreum import format
9
+ from .format import encode, decode
10
10
  from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
11
11
  from cryptography.hazmat.primitives import serialization
12
- from astreum.crypto import ed25519, x25519
12
+ from .crypto import ed25519, x25519
13
13
  from enum import IntEnum
14
14
  import blake3
15
15
  import struct
@@ -29,11 +29,11 @@ class ObjectRequest:
29
29
  self.hash = hash
30
30
 
31
31
  def to_bytes(self):
32
- return format.encode([self.type.value, self.data, self.hash])
32
+ return encode([self.type.value, self.data, self.hash])
33
33
 
34
34
  @classmethod
35
35
  def from_bytes(cls, data: bytes):
36
- type_val, data_val, hash_val = format.decode(data)
36
+ type_val, data_val, hash_val = decode(data)
37
37
  return cls(type=ObjectRequestType(type_val[0]), data=data_val, hash=hash_val)
38
38
 
39
39
  class ObjectResponseType(IntEnum):
@@ -52,11 +52,11 @@ class ObjectResponse:
52
52
  self.hash = hash
53
53
 
54
54
  def to_bytes(self):
55
- return format.encode([self.type.value, self.data, self.hash])
55
+ return encode([self.type.value, self.data, self.hash])
56
56
 
57
57
  @classmethod
58
58
  def from_bytes(cls, data: bytes):
59
- type_val, data_val, hash_val = format.decode(data)
59
+ type_val, data_val, hash_val = decode(data)
60
60
  return cls(type=ObjectResponseType(type_val[0]), data=data_val, hash=hash_val)
61
61
 
62
62
  class MessageTopic(IntEnum):
@@ -71,11 +71,11 @@ class Message:
71
71
  topic: MessageTopic
72
72
 
73
73
  def to_bytes(self):
74
- return format.encode([self.body, [self.topic.value]])
74
+ return encode([self.body, [self.topic.value]])
75
75
 
76
76
  @classmethod
77
77
  def from_bytes(cls, data: bytes):
78
- body, topic = format.decode(data)
78
+ body, topic = decode(data)
79
79
  return cls(body=body, topic=MessageTopic(topic[0]))
80
80
 
81
81
  class Envelope:
@@ -90,6 +90,7 @@ class Envelope:
90
90
  encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
91
91
 
92
92
  self.message = message
93
+ message_bytes = message.to_bytes()
93
94
 
94
95
  self.sender = sender
95
96
  self.sender_bytes = sender.public_bytes()
@@ -119,14 +120,14 @@ class Envelope:
119
120
  return count
120
121
 
121
122
  while True:
122
- envelope_bytes = format.encode([
123
+ envelope_bytes = encode([
123
124
  encrypted_bytes,
124
125
  message_bytes,
125
126
  self.nonce,
126
127
  self.sender_bytes,
127
128
  timestamp_int
128
129
  ])
129
- envelope_hash = utils.blake3_hash(envelope_bytes)
130
+ envelope_hash = blake3.blake3(envelope_bytes).digest()
130
131
  if count_leading_zero_bits(envelope_hash) >= difficulty:
131
132
  self.hash = envelope_hash
132
133
  break
@@ -135,7 +136,7 @@ class Envelope:
135
136
  def to_bytes(self):
136
137
  encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
137
138
 
138
- return format.encode([
139
+ return encode([
139
140
  encrypted_bytes,
140
141
  self.message.to_bytes(),
141
142
  self.nonce,
@@ -145,7 +146,7 @@ class Envelope:
145
146
 
146
147
  @classmethod
147
148
  def from_bytes(cls, data: bytes):
148
- encrypted_bytes, message_bytes, nonce, sender_bytes, timestamp_int = format.decode(data)
149
+ encrypted_bytes, message_bytes, nonce, sender_bytes, timestamp_int = decode(data)
149
150
  return cls(
150
151
  encrypted=(encrypted_bytes == b'\x01'),
151
152
  message=Message.from_bytes(message_bytes),
@@ -278,7 +279,7 @@ class Expr:
278
279
  return f"(fn ({params_str}) {body_str})"
279
280
 
280
281
  class Error:
281
- def __init__(self, message: str, origin: 'Expr' | None = None):
282
+ def __init__(self, message: str, origin: Optional['Expr'] = None):
282
283
  self.message = message
283
284
  self.origin = origin
284
285
 
@@ -288,9 +289,9 @@ class Expr:
288
289
  return f'(error "{self.message}" in {self.origin})'
289
290
 
290
291
  class Env:
291
- def __init__(self, parent: 'Env' = None):
292
+ def __init__(self, parent_id: uuid.UUID = None):
292
293
  self.data: Dict[str, Expr] = {}
293
- self.parent = parent
294
+ self.parent_id = parent_id
294
295
 
295
296
  def put(self, name: str, value: Expr):
296
297
  self.data[name] = value
@@ -459,7 +460,7 @@ class Node:
459
460
  self.peers[envelope.sender].timestamp = datetime.now(timezone.utc)
460
461
  continue
461
462
 
462
- is_validator_flag = format.decode(envelope.message.body)
463
+ is_validator_flag = decode(envelope.message.body)
463
464
 
464
465
  if envelope.sender not in self.peers:
465
466
  self._send_ping(addr)
@@ -514,7 +515,7 @@ class Node:
514
515
  nearest = self._get_closest_local_peer(object_hash)
515
516
  if nearest:
516
517
  nearest_key, nearest_peer = nearest
517
- peer_info = format.encode([
518
+ peer_info = encode([
518
519
  nearest_key.public_bytes(
519
520
  encoding=serialization.Encoding.Raw,
520
521
  format=serialization.PublicFormat.Raw
@@ -533,7 +534,7 @@ class Node:
533
534
  # -------------- OBJECT_PUT --------------
534
535
  case ObjectRequestType.OBJECT_PUT:
535
536
  # Ensure the hash is present / correct.
536
- obj_hash = object_request.hash or blake3.blake3(object_request.data).digest()
537
+ obj_hash = object_request.hash or blake30.blake3(object_request.data).digest()
537
538
 
538
539
  nearest = self._get_closest_local_peer(obj_hash)
539
540
  # If a strictly nearer peer exists, forward the PUT.
@@ -548,7 +549,7 @@ class Node:
548
549
  self.outgoing_queue.put((fwd_env.to_bytes(), nearest[1].address))
549
550
  else:
550
551
  # We are closest → remember who can provide the object.
551
- provider_record = format.encode([
552
+ provider_record = encode([
552
553
  envelope.sender.public_bytes(),
553
554
  encode_ip_address(*addr)
554
555
  ])
@@ -567,13 +568,13 @@ class Node:
567
568
 
568
569
  match object_response.type:
569
570
  case ObjectResponseType.OBJECT_FOUND:
570
- if object_response.hash != blake3.blake3(object_response.data).digest():
571
+ if object_response.hash != blake30.blake3(object_response.data).digest():
571
572
  continue
572
573
  self.object_request_queue.remove(object_response.hash)
573
574
  self._local_object_put(object_response.hash, object_response.data)
574
575
 
575
576
  case ObjectResponseType.OBJECT_PROVIDER:
576
- _provider_public_key, provider_address = format.decode(object_response.data)
577
+ _provider_public_key, provider_address = decode(object_response.data)
577
578
  provider_ip, provider_port = decode_ip_address(provider_address)
578
579
  object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST, body=object_hash)
579
580
  object_request_envelope = Envelope(message=object_request_message, sender=self.relay_public_key)
@@ -582,7 +583,7 @@ class Node:
582
583
  case ObjectResponseType.OBJECT_NEAREST_PEER:
583
584
  # -- decode the peer info sent back
584
585
  nearest_peer_public_key_bytes, nearest_peer_address = (
585
- format.decode(object_response.data)
586
+ decode(object_response.data)
586
587
  )
587
588
  nearest_peer_public_key = X25519PublicKey.from_public_bytes(
588
589
  nearest_peer_public_key_bytes
@@ -648,12 +649,12 @@ class Node:
648
649
  print(f"Error in _peer_manager_thread: {e}")
649
650
 
650
651
  def _send_ping(self, addr: Tuple[str, int]):
651
- is_validator_flag = format.encode([1] if self.validation_secret_key else [0])
652
+ is_validator_flag = encode([1] if self.validation_secret_key else [0])
652
653
  ping_message = Message(topic=MessageTopic.PING, body=is_validator_flag)
653
654
  ping_envelope = Envelope(message=ping_message, sender=self.relay_public_key)
654
655
  self.outgoing_queue.put((ping_envelope.to_bytes(), addr))
655
656
 
656
- def _get_closest_local_peer(self, hash: bytes) -> Optional[(X25519PublicKey, Peer)]:
657
+ def _get_closest_local_peer(self, hash: bytes) -> Optional[Tuple[X25519PublicKey, Peer]]:
657
658
  # Find the globally closest peer using XOR distance
658
659
  closest_peer = None
659
660
  closest_distance = None
@@ -694,44 +695,58 @@ class Node:
694
695
 
695
696
  # MACHINE
696
697
  def _machine_setup(self):
697
- self.sessions: Dict[uuid.UUID, Env] = {}
698
- self.lock = threading.Lock()
699
-
700
- def machine_session_create(self) -> uuid.UUID:
701
- session_id = uuid.uuid4()
702
- with self.lock:
703
- self.sessions[session_id] = Env()
704
- return session_id
698
+ self.environments: Dict[uuid.UUID, Env] = {}
699
+ self.machine_environments_lock = threading.Lock()
700
+
701
+ def machine_create_environment(self, parent_id: Optional[uuid.UUID] = None) -> uuid.UUID:
702
+ env_id = uuid.uuid4()
703
+ with self.machine_environments_lock:
704
+ while env_id in self.environments:
705
+ env_id = uuid.uuid4()
706
+ self.environments[env_id] = Env(parent_id=parent_id)
707
+ return env_id
705
708
 
706
- def machine_session_delete(self, session_id: str) -> bool:
707
- with self.lock:
708
- if session_id in self.sessions:
709
- del self.sessions[session_id]
710
- return True
711
- else:
712
- return False
713
-
714
- def machine_expr_get(self, session_id: uuid.UUID, name: str) -> Optional[Expr]:
715
- with self.lock:
716
- env = self.sessions.get(session_id)
717
- if env is None:
718
- return None
719
- return env.get(name)
709
+ def machine_get_or_create_environment(self, env_id: Optional[uuid.UUID] = None, parent_id: Optional[uuid.UUID] = None) -> uuid.UUID:
710
+ with self.machine_environments_lock:
711
+ if env_id is not None and env_id in self.environments:
712
+ return env_id
713
+ new_id = env_id if env_id is not None else uuid.uuid4()
714
+ while new_id in self.environments:
715
+ new_id = uuid.uuid4()
716
+ self.environments[new_id] = Env(parent_id=parent_id)
717
+ return new_id
718
+
719
+ def machine_delete_environment(self, env_id: uuid.UUID) -> bool:
720
+ with self.machine_environments_lock:
721
+ removed = self.environments.pop(env_id, None)
722
+ return removed is not None
723
+
724
+ def machine_expr_get(self, env_id: uuid.UUID, name: str) -> Optional[Expr]:
725
+ with self.machine_environments_lock:
726
+ cur = self.environments.get(env_id)
727
+ while cur is not None:
728
+ if name in cur.data:
729
+ return cur.data[name]
730
+ if cur.parent_id:
731
+ cur = self.environments.get(cur.parent_id)
732
+ else:
733
+ cur = None
734
+ return None
720
735
 
721
- def machine_expr_put(self, session_id: uuid.UUID, name: str, expr: Expr):
722
- with self.lock:
723
- env = self.sessions.get(session_id)
736
+ def machine_expr_put(self, env_id: uuid.UUID, name: str, expr: Expr):
737
+ with self.machine_environments_lock:
738
+ env = self.environments.get(env_id)
724
739
  if env is None:
725
740
  return False
726
741
  env.put(name, expr)
727
742
  return True
728
743
 
729
- def machine_expr_eval(self, env: Env, expr: Expr) -> Expr:
744
+ def machine_expr_eval(self, env_id: uuid.UUID, expr: Expr) -> Expr:
730
745
  if isinstance(expr, Expr.Boolean) or isinstance(expr, Expr.Integer) or isinstance(expr, Expr.String) or isinstance(expr, Expr.Error):
731
746
  return expr
732
747
 
733
748
  elif isinstance(expr, Expr.Symbol):
734
- value = env.get(expr.value)
749
+ value = self.machine_expr_get(env_id=env_id, name=expr.value)
735
750
  if value:
736
751
  return value
737
752
  else:
@@ -741,24 +756,26 @@ class Node:
741
756
  if len(expr.elements) == 0:
742
757
  return expr
743
758
  if len(expr.elements) == 1:
744
- return self.machine_expr_eval(expr=expr.elements[0], env=env)
759
+ return self.machine_expr_eval(expr=expr.elements[0], env_id=env_id)
745
760
  first = expr.elements[0]
746
761
  if isinstance(first, Expr.Symbol):
747
- first_symbol_value = env.get(first.value)
762
+ first_symbol_value = self.machine_expr_get(env_id=env_id, name=first.value)
748
763
 
749
764
  if first_symbol_value and not isinstance(first_symbol_value, Expr.Function):
750
- evaluated_elements = [self.evaluate_expression(e, env) for e in expr.elements]
765
+ evaluated_elements = [self.machine_expr_eval(env_id=env_id, expr=e) for e in expr.elements]
751
766
  return Expr.ListExpr(evaluated_elements)
752
767
 
753
768
  elif first.value == "def":
769
+ args = expr.elements[1:]
754
770
  if len(args) != 2:
755
771
  return Expr.Error(message=f"'def' expects exactly 2 arguments, got {len(args)}", origin=expr)
756
772
  if not isinstance(args[0], Expr.Symbol):
757
773
  return Expr.Error(message="first argument to 'def' must be a symbol", origin=args[0])
758
- result = self.machine_expr_eval(env=env, expr=args[1])
774
+ result = self.machine_expr_eval(env_id=env_id, expr=args[1])
759
775
  if isinstance(result, Expr.Error):
760
776
  return result
761
- env.put(name=args[0].value, value=result)
777
+
778
+ self.machine_expr_put(env_id=env_id, name=args[0].value, expr=result)
762
779
  return result
763
780
 
764
781
  # # List
@@ -901,7 +918,7 @@ class Node:
901
918
  return Expr.Error(message="'+' expects at least 1 argument", origin=expr)
902
919
  evaluated_args = []
903
920
  for arg in args:
904
- val = self.evaluate_expression(arg, env)
921
+ val = self.machine_expr_eval(env_id==env_id, expr=arg)
905
922
  if isinstance(val, Expr.Error):
906
923
  return val
907
924
  evaluated_args.append(val)
@@ -1010,7 +1027,7 @@ class Node:
1010
1027
  # return Expr.Integer(dividend % divisor)
1011
1028
 
1012
1029
  else:
1013
- evaluated_elements = [self.evaluate_expression(e, env) for e in expr.elements]
1030
+ evaluated_elements = [self.machine_expr_eval(env_id=env_id, expr=e) for e in expr.elements]
1014
1031
  return Expr.ListExpr(evaluated_elements)
1015
1032
 
1016
1033
  elif isinstance(expr, Expr.Function):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.1
3
+ Version: 0.2.4
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
@@ -73,7 +73,7 @@ node = Node(config)
73
73
 
74
74
  ## Lispeum Machine Quickstart
75
75
 
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 *session* (lexical environment).
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
79
  from astreum.node import Node
@@ -83,16 +83,15 @@ from astreum.machine.parser import parse
83
83
  # 1. Spin‑up a stand‑alone VM (machine‑only node).
84
84
  node = Node({"machine-only": True})
85
85
 
86
- # 2. Create a fresh session (environment).
87
- session_id = node.machine_session_create()
86
+ # 2. Create an environment.
87
+ env_id = node.machine_create_environment()
88
88
 
89
89
  # 3. Convert Lispeum source → Expr AST.
90
90
  source = '(+ 1 (* 2 3))'
91
91
  expr, _ = parse(tokenize(source))
92
92
 
93
- # 4. Evaluate inside that session.
94
- env = node.sessions[session_id] # fetch the Env
95
- result = node.machine_expr_eval(env, expr) # -> Expr.Integer(7)
93
+ # 4. Evaluate
94
+ result = node.machine_expr_eval(env_id=env_id, expr=expr) # -> Expr.Integer(7)
96
95
 
97
96
  print(result.value) # 7
98
97
  ```
@@ -119,5 +118,6 @@ except ParseError as e:
119
118
  ## Testing
120
119
 
121
120
  ```bash
121
+
122
122
  python3 -m unittest discover -s tests
123
123
  ```
@@ -41,7 +41,6 @@ src/astreum/lispeum/expression.py
41
41
  src/astreum/lispeum/parser.py
42
42
  src/astreum/lispeum/storage.py
43
43
  src/astreum/lispeum/tokenizer.py
44
- src/astreum/lispeum/utils.py
45
44
  src/astreum/lispeum/special/__init__.py
46
45
  src/astreum/lispeum/special/definition.py
47
46
  src/astreum/lispeum/special/list/__init__.py
@@ -57,4 +56,5 @@ src/astreum/lispeum/special/number/__init__.py
57
56
  src/astreum/lispeum/special/number/addition.py
58
57
  src/astreum/machine/__init__.py
59
58
  src/astreum/machine/environment.py
60
- src/astreum/machine/error.py
59
+ src/astreum/machine/error.py
60
+ tests/test_node_machine.py
@@ -0,0 +1,56 @@
1
+ import unittest
2
+ from src.astreum.node import Node, Expr
3
+ from src.astreum.lispeum import tokenize, parse
4
+
5
+
6
+ class TestNodeMachine(unittest.TestCase):
7
+ """Integration tests for the Lispeum VM embedded in astreum.Node."""
8
+
9
+ def setUp(self):
10
+ # Spin‑up a stand‑alone VM
11
+ self.node = Node({"machine-only": True})
12
+ self.env_id = self.node.machine_create_environment()
13
+ self.env = self.node.environments[self.env_id]
14
+
15
+ # ---------- helpers --------------------------------------------------
16
+ def _eval(self, source: str) -> Expr:
17
+ """Tokenize → parse → eval a Lispeum snippet inside the current env."""
18
+ tokens = tokenize(source)
19
+ expr, _ = parse(tokens)
20
+ return self.node.machine_expr_eval(env_id=self.env_id, expr=expr)
21
+
22
+ # ---------- core tests ----------------------------------------------
23
+ def test_int_addition(self):
24
+ """(+ 2 3) ⇒ 5"""
25
+ result = self._eval("(+ 2 3)")
26
+ self.assertEqual(result.value, 5)
27
+
28
+ def test_variable_definition_and_lookup(self):
29
+ """(def numero 42) then numero ⇒ 42"""
30
+ self._eval("(def numero 42)")
31
+ lookup_result = self._eval("numero")
32
+ self.assertEqual(lookup_result.value, 42)
33
+
34
+ def test_session_isolation(self):
35
+ """Variables defined in one session must not leak into another."""
36
+ # Define in first env
37
+ self._eval("(def a 1)")
38
+
39
+ # Create second session
40
+ other_env = self.node.machine_create_environment()
41
+
42
+ tokens = tokenize("a")
43
+ expr, _ = parse(tokens)
44
+ result_other = self.node.machine_expr_eval(env_id=other_env, expr=expr)
45
+
46
+ # Expect an Expr.Error or any non‑1 value
47
+ self.assertTrue(
48
+ isinstance(result_other, Expr.Error) or getattr(result_other, "value", None) != 1,
49
+ "Variable 'a' leaked across sessions"
50
+ )
51
+
52
+ self.node.machine_delete_environment(other_env)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ unittest.main()
@@ -1 +0,0 @@
1
- from node import Node
@@ -1,2 +0,0 @@
1
- from parser import parse
2
- from tokenizer import tokenize
@@ -1,17 +0,0 @@
1
- """
2
- Utility functions for the Lispeum module.
3
- """
4
-
5
- import blake3
6
-
7
- def hash_data(data: bytes) -> bytes:
8
- """
9
- Hash data using BLAKE3.
10
-
11
- Args:
12
- data: Data to hash
13
-
14
- Returns:
15
- 32-byte BLAKE3 hash
16
- """
17
- return blake3.blake3(data).digest()
@@ -1,2 +0,0 @@
1
- class ParseError(Exception):
2
- pass
File without changes
File without changes
File without changes