forked from M-Labs/artiq
Merge branch 'master' of github.com:m-labs/artiq
This commit is contained in:
commit
79986791bc
|
@ -97,6 +97,7 @@ class Target:
|
||||||
llpassmgrbuilder = llvm.create_pass_manager_builder()
|
llpassmgrbuilder = llvm.create_pass_manager_builder()
|
||||||
llpassmgrbuilder.opt_level = 2 # -O2
|
llpassmgrbuilder.opt_level = 2 # -O2
|
||||||
llpassmgrbuilder.size_level = 1 # -Os
|
llpassmgrbuilder.size_level = 1 # -Os
|
||||||
|
llpassmgrbuilder.inlining_threshold = 75 # -Os threshold
|
||||||
|
|
||||||
llpassmgr = llvm.create_module_pass_manager()
|
llpassmgr = llvm.create_module_pass_manager()
|
||||||
llpassmgrbuilder.populate(llpassmgr)
|
llpassmgrbuilder.populate(llpassmgr)
|
||||||
|
@ -147,6 +148,9 @@ class Target:
|
||||||
return results["output"].read()
|
return results["output"].read()
|
||||||
|
|
||||||
def symbolize(self, library, addresses):
|
def symbolize(self, library, addresses):
|
||||||
|
if addresses == []:
|
||||||
|
return []
|
||||||
|
|
||||||
# Addresses point one instruction past the jump; offset them back by 1.
|
# Addresses point one instruction past the jump; offset them back by 1.
|
||||||
offset_addresses = [hex(addr - 1) for addr in addresses]
|
offset_addresses = [hex(addr - 1) for addr in addresses]
|
||||||
with RunTool([self.triple + "-addr2line", "--functions", "--inlines",
|
with RunTool([self.triple + "-addr2line", "--functions", "--inlines",
|
||||||
|
|
|
@ -320,7 +320,8 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
||||||
return self.append(ir.Closure(func, self.current_env))
|
return self.append(ir.Closure(func, self.current_env))
|
||||||
|
|
||||||
def visit_FunctionDefT(self, node, in_class=None):
|
def visit_FunctionDefT(self, node, in_class=None):
|
||||||
func = self.visit_function(node, is_lambda=False, is_internal=len(self.name) > 2)
|
func = self.visit_function(node, is_lambda=False,
|
||||||
|
is_internal=len(self.name) > 0 or '.' in node.name)
|
||||||
if in_class is None:
|
if in_class is None:
|
||||||
self._set_local(node.name, func)
|
self._set_local(node.name, func)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -176,6 +176,7 @@ class LLVMIRGenerator:
|
||||||
self.llobject_map = {}
|
self.llobject_map = {}
|
||||||
self.phis = []
|
self.phis = []
|
||||||
self.debug_info_emitter = DebugInfoEmitter(self.llmodule)
|
self.debug_info_emitter = DebugInfoEmitter(self.llmodule)
|
||||||
|
self.empty_metadata = self.llmodule.add_metadata([])
|
||||||
|
|
||||||
def needs_sret(self, lltyp, may_be_large=True):
|
def needs_sret(self, lltyp, may_be_large=True):
|
||||||
if isinstance(lltyp, ll.VoidType):
|
if isinstance(lltyp, ll.VoidType):
|
||||||
|
@ -190,8 +191,13 @@ class LLVMIRGenerator:
|
||||||
and len(lltyp.elements) <= 2:
|
and len(lltyp.elements) <= 2:
|
||||||
return not any([self.needs_sret(elt, may_be_large=False) for elt in lltyp.elements])
|
return not any([self.needs_sret(elt, may_be_large=False) for elt in lltyp.elements])
|
||||||
else:
|
else:
|
||||||
|
assert isinstance(lltyp, ll.Type)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def has_sret(self, functy):
|
||||||
|
llretty = self.llty_of_type(functy.ret, for_return=True)
|
||||||
|
return self.needs_sret(llretty)
|
||||||
|
|
||||||
def llty_of_type(self, typ, bare=False, for_return=False):
|
def llty_of_type(self, typ, bare=False, for_return=False):
|
||||||
typ = typ.find()
|
typ = typ.find()
|
||||||
if types.is_tuple(typ):
|
if types.is_tuple(typ):
|
||||||
|
@ -218,22 +224,14 @@ class LLVMIRGenerator:
|
||||||
for arg in typ.optargs],
|
for arg in typ.optargs],
|
||||||
return_type=llretty)
|
return_type=llretty)
|
||||||
|
|
||||||
# TODO: actually mark the first argument as sret (also noalias nocapture).
|
|
||||||
# llvmlite currently does not have support for this;
|
|
||||||
# https://github.com/numba/llvmlite/issues/91.
|
|
||||||
if sretarg:
|
|
||||||
llty.__has_sret = True
|
|
||||||
else:
|
|
||||||
llty.__has_sret = False
|
|
||||||
|
|
||||||
if bare:
|
if bare:
|
||||||
return llty
|
return llty
|
||||||
else:
|
else:
|
||||||
return ll.LiteralStructType([envarg, llty.as_pointer()])
|
return ll.LiteralStructType([envarg, llty.as_pointer()])
|
||||||
elif types.is_method(typ):
|
elif types.is_method(typ):
|
||||||
llfuncty = self.llty_of_type(types.get_method_function(typ))
|
llfunty = self.llty_of_type(types.get_method_function(typ))
|
||||||
llselfty = self.llty_of_type(types.get_method_self(typ))
|
llselfty = self.llty_of_type(types.get_method_self(typ))
|
||||||
return ll.LiteralStructType([llfuncty, llselfty])
|
return ll.LiteralStructType([llfunty, llselfty])
|
||||||
elif builtins.is_none(typ):
|
elif builtins.is_none(typ):
|
||||||
if for_return:
|
if for_return:
|
||||||
return llvoid
|
return llvoid
|
||||||
|
@ -399,8 +397,13 @@ class LLVMIRGenerator:
|
||||||
elif isinstance(value, ir.Function):
|
elif isinstance(value, ir.Function):
|
||||||
llfun = self.llmodule.get_global(value.name)
|
llfun = self.llmodule.get_global(value.name)
|
||||||
if llfun is None:
|
if llfun is None:
|
||||||
llfun = ll.Function(self.llmodule, self.llty_of_type(value.type, bare=True),
|
llfunty = self.llty_of_type(value.type, bare=True)
|
||||||
value.name)
|
llfun = ll.Function(self.llmodule, llfunty, value.name)
|
||||||
|
|
||||||
|
llretty = self.llty_of_type(value.type.ret, for_return=True)
|
||||||
|
if self.needs_sret(llretty):
|
||||||
|
llfun.args[0].add_attribute('sret')
|
||||||
|
|
||||||
return llfun
|
return llfun
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
|
@ -515,11 +518,7 @@ class LLVMIRGenerator:
|
||||||
|
|
||||||
def process_function(self, func):
|
def process_function(self, func):
|
||||||
try:
|
try:
|
||||||
self.llfunction = self.llmodule.get_global(func.name)
|
self.llfunction = self.map(func)
|
||||||
|
|
||||||
if self.llfunction is None:
|
|
||||||
llfunty = self.llty_of_type(func.type, bare=True)
|
|
||||||
self.llfunction = ll.Function(self.llmodule, llfunty, func.name)
|
|
||||||
|
|
||||||
if func.is_internal:
|
if func.is_internal:
|
||||||
self.llfunction.linkage = 'internal'
|
self.llfunction.linkage = 'internal'
|
||||||
|
@ -532,7 +531,7 @@ class LLVMIRGenerator:
|
||||||
disubprogram = self.debug_info_emitter.emit_subprogram(func, self.llfunction)
|
disubprogram = self.debug_info_emitter.emit_subprogram(func, self.llfunction)
|
||||||
|
|
||||||
# First, map arguments.
|
# First, map arguments.
|
||||||
if self.llfunction.type.pointee.__has_sret:
|
if self.has_sret(func.type):
|
||||||
llactualargs = self.llfunction.args[1:]
|
llactualargs = self.llfunction.args[1:]
|
||||||
else:
|
else:
|
||||||
llactualargs = self.llfunction.args
|
llactualargs = self.llfunction.args
|
||||||
|
@ -615,7 +614,8 @@ class LLVMIRGenerator:
|
||||||
llalloc = self.llbuilder.alloca(self.llty_of_type(insn.type, bare=True))
|
llalloc = self.llbuilder.alloca(self.llty_of_type(insn.type, bare=True))
|
||||||
for index, operand in enumerate(insn.operands):
|
for index, operand in enumerate(insn.operands):
|
||||||
lloperand = self.map(operand)
|
lloperand = self.map(operand)
|
||||||
llfieldptr = self.llbuilder.gep(llalloc, [self.llindex(0), self.llindex(index)])
|
llfieldptr = self.llbuilder.gep(llalloc, [self.llindex(0), self.llindex(index)],
|
||||||
|
inbounds=True)
|
||||||
self.llbuilder.store(lloperand, llfieldptr)
|
self.llbuilder.store(lloperand, llfieldptr)
|
||||||
return llalloc
|
return llalloc
|
||||||
|
|
||||||
|
@ -623,11 +623,14 @@ class LLVMIRGenerator:
|
||||||
if var_name in env_ty.params and (var_type is None or
|
if var_name in env_ty.params and (var_type is None or
|
||||||
env_ty.params[var_name] == var_type):
|
env_ty.params[var_name] == var_type):
|
||||||
var_index = list(env_ty.params.keys()).index(var_name)
|
var_index = list(env_ty.params.keys()).index(var_name)
|
||||||
return self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(var_index)])
|
return self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(var_index)],
|
||||||
|
inbounds=True)
|
||||||
else:
|
else:
|
||||||
outer_index = list(env_ty.params.keys()).index("$outer")
|
outer_index = list(env_ty.params.keys()).index("$outer")
|
||||||
llptr = self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(outer_index)])
|
llptr = self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(outer_index)],
|
||||||
|
inbounds=True)
|
||||||
llouterenv = self.llbuilder.load(llptr)
|
llouterenv = self.llbuilder.load(llptr)
|
||||||
|
llouterenv.metadata['invariant.load'] = self.empty_metadata
|
||||||
return self.llptr_to_var(llouterenv, env_ty.params["$outer"], var_name)
|
return self.llptr_to_var(llouterenv, env_ty.params["$outer"], var_name)
|
||||||
|
|
||||||
def process_GetLocal(self, insn):
|
def process_GetLocal(self, insn):
|
||||||
|
@ -638,7 +641,9 @@ class LLVMIRGenerator:
|
||||||
def process_GetConstructor(self, insn):
|
def process_GetConstructor(self, insn):
|
||||||
env = insn.environment()
|
env = insn.environment()
|
||||||
llptr = self.llptr_to_var(self.map(env), env.type, insn.var_name, insn.type)
|
llptr = self.llptr_to_var(self.map(env), env.type, insn.var_name, insn.type)
|
||||||
return self.llbuilder.load(llptr)
|
llconstr = self.llbuilder.load(llptr)
|
||||||
|
llconstr.metadata['invariant.load'] = self.empty_metadata
|
||||||
|
return llconstr
|
||||||
|
|
||||||
def process_SetLocal(self, insn):
|
def process_SetLocal(self, insn):
|
||||||
env = insn.environment()
|
env = insn.environment()
|
||||||
|
@ -667,14 +672,14 @@ class LLVMIRGenerator:
|
||||||
else:
|
else:
|
||||||
llptr = self.llbuilder.gep(self.map(insn.object()),
|
llptr = self.llbuilder.gep(self.map(insn.object()),
|
||||||
[self.llindex(0), self.llindex(self.attr_index(insn))],
|
[self.llindex(0), self.llindex(self.attr_index(insn))],
|
||||||
name=insn.name)
|
inbounds=True, name=insn.name)
|
||||||
return self.llbuilder.load(llptr)
|
return self.llbuilder.load(llptr)
|
||||||
|
|
||||||
def process_SetAttr(self, insn):
|
def process_SetAttr(self, insn):
|
||||||
assert builtins.is_allocated(insn.object().type)
|
assert builtins.is_allocated(insn.object().type)
|
||||||
llptr = self.llbuilder.gep(self.map(insn.object()),
|
llptr = self.llbuilder.gep(self.map(insn.object()),
|
||||||
[self.llindex(0), self.llindex(self.attr_index(insn))],
|
[self.llindex(0), self.llindex(self.attr_index(insn))],
|
||||||
name=insn.name)
|
inbounds=True, name=insn.name)
|
||||||
return self.llbuilder.store(self.map(insn.value()), llptr)
|
return self.llbuilder.store(self.map(insn.value()), llptr)
|
||||||
|
|
||||||
def process_GetElem(self, insn):
|
def process_GetElem(self, insn):
|
||||||
|
@ -872,8 +877,10 @@ class LLVMIRGenerator:
|
||||||
def get_outer(llenv, env_ty):
|
def get_outer(llenv, env_ty):
|
||||||
if "$outer" in env_ty.params:
|
if "$outer" in env_ty.params:
|
||||||
outer_index = list(env_ty.params.keys()).index("$outer")
|
outer_index = list(env_ty.params.keys()).index("$outer")
|
||||||
llptr = self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(outer_index)])
|
llptr = self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(outer_index)],
|
||||||
|
inbounds=True)
|
||||||
llouterenv = self.llbuilder.load(llptr)
|
llouterenv = self.llbuilder.load(llptr)
|
||||||
|
llouterenv.metadata['invariant.load'] = self.empty_metadata
|
||||||
return self.llptr_to_var(llouterenv, env_ty.params["$outer"], var_name)
|
return self.llptr_to_var(llouterenv, env_ty.params["$outer"], var_name)
|
||||||
else:
|
else:
|
||||||
return llenv
|
return llenv
|
||||||
|
@ -920,21 +927,46 @@ class LLVMIRGenerator:
|
||||||
return llvalue
|
return llvalue
|
||||||
|
|
||||||
def _prepare_closure_call(self, insn):
|
def _prepare_closure_call(self, insn):
|
||||||
llclosure = self.map(insn.target_function())
|
|
||||||
llargs = [self.map(arg) for arg in insn.arguments()]
|
llargs = [self.map(arg) for arg in insn.arguments()]
|
||||||
|
llclosure = self.map(insn.target_function())
|
||||||
llenv = self.llbuilder.extract_value(llclosure, 0)
|
llenv = self.llbuilder.extract_value(llclosure, 0)
|
||||||
llfun = self.llbuilder.extract_value(llclosure, 1)
|
if insn.static_target_function is None:
|
||||||
|
llfun = self.llbuilder.extract_value(llclosure, 1)
|
||||||
|
else:
|
||||||
|
llfun = self.map(insn.static_target_function)
|
||||||
return llfun, [llenv] + list(llargs)
|
return llfun, [llenv] + list(llargs)
|
||||||
|
|
||||||
def _prepare_ffi_call(self, insn):
|
def _prepare_ffi_call(self, insn):
|
||||||
llargs = [self.map(arg) for arg in insn.arguments()]
|
llargs = []
|
||||||
|
byvals = []
|
||||||
|
for i, arg in enumerate(insn.arguments()):
|
||||||
|
llarg = self.map(arg)
|
||||||
|
if isinstance(llarg.type, (ll.LiteralStructType, ll.IdentifiedStructType)):
|
||||||
|
llslot = self.llbuilder.alloca(llarg.type)
|
||||||
|
self.llbuilder.store(llarg, llslot)
|
||||||
|
llargs.append(llslot)
|
||||||
|
byvals.append(i)
|
||||||
|
else:
|
||||||
|
llargs.append(llarg)
|
||||||
|
|
||||||
llfunname = insn.target_function().type.name
|
llfunname = insn.target_function().type.name
|
||||||
llfun = self.llmodule.get_global(llfunname)
|
llfun = self.llmodule.get_global(llfunname)
|
||||||
if llfun is None:
|
if llfun is None:
|
||||||
llfunty = ll.FunctionType(self.llty_of_type(insn.type, for_return=True),
|
llretty = self.llty_of_type(insn.type, for_return=True)
|
||||||
[llarg.type for llarg in llargs])
|
if self.needs_sret(llretty):
|
||||||
llfun = ll.Function(self.llmodule, llfunty,
|
llfunty = ll.FunctionType(llvoid, [llretty.as_pointer()] +
|
||||||
insn.target_function().type.name)
|
[llarg.type for llarg in llargs])
|
||||||
|
else:
|
||||||
|
llfunty = ll.FunctionType(llretty, [llarg.type for llarg in llargs])
|
||||||
|
|
||||||
|
llfun = ll.Function(self.llmodule, llfunty,
|
||||||
|
insn.target_function().type.name)
|
||||||
|
if self.needs_sret(llretty):
|
||||||
|
llfun.args[0].add_attribute('sret')
|
||||||
|
byvals = [i + 1 for i in byvals]
|
||||||
|
for i in byvals:
|
||||||
|
llfun.args[i].add_attribute('byval')
|
||||||
|
|
||||||
return llfun, list(llargs)
|
return llfun, list(llargs)
|
||||||
|
|
||||||
# See session.c:{send,receive}_rpc_value and comm_generic.py:_{send,receive}_rpc_value.
|
# See session.c:{send,receive}_rpc_value and comm_generic.py:_{send,receive}_rpc_value.
|
||||||
|
@ -1078,24 +1110,22 @@ class LLVMIRGenerator:
|
||||||
llnormalblock=None, llunwindblock=None)
|
llnormalblock=None, llunwindblock=None)
|
||||||
elif types.is_c_function(insn.target_function().type):
|
elif types.is_c_function(insn.target_function().type):
|
||||||
llfun, llargs = self._prepare_ffi_call(insn)
|
llfun, llargs = self._prepare_ffi_call(insn)
|
||||||
return self.llbuilder.call(llfun, llargs,
|
|
||||||
name=insn.name)
|
|
||||||
else:
|
else:
|
||||||
llfun, llargs = self._prepare_closure_call(insn)
|
llfun, llargs = self._prepare_closure_call(insn)
|
||||||
|
|
||||||
if llfun.type.pointee.__has_sret:
|
if self.has_sret(insn.target_function().type):
|
||||||
llstackptr = self.llbuilder.call(self.llbuiltin("llvm.stacksave"), [])
|
llstackptr = self.llbuilder.call(self.llbuiltin("llvm.stacksave"), [])
|
||||||
|
|
||||||
llresultslot = self.llbuilder.alloca(llfun.type.pointee.args[0].pointee)
|
llresultslot = self.llbuilder.alloca(llfun.type.pointee.args[0].pointee)
|
||||||
self.llbuilder.call(llfun, [llresultslot] + llargs)
|
self.llbuilder.call(llfun, [llresultslot] + llargs)
|
||||||
llresult = self.llbuilder.load(llresultslot)
|
llresult = self.llbuilder.load(llresultslot)
|
||||||
|
|
||||||
self.llbuilder.call(self.llbuiltin("llvm.stackrestore"), [llstackptr])
|
self.llbuilder.call(self.llbuiltin("llvm.stackrestore"), [llstackptr])
|
||||||
|
|
||||||
return llresult
|
return llresult
|
||||||
else:
|
else:
|
||||||
return self.llbuilder.call(llfun, llargs,
|
return self.llbuilder.call(llfun, llargs,
|
||||||
name=insn.name)
|
name=insn.name)
|
||||||
|
|
||||||
def process_Invoke(self, insn):
|
def process_Invoke(self, insn):
|
||||||
llnormalblock = self.map(insn.normal_target())
|
llnormalblock = self.map(insn.normal_target())
|
||||||
|
@ -1209,11 +1239,12 @@ class LLVMIRGenerator:
|
||||||
if builtins.is_none(insn.value().type):
|
if builtins.is_none(insn.value().type):
|
||||||
return self.llbuilder.ret_void()
|
return self.llbuilder.ret_void()
|
||||||
else:
|
else:
|
||||||
if self.llfunction.type.pointee.__has_sret:
|
llvalue = self.map(insn.value())
|
||||||
self.llbuilder.store(self.map(insn.value()), self.llfunction.args[0])
|
if self.needs_sret(llvalue.type):
|
||||||
|
self.llbuilder.store(llvalue, self.llfunction.args[0])
|
||||||
return self.llbuilder.ret_void()
|
return self.llbuilder.ret_void()
|
||||||
else:
|
else:
|
||||||
return self.llbuilder.ret(self.map(insn.value()))
|
return self.llbuilder.ret(llvalue)
|
||||||
|
|
||||||
def process_Unreachable(self, insn):
|
def process_Unreachable(self, insn):
|
||||||
return self.llbuilder.unreachable()
|
return self.llbuilder.unreachable()
|
||||||
|
@ -1250,7 +1281,8 @@ class LLVMIRGenerator:
|
||||||
cleanup=True)
|
cleanup=True)
|
||||||
llrawexn = self.llbuilder.extract_value(lllandingpad, 1)
|
llrawexn = self.llbuilder.extract_value(lllandingpad, 1)
|
||||||
llexn = self.llbuilder.bitcast(llrawexn, self.llty_of_type(insn.type))
|
llexn = self.llbuilder.bitcast(llrawexn, self.llty_of_type(insn.type))
|
||||||
llexnnameptr = self.llbuilder.gep(llexn, [self.llindex(0), self.llindex(0)])
|
llexnnameptr = self.llbuilder.gep(llexn, [self.llindex(0), self.llindex(0)],
|
||||||
|
inbounds=True)
|
||||||
llexnname = self.llbuilder.load(llexnnameptr)
|
llexnname = self.llbuilder.load(llexnnameptr)
|
||||||
|
|
||||||
for target, typ in insn.clauses():
|
for target, typ in insn.clauses():
|
||||||
|
|
|
@ -610,8 +610,13 @@ def is_function(typ):
|
||||||
def is_rpc_function(typ):
|
def is_rpc_function(typ):
|
||||||
return isinstance(typ.find(), TRPCFunction)
|
return isinstance(typ.find(), TRPCFunction)
|
||||||
|
|
||||||
def is_c_function(typ):
|
def is_c_function(typ, name=None):
|
||||||
return isinstance(typ.find(), TCFunction)
|
typ = typ.find()
|
||||||
|
if name is None:
|
||||||
|
return isinstance(typ, TCFunction)
|
||||||
|
else:
|
||||||
|
return isinstance(typ, TCFunction) and \
|
||||||
|
typ.name == name
|
||||||
|
|
||||||
def is_builtin(typ, name=None):
|
def is_builtin(typ, name=None):
|
||||||
typ = typ.find()
|
typ = typ.find()
|
||||||
|
|
|
@ -87,7 +87,13 @@ class RegionOf(algorithm.Visitor):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
visit_BinOpT = visit_sometimes_allocating
|
visit_BinOpT = visit_sometimes_allocating
|
||||||
visit_CallT = visit_sometimes_allocating
|
|
||||||
|
def visit_CallT(self, node):
|
||||||
|
if types.is_c_function(node.func.type, "cache_get"):
|
||||||
|
# The cache is borrow checked dynamically
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
self.visit_sometimes_allocating(node)
|
||||||
|
|
||||||
# Value lives as long as the object/container, if it's mutable,
|
# Value lives as long as the object/container, if it's mutable,
|
||||||
# or else forever
|
# or else forever
|
||||||
|
|
|
@ -512,7 +512,7 @@ class CommGeneric:
|
||||||
assert exception.id != 0
|
assert exception.id != 0
|
||||||
python_exn_type = object_map.retrieve(exception.id)
|
python_exn_type = object_map.retrieve(exception.id)
|
||||||
|
|
||||||
python_exn = python_exn_type(message)
|
python_exn = python_exn_type(message.format(*params))
|
||||||
python_exn.artiq_exception = exception
|
python_exn.artiq_exception = exception
|
||||||
raise python_exn
|
raise python_exn
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,14 @@ class CompileError(Exception):
|
||||||
def rtio_get_counter() -> TInt64:
|
def rtio_get_counter() -> TInt64:
|
||||||
raise NotImplementedError("syscall not simulated")
|
raise NotImplementedError("syscall not simulated")
|
||||||
|
|
||||||
|
@syscall
|
||||||
|
def cache_get(key: TStr) -> TList(TInt32):
|
||||||
|
raise NotImplementedError("syscall not simulated")
|
||||||
|
|
||||||
|
@syscall
|
||||||
|
def cache_put(key: TStr, value: TList(TInt32)) -> TNone:
|
||||||
|
raise NotImplementedError("syscall not simulated")
|
||||||
|
|
||||||
class Core:
|
class Core:
|
||||||
"""Core device driver.
|
"""Core device driver.
|
||||||
|
|
||||||
|
@ -108,3 +116,11 @@ class Core:
|
||||||
min_now = rtio_get_counter() + 125000
|
min_now = rtio_get_counter() + 125000
|
||||||
if now_mu() < min_now:
|
if now_mu() < min_now:
|
||||||
at_mu(min_now)
|
at_mu(min_now)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def get_cache(self, key):
|
||||||
|
return cache_get(key)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def put_cache(self, key, value):
|
||||||
|
return cache_put(key, value)
|
||||||
|
|
|
@ -11,6 +11,10 @@ class InternalError(Exception):
|
||||||
"""Raised when the runtime encounters an internal error condition."""
|
"""Raised when the runtime encounters an internal error condition."""
|
||||||
|
|
||||||
|
|
||||||
|
class CacheError(Exception):
|
||||||
|
"""Raised when putting a value into a cache row would violate memory safety."""
|
||||||
|
|
||||||
|
|
||||||
class RTIOUnderflow(Exception):
|
class RTIOUnderflow(Exception):
|
||||||
"""Raised when the CPU fails to submit a RTIO event early enough
|
"""Raised when the CPU fails to submit a RTIO event early enough
|
||||||
(with respect to the event's timestamp).
|
(with respect to the event's timestamp).
|
||||||
|
|
|
@ -7,7 +7,7 @@ from misoc.integration.soc_core import mem_decoder
|
||||||
|
|
||||||
class KernelCPU(Module):
|
class KernelCPU(Module):
|
||||||
def __init__(self, platform,
|
def __init__(self, platform,
|
||||||
exec_address=0x40800000,
|
exec_address=0x42000000,
|
||||||
main_mem_origin=0x40000000,
|
main_mem_origin=0x40000000,
|
||||||
l2_size=8192):
|
l2_size=8192):
|
||||||
self._reset = CSRStorage(reset=1)
|
self._reset = CSRStorage(reset=1)
|
||||||
|
|
|
@ -9,7 +9,8 @@ OBJECTS := isr.o clock.o rtiocrg.o flash_storage.o mailbox.o \
|
||||||
OBJECTS_KSUPPORT := ksupport.o artiq_personality.o mailbox.o \
|
OBJECTS_KSUPPORT := ksupport.o artiq_personality.o mailbox.o \
|
||||||
bridge.o rtio.o ttl.o dds.o
|
bridge.o rtio.o ttl.o dds.o
|
||||||
|
|
||||||
CFLAGS += -I$(MISOC_DIRECTORY)/software/include/dyld \
|
CFLAGS += -I$(LIBALLOC_DIRECTORY) \
|
||||||
|
-I$(MISOC_DIRECTORY)/software/include/dyld \
|
||||||
-I$(LIBDYLD_DIRECTORY)/include \
|
-I$(LIBDYLD_DIRECTORY)/include \
|
||||||
-I$(LIBUNWIND_DIRECTORY) \
|
-I$(LIBUNWIND_DIRECTORY) \
|
||||||
-I$(LIBUNWIND_DIRECTORY)/../unwinder/include \
|
-I$(LIBUNWIND_DIRECTORY)/../unwinder/include \
|
||||||
|
@ -31,10 +32,11 @@ runtime.elf: $(OBJECTS)
|
||||||
-N -o $@ \
|
-N -o $@ \
|
||||||
../libbase/crt0-$(CPU).o \
|
../libbase/crt0-$(CPU).o \
|
||||||
$(OBJECTS) \
|
$(OBJECTS) \
|
||||||
-L../libbase \
|
|
||||||
-L../libcompiler_rt \
|
-L../libcompiler_rt \
|
||||||
|
-L../libbase \
|
||||||
|
-L../liballoc \
|
||||||
-L../liblwip \
|
-L../liblwip \
|
||||||
-lbase -lcompiler_rt -llwip
|
-lbase -lcompiler_rt -lalloc -llwip
|
||||||
@chmod -x $@
|
@chmod -x $@
|
||||||
|
|
||||||
ksupport.elf: $(OBJECTS_KSUPPORT)
|
ksupport.elf: $(OBJECTS_KSUPPORT)
|
||||||
|
@ -48,7 +50,7 @@ ksupport.elf: $(OBJECTS_KSUPPORT)
|
||||||
-L../libcompiler_rt \
|
-L../libcompiler_rt \
|
||||||
-L../libunwind \
|
-L../libunwind \
|
||||||
-L../libdyld \
|
-L../libdyld \
|
||||||
-lbase -lcompiler_rt -lunwind -ldyld
|
-lbase -lcompiler_rt -ldyld -lunwind
|
||||||
@chmod -x $@
|
@chmod -x $@
|
||||||
|
|
||||||
ksupport_data.o: ksupport.elf
|
ksupport_data.o: ksupport.elf
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
#include "artiq_personality.h"
|
#include "artiq_personality.h"
|
||||||
|
|
||||||
#define KERNELCPU_EXEC_ADDRESS 0x40800000
|
#define KERNELCPU_EXEC_ADDRESS 0x42000000
|
||||||
#define KERNELCPU_PAYLOAD_ADDRESS 0x40820000
|
#define KERNELCPU_PAYLOAD_ADDRESS 0x42020000
|
||||||
#define KERNELCPU_LAST_ADDRESS (0x4fffffff - 1024*1024)
|
#define KERNELCPU_LAST_ADDRESS (0x4fffffff - 1024*1024)
|
||||||
#define KSUPPORT_HEADER_SIZE 0x80
|
#define KSUPPORT_HEADER_SIZE 0x80
|
||||||
|
|
||||||
|
|
|
@ -122,6 +122,9 @@ static const struct symbol runtime_exports[] = {
|
||||||
{"dds_batch_exit", &dds_batch_exit},
|
{"dds_batch_exit", &dds_batch_exit},
|
||||||
{"dds_set", &dds_set},
|
{"dds_set", &dds_set},
|
||||||
|
|
||||||
|
{"cache_get", &cache_get},
|
||||||
|
{"cache_put", &cache_put},
|
||||||
|
|
||||||
/* end */
|
/* end */
|
||||||
{NULL, NULL}
|
{NULL, NULL}
|
||||||
};
|
};
|
||||||
|
@ -222,7 +225,7 @@ void exception_handler(unsigned long vect, unsigned long *regs,
|
||||||
unsigned long pc, unsigned long ea)
|
unsigned long pc, unsigned long ea)
|
||||||
{
|
{
|
||||||
artiq_raise_from_c("InternalError",
|
artiq_raise_from_c("InternalError",
|
||||||
"Hardware exception {0} at PC {1}, EA {2}",
|
"Hardware exception {0} at PC 0x{1:08x}, EA 0x{2:08x}",
|
||||||
vect, pc, ea);
|
vect, pc, ea);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,6 +447,50 @@ void attribute_writeback(void *utypes) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct artiq_list cache_get(const char *key)
|
||||||
|
{
|
||||||
|
struct msg_cache_get_request request;
|
||||||
|
struct msg_cache_get_reply *reply;
|
||||||
|
|
||||||
|
request.type = MESSAGE_TYPE_CACHE_GET_REQUEST;
|
||||||
|
request.key = key;
|
||||||
|
mailbox_send_and_wait(&request);
|
||||||
|
|
||||||
|
reply = mailbox_wait_and_receive();
|
||||||
|
if(reply->type != MESSAGE_TYPE_CACHE_GET_REPLY) {
|
||||||
|
log("Malformed MESSAGE_TYPE_CACHE_GET_REQUEST reply type %d",
|
||||||
|
reply->type);
|
||||||
|
while(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (struct artiq_list) { reply->length, reply->elements };
|
||||||
|
}
|
||||||
|
|
||||||
|
void cache_put(const char *key, struct artiq_list value)
|
||||||
|
{
|
||||||
|
struct msg_cache_put_request request;
|
||||||
|
struct msg_cache_put_reply *reply;
|
||||||
|
|
||||||
|
request.type = MESSAGE_TYPE_CACHE_PUT_REQUEST;
|
||||||
|
request.key = key;
|
||||||
|
request.elements = value.elements;
|
||||||
|
request.length = value.length;
|
||||||
|
mailbox_send_and_wait(&request);
|
||||||
|
|
||||||
|
reply = mailbox_wait_and_receive();
|
||||||
|
if(reply->type != MESSAGE_TYPE_CACHE_PUT_REPLY) {
|
||||||
|
log("Malformed MESSAGE_TYPE_CACHE_PUT_REQUEST reply type %d",
|
||||||
|
reply->type);
|
||||||
|
while(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!reply->succeeded) {
|
||||||
|
artiq_raise_from_c("CacheError",
|
||||||
|
"cannot put into a busy cache row",
|
||||||
|
0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void lognonl(const char *fmt, ...)
|
void lognonl(const char *fmt, ...)
|
||||||
{
|
{
|
||||||
struct msg_log request;
|
struct msg_log request;
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
#ifndef __KSTARTUP_H
|
#ifndef __KSTARTUP_H
|
||||||
#define __KSTARTUP_H
|
#define __KSTARTUP_H
|
||||||
|
|
||||||
|
struct artiq_list {
|
||||||
|
int32_t length;
|
||||||
|
int32_t *elements;
|
||||||
|
};
|
||||||
|
|
||||||
long long int now_init(void);
|
long long int now_init(void);
|
||||||
void now_save(long long int now);
|
void now_save(long long int now);
|
||||||
int watchdog_set(int ms);
|
int watchdog_set(int ms);
|
||||||
void watchdog_clear(int id);
|
void watchdog_clear(int id);
|
||||||
void send_rpc(int service, const char *tag, ...);
|
void send_rpc(int service, const char *tag, ...);
|
||||||
int recv_rpc(void *slot);
|
int recv_rpc(void *slot);
|
||||||
|
struct artiq_list cache_get(const char *key);
|
||||||
|
void cache_put(const char *key, struct artiq_list value);
|
||||||
void lognonl(const char *fmt, ...);
|
void lognonl(const char *fmt, ...);
|
||||||
void log(const char *fmt, ...);
|
void log(const char *fmt, ...);
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,12 @@ ENTRY(_start)
|
||||||
|
|
||||||
INCLUDE generated/regions.ld
|
INCLUDE generated/regions.ld
|
||||||
|
|
||||||
/* First 8M of main memory are reserved for runtime code/data
|
/* First 32M of main memory are reserved for runtime
|
||||||
* then comes kernel memory. First 128K of kernel memory are for support code.
|
* code/data/heap, then comes kernel memory.
|
||||||
|
* First 128K of kernel memory are for support code.
|
||||||
*/
|
*/
|
||||||
MEMORY {
|
MEMORY {
|
||||||
ksupport (RWX) : ORIGIN = 0x40800000, LENGTH = 0x20000
|
ksupport (RWX) : ORIGIN = 0x42000000, LENGTH = 0x20000
|
||||||
}
|
}
|
||||||
|
|
||||||
/* On AMP systems, kernel stack is at the end of main RAM,
|
/* On AMP systems, kernel stack is at the end of main RAM,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <alloc.h>
|
||||||
#include <irq.h>
|
#include <irq.h>
|
||||||
#include <uart.h>
|
#include <uart.h>
|
||||||
#include <console.h>
|
#include <console.h>
|
||||||
|
@ -263,6 +264,8 @@ static int check_test_mode(void)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern void _fheap, _eheap;
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
irq_setmask(0);
|
irq_setmask(0);
|
||||||
|
@ -271,6 +274,7 @@ int main(void)
|
||||||
|
|
||||||
puts("ARTIQ runtime built "__DATE__" "__TIME__"\n");
|
puts("ARTIQ runtime built "__DATE__" "__TIME__"\n");
|
||||||
|
|
||||||
|
alloc_give(&_fheap, &_eheap - &_fheap);
|
||||||
clock_init();
|
clock_init();
|
||||||
rtiocrg_init();
|
rtiocrg_init();
|
||||||
puts("Press 't' to enter test mode...");
|
puts("Press 't' to enter test mode...");
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
MESSAGE_TYPE_LOAD_REPLY,
|
MESSAGE_TYPE_LOAD_REPLY,
|
||||||
|
@ -18,6 +19,10 @@ enum {
|
||||||
MESSAGE_TYPE_RPC_RECV_REQUEST,
|
MESSAGE_TYPE_RPC_RECV_REQUEST,
|
||||||
MESSAGE_TYPE_RPC_RECV_REPLY,
|
MESSAGE_TYPE_RPC_RECV_REPLY,
|
||||||
MESSAGE_TYPE_RPC_BATCH,
|
MESSAGE_TYPE_RPC_BATCH,
|
||||||
|
MESSAGE_TYPE_CACHE_GET_REQUEST,
|
||||||
|
MESSAGE_TYPE_CACHE_GET_REPLY,
|
||||||
|
MESSAGE_TYPE_CACHE_PUT_REQUEST,
|
||||||
|
MESSAGE_TYPE_CACHE_PUT_REPLY,
|
||||||
MESSAGE_TYPE_LOG,
|
MESSAGE_TYPE_LOG,
|
||||||
|
|
||||||
MESSAGE_TYPE_BRG_READY,
|
MESSAGE_TYPE_BRG_READY,
|
||||||
|
@ -105,6 +110,29 @@ struct msg_rpc_batch {
|
||||||
void *ptr;
|
void *ptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct msg_cache_get_request {
|
||||||
|
int type;
|
||||||
|
const char *key;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_cache_get_reply {
|
||||||
|
int type;
|
||||||
|
size_t length;
|
||||||
|
int32_t *elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_cache_put_request {
|
||||||
|
int type;
|
||||||
|
const char *key;
|
||||||
|
size_t length;
|
||||||
|
int32_t *elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_cache_put_reply {
|
||||||
|
int type;
|
||||||
|
int succeeded;
|
||||||
|
};
|
||||||
|
|
||||||
struct msg_log {
|
struct msg_log {
|
||||||
int type;
|
int type;
|
||||||
const char *fmt;
|
const char *fmt;
|
||||||
|
|
|
@ -7,7 +7,7 @@ INCLUDE generated/regions.ld
|
||||||
* ld does not allow this expression here.
|
* ld does not allow this expression here.
|
||||||
*/
|
*/
|
||||||
MEMORY {
|
MEMORY {
|
||||||
runtime : ORIGIN = 0x40000000, LENGTH = 0x800000 /* 8M */
|
runtime : ORIGIN = 0x40000000, LENGTH = 0x2000000 /* 32M */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Kernel memory space start right after the runtime,
|
/* Kernel memory space start right after the runtime,
|
||||||
|
@ -65,5 +65,7 @@ SECTIONS
|
||||||
*(.eh_frame)
|
*(.eh_frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
_heapstart = .;
|
_fheap = .;
|
||||||
|
. += 0x1800000;
|
||||||
|
_eheap = .;
|
||||||
}
|
}
|
||||||
|
|
|
@ -908,6 +908,16 @@ static int send_rpc_request(int service, const char *tag, va_list args)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct cache_row {
|
||||||
|
struct cache_row *next;
|
||||||
|
char *key;
|
||||||
|
size_t length;
|
||||||
|
int32_t *elements;
|
||||||
|
int borrowed;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct cache_row *cache;
|
||||||
|
|
||||||
/* assumes output buffer is empty when called */
|
/* assumes output buffer is empty when called */
|
||||||
static int process_kmsg(struct msg_base *umsg)
|
static int process_kmsg(struct msg_base *umsg)
|
||||||
{
|
{
|
||||||
|
@ -930,9 +940,12 @@ static int process_kmsg(struct msg_base *umsg)
|
||||||
case MESSAGE_TYPE_FINISHED:
|
case MESSAGE_TYPE_FINISHED:
|
||||||
out_packet_empty(REMOTEMSG_TYPE_KERNEL_FINISHED);
|
out_packet_empty(REMOTEMSG_TYPE_KERNEL_FINISHED);
|
||||||
|
|
||||||
|
for(struct cache_row *iter = cache; iter; iter = iter->next)
|
||||||
|
iter->borrowed = 0;
|
||||||
|
|
||||||
kloader_stop();
|
kloader_stop();
|
||||||
user_kernel_state = USER_KERNEL_LOADED;
|
user_kernel_state = USER_KERNEL_LOADED;
|
||||||
mailbox_acknowledge();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MESSAGE_TYPE_EXCEPTION: {
|
case MESSAGE_TYPE_EXCEPTION: {
|
||||||
|
@ -984,6 +997,67 @@ static int process_kmsg(struct msg_base *umsg)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case MESSAGE_TYPE_CACHE_GET_REQUEST: {
|
||||||
|
struct msg_cache_get_request *request = (struct msg_cache_get_request *)umsg;
|
||||||
|
struct msg_cache_get_reply reply;
|
||||||
|
|
||||||
|
reply.type = MESSAGE_TYPE_CACHE_GET_REPLY;
|
||||||
|
reply.length = 0;
|
||||||
|
reply.elements = NULL;
|
||||||
|
|
||||||
|
for(struct cache_row *iter = cache; iter; iter = iter->next) {
|
||||||
|
if(!strcmp(iter->key, request->key)) {
|
||||||
|
reply.length = iter->length;
|
||||||
|
reply.elements = iter->elements;
|
||||||
|
iter->borrowed = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox_send(&reply);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MESSAGE_TYPE_CACHE_PUT_REQUEST: {
|
||||||
|
struct msg_cache_put_request *request = (struct msg_cache_put_request *)umsg;
|
||||||
|
struct msg_cache_put_reply reply;
|
||||||
|
|
||||||
|
reply.type = MESSAGE_TYPE_CACHE_PUT_REPLY;
|
||||||
|
|
||||||
|
struct cache_row *row = NULL;
|
||||||
|
for(struct cache_row *iter = cache; iter; iter = iter->next) {
|
||||||
|
if(!strcmp(iter->key, request->key)) {
|
||||||
|
free(iter->elements);
|
||||||
|
row = iter;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!row) {
|
||||||
|
row = calloc(1, sizeof(struct cache_row));
|
||||||
|
row->key = calloc(strlen(request->key) + 1, 1);
|
||||||
|
strcpy(row->key, request->key);
|
||||||
|
row->next = cache;
|
||||||
|
cache = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!row->borrowed) {
|
||||||
|
if(request->length != 0) {
|
||||||
|
row->length = request->length;
|
||||||
|
row->elements = calloc(row->length, sizeof(int32_t));
|
||||||
|
memcpy(row->elements, request->elements,
|
||||||
|
sizeof(int32_t) * row->length);
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.succeeded = 1;
|
||||||
|
} else {
|
||||||
|
reply.succeeded = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox_send(&reply);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
log("Received invalid message type %d from kernel CPU",
|
log("Received invalid message type %d from kernel CPU",
|
||||||
umsg->type);
|
umsg->type);
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
from artiq.language import *
|
||||||
|
from artiq.coredevice.exceptions import *
|
||||||
|
from artiq.test.hardware_testbench import ExperimentCase
|
||||||
|
|
||||||
|
|
||||||
|
class _Cache(EnvExperiment):
|
||||||
|
def build(self):
|
||||||
|
self.setattr_device("core")
|
||||||
|
self.print = lambda x: print(x)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def get(self, key):
|
||||||
|
return self.core.get_cache(key)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def put(self, key, value):
|
||||||
|
self.core.put_cache(key, value)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def get_put(self, key, value):
|
||||||
|
self.get(key)
|
||||||
|
self.put(key, value)
|
||||||
|
|
||||||
|
class CacheTest(ExperimentCase):
|
||||||
|
def test_get_empty(self):
|
||||||
|
exp = self.create(_Cache)
|
||||||
|
self.assertEqual(exp.get("x1"), [])
|
||||||
|
|
||||||
|
def test_put_get(self):
|
||||||
|
exp = self.create(_Cache)
|
||||||
|
exp.put("x2", [1, 2, 3])
|
||||||
|
self.assertEqual(exp.get("x2"), [1, 2, 3])
|
||||||
|
|
||||||
|
def test_replace(self):
|
||||||
|
exp = self.create(_Cache)
|
||||||
|
exp.put("x3", [1, 2, 3])
|
||||||
|
exp.put("x3", [1, 2, 3, 4, 5])
|
||||||
|
self.assertEqual(exp.get("x3"), [1, 2, 3, 4, 5])
|
||||||
|
|
||||||
|
def test_borrow(self):
|
||||||
|
exp = self.create(_Cache)
|
||||||
|
exp.put("x4", [1, 2, 3])
|
||||||
|
with self.assertRaises(CacheError):
|
||||||
|
exp.get_put("x4", [])
|
1
setup.py
1
setup.py
|
@ -44,6 +44,7 @@ scripts = [
|
||||||
setup(
|
setup(
|
||||||
name="artiq",
|
name="artiq",
|
||||||
version=versioneer.get_version(),
|
version=versioneer.get_version(),
|
||||||
|
cmdclass=versioneer.get_cmdclass(),
|
||||||
author="M-Labs / NIST Ion Storage Group",
|
author="M-Labs / NIST Ion Storage Group",
|
||||||
author_email="sb@m-labs.hk",
|
author_email="sb@m-labs.hk",
|
||||||
url="http://m-labs.hk/artiq",
|
url="http://m-labs.hk/artiq",
|
||||||
|
|
Loading…
Reference in New Issue