mirror of https://github.com/Nonannet/copapy.git
Added multi-target support for coparun-module
This commit is contained in:
parent
d9f361a6d6
commit
c8e6848530
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Iterable, overload, TypeVar, Any
|
||||
from . import _binwrite as binw
|
||||
from coparun_module import coparun, read_data_mem
|
||||
from coparun_module import coparun, read_data_mem, create_target, clear_target
|
||||
import struct
|
||||
from ._basic_types import stencil_db_from_package
|
||||
from ._basic_types import value, Net, Node, Write, NumLike
|
||||
|
|
@ -29,6 +29,10 @@ class Target():
|
|||
"""
|
||||
self.sdb = stencil_db_from_package(arch, optimization)
|
||||
self._values: dict[Net, tuple[int, int, str]] = {}
|
||||
self._context = create_target()
|
||||
|
||||
def __del__(self) -> None:
|
||||
clear_target(self._context)
|
||||
|
||||
def compile(self, *values: int | float | value[int] | value[float] | Iterable[int | float | value[int] | value[float]]) -> None:
|
||||
"""Compiles the code to compute the given values.
|
||||
|
|
@ -48,7 +52,7 @@ class Target():
|
|||
|
||||
dw, self._values = compile_to_dag(nodes, self.sdb)
|
||||
dw.write_com(binw.Command.END_COM)
|
||||
assert coparun(dw.get_data()) > 0
|
||||
assert coparun(self._context, dw.get_data()) > 0
|
||||
|
||||
def run(self) -> None:
|
||||
"""Runs the compiled code on the target device.
|
||||
|
|
@ -56,7 +60,7 @@ class Target():
|
|||
dw = binw.data_writer(self.sdb.byteorder)
|
||||
dw.write_com(binw.Command.RUN_PROG)
|
||||
dw.write_com(binw.Command.END_COM)
|
||||
assert coparun(dw.get_data()) > 0
|
||||
assert coparun(self._context, dw.get_data()) > 0
|
||||
|
||||
@overload
|
||||
def read_value(self, net: value[T]) -> T: ...
|
||||
|
|
@ -84,7 +88,7 @@ class Target():
|
|||
assert net in self._values, f"Value {net} not found. It might not have been compiled for the target."
|
||||
addr, lengths, var_type = self._values[net]
|
||||
assert lengths > 0
|
||||
data = read_data_mem(addr, lengths)
|
||||
data = read_data_mem(self._context, addr, lengths)
|
||||
assert data is not None and len(data) == lengths, f"Failed to read value {net}"
|
||||
en = {'little': '<', 'big': '>'}[self.sdb.byteorder]
|
||||
if var_type == 'float':
|
||||
|
|
@ -111,4 +115,4 @@ class Target():
|
|||
"""Reads the raw data of a value by the runner."""
|
||||
dw = binw.data_writer(self.sdb.byteorder)
|
||||
add_read_command(dw, self._values, net)
|
||||
assert coparun(dw.get_data()) > 0
|
||||
assert coparun(self._context, dw.get_data()) > 0
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ int main(int argc, char *argv[]) {
|
|||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
int ret = parse_commands(file_buff);
|
||||
runmem_t targ;
|
||||
int ret = parse_commands(&targ, file_buff);
|
||||
|
||||
if (ret == 2) {
|
||||
/* Dump code for debugging */
|
||||
|
|
@ -54,11 +55,11 @@ int main(int argc, char *argv[]) {
|
|||
return EXIT_FAILURE;
|
||||
}
|
||||
f = fopen(argv[2], "wb");
|
||||
fwrite(executable_memory, 1, (size_t)executable_memory_len, f);
|
||||
fwrite(targ.executable_memory, 1, (size_t)targ.executable_memory_len, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
free_memory();
|
||||
free_memory(&targ);
|
||||
|
||||
return ret < 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,41 @@
|
|||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#include "runmem.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
static PyObject* coparun(PyObject* self, PyObject* args) {
|
||||
PyObject *handle_obj;
|
||||
const char *buf;
|
||||
Py_ssize_t buf_len;
|
||||
int result;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "y#", &buf, &buf_len)) {
|
||||
// Expect: handle, bytes
|
||||
if (!PyArg_ParseTuple(args, "Oy#", &handle_obj, &buf, &buf_len)) {
|
||||
return NULL; /* TypeError set by PyArg_ParseTuple */
|
||||
}
|
||||
|
||||
void *ptr = PyLong_AsVoidPtr(handle_obj);
|
||||
if (!ptr) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid context handle");
|
||||
return NULL;
|
||||
}
|
||||
runmem_t *context = (runmem_t*)ptr;
|
||||
|
||||
/* If parse_commands may run for a long time, release the GIL. */
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
result = parse_commands((uint8_t*)buf);
|
||||
result = parse_commands(context, (uint8_t*)buf);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
return PyLong_FromLong(result);
|
||||
}
|
||||
|
||||
static PyObject* read_data_mem(PyObject* self, PyObject* args) {
|
||||
PyObject *handle_obj;
|
||||
unsigned long rel_addr;
|
||||
unsigned long length;
|
||||
|
||||
// Parse arguments: unsigned long (relative address), Py_ssize_t (length)
|
||||
if (!PyArg_ParseTuple(args, "nn", &rel_addr, &length)) {
|
||||
// Expect: handle, rel_addr, length
|
||||
if (!PyArg_ParseTuple(args, "Onn", &handle_obj, &rel_addr, &length)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -33,9 +44,21 @@ static PyObject* read_data_mem(PyObject* self, PyObject* args) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
const char *ptr = (const char *)(data_memory + rel_addr);
|
||||
void *ptr = PyLong_AsVoidPtr(handle_obj);
|
||||
if (!ptr) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid context handle");
|
||||
return NULL;
|
||||
}
|
||||
runmem_t *context = (runmem_t*)ptr;
|
||||
|
||||
PyObject *result = PyBytes_FromStringAndSize(ptr, length);
|
||||
if (!context->data_memory || rel_addr + length > context->data_memory_len) {
|
||||
PyErr_SetString(PyExc_ValueError, "Read out of bounds");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *data_ptr = (const char *)(context->data_memory + rel_addr);
|
||||
|
||||
PyObject *result = PyBytes_FromStringAndSize(data_ptr, length);
|
||||
if (!result) {
|
||||
return PyErr_NoMemory();
|
||||
}
|
||||
|
|
@ -43,9 +66,36 @@ static PyObject* read_data_mem(PyObject* self, PyObject* args) {
|
|||
return result;
|
||||
}
|
||||
|
||||
static PyObject* create_target(PyObject* self, PyObject* args) {
|
||||
runmem_t *context = (runmem_t*)calloc(1, sizeof(runmem_t));
|
||||
if (!context) {
|
||||
return PyErr_NoMemory();
|
||||
}
|
||||
// Return the pointer as a Python integer (handle)
|
||||
return PyLong_FromVoidPtr((void*)context);
|
||||
}
|
||||
|
||||
static PyObject* clear_target(PyObject* self, PyObject* args) {
|
||||
PyObject *handle_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &handle_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
void *ptr = PyLong_AsVoidPtr(handle_obj);
|
||||
if (!ptr) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid handle");
|
||||
return NULL;
|
||||
}
|
||||
runmem_t *context = (runmem_t*)ptr;
|
||||
free_memory(context);
|
||||
free(context);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyMethodDef MyMethods[] = {
|
||||
{"coparun", coparun, METH_VARARGS, "Pass raw command data to coparun"},
|
||||
{"read_data_mem", read_data_mem, METH_VARARGS, "Read memory and return as bytes"},
|
||||
{"create_target", create_target, METH_NOARGS, "Create and return a handle to a zero-initialized runmem_t struct"},
|
||||
{"clear_target", clear_target, METH_VARARGS, "Free all memory associated with the given target handle"},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,6 @@
|
|||
#include "runmem.h"
|
||||
#include "mem_man.h"
|
||||
|
||||
/* Globals declared extern in runmem.h */
|
||||
uint8_t *data_memory = NULL;
|
||||
uint32_t data_memory_len = 0;
|
||||
uint8_t *executable_memory = NULL;
|
||||
uint32_t executable_memory_len = 0;
|
||||
entry_point_t entr_point = NULL;
|
||||
int data_offs = 0;
|
||||
|
||||
void patch(uint8_t *patch_addr, uint32_t patch_mask, int32_t value) {
|
||||
uint32_t *val_ptr = (uint32_t*)patch_addr;
|
||||
uint32_t original = *val_ptr;
|
||||
|
|
@ -58,23 +50,25 @@ void patch_arm32_abs(uint8_t *patch_addr, uint32_t imm16)
|
|||
*((uint32_t *)patch_addr) = instr;
|
||||
}
|
||||
|
||||
void free_memory() {
|
||||
deallocate_memory(executable_memory, executable_memory_len);
|
||||
deallocate_memory(data_memory, data_memory_len);
|
||||
executable_memory_len = 0;
|
||||
data_memory_len = 0;
|
||||
void free_memory(runmem_t *context) {
|
||||
deallocate_memory(context->executable_memory, context->executable_memory_len);
|
||||
deallocate_memory(context->data_memory, context->data_memory_len);
|
||||
context->executable_memory_len = 0;
|
||||
context->data_memory_len = 0;
|
||||
context->executable_memory = NULL;
|
||||
context->data_memory = NULL;
|
||||
context->entr_point = NULL;
|
||||
context->data_offs = 0;
|
||||
}
|
||||
|
||||
int update_data_offs() {
|
||||
if (data_memory && executable_memory && (data_memory - executable_memory > 0x7FFFFFFF || executable_memory - data_memory > 0x7FFFFFFF)) {
|
||||
int update_data_offs(runmem_t *context) {
|
||||
if (context->data_memory && context->executable_memory &&
|
||||
(context->data_memory - context->executable_memory > 0x7FFFFFFF ||
|
||||
context->executable_memory - context->data_memory > 0x7FFFFFFF)) {
|
||||
perror("Error: code and data memory to far apart");
|
||||
return 0;
|
||||
}
|
||||
if (data_memory && executable_memory && (data_memory - executable_memory > 0x7FFFFFFF || executable_memory - data_memory > 0x7FFFFFFF)) {
|
||||
perror("Error: code and data memory to far apart");
|
||||
return 0;
|
||||
}
|
||||
data_offs = (int)(data_memory - executable_memory);
|
||||
context->data_offs = (int)(context->data_memory - context->executable_memory);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +76,7 @@ int floor_div(int a, int b) {
|
|||
return a / b - ((a % b != 0) && ((a < 0) != (b < 0)));
|
||||
}
|
||||
|
||||
int parse_commands(uint8_t *bytes) {
|
||||
int parse_commands(runmem_t *context, uint8_t *bytes) {
|
||||
int32_t value;
|
||||
uint32_t command;
|
||||
uint32_t patch_mask;
|
||||
|
|
@ -98,33 +92,32 @@ int parse_commands(uint8_t *bytes) {
|
|||
switch(command) {
|
||||
case ALLOCATE_DATA:
|
||||
size = *(uint32_t*)bytes; bytes += 4;
|
||||
data_memory = allocate_data_memory(size);
|
||||
data_memory_len = size;
|
||||
LOG("ALLOCATE_DATA size=%i mem_addr=%p\n", size, (void*)data_memory);
|
||||
if (!update_data_offs()) end_flag = -4;
|
||||
context->data_memory = allocate_data_memory(size);
|
||||
context->data_memory_len = size;
|
||||
LOG("ALLOCATE_DATA size=%i mem_addr=%p\n", size, (void*)context->data_memory);
|
||||
if (!update_data_offs(context)) end_flag = -4;
|
||||
break;
|
||||
|
||||
case COPY_DATA:
|
||||
offs = *(uint32_t*)bytes; bytes += 4;
|
||||
size = *(uint32_t*)bytes; bytes += 4;
|
||||
LOG("COPY_DATA offs=%i size=%i\n", offs, size);
|
||||
memcpy(data_memory + offs, bytes, size); bytes += size;
|
||||
memcpy(context->data_memory + offs, bytes, size); bytes += size;
|
||||
break;
|
||||
|
||||
case ALLOCATE_CODE:
|
||||
size = *(uint32_t*)bytes; bytes += 4;
|
||||
executable_memory = allocate_executable_memory(size);
|
||||
executable_memory_len = size;
|
||||
LOG("ALLOCATE_CODE size=%i mem_addr=%p\n", size, (void*)executable_memory);
|
||||
//LOG("# d %i c %i off %i\n", data_memory, executable_memory, data_offs);
|
||||
if (!update_data_offs()) end_flag = -4;
|
||||
context->executable_memory = allocate_executable_memory(size);
|
||||
context->executable_memory_len = size;
|
||||
LOG("ALLOCATE_CODE size=%i mem_addr=%p\n", size, (void*)context->executable_memory);
|
||||
if (!update_data_offs(context)) end_flag = -4;
|
||||
break;
|
||||
|
||||
case COPY_CODE:
|
||||
offs = *(uint32_t*)bytes; bytes += 4;
|
||||
size = *(uint32_t*)bytes; bytes += 4;
|
||||
LOG("COPY_CODE offs=%i size=%i\n", offs, size);
|
||||
memcpy(executable_memory + offs, bytes, size); bytes += size;
|
||||
memcpy(context->executable_memory + offs, bytes, size); bytes += size;
|
||||
break;
|
||||
|
||||
case PATCH_FUNC:
|
||||
|
|
@ -134,7 +127,7 @@ int parse_commands(uint8_t *bytes) {
|
|||
value = *(int32_t*)bytes; bytes += 4;
|
||||
LOG("PATCH_FUNC patch_offs=%i patch_mask=%#08x scale=%i value=%i\n",
|
||||
offs, patch_mask, patch_scale, value);
|
||||
patch(executable_memory + offs, patch_mask, value / patch_scale);
|
||||
patch(context->executable_memory + offs, patch_mask, value / patch_scale);
|
||||
break;
|
||||
|
||||
case PATCH_OBJECT:
|
||||
|
|
@ -144,7 +137,7 @@ int parse_commands(uint8_t *bytes) {
|
|||
value = *(int32_t*)bytes; bytes += 4;
|
||||
LOG("PATCH_OBJECT patch_offs=%i patch_mask=%#08x scale=%i value=%i\n",
|
||||
offs, patch_mask, patch_scale, value);
|
||||
patch(executable_memory + offs, patch_mask, value / patch_scale + data_offs / patch_scale);
|
||||
patch(context->executable_memory + offs, patch_mask, value / patch_scale + context->data_offs / patch_scale);
|
||||
break;
|
||||
|
||||
case PATCH_OBJECT_ABS:
|
||||
|
|
@ -154,7 +147,7 @@ int parse_commands(uint8_t *bytes) {
|
|||
value = *(int32_t*)bytes; bytes += 4;
|
||||
LOG("PATCH_OBJECT_ABS patch_offs=%i patch_mask=%#08x scale=%i value=%i\n",
|
||||
offs, patch_mask, patch_scale, value);
|
||||
patch(executable_memory + offs, patch_mask, value / patch_scale);
|
||||
patch(context->executable_memory + offs, patch_mask, value / patch_scale);
|
||||
break;
|
||||
|
||||
case PATCH_OBJECT_REL:
|
||||
|
|
@ -163,8 +156,8 @@ int parse_commands(uint8_t *bytes) {
|
|||
patch_scale = *(int32_t*)bytes; bytes += 4;
|
||||
value = *(int32_t*)bytes; bytes += 4;
|
||||
LOG("PATCH_OBJECT_REL patch_offs=%i patch_addr=%p scale=%i value=%i\n",
|
||||
offs, (void*)(data_memory + value), patch_scale, value);
|
||||
*(void **)(executable_memory + offs) = data_memory + value; // / patch_scale;
|
||||
offs, (void*)(context->data_memory + value), patch_scale, value);
|
||||
*(void **)(context->executable_memory + offs) = context->data_memory + value;
|
||||
break;
|
||||
|
||||
case PATCH_OBJECT_HI21:
|
||||
|
|
@ -173,8 +166,8 @@ int parse_commands(uint8_t *bytes) {
|
|||
patch_scale = *(int32_t*)bytes; bytes += 4;
|
||||
value = *(int32_t*)bytes; bytes += 4;
|
||||
LOG("PATCH_OBJECT_HI21 patch_offs=%i scale=%i value=%i res_value=%i\n",
|
||||
offs, patch_scale, value, floor_div(data_offs + value, patch_scale) - (int32_t)offs / patch_scale);
|
||||
patch_hi21(executable_memory + offs, floor_div(data_offs + value, patch_scale) - (int32_t)offs / patch_scale);
|
||||
offs, patch_scale, value, floor_div(context->data_offs + value, patch_scale) - (int32_t)offs / patch_scale);
|
||||
patch_hi21(context->executable_memory + offs, floor_div(context->data_offs + value, patch_scale) - (int32_t)offs / patch_scale);
|
||||
break;
|
||||
|
||||
case PATCH_OBJECT_ARM32_ABS:
|
||||
|
|
@ -183,21 +176,24 @@ int parse_commands(uint8_t *bytes) {
|
|||
patch_scale = *(int32_t*)bytes; bytes += 4;
|
||||
value = *(int32_t*)bytes; bytes += 4;
|
||||
LOG("PATCH_OBJECT_ARM32_ABS patch_offs=%i patch_mask=%#08x scale=%i value=%i imm16=%#04x\n",
|
||||
offs, patch_mask, patch_scale, value, (uint32_t)((uintptr_t)(data_memory + value) & patch_mask) / (uint32_t)patch_scale);
|
||||
patch_arm32_abs(executable_memory + offs, (uint32_t)((uintptr_t)(data_memory + value) & patch_mask) / (uint32_t)patch_scale);
|
||||
offs, patch_mask, patch_scale, value, (uint32_t)((uintptr_t)(context->data_memory + value) & patch_mask) / (uint32_t)patch_scale);
|
||||
patch_arm32_abs(context->executable_memory + offs, (uint32_t)((uintptr_t)(context->data_memory + value) & patch_mask) / (uint32_t)patch_scale);
|
||||
break;
|
||||
|
||||
case ENTRY_POINT:
|
||||
rel_entr_point = *(uint32_t*)bytes; bytes += 4;
|
||||
entr_point = (entry_point_t)(executable_memory + rel_entr_point);
|
||||
context->entr_point = (entry_point_t)(context->executable_memory + rel_entr_point);
|
||||
LOG("ENTRY_POINT rel_entr_point=%i\n", rel_entr_point);
|
||||
mark_mem_executable(executable_memory, executable_memory_len);
|
||||
mark_mem_executable(context->executable_memory, context->executable_memory_len);
|
||||
break;
|
||||
|
||||
case RUN_PROG:
|
||||
LOG("RUN_PROG\n");
|
||||
int ret = entr_point();
|
||||
BLOG("Return value: %i\n", ret);
|
||||
{
|
||||
int ret = context->entr_point();
|
||||
(void)ret;
|
||||
BLOG("Return value: %i\n", ret);
|
||||
}
|
||||
break;
|
||||
|
||||
case READ_DATA:
|
||||
|
|
@ -205,14 +201,14 @@ int parse_commands(uint8_t *bytes) {
|
|||
size = *(uint32_t*)bytes; bytes += 4;
|
||||
BLOG("READ_DATA offs=%i size=%i data=", offs, size);
|
||||
for (uint32_t i = 0; i < size; i++) {
|
||||
printf("%02X ", data_memory[offs + i]);
|
||||
printf("%02X ", context->data_memory[offs + i]);
|
||||
}
|
||||
printf("\n");
|
||||
break;
|
||||
|
||||
case FREE_MEMORY:
|
||||
LOG("FREE_MENORY\n");
|
||||
free_memory();
|
||||
free_memory(context);
|
||||
break;
|
||||
|
||||
case DUMP_CODE:
|
||||
|
|
|
|||
|
|
@ -32,23 +32,24 @@
|
|||
#define FREE_MEMORY 257
|
||||
#define DUMP_CODE 258
|
||||
|
||||
/* Memory blobs accessible by other translation units */
|
||||
extern uint8_t *data_memory;
|
||||
extern uint32_t data_memory_len;
|
||||
extern uint8_t *executable_memory;
|
||||
extern uint32_t executable_memory_len;
|
||||
extern int data_offs;
|
||||
|
||||
/* Entry point type and variable */
|
||||
/* Entry point type */
|
||||
typedef int (*entry_point_t)(void);
|
||||
extern entry_point_t entr_point;
|
||||
|
||||
/* Struct for run-time memory state */
|
||||
typedef struct runmem_s {
|
||||
uint8_t *data_memory; // Pointer to data memory
|
||||
uint32_t data_memory_len; // Length of data memory
|
||||
uint8_t *executable_memory; // Pointer to executable memory
|
||||
uint32_t executable_memory_len; // Length of executable memory
|
||||
int data_offs; // Offset of data memory relative to executable memory
|
||||
entry_point_t entr_point; // Entry point function pointer
|
||||
} runmem_t;
|
||||
|
||||
/* Command parser: takes a pointer to the command stream and returns
|
||||
an error flag (0 on success according to current code) */
|
||||
int parse_commands(uint8_t *bytes);
|
||||
int parse_commands(runmem_t *context, uint8_t *bytes);
|
||||
|
||||
/* Free program and data memory */
|
||||
void free_memory();
|
||||
void free_memory(runmem_t *context);
|
||||
|
||||
#endif /* RUNMEM_H */
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
def coparun(data: bytes) -> int: ...
|
||||
def read_data_mem(rel_addr: int, length: int) -> bytes: ...
|
||||
def coparun(context: int, data: bytes) -> int: ...
|
||||
def read_data_mem(context: int, rel_addr: int, length: int) -> bytes: ...
|
||||
def create_target() -> int: ...
|
||||
def clear_target(context: int) -> None: ...
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import copapy as cp
|
||||
import pytest
|
||||
|
||||
def test_multi_target():
|
||||
# Define variables
|
||||
a = cp.value(0.25)
|
||||
b = cp.value(0.87)
|
||||
|
||||
# Define computations
|
||||
c = a + b * 2.0
|
||||
d = c ** 2 + cp.sin(a)
|
||||
e = d + cp.sqrt(b)
|
||||
|
||||
# Create a target, compile and run
|
||||
tg1 = cp.Target()
|
||||
tg1.compile(e)
|
||||
|
||||
# Patch constant value
|
||||
a.source = cp._basic_types.CPConstant(1000.0)
|
||||
|
||||
tg2 = cp.Target()
|
||||
tg2.compile(e)
|
||||
|
||||
tg1.run()
|
||||
tg2.run()
|
||||
|
||||
print("Result tg1:", tg1.read_value(e))
|
||||
print("Result tg2:", tg2.read_value(e))
|
||||
|
||||
# Assertions to verify correctness
|
||||
assert tg1.read_value(e) == pytest.approx((0.25 + 0.87 * 2.0) ** 2 + cp.sin(0.25) + cp.sqrt(0.87), 0.005) # pyright: ignore[reportUnknownMemberType]
|
||||
assert tg2.read_value(e) == pytest.approx((1000.0 + 0.87 * 2.0) ** 2 + cp.sin(1000.0) + cp.sqrt(0.87), 0.005) # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_multi_target()
|
||||
Loading…
Reference in New Issue