zexus 1.8.0 → 1.8.1

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 (44) hide show
  1. package/README.md +34 -6
  2. package/package.json +2 -1
  3. package/rust_core/Cargo.lock +603 -0
  4. package/rust_core/Cargo.toml +26 -0
  5. package/rust_core/README.md +15 -0
  6. package/rust_core/pyproject.toml +25 -0
  7. package/rust_core/src/binary_bytecode.rs +543 -0
  8. package/rust_core/src/contract_vm.rs +643 -0
  9. package/rust_core/src/executor.rs +847 -0
  10. package/rust_core/src/hasher.rs +90 -0
  11. package/rust_core/src/lib.rs +71 -0
  12. package/rust_core/src/merkle.rs +128 -0
  13. package/rust_core/src/rust_vm.rs +2313 -0
  14. package/rust_core/src/signature.rs +79 -0
  15. package/rust_core/src/state_adapter.rs +281 -0
  16. package/rust_core/src/validator.rs +116 -0
  17. package/scripts/postinstall.js +34 -2
  18. package/src/zexus/__init__.py +1 -1
  19. package/src/zexus/cli/main.py +1 -1
  20. package/src/zexus/cli/zpm.py +1 -1
  21. package/src/zexus/evaluator/bytecode_compiler.py +150 -52
  22. package/src/zexus/evaluator/core.py +151 -809
  23. package/src/zexus/evaluator/expressions.py +27 -22
  24. package/src/zexus/evaluator/functions.py +171 -126
  25. package/src/zexus/evaluator/statements.py +55 -112
  26. package/src/zexus/module_cache.py +20 -9
  27. package/src/zexus/object.py +330 -38
  28. package/src/zexus/parser/parser.py +69 -14
  29. package/src/zexus/parser/strategy_context.py +228 -5
  30. package/src/zexus/parser/strategy_structural.py +2 -2
  31. package/src/zexus/persistence.py +46 -17
  32. package/src/zexus/security.py +140 -234
  33. package/src/zexus/type_checker.py +44 -5
  34. package/src/zexus/vm/binary_bytecode.py +7 -3
  35. package/src/zexus/vm/bytecode.py +6 -0
  36. package/src/zexus/vm/cache.py +24 -46
  37. package/src/zexus/vm/compiler.py +80 -20
  38. package/src/zexus/vm/memory_pool.py +21 -9
  39. package/src/zexus/vm/vm.py +364 -67
  40. package/src/zexus/zpm/package_manager.py +1 -1
  41. package/src/zexus.egg-info/PKG-INFO +56 -12
  42. package/src/zexus.egg-info/SOURCES.txt +6 -1
  43. package/src/zexus.egg-info/requires.txt +26 -0
  44. package/src/zexus.egg-info/entry_points.txt +0 -4
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "zexus_core"
3
+ version = "0.4.0"
4
+ edition = "2021"
5
+ description = "High-performance Rust execution core for the Zexus blockchain"
6
+ license = "MIT"
7
+
8
+ [lib]
9
+ name = "zexus_core"
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ pyo3 = { version = "0.22", features = ["extension-module"] }
14
+ sha2 = "0.10"
15
+ rayon = "1.10"
16
+ serde = { version = "1", features = ["derive"] }
17
+ serde_json = "1"
18
+ hex = "0.4"
19
+ k256 = { version = "0.13", features = ["ecdsa", "sha256"] }
20
+ tiny-keccak = { version = "2", features = ["keccak"] }
21
+
22
+ [profile.release]
23
+ opt-level = 3
24
+ lto = true
25
+ codegen-units = 1
26
+ strip = true
@@ -0,0 +1,15 @@
1
+ # zexus-core
2
+
3
+ This directory contains the optional Rust execution core for Zexus, exposed to Python as the `zexus_core` module via PyO3.
4
+
5
+ ## Build (from this repo)
6
+
7
+ Requirements:
8
+ - Python 3.8+
9
+ - Rust toolchain (`cargo`)
10
+
11
+ Commands:
12
+ - `python -m pip install -U maturin`
13
+ - `python -m maturin develop -m rust_core/Cargo.toml --release`
14
+
15
+ If the extension is not available, Zexus falls back to the pure-Python VM automatically.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.5,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "zexus-core"
7
+ version = "0.4.0"
8
+ description = "High-performance Rust execution core for the Zexus blockchain (PyO3 extension)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Zaidux" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Rust",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+
22
+ [tool.maturin]
23
+ module-name = "zexus_core"
24
+ manifest-path = "Cargo.toml"
25
+ features = ["pyo3/extension-module"]
@@ -0,0 +1,543 @@
1
+ // ─────────────────────────────────────────────────────────────────────
2
+ // Zexus Blockchain — Binary Bytecode Format (Rust Deserializer)
3
+ // ─────────────────────────────────────────────────────────────────────
4
+ //
5
+ // Deserializes the .zxc binary bytecode format produced by Python's
6
+ // `binary_bytecode.serialize()`. The format is intentionally simple
7
+ // (little-endian, no alignment padding) so both Python and Rust can
8
+ // read it without external dependencies.
9
+ //
10
+ // This module is GIL-free — it operates on raw `&[u8]` data that was
11
+ // already copied out of Python.
12
+
13
+ use pyo3::prelude::*;
14
+ use pyo3::types::{PyBytes, PyDict, PyList};
15
+ use pyo3::ToPyObject;
16
+ use serde::{Deserialize, Serialize};
17
+ use std::fmt;
18
+
19
+ // ── Format constants ──────────────────────────────────────────────────
20
+
21
+ const ZXC_MAGIC: &[u8; 4] = b"ZXC\x00";
22
+ const ZXC_HEADER_SIZE: usize = 16;
23
+
24
+ // ── Constant tags ─────────────────────────────────────────────────────
25
+
26
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
27
+ #[repr(u8)]
28
+ enum ConstTag {
29
+ Null = 0x00,
30
+ Bool = 0x01,
31
+ Int32 = 0x02,
32
+ Int64 = 0x03,
33
+ Float64 = 0x04,
34
+ String = 0x05,
35
+ FuncDesc = 0x06,
36
+ List = 0x07,
37
+ Map = 0x08,
38
+ Opaque = 0xFF,
39
+ }
40
+
41
+ impl ConstTag {
42
+ fn from_u8(val: u8) -> Option<Self> {
43
+ match val {
44
+ 0x00 => Some(Self::Null),
45
+ 0x01 => Some(Self::Bool),
46
+ 0x02 => Some(Self::Int32),
47
+ 0x03 => Some(Self::Int64),
48
+ 0x04 => Some(Self::Float64),
49
+ 0x05 => Some(Self::String),
50
+ 0x06 => Some(Self::FuncDesc),
51
+ 0x07 => Some(Self::List),
52
+ 0x08 => Some(Self::Map),
53
+ 0xFF => Some(Self::Opaque),
54
+ _ => None,
55
+ }
56
+ }
57
+ }
58
+
59
+ // ── Operand types ─────────────────────────────────────────────────────
60
+
61
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
62
+ #[repr(u8)]
63
+ enum OperandType {
64
+ None = 0x00,
65
+ U32 = 0x01,
66
+ I64 = 0x02,
67
+ PairU32 = 0x03,
68
+ TripleU32 = 0x04,
69
+ }
70
+
71
+ impl OperandType {
72
+ fn from_u8(val: u8) -> Option<Self> {
73
+ match val {
74
+ 0x00 => Some(Self::None),
75
+ 0x01 => Some(Self::U32),
76
+ 0x02 => Some(Self::I64),
77
+ 0x03 => Some(Self::PairU32),
78
+ 0x04 => Some(Self::TripleU32),
79
+ _ => None,
80
+ }
81
+ }
82
+ }
83
+
84
+ // ── Value types for the constant pool ─────────────────────────────────
85
+
86
+ #[derive(Debug, Clone, Serialize, Deserialize)]
87
+ pub enum ZxcValue {
88
+ Null,
89
+ Bool(bool),
90
+ Int(i64),
91
+ Float(f64),
92
+ String(String),
93
+ FuncDesc(String), // JSON-encoded function descriptor
94
+ List(Vec<ZxcValue>),
95
+ Map(Vec<(ZxcValue, ZxcValue)>),
96
+ Opaque(Vec<u8>), // Python-pickled data (not interpreted in Rust)
97
+ }
98
+
99
+ impl fmt::Display for ZxcValue {
100
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101
+ match self {
102
+ ZxcValue::Null => write!(f, "null"),
103
+ ZxcValue::Bool(b) => write!(f, "{}", b),
104
+ ZxcValue::Int(i) => write!(f, "{}", i),
105
+ ZxcValue::Float(v) => write!(f, "{}", v),
106
+ ZxcValue::String(s) => write!(f, "\"{}\"", s),
107
+ ZxcValue::FuncDesc(s) => write!(f, "func({})", s),
108
+ ZxcValue::List(items) => write!(f, "[{} items]", items.len()),
109
+ ZxcValue::Map(pairs) => write!(f, "{{{} pairs}}", pairs.len()),
110
+ ZxcValue::Opaque(data) => write!(f, "<opaque {} bytes>", data.len()),
111
+ }
112
+ }
113
+ }
114
+
115
+ // ── Instruction operand ───────────────────────────────────────────────
116
+
117
+ #[derive(Debug, Clone, Serialize, Deserialize)]
118
+ pub enum Operand {
119
+ None,
120
+ U32(u32),
121
+ I64(i64),
122
+ Pair(u32, u32),
123
+ Triple(u32, u32, u32),
124
+ }
125
+
126
+ // ── Decoded instruction ───────────────────────────────────────────────
127
+
128
+ #[derive(Debug, Clone, Serialize, Deserialize)]
129
+ pub struct Instruction {
130
+ pub opcode: u16,
131
+ pub operand: Operand,
132
+ }
133
+
134
+ // ── Decoded bytecode module ───────────────────────────────────────────
135
+
136
+ #[derive(Debug, Clone, Serialize, Deserialize)]
137
+ pub struct ZxcModule {
138
+ pub version: u16,
139
+ pub flags: u16,
140
+ pub constants: Vec<ZxcValue>,
141
+ pub instructions: Vec<Instruction>,
142
+ }
143
+
144
+ impl ZxcModule {
145
+ pub fn n_constants(&self) -> usize {
146
+ self.constants.len()
147
+ }
148
+
149
+ pub fn n_instructions(&self) -> usize {
150
+ self.instructions.len()
151
+ }
152
+ }
153
+
154
+ // ── Binary reader ─────────────────────────────────────────────────────
155
+
156
+ struct Reader<'a> {
157
+ data: &'a [u8],
158
+ pos: usize,
159
+ }
160
+
161
+ impl<'a> Reader<'a> {
162
+ fn new(data: &'a [u8]) -> Self {
163
+ Self { data, pos: 0 }
164
+ }
165
+
166
+ fn remaining(&self) -> usize {
167
+ self.data.len().saturating_sub(self.pos)
168
+ }
169
+
170
+ fn read(&mut self, n: usize) -> Result<&'a [u8], String> {
171
+ let end = self.pos + n;
172
+ if end > self.data.len() {
173
+ return Err(format!(
174
+ "Unexpected end of data at offset {}, need {} bytes",
175
+ self.pos, n
176
+ ));
177
+ }
178
+ let slice = &self.data[self.pos..end];
179
+ self.pos = end;
180
+ Ok(slice)
181
+ }
182
+
183
+ fn u8(&mut self) -> Result<u8, String> {
184
+ Ok(self.read(1)?[0])
185
+ }
186
+
187
+ fn u16(&mut self) -> Result<u16, String> {
188
+ let bytes = self.read(2)?;
189
+ Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
190
+ }
191
+
192
+ fn u32(&mut self) -> Result<u32, String> {
193
+ let bytes = self.read(4)?;
194
+ Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
195
+ }
196
+
197
+ fn i32(&mut self) -> Result<i32, String> {
198
+ let bytes = self.read(4)?;
199
+ Ok(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
200
+ }
201
+
202
+ fn i64(&mut self) -> Result<i64, String> {
203
+ let bytes = self.read(8)?;
204
+ Ok(i64::from_le_bytes([
205
+ bytes[0], bytes[1], bytes[2], bytes[3],
206
+ bytes[4], bytes[5], bytes[6], bytes[7],
207
+ ]))
208
+ }
209
+
210
+ fn f64(&mut self) -> Result<f64, String> {
211
+ let bytes = self.read(8)?;
212
+ Ok(f64::from_le_bytes([
213
+ bytes[0], bytes[1], bytes[2], bytes[3],
214
+ bytes[4], bytes[5], bytes[6], bytes[7],
215
+ ]))
216
+ }
217
+
218
+ fn string(&mut self) -> Result<String, String> {
219
+ let len = self.u32()? as usize;
220
+ let bytes = self.read(len)?;
221
+ String::from_utf8(bytes.to_vec())
222
+ .map_err(|e| format!("Invalid UTF-8 string at offset {}: {}", self.pos - len, e))
223
+ }
224
+
225
+ fn raw_bytes(&mut self) -> Result<Vec<u8>, String> {
226
+ let len = self.u32()? as usize;
227
+ Ok(self.read(len)?.to_vec())
228
+ }
229
+ }
230
+
231
+ // ── Deserialization functions ─────────────────────────────────────────
232
+
233
+ fn read_constant(r: &mut Reader) -> Result<ZxcValue, String> {
234
+ let tag_byte = r.u8()?;
235
+ let tag = ConstTag::from_u8(tag_byte)
236
+ .ok_or_else(|| format!("Unknown constant tag: 0x{:02x}", tag_byte))?;
237
+
238
+ match tag {
239
+ ConstTag::Null => Ok(ZxcValue::Null),
240
+ ConstTag::Bool => Ok(ZxcValue::Bool(r.u8()? != 0)),
241
+ ConstTag::Int32 => Ok(ZxcValue::Int(r.i32()? as i64)),
242
+ ConstTag::Int64 => Ok(ZxcValue::Int(r.i64()?)),
243
+ ConstTag::Float64 => Ok(ZxcValue::Float(r.f64()?)),
244
+ ConstTag::String => Ok(ZxcValue::String(r.string()?)),
245
+ ConstTag::FuncDesc => Ok(ZxcValue::FuncDesc(r.string()?)),
246
+ ConstTag::List => {
247
+ let count = r.u32()? as usize;
248
+ let mut items = Vec::with_capacity(count);
249
+ for _ in 0..count {
250
+ items.push(read_constant(r)?);
251
+ }
252
+ Ok(ZxcValue::List(items))
253
+ }
254
+ ConstTag::Map => {
255
+ let count = r.u32()? as usize;
256
+ let mut pairs = Vec::with_capacity(count);
257
+ for _ in 0..count {
258
+ let key = read_constant(r)?;
259
+ let val = read_constant(r)?;
260
+ pairs.push((key, val));
261
+ }
262
+ Ok(ZxcValue::Map(pairs))
263
+ }
264
+ ConstTag::Opaque => Ok(ZxcValue::Opaque(r.raw_bytes()?)),
265
+ }
266
+ }
267
+
268
+ fn read_instruction(r: &mut Reader) -> Result<Instruction, String> {
269
+ let opcode = r.u16()?;
270
+ let op_type_byte = r.u8()?;
271
+ let op_type = OperandType::from_u8(op_type_byte)
272
+ .ok_or_else(|| format!("Unknown operand type: 0x{:02x}", op_type_byte))?;
273
+
274
+ let operand = match op_type {
275
+ OperandType::None => Operand::None,
276
+ OperandType::U32 => Operand::U32(r.u32()?),
277
+ OperandType::I64 => Operand::I64(r.i64()?),
278
+ OperandType::PairU32 => {
279
+ let a = r.u32()?;
280
+ let b = r.u32()?;
281
+ Operand::Pair(a, b)
282
+ }
283
+ OperandType::TripleU32 => {
284
+ let a = r.u32()?;
285
+ let b = r.u32()?;
286
+ let c = r.u32()?;
287
+ Operand::Triple(a, b, c)
288
+ }
289
+ };
290
+
291
+ Ok(Instruction { opcode, operand })
292
+ }
293
+
294
+ /// Deserialize a .zxc binary buffer into a ZxcModule.
295
+ ///
296
+ /// This function is fully GIL-free — it operates on raw bytes.
297
+ pub fn deserialize_zxc(data: &[u8], verify_checksum: bool) -> Result<ZxcModule, String> {
298
+ if data.len() < ZXC_HEADER_SIZE {
299
+ return Err(format!(
300
+ "Data too short for header: {} bytes (need {})",
301
+ data.len(),
302
+ ZXC_HEADER_SIZE
303
+ ));
304
+ }
305
+
306
+ // Verify CRC32 checksum (last 4 bytes)
307
+ let body = if verify_checksum && data.len() > ZXC_HEADER_SIZE + 4 {
308
+ let body = &data[..data.len() - 4];
309
+ let stored_bytes = &data[data.len() - 4..];
310
+ let stored_crc = u32::from_le_bytes([
311
+ stored_bytes[0],
312
+ stored_bytes[1],
313
+ stored_bytes[2],
314
+ stored_bytes[3],
315
+ ]);
316
+ let computed_crc = crc32(body);
317
+ if stored_crc != computed_crc {
318
+ return Err(format!(
319
+ "Checksum mismatch: stored=0x{:08x}, computed=0x{:08x}",
320
+ stored_crc, computed_crc
321
+ ));
322
+ }
323
+ body
324
+ } else {
325
+ data
326
+ };
327
+
328
+ let mut r = Reader::new(body);
329
+
330
+ // Header
331
+ let magic = r.read(4)?;
332
+ if magic != ZXC_MAGIC {
333
+ return Err(format!("Invalid magic: {:?}", magic));
334
+ }
335
+
336
+ let version = r.u16()?;
337
+ if version > 1 {
338
+ return Err(format!("Unsupported version: {}", version));
339
+ }
340
+
341
+ let flags = r.u16()?;
342
+ let n_consts = r.u32()? as usize;
343
+ let n_instrs = r.u32()? as usize;
344
+
345
+ // Constants
346
+ let mut constants = Vec::with_capacity(n_consts);
347
+ for _ in 0..n_consts {
348
+ constants.push(read_constant(&mut r)?);
349
+ }
350
+
351
+ // Instructions
352
+ let mut instructions = Vec::with_capacity(n_instrs);
353
+ for _ in 0..n_instrs {
354
+ instructions.push(read_instruction(&mut r)?);
355
+ }
356
+
357
+ Ok(ZxcModule {
358
+ version,
359
+ flags,
360
+ constants,
361
+ instructions,
362
+ })
363
+ }
364
+
365
+ // ── CRC32 (IEEE 802.3 / zlib-compatible) ──────────────────────────────
366
+
367
+ /// Compute CRC32 matching Python's `zlib.crc32()`.
368
+ fn crc32(data: &[u8]) -> u32 {
369
+ let mut crc: u32 = 0xFFFFFFFF;
370
+ for &byte in data {
371
+ crc ^= byte as u32;
372
+ for _ in 0..8 {
373
+ if crc & 1 != 0 {
374
+ crc = (crc >> 1) ^ 0xEDB88320;
375
+ } else {
376
+ crc >>= 1;
377
+ }
378
+ }
379
+ }
380
+ crc ^ 0xFFFFFFFF
381
+ }
382
+
383
+ // ── PyO3 wrapper ──────────────────────────────────────────────────────
384
+
385
+ /// Python-visible bytecode deserializer.
386
+ #[pyclass(name = "RustBytecodeReader")]
387
+ pub struct RustBytecodeReader;
388
+
389
+ #[pymethods]
390
+ impl RustBytecodeReader {
391
+ #[new]
392
+ fn new() -> Self {
393
+ Self
394
+ }
395
+
396
+ /// Deserialize a .zxc binary buffer and return a dict with the module contents.
397
+ ///
398
+ /// Returns a dict with keys: "version", "flags", "n_constants", "n_instructions",
399
+ /// "constants" (list of dicts), "instructions" (list of dicts).
400
+ #[pyo3(signature = (data, verify_checksum = true))]
401
+ fn deserialize<'py>(
402
+ &self,
403
+ py: Python<'py>,
404
+ data: &Bound<'py, PyBytes>,
405
+ verify_checksum: bool,
406
+ ) -> PyResult<Bound<'py, PyDict>> {
407
+ let bytes = data.as_bytes();
408
+
409
+ // Deserialize outside the GIL for CPU-heavy work
410
+ let module = py.allow_threads(|| deserialize_zxc(bytes, verify_checksum))
411
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(e))?;
412
+
413
+ // Convert to Python dict
414
+ let result = PyDict::new_bound(py);
415
+ result.set_item("version", module.version)?;
416
+ result.set_item("flags", module.flags)?;
417
+ result.set_item("n_constants", module.n_constants())?;
418
+ result.set_item("n_instructions", module.n_instructions())?;
419
+
420
+ // Constants
421
+ let py_consts = PyList::empty_bound(py);
422
+ for c in &module.constants {
423
+ py_consts.append(zxc_value_to_py(py, c)?)?;
424
+ }
425
+ result.set_item("constants", py_consts)?;
426
+
427
+ // Instructions
428
+ let py_instrs = PyList::empty_bound(py);
429
+ for instr in &module.instructions {
430
+ let d = PyDict::new_bound(py);
431
+ d.set_item("opcode", instr.opcode)?;
432
+ match &instr.operand {
433
+ Operand::None => d.set_item("operand", py.None())?,
434
+ Operand::U32(v) => d.set_item("operand", *v)?,
435
+ Operand::I64(v) => d.set_item("operand", *v)?,
436
+ Operand::Pair(a, b) => d.set_item("operand", (*a, *b))?,
437
+ Operand::Triple(a, b, c) => d.set_item("operand", (*a, *b, *c))?,
438
+ }
439
+ py_instrs.append(d)?;
440
+ }
441
+ result.set_item("instructions", py_instrs)?;
442
+
443
+ Ok(result)
444
+ }
445
+
446
+ /// Quick validation — check if binary data is a valid .zxc file.
447
+ #[pyo3(signature = (data, verify_checksum = true))]
448
+ fn validate(&self, py: Python<'_>, data: &Bound<'_, PyBytes>, verify_checksum: bool) -> PyResult<bool> {
449
+ let bytes = data.as_bytes();
450
+ let result = py.allow_threads(|| deserialize_zxc(bytes, verify_checksum));
451
+ Ok(result.is_ok())
452
+ }
453
+
454
+ /// Get basic info from .zxc header without full deserialization.
455
+ fn header_info<'py>(&self, py: Python<'py>, data: &Bound<'py, PyBytes>) -> PyResult<Bound<'py, PyDict>> {
456
+ let bytes = data.as_bytes();
457
+ if bytes.len() < ZXC_HEADER_SIZE {
458
+ return Err(pyo3::exceptions::PyValueError::new_err("Data too short"));
459
+ }
460
+
461
+ let magic = &bytes[0..4];
462
+ let version = u16::from_le_bytes([bytes[4], bytes[5]]);
463
+ let flags = u16::from_le_bytes([bytes[6], bytes[7]]);
464
+ let n_consts = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
465
+ let n_instrs = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
466
+
467
+ let result = PyDict::new_bound(py);
468
+ result.set_item("magic_ok", magic == ZXC_MAGIC)?;
469
+ result.set_item("version", version)?;
470
+ result.set_item("flags", flags)?;
471
+ result.set_item("n_constants", n_consts)?;
472
+ result.set_item("n_instructions", n_instrs)?;
473
+ result.set_item("total_bytes", bytes.len())?;
474
+ Ok(result)
475
+ }
476
+ }
477
+
478
+ /// Convert a ZxcValue to a Python object.
479
+ fn zxc_value_to_py(py: Python<'_>, val: &ZxcValue) -> PyResult<PyObject> {
480
+ match val {
481
+ ZxcValue::Null => Ok(py.None()),
482
+ ZxcValue::Bool(b) => Ok(b.to_object(py)),
483
+ ZxcValue::Int(i) => Ok(i.to_object(py)),
484
+ ZxcValue::Float(f) => Ok(f.to_object(py)),
485
+ ZxcValue::String(s) => Ok(s.to_object(py)),
486
+ ZxcValue::FuncDesc(s) => {
487
+ // Parse JSON back to Python dict
488
+ let json_val: serde_json::Value = serde_json::from_str(s)
489
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid JSON: {}", e)))?;
490
+ json_to_py(py, &json_val)
491
+ }
492
+ ZxcValue::List(items) => {
493
+ let list = PyList::empty_bound(py);
494
+ for item in items {
495
+ list.append(zxc_value_to_py(py, item)?)?;
496
+ }
497
+ Ok(list.to_object(py))
498
+ }
499
+ ZxcValue::Map(pairs) => {
500
+ let dict = PyDict::new_bound(py);
501
+ for (k, v) in pairs {
502
+ dict.set_item(zxc_value_to_py(py, k)?, zxc_value_to_py(py, v)?)?;
503
+ }
504
+ Ok(dict.to_object(py))
505
+ }
506
+ ZxcValue::Opaque(data) => {
507
+ // Return raw bytes — Python can unpickle if needed
508
+ Ok(PyBytes::new_bound(py, data).to_object(py))
509
+ }
510
+ }
511
+ }
512
+
513
+ /// Convert serde_json::Value to a Python object.
514
+ fn json_to_py(py: Python<'_>, val: &serde_json::Value) -> PyResult<PyObject> {
515
+ match val {
516
+ serde_json::Value::Null => Ok(py.None()),
517
+ serde_json::Value::Bool(b) => Ok(b.to_object(py)),
518
+ serde_json::Value::Number(n) => {
519
+ if let Some(i) = n.as_i64() {
520
+ Ok(i.to_object(py))
521
+ } else if let Some(f) = n.as_f64() {
522
+ Ok(f.to_object(py))
523
+ } else {
524
+ Ok(py.None())
525
+ }
526
+ }
527
+ serde_json::Value::String(s) => Ok(s.to_object(py)),
528
+ serde_json::Value::Array(arr) => {
529
+ let list = PyList::empty_bound(py);
530
+ for item in arr {
531
+ list.append(json_to_py(py, item)?)?;
532
+ }
533
+ Ok(list.to_object(py))
534
+ }
535
+ serde_json::Value::Object(obj) => {
536
+ let dict = PyDict::new_bound(py);
537
+ for (k, v) in obj {
538
+ dict.set_item(k, json_to_py(py, v)?)?;
539
+ }
540
+ Ok(dict.to_object(py))
541
+ }
542
+ }
543
+ }