Skip to content

Commit 1a5cb17

Browse files
authored
make SExp wrap any tree implementing the CLVMObject protocol. (#94)
simplify CLVMObject a bit, remove its cons() function
1 parent ea73c29 commit 1a5cb17

File tree

4 files changed

+276
-102
lines changed

4 files changed

+276
-102
lines changed

clvm/CLVMObject.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,29 @@
1-
"""
2-
This is the minimal `SExp` type that defines how and where its contents are
3-
stored in the heap. The methods here are the only ones required for `run_program`.
4-
A native implementation of `run_program` should implement this base class.
5-
"""
6-
71
import typing
82

9-
# Set this to "1" to do a run-time type check
10-
# This may slow things down a bit
11-
12-
TYPE_CHECK = 0
13-
143

154
class CLVMObject:
5+
"""
6+
This class implements the CLVM Object protocol in the simplest possible way,
7+
by just having an "atom" and a "pair" field
8+
"""
169

1710
atom: typing.Optional[bytes]
18-
pair: typing.Optional[typing.Tuple["CLVMObject", "CLVMObject"]]
11+
12+
# this is always a 2-tuple of an object implementing the CLVM object
13+
# protocol.
14+
pair: typing.Optional[typing.Tuple[typing.Any, typing.Any]]
1915
__slots__ = ["atom", "pair"]
2016

21-
def __new__(class_, v: "SExpType"):
17+
def __new__(class_, v):
2218
if isinstance(v, CLVMObject):
2319
return v
24-
if TYPE_CHECK:
25-
type_ok = (
26-
isinstance(v, tuple)
27-
and len(v) == 2
28-
and isinstance(v[0], CLVMObject)
29-
and isinstance(v[1], CLVMObject)
30-
) or isinstance(v, bytes)
31-
# uncomment next line for debugging help
32-
# if not type_ok: breakpoint()
33-
assert type_ok
3420
self = super(CLVMObject, class_).__new__(class_)
3521
if isinstance(v, tuple):
22+
if len(v) != 2:
23+
raise ValueError("tuples must be of size 2, cannot create CLVMObject from: %s" % str(v))
3624
self.pair = v
3725
self.atom = None
3826
else:
3927
self.atom = v
4028
self.pair = None
4129
return self
42-
43-
def cons(self, right: "CLVMObject"):
44-
return self.__class__((self, right))
45-
46-
47-
SExpType = typing.Union[bytes, typing.Tuple[CLVMObject, CLVMObject]]

clvm/SExp.py

Lines changed: 106 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from blspy import G1Element
55

66
from .as_python import as_python
7-
from .CLVMObject import CLVMObject, SExpType
7+
from .CLVMObject import CLVMObject
88

99
from .EvalError import EvalError
1010

@@ -17,113 +17,152 @@
1717

1818
CastableType = typing.Union[
1919
"SExp",
20-
CLVMObject,
20+
"CLVMObject",
2121
bytes,
22+
str,
2223
int,
2324
None,
24-
SExpType,
2525
G1Element,
26+
list,
2627
typing.Tuple[typing.Any, typing.Any],
2728
]
2829

30+
2931
NULL = b""
3032

3133

34+
def looks_like_clvm_object(o: typing.Any) -> bool:
35+
d = dir(o)
36+
return "atom" in d and "pair" in d
37+
38+
39+
# this function recognizes some common types and turns them into plain bytes,
40+
def convert_atom_to_bytes(
41+
v: typing.Union[bytes, str, int, G1Element, None, list],
42+
) -> bytes:
43+
44+
if isinstance(v, bytes):
45+
return v
46+
if isinstance(v, str):
47+
return v.encode()
48+
if isinstance(v, int):
49+
return int_to_bytes(v)
50+
if isinstance(v, G1Element):
51+
return bytes(v)
52+
if v is None:
53+
return b""
54+
if v == []:
55+
return b""
56+
57+
raise ValueError("can't cast %s (%s) to bytes" % (type(v), v))
58+
59+
60+
# returns a clvm-object like object
3261
def to_sexp_type(
3362
v: CastableType,
34-
) -> SExpType:
63+
):
3564
stack = [v]
3665
ops = [(0, None)] # convert
3766

3867
while len(ops) > 0:
3968
op, target = ops.pop()
4069
# convert value
4170
if op == 0:
71+
if looks_like_clvm_object(stack[-1]):
72+
continue
4273
v = stack.pop()
4374
if isinstance(v, tuple):
4475
if len(v) != 2:
4576
raise ValueError("can't cast tuple of size %d" % len(v))
4677
left, right = v
4778
target = len(stack)
48-
stack.append((left, right))
49-
if type(right) != CLVMObject:
79+
stack.append(CLVMObject((left, right)))
80+
if not looks_like_clvm_object(right):
5081
stack.append(right)
5182
ops.append((2, target)) # set right
5283
ops.append((0, None)) # convert
53-
if type(left) != CLVMObject:
84+
if not looks_like_clvm_object(left):
5485
stack.append(left)
5586
ops.append((1, target)) # set left
5687
ops.append((0, None)) # convert
5788
continue
58-
if isinstance(v, CLVMObject):
59-
stack.append(v.pair or v.atom)
60-
continue
61-
if isinstance(v, bytes):
62-
stack.append(v)
63-
continue
64-
if isinstance(v, str):
65-
stack.append(v.encode())
66-
continue
67-
if isinstance(v, int):
68-
stack.append(int_to_bytes(v))
69-
continue
70-
if isinstance(v, G1Element):
71-
stack.append(bytes(v))
72-
continue
73-
if v is None:
74-
stack.append(NULL)
75-
continue
76-
if v == []:
77-
stack.append(NULL)
78-
continue
79-
80-
if hasattr(v, "__iter__"):
89+
if isinstance(v, list):
8190
target = len(stack)
82-
stack.append(NULL)
91+
stack.append(CLVMObject(NULL))
8392
for _ in v:
8493
stack.append(_)
8594
ops.append((3, target)) # prepend list
8695
# we only need to convert if it's not already the right
8796
# type
88-
if type(_) != CLVMObject:
97+
if not looks_like_clvm_object(_):
8998
ops.append((0, None)) # convert
9099
continue
100+
stack.append(CLVMObject(convert_atom_to_bytes(v)))
101+
continue
91102

92-
raise ValueError("can't cast to CLVMObject: %s" % v)
93103
if op == 1: # set left
94-
stack[target] = (CLVMObject(stack.pop()), stack[target][1])
104+
stack[target].pair = (CLVMObject(stack.pop()), stack[target].pair[1])
95105
continue
96106
if op == 2: # set right
97-
stack[target] = (stack[target][0], CLVMObject(stack.pop()))
107+
stack[target].pair = (stack[target].pair[0], CLVMObject(stack.pop()))
98108
continue
99109
if op == 3: # prepend list
100-
stack[target] = (CLVMObject(stack.pop()), CLVMObject(stack[target]))
110+
stack[target] = CLVMObject((stack.pop(), stack[target]))
101111
continue
102112
# there's exactly one item left at this point
103113
if len(stack) != 1:
104114
raise ValueError("internal error")
115+
116+
# stack[0] implements the clvm object protocol and can be wrapped by an SExp
105117
return stack[0]
106118

107119

108-
class SExp(CLVMObject):
120+
class SExp:
121+
"""
122+
SExp provides higher level API on top of any object implementing the CLVM
123+
object protocol.
124+
The tree of values is not a tree of SExp objects, it's a tree of CLVMObject
125+
like objects. SExp simply wraps them to privide a uniform view of any
126+
underlying conforming tree structure.
127+
128+
The CLVM object protocol (concept) exposes two attributes:
129+
1. "atom" which is either None or bytes
130+
2. "pair" which is either None or a tuple of exactly two elements. Both
131+
elements implementing the CLVM object protocol.
132+
Exactly one of "atom" and "pair" must be None.
133+
"""
109134
true: "SExp"
110135
false: "SExp"
111136
__null__: "SExp"
112137

113-
def as_pair(self):
138+
# the underlying object implementing the clvm object protocol
139+
atom: typing.Optional[bytes]
140+
141+
# this is a tuple of the otherlying CLVMObject-like objects. i.e. not
142+
# SExp objects with higher level functions, or None
143+
pair: typing.Optional[typing.Tuple[typing.Any, typing.Any]]
144+
145+
def __init__(self, obj):
146+
self.atom = obj.atom
147+
self.pair = obj.pair
148+
149+
# this returns a tuple of two SExp objects, or None
150+
def as_pair(self) -> typing.Tuple["SExp", "SExp"]:
114151
pair = self.pair
115152
if pair is None:
116153
return pair
117-
return (self.to(pair[0]), self.to(pair[1]))
154+
return (self.__class__(pair[0]), self.__class__(pair[1]))
118155

156+
# TODO: deprecate this. Same as .atom property
119157
def as_atom(self):
120158
return self.atom
121159

122160
def listp(self):
123161
return self.pair is not None
124162

125163
def nullp(self):
126-
return self.atom == b""
164+
v = self.atom
165+
return v is not None and len(v) == 0
127166

128167
def as_int(self):
129168
return int_from_bytes(self.atom)
@@ -134,26 +173,29 @@ def as_bin(self):
134173
return f.getvalue()
135174

136175
@classmethod
137-
def to(class_, v: CastableType):
176+
def to(class_, v: CastableType) -> "SExp":
138177
if isinstance(v, class_):
139178
return v
140-
v1 = to_sexp_type(v)
141-
return class_(v1)
142179

143-
def cons(self, right: "CLVMObject"):
144-
s = (self, right)
145-
return self.to(s)
180+
if looks_like_clvm_object(v):
181+
return class_(v)
182+
183+
# this will lazily convert elements
184+
return class_(to_sexp_type(v))
185+
186+
def cons(self, right):
187+
return self.to((self, right))
146188

147189
def first(self):
148190
pair = self.pair
149191
if pair:
150-
return self.to(pair[0])
192+
return self.__class__(pair[0])
151193
raise EvalError("first of non-cons", self)
152194

153195
def rest(self):
154196
pair = self.pair
155197
if pair:
156-
return self.to(pair[1])
198+
return self.__class__(pair[1])
157199
raise EvalError("rest of non-cons", self)
158200

159201
@classmethod
@@ -169,22 +211,22 @@ def as_iter(self):
169211
def __eq__(self, other: CastableType):
170212
try:
171213
other = self.to(other)
214+
to_compare_stack = [(self, other)]
215+
while to_compare_stack:
216+
s1, s2 = to_compare_stack.pop()
217+
p1 = s1.as_pair()
218+
if p1:
219+
p2 = s2.as_pair()
220+
if p2:
221+
to_compare_stack.append((p1[0], p2[0]))
222+
to_compare_stack.append((p1[1], p2[1]))
223+
else:
224+
return False
225+
elif s2.as_pair() or s1.as_atom() != s2.as_atom():
226+
return False
227+
return True
172228
except ValueError:
173229
return False
174-
to_compare_stack = [(self, other)]
175-
while to_compare_stack:
176-
s1, s2 = to_compare_stack.pop()
177-
p1 = s1.as_pair()
178-
if p1:
179-
p2 = s2.as_pair()
180-
if p2:
181-
to_compare_stack.append((p1[0], p2[0]))
182-
to_compare_stack.append((p1[1], p2[1]))
183-
else:
184-
return False
185-
elif s2.as_pair() or s1.as_atom() != s2.as_atom():
186-
return False
187-
return True
188230

189231
def list_len(self):
190232
v = self
@@ -204,5 +246,5 @@ def __repr__(self):
204246
return "%s(%s)" % (self.__class__.__name__, str(self))
205247

206248

207-
SExp.false = SExp.__null__ = SExp(b"")
208-
SExp.true = SExp(b"\1")
249+
SExp.false = SExp.__null__ = SExp(CLVMObject(b""))
250+
SExp.true = SExp(CLVMObject(b"\1"))

tests/as_python_test.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def __init__(self):
1111
self.i = 0
1212

1313

14-
def gen_tree(depth):
14+
def gen_tree(depth: int) -> SExp:
1515
if depth == 0:
1616
return SExp.to(1337)
1717
subtree = gen_tree(depth - 1)
@@ -58,6 +58,8 @@ def test_list_of_one(self):
5858
v = SExp.to([1])
5959
self.assertEqual(type(v.pair[0]), CLVMObject)
6060
self.assertEqual(type(v.pair[1]), CLVMObject)
61+
self.assertEqual(type(v.as_pair()[0]), SExp)
62+
self.assertEqual(type(v.as_pair()[1]), SExp)
6163
self.assertEqual(v.pair[0].atom, b"\x01")
6264
self.assertEqual(v.pair[1].atom, b"")
6365

@@ -162,13 +164,21 @@ def test_long_list(self):
162164
self.assertEqual(v.as_atom(), SExp.null())
163165

164166
def test_invalid_type(self):
165-
self.assertRaises(ValueError, lambda: SExp.to(dummy_class))
167+
with self.assertRaises(ValueError):
168+
s = SExp.to(dummy_class)
169+
# conversions are deferred, this is where it will fail:
170+
b = list(s.as_iter())
171+
print(b)
166172

167173
def test_invalid_tuple(self):
168-
self.assertRaises(ValueError, lambda: SExp.to((dummy_class, dummy_class)))
169-
self.assertRaises(
170-
ValueError, lambda: SExp.to((dummy_class, dummy_class, dummy_class))
171-
)
174+
with self.assertRaises(ValueError):
175+
s = SExp.to((dummy_class, dummy_class))
176+
# conversions are deferred, this is where it will fail:
177+
b = list(s.as_iter())
178+
print(b)
179+
180+
with self.assertRaises(ValueError):
181+
s = SExp.to((dummy_class, dummy_class, dummy_class))
172182

173183
def test_clvm_object_tuple(self):
174184
o1 = CLVMObject(b"foo")

0 commit comments

Comments
 (0)