yaffle 0.0.1__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.
- yaffle-0.0.1/.gitignore +3 -0
- yaffle-0.0.1/PKG-INFO +26 -0
- yaffle-0.0.1/README.md +8 -0
- yaffle-0.0.1/pyproject.toml +32 -0
- yaffle-0.0.1/tests/test_runtime.py +390 -0
- yaffle-0.0.1/yaffle/__init__.py +77 -0
- yaffle-0.0.1/yaffle/_harness.py +288 -0
- yaffle-0.0.1/yaffle/api.py +212 -0
- yaffle-0.0.1/yaffle/arrays.py +242 -0
- yaffle-0.0.1/yaffle/async_view.py +288 -0
- yaffle-0.0.1/yaffle/checksum.py +20 -0
- yaffle-0.0.1/yaffle/codec.py +183 -0
- yaffle-0.0.1/yaffle/commit.py +212 -0
- yaffle-0.0.1/yaffle/errors.py +114 -0
- yaffle-0.0.1/yaffle/externs.py +139 -0
- yaffle-0.0.1/yaffle/ints.py +99 -0
- yaffle-0.0.1/yaffle/jsonc.py +306 -0
- yaffle-0.0.1/yaffle/layout.py +495 -0
- yaffle-0.0.1/yaffle/prim.py +105 -0
- yaffle-0.0.1/yaffle/reader.py +608 -0
- yaffle-0.0.1/yaffle/store.py +656 -0
- yaffle-0.0.1/yaffle/streams.py +187 -0
- yaffle-0.0.1/yaffle/strings.py +98 -0
- yaffle-0.0.1/yaffle/values.py +168 -0
- yaffle-0.0.1/yaffle/view.py +1070 -0
- yaffle-0.0.1/yaffle/writer.py +980 -0
yaffle-0.0.1/.gitignore
ADDED
yaffle-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: yaffle
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Runtime for Python code generated by the yaffle compiler
|
|
5
|
+
Project-URL: Homepage, https://yafflelang.org
|
|
6
|
+
Project-URL: Repository, https://github.com/noahcoolboy/yaffle
|
|
7
|
+
Project-URL: Documentation, https://github.com/noahcoolboy/yaffle/blob/main/docs/targets/python.md
|
|
8
|
+
Author: Noah
|
|
9
|
+
License-Expression: ISC
|
|
10
|
+
Keywords: binary,file-format,parser,serialization,yaffle
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# yaffle
|
|
20
|
+
|
|
21
|
+
The runtime for Python code generated by the [yaffle](https://yafflelang.org) compiler. yaffle
|
|
22
|
+
is a language for binary file formats; the compiler turns a schema into readers, writers and lazy
|
|
23
|
+
views. Generated modules import `yaffle`; it needs Python 3.12+ and has no dependencies.
|
|
24
|
+
|
|
25
|
+
See the [Python target documentation](https://github.com/noahcoolboy/yaffle/blob/main/docs/targets/python.md).
|
|
26
|
+
License: ISC.
|
yaffle-0.0.1/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# yaffle
|
|
2
|
+
|
|
3
|
+
The runtime for Python code generated by the [yaffle](https://yafflelang.org) compiler. yaffle
|
|
4
|
+
is a language for binary file formats; the compiler turns a schema into readers, writers and lazy
|
|
5
|
+
views. Generated modules import `yaffle`; it needs Python 3.12+ and has no dependencies.
|
|
6
|
+
|
|
7
|
+
See the [Python target documentation](https://github.com/noahcoolboy/yaffle/blob/main/docs/targets/python.md).
|
|
8
|
+
License: ISC.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yaffle"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Runtime for Python code generated by the yaffle compiler"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "ISC"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [{ name = "Noah" }]
|
|
13
|
+
keywords = ["yaffle", "binary", "parser", "serialization", "file-format"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://yafflelang.org"
|
|
25
|
+
Repository = "https://github.com/noahcoolboy/yaffle"
|
|
26
|
+
Documentation = "https://github.com/noahcoolboy/yaffle/blob/main/docs/targets/python.md"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["yaffle"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.sdist]
|
|
32
|
+
include = ["yaffle", "tests", "README.md"]
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Unit tests of yaffle (run: python -m unittest discover -s packages/runtime-python/tests)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import gc
|
|
7
|
+
import math
|
|
8
|
+
import unittest
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaffle as rt
|
|
13
|
+
from yaffle.layout import StickyPolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def code_of(f: Any) -> tuple[str, list[Any]]:
|
|
17
|
+
try:
|
|
18
|
+
f()
|
|
19
|
+
except rt.YaffleError as e:
|
|
20
|
+
return e.code, e.path
|
|
21
|
+
raise AssertionError("no YaffleError")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Primitives(unittest.TestCase):
|
|
25
|
+
def test_every_prim_round_trips_in_both_endiannesses(self) -> None:
|
|
26
|
+
samples = {
|
|
27
|
+
"u8": [0, 255], "i8": [-128, 127], "u16": [0, 0xFFFF], "i16": [-0x8000, 0x7FFF],
|
|
28
|
+
"u24": [0, 0xFFFFFF], "i24": [-0x800000, 0x7FFFFF], "u32": [0, 0xFFFFFFFF],
|
|
29
|
+
"i32": [-(2**31), 2**31 - 1], "u40": [2**40 - 1], "i40": [-(2**39)], "u48": [2**48 - 1],
|
|
30
|
+
"i48": [-(2**47), 2**47 - 1], "u56": [2**56 - 1], "i56": [-(2**55)],
|
|
31
|
+
"u64": [2**64 - 1], "i64": [-(2**63), 2**63 - 1],
|
|
32
|
+
} # fmt: skip
|
|
33
|
+
for kind, values in samples.items():
|
|
34
|
+
for le in (True, False):
|
|
35
|
+
for v in values:
|
|
36
|
+
b = rt.pack_prim(kind, v, le)
|
|
37
|
+
self.assertEqual(len(b), rt.PRIMS[kind].size)
|
|
38
|
+
self.assertEqual(rt.read_prim(b, 0, kind, le), v, (kind, le, v))
|
|
39
|
+
r = rt.Reader(b)
|
|
40
|
+
self.assertEqual(r.prim(kind, le), v)
|
|
41
|
+
|
|
42
|
+
def test_u24_byte_order(self) -> None:
|
|
43
|
+
self.assertEqual(rt.pack_prim("u24", 0x010203, True), b"\x03\x02\x01")
|
|
44
|
+
self.assertEqual(rt.pack_prim("u24", 0x010203, False), b"\x01\x02\x03")
|
|
45
|
+
self.assertEqual(rt.Reader(b"\xff\xff\xff").i_n(3, True), -1)
|
|
46
|
+
|
|
47
|
+
def test_floats(self) -> None:
|
|
48
|
+
self.assertEqual(rt.pack_prim("f16", 1.0, True), b"\x00\x3c")
|
|
49
|
+
self.assertEqual(rt.pack_prim("f16", float("nan"), True), b"\x00\x7e")
|
|
50
|
+
self.assertEqual(rt.pack_prim("f16", 1e9, False), b"\x7c\x00") # overflow -> Infinity
|
|
51
|
+
self.assertEqual(rt.pack_prim("f32", -1e40, False), b"\xff\x80\x00\x00")
|
|
52
|
+
self.assertEqual(rt.pack_prim("f32", float("nan"), False), b"\x7f\xc0\x00\x00")
|
|
53
|
+
# round to nearest, ties to even: 1 + 2^-11 is halfway between two halfs
|
|
54
|
+
self.assertEqual(rt.pack_prim("f16", 1 + 2**-11, True), b"\x00\x3c")
|
|
55
|
+
self.assertEqual(rt.Reader(b"\x00\x00\x80\x3f").f32(True), 1.0)
|
|
56
|
+
self.assertTrue(math.isnan(rt.Reader(b"\x01\x7c").f16(True)))
|
|
57
|
+
|
|
58
|
+
def test_check_prim(self) -> None:
|
|
59
|
+
self.assertEqual(code_of(lambda: rt.check_prim(256, "u8"))[0], "RANGE")
|
|
60
|
+
self.assertEqual(code_of(lambda: rt.check_prim(True, "u8"))[0], "INPUT")
|
|
61
|
+
self.assertEqual(code_of(lambda: rt.check_prim("1", "u8"))[0], "INPUT")
|
|
62
|
+
self.assertEqual(code_of(lambda: rt.check_prim(1.5, "u8"))[0], "RANGE")
|
|
63
|
+
self.assertEqual(code_of(lambda: rt.check_prim(float("inf"), "u8"))[0], "INPUT")
|
|
64
|
+
self.assertEqual(rt.check_prim(2.0, "u8"), 2)
|
|
65
|
+
self.assertEqual(rt.check_prim(3, "f32"), 3.0)
|
|
66
|
+
self.assertEqual(code_of(lambda: rt.check_prim(None, "f32"))[0], "INPUT")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Integers(unittest.TestCase):
|
|
70
|
+
def test_truncating_division_and_remainder(self) -> None:
|
|
71
|
+
self.assertEqual((rt.div(7, 2), rt.div(-7, 2), rt.div(7, -2), rt.div(-7, -2)), (3, -3, -3, 3))
|
|
72
|
+
self.assertEqual((rt.mod(7, 2), rt.mod(-7, 2), rt.mod(7, -2), rt.mod(-7, -2)), (1, -1, 1, -1))
|
|
73
|
+
self.assertEqual(code_of(lambda: rt.div(1, 0))[0], "RANGE")
|
|
74
|
+
self.assertEqual(code_of(lambda: rt.mod(1, 0))[0], "RANGE")
|
|
75
|
+
|
|
76
|
+
def test_shifts_and_rounding(self) -> None:
|
|
77
|
+
self.assertEqual(rt.shl(1, 63), 2**63)
|
|
78
|
+
self.assertEqual(rt.shr(-8, 1), -4)
|
|
79
|
+
self.assertEqual(code_of(lambda: rt.shl(1, 64))[0], "RANGE")
|
|
80
|
+
self.assertEqual(code_of(lambda: rt.shr(1, -1))[0], "RANGE")
|
|
81
|
+
self.assertEqual([rt.round_half_away(x) for x in (0.5, 1.5, 2.5, -0.5, -2.5, 2.4)], [1, 2, 3, -1, -3, 2])
|
|
82
|
+
self.assertEqual(rt.trunc(-2.9), -2)
|
|
83
|
+
self.assertEqual(code_of(lambda: rt.round_half_away(float("nan")))[0], "RANGE")
|
|
84
|
+
|
|
85
|
+
def test_float_division(self) -> None:
|
|
86
|
+
self.assertEqual(rt.fdiv(1.0, 0.0), math.inf)
|
|
87
|
+
self.assertEqual(rt.fdiv(-1.0, 0.0), -math.inf)
|
|
88
|
+
self.assertTrue(math.isnan(rt.fdiv(0.0, 0.0)))
|
|
89
|
+
self.assertTrue(math.isnan(rt.fmod(1.0, 0.0)))
|
|
90
|
+
self.assertEqual(rt.fmod(-7.0, 2.0), -1.0)
|
|
91
|
+
|
|
92
|
+
def test_lengths_and_aggregates(self) -> None:
|
|
93
|
+
self.assertEqual(code_of(lambda: rt.length(-1))[0], "RANGE")
|
|
94
|
+
self.assertEqual(rt.sum_of([1, 2, 3]), 6)
|
|
95
|
+
self.assertEqual(rt.max_of([1, 5, 3]), 5)
|
|
96
|
+
self.assertEqual(code_of(lambda: rt.min_of([]))[0], "RANGE")
|
|
97
|
+
self.assertEqual(rt.lengths([[1], [], None, [1, 2]]), [1, 0, 0, 2])
|
|
98
|
+
self.assertEqual(code_of(lambda: rt.at([1], 1))[0], "RANGE")
|
|
99
|
+
self.assertEqual(rt.index_of(["a", "b"], "b"), 1)
|
|
100
|
+
self.assertEqual(code_of(lambda: rt.index_of(["a"], "c"))[0], "INPUT")
|
|
101
|
+
|
|
102
|
+
def test_equals(self) -> None:
|
|
103
|
+
self.assertTrue(rt.equals(1, 1.0))
|
|
104
|
+
self.assertFalse(rt.equals(True, 1))
|
|
105
|
+
self.assertTrue(rt.equals(float("nan"), float("nan")))
|
|
106
|
+
self.assertTrue(rt.equals(b"ab", bytearray(b"ab")))
|
|
107
|
+
self.assertTrue(rt.equals([1, [2]], [1, [2]]))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Strings(unittest.TestCase):
|
|
111
|
+
def test_decode_and_nul(self) -> None:
|
|
112
|
+
r = rt.Reader(b"hi\x00xyz")
|
|
113
|
+
self.assertEqual(r.str(6, 1, "utf8", True), "hi")
|
|
114
|
+
self.assertEqual(rt.Reader("é\x00".encode("utf-16-be")).str_until(2, "utf16", False), "é")
|
|
115
|
+
self.assertEqual(rt.Reader(b"\x00\xf6\x01\x00").str(1, 4, "utf32", True), "\U0001f600")
|
|
116
|
+
|
|
117
|
+
def test_invalid_input_is_check(self) -> None:
|
|
118
|
+
self.assertEqual(code_of(lambda: rt.Reader(b"\xff").str(1, 1, "utf8", True))[0], "CHECK")
|
|
119
|
+
self.assertEqual(code_of(lambda: rt.Reader(b"\x80").str(1, 1, "ascii", True))[0], "CHECK")
|
|
120
|
+
self.assertEqual(code_of(lambda: rt.Reader(b"\x00\xd8").str(1, 2, "utf16", True))[0], "CHECK")
|
|
121
|
+
self.assertEqual(code_of(lambda: rt.Reader(b"\x00\x00\x11\x00").str(1, 4, "utf32", True))[0], "CHECK")
|
|
122
|
+
self.assertEqual(rt.Reader(b"\xe9").str(1, 1, "latin1", True), "é")
|
|
123
|
+
|
|
124
|
+
def test_encode(self) -> None:
|
|
125
|
+
self.assertEqual(code_of(lambda: rt.encode_string("a\x00", "utf8", True))[0], "INPUT")
|
|
126
|
+
self.assertEqual(code_of(lambda: rt.encode_string("é", "ascii", True))[0], "INPUT")
|
|
127
|
+
self.assertEqual(code_of(lambda: rt.encode_string("\ud800", "utf16", True))[0], "INPUT")
|
|
128
|
+
self.assertEqual(rt.code_units("\U0001f600", "utf16"), 2)
|
|
129
|
+
self.assertEqual(rt.code_units("\U0001f600", "utf8"), 4)
|
|
130
|
+
self.assertEqual(rt.encoded_bytes("ab", "utf32"), 8)
|
|
131
|
+
w = rt.Writer(rt.Region())
|
|
132
|
+
self.assertEqual(code_of(lambda: w.str("abc", 1, "utf8", True, "count", 2))[0], "INPUT")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Checksums(unittest.TestCase):
|
|
136
|
+
def test_known_values(self) -> None:
|
|
137
|
+
self.assertEqual(rt.crc32(b"123456789"), 0xCBF43926)
|
|
138
|
+
self.assertEqual(rt.adler32(b"Wikipedia"), 0x11E60398)
|
|
139
|
+
self.assertEqual(rt.crc32("123456789"), 0xCBF43926)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Json(unittest.TestCase):
|
|
143
|
+
def test_scalars(self) -> None:
|
|
144
|
+
self.assertEqual([rt.float_to_json(x) for x in (math.nan, math.inf, -math.inf, -0.0, 1.5)],
|
|
145
|
+
["NaN", "Infinity", "-Infinity", "-0", 1.5]) # fmt: skip
|
|
146
|
+
self.assertTrue(math.isnan(rt.float_from_json("NaN")))
|
|
147
|
+
self.assertEqual(math.copysign(1, rt.float_from_json("-0")), -1)
|
|
148
|
+
self.assertEqual(code_of(lambda: rt.int_from_json(True))[0], "INPUT")
|
|
149
|
+
self.assertEqual(code_of(lambda: rt.int_from_json(1.5))[0], "RANGE")
|
|
150
|
+
self.assertEqual(rt.big_from_json("-12"), -12)
|
|
151
|
+
self.assertEqual(code_of(lambda: rt.big_from_json(12))[0], "INPUT")
|
|
152
|
+
self.assertEqual(rt.from_hex("0a 0B"), b"\x0a\x0b")
|
|
153
|
+
self.assertEqual(code_of(lambda: rt.from_hex("0"))[0], "INPUT")
|
|
154
|
+
|
|
155
|
+
def test_refs(self) -> None:
|
|
156
|
+
c = rt.ToJsonCtx()
|
|
157
|
+
shared = [1]
|
|
158
|
+
self.assertIsNone(c.ref(shared, "/a"))
|
|
159
|
+
self.assertEqual(c.ref(shared, "/b"), {"$ref": "/a"})
|
|
160
|
+
self.assertIsNone(c.ref([], "/c"))
|
|
161
|
+
self.assertIsNone(c.ref(b"x", "/d"))
|
|
162
|
+
f = rt.FromJsonCtx()
|
|
163
|
+
out = f.arr([1, 2], "/x", lambda j, p: j)
|
|
164
|
+
self.assertIs(f.deref({"$ref": "/x"})[0], out) # type: ignore[index]
|
|
165
|
+
self.assertEqual(code_of(lambda: f.deref({"$ref": "/nope"}))[0], "INPUT")
|
|
166
|
+
self.assertEqual(rt.ptr("/a", "b/c~"), "/a/b~1c~0")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class Reading(unittest.TestCase):
|
|
170
|
+
def test_eof_and_windows(self) -> None:
|
|
171
|
+
r = rt.Reader(b"\x01\x02\x03")
|
|
172
|
+
self.assertEqual(code_of(lambda: r.u32(True))[0], "EOF")
|
|
173
|
+
saved = r.window(2)
|
|
174
|
+
self.assertEqual(r.u8(), 1)
|
|
175
|
+
self.assertEqual(code_of(lambda: r.u16(True))[0], "EOF")
|
|
176
|
+
r.close_window(saved, False)
|
|
177
|
+
self.assertEqual(r.pos, 2)
|
|
178
|
+
self.assertEqual(code_of(lambda: rt.Reader(b"\x01").pad(1, 0, True))[0], "CHECK")
|
|
179
|
+
self.assertEqual(code_of(lambda: rt.Reader(b"\x05").bool(True))[0], "CHECK")
|
|
180
|
+
|
|
181
|
+
def test_targets_share_identity(self) -> None:
|
|
182
|
+
@dataclass
|
|
183
|
+
class S:
|
|
184
|
+
x: int = 0
|
|
185
|
+
|
|
186
|
+
r = rt.Reader(b"\x07\x00")
|
|
187
|
+
|
|
188
|
+
def read() -> S:
|
|
189
|
+
s = r.take_into(S)
|
|
190
|
+
s.x = r.u8()
|
|
191
|
+
return s
|
|
192
|
+
|
|
193
|
+
a = r.target_struct(0, "S", read, S)
|
|
194
|
+
b = r.target_struct(0, "S", read, S)
|
|
195
|
+
self.assertIs(a, b)
|
|
196
|
+
self.assertEqual(r.pos, 0)
|
|
197
|
+
|
|
198
|
+
def test_union_errors(self) -> None:
|
|
199
|
+
r = rt.Reader(b"\x01")
|
|
200
|
+
|
|
201
|
+
def bad() -> int:
|
|
202
|
+
r.u16(True)
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
code, _ = code_of(lambda: r.union([("A", bad), ("B", bad)]))
|
|
206
|
+
self.assertEqual(code, "UNION")
|
|
207
|
+
v = r.union([("A", bad), ("B", r.u8)])
|
|
208
|
+
self.assertEqual((v.type, v.value), ("B", 1))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class Writing(unittest.TestCase):
|
|
212
|
+
def test_placeables_and_pointers(self) -> None:
|
|
213
|
+
region = rt.Region()
|
|
214
|
+
w = rt.Writer(region)
|
|
215
|
+
w.u8(1)
|
|
216
|
+
target = [1, 2]
|
|
217
|
+
|
|
218
|
+
def body() -> None:
|
|
219
|
+
w.u8(0xAA)
|
|
220
|
+
w.u8(0xBB)
|
|
221
|
+
|
|
222
|
+
p = w.target(target, "k", "t", body)
|
|
223
|
+
p2 = w.target(target, "k", "t", body)
|
|
224
|
+
self.assertIs(p, p2)
|
|
225
|
+
w.pointer("u16", True, p, False, None)
|
|
226
|
+
w.pointer("u16", True, None, False, 0)
|
|
227
|
+
self.assertEqual(bytes(region.finish()), b"\x01\x05\x00\x00\x00\xaa\xbb")
|
|
228
|
+
|
|
229
|
+
def test_alignment_constraints_and_overlap(self) -> None:
|
|
230
|
+
region = rt.Region()
|
|
231
|
+
w = rt.Writer(region)
|
|
232
|
+
w.u8(1)
|
|
233
|
+
a = w.place("a", None, lambda: w.u8(2), constraint=(4, 0))
|
|
234
|
+
w.place("b", None, lambda: w.u8(3), fixed=2)
|
|
235
|
+
out = bytes(region.finish())
|
|
236
|
+
self.assertEqual(a.block.pos, 4)
|
|
237
|
+
self.assertEqual(out, b"\x01\x00\x03\x00\x02")
|
|
238
|
+
region = rt.Region()
|
|
239
|
+
w = rt.Writer(region)
|
|
240
|
+
w.u32(0, True)
|
|
241
|
+
w.place("b", None, lambda: w.u8(3), fixed=2)
|
|
242
|
+
self.assertEqual(code_of(region.finish)[0], "OVERLAP")
|
|
243
|
+
|
|
244
|
+
def test_layout_items_order(self) -> None:
|
|
245
|
+
region = rt.Region()
|
|
246
|
+
w = rt.Writer(region)
|
|
247
|
+
# The region's layout orders its root struct's targets (a struct's own `layout`).
|
|
248
|
+
region.layout = ((("y",), (("align", 4),)), (("x",), ()))
|
|
249
|
+
lroot = rt.LStruct()
|
|
250
|
+
x = w.place("x", None, lambda: w.u8(0x11))
|
|
251
|
+
y = w.place("y", None, lambda: w.u8(0x22))
|
|
252
|
+
lroot.set("x", x)
|
|
253
|
+
lroot.set("y", y)
|
|
254
|
+
w.u8(0)
|
|
255
|
+
region.lroot = lroot
|
|
256
|
+
out = bytes(region.finish())
|
|
257
|
+
self.assertEqual(out, b"\x00\x00\x00\x00\x22\x11")
|
|
258
|
+
|
|
259
|
+
def test_layout_from_the_root_struct(self) -> None:
|
|
260
|
+
# A layout computed by the struct's writer reaches a region without its own.
|
|
261
|
+
region = rt.Region()
|
|
262
|
+
w = rt.Writer(region)
|
|
263
|
+
w.u8(0)
|
|
264
|
+
lroot = rt.LStruct()
|
|
265
|
+
lroot.set("x", w.place("x", None, lambda: w.u8(0x11)))
|
|
266
|
+
lroot.layout = ((("x",), (("align", rt.layout_align(8)),)),)
|
|
267
|
+
region.attach_root(lroot)
|
|
268
|
+
self.assertEqual(bytes(region.finish()), b"\x00" + b"\x00" * 7 + b"\x11")
|
|
269
|
+
self.assertEqual(code_of(lambda: rt.layout_align(0))[0], "RANGE")
|
|
270
|
+
self.assertEqual(code_of(lambda: rt.layout_align(True))[0], "RANGE")
|
|
271
|
+
self.assertEqual(code_of(lambda: rt.layout_pad(-1))[0], "RANGE")
|
|
272
|
+
self.assertEqual(rt.layout_pad(0), 0)
|
|
273
|
+
|
|
274
|
+
def test_ref_pointers(self) -> None:
|
|
275
|
+
region = rt.Region()
|
|
276
|
+
w = rt.Writer(region)
|
|
277
|
+
start = w.offset
|
|
278
|
+
w.str("abc", 1, "utf8", True, "until")
|
|
279
|
+
w.ref_site("str", start, "abc")
|
|
280
|
+
w.ref_pointer("u8", True, "str", "abc", lambda: b"abc\x00", False, None)
|
|
281
|
+
w.ref_pointer("u8", True, "str", "zzz", lambda: b"zzz\x00", False, None)
|
|
282
|
+
self.assertEqual(code_of(region.finish)[0], "REF")
|
|
283
|
+
|
|
284
|
+
def test_deferred_values_and_extents(self) -> None:
|
|
285
|
+
region = rt.Region()
|
|
286
|
+
w = rt.Writer(region)
|
|
287
|
+
e = w.extent()
|
|
288
|
+
w.defer_prim("u8", True, lambda: e.size)
|
|
289
|
+
w.bytes(b"xyz")
|
|
290
|
+
w.close(e)
|
|
291
|
+
self.assertEqual(bytes(region.finish()), b"\x04xyz")
|
|
292
|
+
|
|
293
|
+
def test_sticky_layout_keeps_positions(self) -> None:
|
|
294
|
+
region = rt.Region()
|
|
295
|
+
region.sticky = StickyPolicy("default", 1, 8)
|
|
296
|
+
w = rt.Writer(region)
|
|
297
|
+
w.u8(0)
|
|
298
|
+
p = w.place("p", None, lambda: w.u8(9))
|
|
299
|
+
p.orig, p.orig_size = 6, 1
|
|
300
|
+
out = bytes(region.finish())
|
|
301
|
+
self.assertEqual(out, b"\x00\x00\x00\x00\x00\x00\x09")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class Codecs(unittest.TestCase):
|
|
305
|
+
def test_reuse_tracks_objects(self) -> None:
|
|
306
|
+
@dataclass
|
|
307
|
+
class V:
|
|
308
|
+
a: int
|
|
309
|
+
|
|
310
|
+
v = V(1)
|
|
311
|
+
rt.remember_codec(v, "s", b"ENC", b"\x01")
|
|
312
|
+
self.assertEqual(rt.reuse_codec(v, "s", b"\x01"), b"ENC")
|
|
313
|
+
self.assertIsNone(rt.reuse_codec(v, "s", b"\x02"))
|
|
314
|
+
self.assertIsNone(rt.reuse_codec(V(1), "s", b"\x01"))
|
|
315
|
+
|
|
316
|
+
owner = V(2)
|
|
317
|
+
data = b"plain bytes value"
|
|
318
|
+
rt.remember_codec(data, "t", b"E", data, owner)
|
|
319
|
+
self.assertEqual(rt.reuse_codec(data, "t", data), b"E")
|
|
320
|
+
del owner
|
|
321
|
+
gc.collect()
|
|
322
|
+
self.assertIsNone(rt.reuse_codec(data, "t", data))
|
|
323
|
+
|
|
324
|
+
def test_codec_errors(self) -> None:
|
|
325
|
+
class Bad:
|
|
326
|
+
def decode(self, data: bytes, ctx: rt.CodecContext) -> bytes:
|
|
327
|
+
raise ValueError("boom")
|
|
328
|
+
|
|
329
|
+
def encode(self, data: bytes, ctx: rt.CodecContext) -> bytes:
|
|
330
|
+
return b""
|
|
331
|
+
|
|
332
|
+
self.assertEqual(code_of(lambda: rt.decode(Bad(), "bad", b"", rt.CodecContext(), False))[0], "CODEC")
|
|
333
|
+
|
|
334
|
+
class Async:
|
|
335
|
+
async def decode(self, data: bytes, ctx: rt.CodecContext) -> bytes:
|
|
336
|
+
return data
|
|
337
|
+
|
|
338
|
+
async def encode(self, data: bytes, ctx: rt.CodecContext) -> bytes:
|
|
339
|
+
return data
|
|
340
|
+
|
|
341
|
+
self.assertEqual(code_of(lambda: rt.decode(Async(), "a", b"x", rt.CodecContext(), False))[0], "CODEC")
|
|
342
|
+
out = asyncio.run(rt.decode_async(Async(), "a", b"x", rt.CodecContext(), False))
|
|
343
|
+
self.assertEqual(out, (b"x", 1))
|
|
344
|
+
|
|
345
|
+
def test_streams(self) -> None:
|
|
346
|
+
self.assertEqual(rt.join_stream([b"ab", b"c"]), b"abc")
|
|
347
|
+
self.assertEqual(rt.split_by_sizes(b"abc", [1, 2]), [b"a", b"bc"])
|
|
348
|
+
self.assertEqual(code_of(lambda: rt.split_by_sizes(b"abc", [1]))[0], "DERIVE")
|
|
349
|
+
self.assertEqual(code_of(lambda: rt.slice_stream(b"abc", 2, 5))[0], "EOF")
|
|
350
|
+
|
|
351
|
+
def test_leb128(self) -> None:
|
|
352
|
+
self.assertEqual(rt.uleb128.write(624485), bytes.fromhex("e58e26"))
|
|
353
|
+
self.assertEqual(rt.uleb128.read(bytes.fromhex("e58e26")), 624485)
|
|
354
|
+
self.assertEqual(rt.sleb128.write(-123456), bytes.fromhex("c0bb78"))
|
|
355
|
+
self.assertEqual(rt.sleb128.read(bytes.fromhex("c0bb78")), -123456)
|
|
356
|
+
self.assertEqual(rt.uleb128.size(b"\x80\x80"), None)
|
|
357
|
+
r = rt.Reader(bytes.fromhex("e58e2600"))
|
|
358
|
+
self.assertEqual(r.extern(rt.uleb128, ()), 624485)
|
|
359
|
+
self.assertEqual(r.pos, 3)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class Errors(unittest.TestCase):
|
|
363
|
+
def test_paths(self) -> None:
|
|
364
|
+
e = rt.YaffleError("CHECK", "bad", 16)
|
|
365
|
+
e.prefix("x", -1, "")
|
|
366
|
+
e.prefix("entries", 2)
|
|
367
|
+
self.assertEqual(e.path, ["entries", 2, "x"])
|
|
368
|
+
self.assertEqual(str(e), "CHECK at entries[2].x (offset 0x10): bad")
|
|
369
|
+
e.sealed = True
|
|
370
|
+
e.prefix("outer")
|
|
371
|
+
self.assertEqual(e.path, ["entries", 2, "x"])
|
|
372
|
+
r = rt.safe(lambda: rt.fail("EOF", "x"))
|
|
373
|
+
self.assertFalse(r.ok)
|
|
374
|
+
self.assertEqual(r.error.code if r.error else None, "EOF")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class Sources(unittest.TestCase):
|
|
378
|
+
def test_async_source_rejected_by_sync_parse(self) -> None:
|
|
379
|
+
class Src:
|
|
380
|
+
size = 1
|
|
381
|
+
|
|
382
|
+
async def read(self, off: int, n: int) -> bytes:
|
|
383
|
+
return b"\x00"
|
|
384
|
+
|
|
385
|
+
self.assertEqual(code_of(lambda: rt.read_all(Src()))[0], "INPUT")
|
|
386
|
+
self.assertEqual(asyncio.run(rt.read_all_async(Src())), b"\x00")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
if __name__ == "__main__":
|
|
390
|
+
unittest.main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""yaffle: the runtime of the Python code the yaffle compiler generates.
|
|
2
|
+
|
|
3
|
+
Generated modules import it as ``_rt``. Pure Python 3.12+, no dependencies. Besides what
|
|
4
|
+
generated code calls, it offers sources (:func:`file_source`), errors (:class:`YaffleError`),
|
|
5
|
+
checksums and canonical JSON helpers.
|
|
6
|
+
|
|
7
|
+
Modules: ``errors``, ``prim`` (primitive encodings), ``ints`` (number semantics), ``strings``,
|
|
8
|
+
``checksum``, ``arrays`` (array builtins and equality), ``jsonc`` (canonical JSON), ``values``
|
|
9
|
+
(checks of values), ``externs`` (host code), ``codec`` (``@via`` and codec reuse), ``streams``
|
|
10
|
+
(streams), ``reader``, ``writer`` and ``layout`` (parse and serialize), ``api`` (root API
|
|
11
|
+
glue and sources), and for views ``store`` (bytes read on demand), ``view``, ``commit`` and
|
|
12
|
+
``async_view``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from . import (
|
|
18
|
+
api,
|
|
19
|
+
arrays,
|
|
20
|
+
async_view,
|
|
21
|
+
checksum,
|
|
22
|
+
codec,
|
|
23
|
+
commit,
|
|
24
|
+
errors,
|
|
25
|
+
externs,
|
|
26
|
+
ints,
|
|
27
|
+
jsonc,
|
|
28
|
+
layout,
|
|
29
|
+
prim,
|
|
30
|
+
reader,
|
|
31
|
+
streams,
|
|
32
|
+
strings,
|
|
33
|
+
values,
|
|
34
|
+
view,
|
|
35
|
+
writer,
|
|
36
|
+
)
|
|
37
|
+
from .api import *
|
|
38
|
+
from .arrays import *
|
|
39
|
+
from .async_view import *
|
|
40
|
+
from .checksum import *
|
|
41
|
+
from .codec import *
|
|
42
|
+
from .commit import *
|
|
43
|
+
from .errors import *
|
|
44
|
+
from .externs import *
|
|
45
|
+
from .ints import *
|
|
46
|
+
from .jsonc import *
|
|
47
|
+
from .layout import *
|
|
48
|
+
from .prim import *
|
|
49
|
+
from .reader import *
|
|
50
|
+
from .streams import *
|
|
51
|
+
from .strings import *
|
|
52
|
+
from .values import *
|
|
53
|
+
from .view import *
|
|
54
|
+
from .writer import *
|
|
55
|
+
|
|
56
|
+
__version__ = "0.0.1"
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
*api.__all__,
|
|
60
|
+
*arrays.__all__,
|
|
61
|
+
*async_view.__all__,
|
|
62
|
+
*checksum.__all__,
|
|
63
|
+
*codec.__all__,
|
|
64
|
+
*commit.__all__,
|
|
65
|
+
*errors.__all__,
|
|
66
|
+
*externs.__all__,
|
|
67
|
+
*ints.__all__,
|
|
68
|
+
*jsonc.__all__,
|
|
69
|
+
*layout.__all__,
|
|
70
|
+
*prim.__all__,
|
|
71
|
+
*reader.__all__,
|
|
72
|
+
*streams.__all__,
|
|
73
|
+
*strings.__all__,
|
|
74
|
+
*values.__all__,
|
|
75
|
+
*view.__all__,
|
|
76
|
+
*writer.__all__,
|
|
77
|
+
]
|