forked from M-Labs/artiq
compiler: maintain both the IR and iodelay forms of delay expressions.
After this commit, the delay instruction (again) does not generate
any LLVM IR: all heavy lifting is relegated to the delay and delay_mu
intrinsics. When the interleave transform needs to adjust the global
timeline, it synthesizes a delay_mu intrinsnic. This way,
the interleave transformation becomes composable, as the input and
the output IR invariants are the same.
Also, code generation is adjusted so that a basic block is split off
not only after a delay call, but also before one; otherwise, e.g.,
code immediately at the beginning of a `with parallel:` branch
would have no choice but to execute after another branch has already
advanced the timeline.
This takes care of issue #1 described in 50e7b44
and is a step
to solving issue #2.
This commit is contained in:
parent
50e7b44d04
commit
cb3b811fd7
@ -38,6 +38,12 @@ class TInt(types.TMono):
|
|||||||
def one():
|
def one():
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
def TInt32():
|
||||||
|
return TInt(types.TValue(32))
|
||||||
|
|
||||||
|
def TInt64():
|
||||||
|
return TInt(types.TValue(64))
|
||||||
|
|
||||||
class TFloat(types.TMono):
|
class TFloat(types.TMono):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("float")
|
super().__init__("float")
|
||||||
|
@ -72,6 +72,16 @@ class Constant(Value):
|
|||||||
return "{} {}".format(type_printer.name(self.type),
|
return "{} {}".format(type_printer.name(self.type),
|
||||||
repr(self.value))
|
repr(self.value))
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.value)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, Constant) and \
|
||||||
|
other.type == self.type and other.value == self.value
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
class NamedValue(Value):
|
class NamedValue(Value):
|
||||||
"""
|
"""
|
||||||
An SSA value that has a name.
|
An SSA value that has a name.
|
||||||
@ -190,7 +200,7 @@ class Instruction(User):
|
|||||||
return ", ".join([operand.as_operand(type_printer) for operand in self.operands])
|
return ", ".join([operand.as_operand(type_printer) for operand in self.operands])
|
||||||
|
|
||||||
def as_entity(self, type_printer):
|
def as_entity(self, type_printer):
|
||||||
if builtins.is_none(self.type):
|
if builtins.is_none(self.type) and len(self.uses) == 0:
|
||||||
prefix = ""
|
prefix = ""
|
||||||
else:
|
else:
|
||||||
prefix = "%{} = {} ".format(escape_name(self.name),
|
prefix = "%{} = {} ".format(escape_name(self.name),
|
||||||
@ -1198,35 +1208,46 @@ class Delay(Terminator):
|
|||||||
|
|
||||||
:ivar expr: (:class:`iodelay.Expr`) expression
|
:ivar expr: (:class:`iodelay.Expr`) expression
|
||||||
:ivar var_names: (list of string)
|
:ivar var_names: (list of string)
|
||||||
iodelay expression names corresponding to operands
|
iodelay variable names corresponding to operands
|
||||||
"""
|
"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:param expr: (:class:`iodelay.Expr`) expression
|
:param expr: (:class:`iodelay.Expr`) expression
|
||||||
:param substs: (dict of str to :class:`Value`)
|
:param substs: (dict of str to :class:`Value`)
|
||||||
|
SSA values corresponding to iodelay variable names
|
||||||
|
:param call: (:class:`Call` or ``Constant(None, builtins.TNone())``)
|
||||||
|
the call instruction that caused this delay, if any
|
||||||
:param target: (:class:`BasicBlock`) branch target
|
:param target: (:class:`BasicBlock`) branch target
|
||||||
"""
|
"""
|
||||||
def __init__(self, expr, substs, target, name=""):
|
def __init__(self, expr, substs, decomposition, target, name=""):
|
||||||
for var_name in substs: assert isinstance(var_name, str)
|
for var_name in substs: assert isinstance(var_name, str)
|
||||||
|
assert isinstance(decomposition, Call) or \
|
||||||
|
isinstance(decomposition, Builtin) and decomposition.op in ("delay", "delay_mu")
|
||||||
assert isinstance(target, BasicBlock)
|
assert isinstance(target, BasicBlock)
|
||||||
super().__init__([target, *substs.values()], builtins.TNone(), name)
|
super().__init__([decomposition, target, *substs.values()], builtins.TNone(), name)
|
||||||
self.expr = expr
|
self.expr = expr
|
||||||
self.var_names = list(substs.keys())
|
self.var_names = list(substs.keys())
|
||||||
|
|
||||||
def target(self):
|
def decomposition(self):
|
||||||
return self.operands[0]
|
return self.operands[0]
|
||||||
|
|
||||||
|
def target(self):
|
||||||
|
return self.operands[1]
|
||||||
|
|
||||||
def set_target(self, new_target):
|
def set_target(self, new_target):
|
||||||
self.operands[0] = new_target
|
self.operands[1] = new_target
|
||||||
|
|
||||||
def substs(self):
|
def substs(self):
|
||||||
return zip(self.var_names, self.operands[1:])
|
return zip(self.var_names, self.operands[2:])
|
||||||
|
|
||||||
def _operands_as_string(self, type_printer):
|
def _operands_as_string(self, type_printer):
|
||||||
substs = []
|
substs = []
|
||||||
for var_name, operand in self.substs():
|
for var_name, operand in self.substs():
|
||||||
substs.append("{}={}".format(var_name, operand))
|
substs.append("{}={}".format(var_name, operand))
|
||||||
return "[{}], {}".format(", ".join(substs), self.target().as_operand(type_printer))
|
result = "[{}]".format(", ".join(substs))
|
||||||
|
result += ", decomp {}, to {}".format(self.decomposition().as_operand(type_printer),
|
||||||
|
self.target().as_operand(type_printer))
|
||||||
|
return result
|
||||||
|
|
||||||
def opcode(self):
|
def opcode(self):
|
||||||
return "delay({})".format(self.expr)
|
return "delay({})".format(self.expr)
|
||||||
|
@ -1418,15 +1418,6 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
|||||||
|
|
||||||
return self.append(ir.Alloc(attributes, typ))
|
return self.append(ir.Alloc(attributes, typ))
|
||||||
|
|
||||||
def _make_delay(self, delay):
|
|
||||||
if not iodelay.is_const(delay, 0):
|
|
||||||
after_delay = self.add_block()
|
|
||||||
self.append(ir.Delay(delay,
|
|
||||||
{var_name: self.current_args[var_name]
|
|
||||||
for var_name in delay.free_vars()},
|
|
||||||
after_delay))
|
|
||||||
self.current_block = after_delay
|
|
||||||
|
|
||||||
def visit_builtin_call(self, node):
|
def visit_builtin_call(self, node):
|
||||||
# A builtin by any other name... Ignore node.func, just use the type.
|
# A builtin by any other name... Ignore node.func, just use the type.
|
||||||
typ = node.func.type
|
typ = node.func.type
|
||||||
@ -1529,7 +1520,7 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
|||||||
return self.append(ir.Arith(ast.Mult(loc=None), now_mu_float, self.ref_period))
|
return self.append(ir.Arith(ast.Mult(loc=None), now_mu_float, self.ref_period))
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
elif types.is_builtin(typ, "at"):
|
elif types.is_builtin(typ, "delay") or types.is_builtin(typ, "at"):
|
||||||
if len(node.args) == 1 and len(node.keywords) == 0:
|
if len(node.args) == 1 and len(node.keywords) == 0:
|
||||||
arg = self.visit(node.args[0])
|
arg = self.visit(node.args[0])
|
||||||
arg_mu_float = self.append(ir.Arith(ast.Div(loc=None), arg, self.ref_period))
|
arg_mu_float = self.append(ir.Arith(ast.Div(loc=None), arg, self.ref_period))
|
||||||
@ -1537,7 +1528,8 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
|||||||
self.append(ir.Builtin(typ.name + "_mu", [arg_mu], builtins.TNone()))
|
self.append(ir.Builtin(typ.name + "_mu", [arg_mu], builtins.TNone()))
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
elif types.is_builtin(typ, "now_mu") or types.is_builtin(typ, "at_mu"):
|
elif types.is_builtin(typ, "now_mu") or types.is_builtin(typ, "delay_mu") \
|
||||||
|
or types.is_builtin(typ, "at_mu"):
|
||||||
return self.append(ir.Builtin(typ.name,
|
return self.append(ir.Builtin(typ.name,
|
||||||
[self.visit(arg) for arg in node.args], node.type))
|
[self.visit(arg) for arg in node.args], node.type))
|
||||||
elif types.is_builtin(typ, "mu_to_seconds"):
|
elif types.is_builtin(typ, "mu_to_seconds"):
|
||||||
@ -1554,9 +1546,6 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
|||||||
return self.append(ir.Coerce(arg_mu, builtins.TInt(types.TValue(64))))
|
return self.append(ir.Coerce(arg_mu, builtins.TInt(types.TValue(64))))
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
elif types.is_builtin(typ, "delay") or types.is_builtin(typ, "delay_mu"):
|
|
||||||
assert node.iodelay is not None
|
|
||||||
self._make_delay(node.iodelay)
|
|
||||||
elif types.is_exn_constructor(typ):
|
elif types.is_exn_constructor(typ):
|
||||||
return self.alloc_exn(node.type, *[self.visit(arg_node) for arg_node in node.args])
|
return self.alloc_exn(node.type, *[self.visit(arg_node) for arg_node in node.args])
|
||||||
elif types.is_constructor(typ):
|
elif types.is_constructor(typ):
|
||||||
@ -1567,77 +1556,82 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
|||||||
def visit_CallT(self, node):
|
def visit_CallT(self, node):
|
||||||
typ = node.func.type.find()
|
typ = node.func.type.find()
|
||||||
|
|
||||||
|
if node.iodelay is not None and not iodelay.is_const(node.iodelay, 0):
|
||||||
|
before_delay = self.current_block
|
||||||
|
during_delay = self.add_block()
|
||||||
|
before_delay.append(ir.Branch(during_delay))
|
||||||
|
self.current_block = during_delay
|
||||||
|
|
||||||
if types.is_builtin(typ):
|
if types.is_builtin(typ):
|
||||||
return self.visit_builtin_call(node)
|
insn = self.visit_builtin_call(node)
|
||||||
|
|
||||||
if types.is_function(typ):
|
|
||||||
func = self.visit(node.func)
|
|
||||||
self_arg = None
|
|
||||||
fn_typ = typ
|
|
||||||
offset = 0
|
|
||||||
elif types.is_method(typ):
|
|
||||||
method = self.visit(node.func)
|
|
||||||
func = self.append(ir.GetAttr(method, "__func__"))
|
|
||||||
self_arg = self.append(ir.GetAttr(method, "__self__"))
|
|
||||||
fn_typ = types.get_method_function(typ)
|
|
||||||
offset = 1
|
|
||||||
else:
|
else:
|
||||||
assert False
|
if types.is_function(typ):
|
||||||
|
func = self.visit(node.func)
|
||||||
args = [None] * (len(fn_typ.args) + len(fn_typ.optargs))
|
self_arg = None
|
||||||
|
fn_typ = typ
|
||||||
for index, arg_node in enumerate(node.args):
|
offset = 0
|
||||||
arg = self.visit(arg_node)
|
elif types.is_method(typ):
|
||||||
if index < len(fn_typ.args):
|
method = self.visit(node.func)
|
||||||
args[index + offset] = arg
|
func = self.append(ir.GetAttr(method, "__func__"))
|
||||||
|
self_arg = self.append(ir.GetAttr(method, "__self__"))
|
||||||
|
fn_typ = types.get_method_function(typ)
|
||||||
|
offset = 1
|
||||||
else:
|
else:
|
||||||
args[index + offset] = self.append(ir.Alloc([arg], ir.TOption(arg.type)))
|
assert False
|
||||||
|
|
||||||
for keyword in node.keywords:
|
args = [None] * (len(fn_typ.args) + len(fn_typ.optargs))
|
||||||
arg = self.visit(keyword.value)
|
|
||||||
if keyword.arg in fn_typ.args:
|
|
||||||
for index, arg_name in enumerate(fn_typ.args):
|
|
||||||
if keyword.arg == arg_name:
|
|
||||||
assert args[index] is None
|
|
||||||
args[index] = arg
|
|
||||||
break
|
|
||||||
elif keyword.arg in fn_typ.optargs:
|
|
||||||
for index, optarg_name in enumerate(fn_typ.optargs):
|
|
||||||
if keyword.arg == optarg_name:
|
|
||||||
assert args[len(fn_typ.args) + index] is None
|
|
||||||
args[len(fn_typ.args) + index] = \
|
|
||||||
self.append(ir.Alloc([arg], ir.TOption(arg.type)))
|
|
||||||
break
|
|
||||||
|
|
||||||
for index, optarg_name in enumerate(fn_typ.optargs):
|
for index, arg_node in enumerate(node.args):
|
||||||
if args[len(fn_typ.args) + index] is None:
|
arg = self.visit(arg_node)
|
||||||
args[len(fn_typ.args) + index] = \
|
if index < len(fn_typ.args):
|
||||||
self.append(ir.Alloc([], ir.TOption(fn_typ.optargs[optarg_name])))
|
args[index + offset] = arg
|
||||||
|
else:
|
||||||
|
args[index + offset] = self.append(ir.Alloc([arg], ir.TOption(arg.type)))
|
||||||
|
|
||||||
if self_arg is not None:
|
for keyword in node.keywords:
|
||||||
assert args[0] is None
|
arg = self.visit(keyword.value)
|
||||||
args[0] = self_arg
|
if keyword.arg in fn_typ.args:
|
||||||
|
for index, arg_name in enumerate(fn_typ.args):
|
||||||
|
if keyword.arg == arg_name:
|
||||||
|
assert args[index] is None
|
||||||
|
args[index] = arg
|
||||||
|
break
|
||||||
|
elif keyword.arg in fn_typ.optargs:
|
||||||
|
for index, optarg_name in enumerate(fn_typ.optargs):
|
||||||
|
if keyword.arg == optarg_name:
|
||||||
|
assert args[len(fn_typ.args) + index] is None
|
||||||
|
args[len(fn_typ.args) + index] = \
|
||||||
|
self.append(ir.Alloc([arg], ir.TOption(arg.type)))
|
||||||
|
break
|
||||||
|
|
||||||
assert None not in args
|
for index, optarg_name in enumerate(fn_typ.optargs):
|
||||||
|
if args[len(fn_typ.args) + index] is None:
|
||||||
|
args[len(fn_typ.args) + index] = \
|
||||||
|
self.append(ir.Alloc([], ir.TOption(fn_typ.optargs[optarg_name])))
|
||||||
|
|
||||||
if self.unwind_target is None:
|
if self_arg is not None:
|
||||||
insn = self.append(ir.Call(func, args))
|
assert args[0] is None
|
||||||
else:
|
args[0] = self_arg
|
||||||
after_invoke = self.add_block()
|
|
||||||
insn = self.append(ir.Invoke(func, args, after_invoke, self.unwind_target))
|
|
||||||
self.current_block = after_invoke
|
|
||||||
|
|
||||||
method_key = None
|
assert None not in args
|
||||||
if isinstance(node.func, asttyped.AttributeT):
|
|
||||||
attr_node = node.func
|
if self.unwind_target is None:
|
||||||
self.method_map[(attr_node.value.type, attr_node.attr)].append(insn)
|
insn = self.append(ir.Call(func, args))
|
||||||
|
else:
|
||||||
|
after_invoke = self.add_block()
|
||||||
|
insn = self.append(ir.Invoke(func, args, after_invoke, self.unwind_target))
|
||||||
|
self.current_block = after_invoke
|
||||||
|
|
||||||
|
method_key = None
|
||||||
|
if isinstance(node.func, asttyped.AttributeT):
|
||||||
|
attr_node = node.func
|
||||||
|
self.method_map[(attr_node.value.type, attr_node.attr)].append(insn)
|
||||||
|
|
||||||
if node.iodelay is not None and not iodelay.is_const(node.iodelay, 0):
|
if node.iodelay is not None and not iodelay.is_const(node.iodelay, 0):
|
||||||
after_delay = self.add_block()
|
after_delay = self.add_block()
|
||||||
self.append(ir.Delay(node.iodelay,
|
substs = {var_name: self.current_args[var_name]
|
||||||
{var_name: self.current_args[var_name]
|
for var_name in node.iodelay.free_vars()}
|
||||||
for var_name in node.iodelay.free_vars()},
|
self.append(ir.Delay(node.iodelay, substs, insn, after_delay))
|
||||||
after_delay))
|
|
||||||
self.current_block = after_delay
|
self.current_block = after_delay
|
||||||
|
|
||||||
return insn
|
return insn
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
the timestamp would always monotonically nondecrease.
|
the timestamp would always monotonically nondecrease.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .. import ir, iodelay
|
from .. import types, builtins, ir, iodelay
|
||||||
from ..analyses import domination
|
from ..analyses import domination
|
||||||
|
|
||||||
def delay_free_subgraph(root, limit):
|
def delay_free_subgraph(root, limit):
|
||||||
@ -81,20 +81,39 @@ class Interleaver:
|
|||||||
target_time_delta = new_target_time - target_time
|
target_time_delta = new_target_time - target_time
|
||||||
|
|
||||||
target_terminator = target_block.terminator()
|
target_terminator = target_block.terminator()
|
||||||
if isinstance(target_terminator, (ir.Delay, ir.Branch)):
|
if isinstance(target_terminator, ir.Parallel):
|
||||||
target_terminator.set_target(source_block)
|
|
||||||
elif isinstance(target_terminator, ir.Parallel):
|
|
||||||
target_terminator.replace_with(ir.Branch(source_block))
|
target_terminator.replace_with(ir.Branch(source_block))
|
||||||
else:
|
else:
|
||||||
assert False
|
assert isinstance(target_terminator, (ir.Delay, ir.Branch))
|
||||||
|
target_terminator.set_target(source_block)
|
||||||
|
|
||||||
source_terminator = source_block.terminator()
|
source_terminator = source_block.terminator()
|
||||||
|
|
||||||
|
if isinstance(source_terminator, ir.Delay):
|
||||||
|
old_decomp = source_terminator.decomposition()
|
||||||
|
else:
|
||||||
|
old_decomp = None
|
||||||
|
|
||||||
if target_time_delta > 0:
|
if target_time_delta > 0:
|
||||||
assert isinstance(source_terminator, ir.Delay)
|
assert isinstance(source_terminator, ir.Delay)
|
||||||
source_terminator.expr = iodelay.Const(target_time_delta)
|
|
||||||
|
if isinstance(old_decomp, ir.Builtin) and \
|
||||||
|
old_decomp.op in ("delay", "delay_mu"):
|
||||||
|
new_decomp_expr = ir.Constant(target_time_delta, builtins.TInt64())
|
||||||
|
new_decomp = ir.Builtin("delay_mu", [new_decomp_expr], builtins.TNone())
|
||||||
|
new_decomp.loc = old_decomp.loc
|
||||||
|
source_terminator.basic_block.insert(source_terminator, new_decomp)
|
||||||
|
else:
|
||||||
|
assert False # TODO
|
||||||
|
|
||||||
|
source_terminator.replace_with(ir.Delay(iodelay.Const(target_time_delta), {},
|
||||||
|
new_decomp, source_terminator.target()))
|
||||||
else:
|
else:
|
||||||
source_terminator.replace_with(ir.Branch(source_terminator.target()))
|
source_terminator.replace_with(ir.Branch(source_terminator.target()))
|
||||||
|
|
||||||
|
if old_decomp is not None:
|
||||||
|
old_decomp.erase()
|
||||||
|
|
||||||
target_block = source_block
|
target_block = source_block
|
||||||
target_time = new_target_time
|
target_time = new_target_time
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import os
|
|||||||
from pythonparser import ast, diagnostic
|
from pythonparser import ast, diagnostic
|
||||||
from llvmlite_artiq import ir as ll
|
from llvmlite_artiq import ir as ll
|
||||||
from ...language import core as language_core
|
from ...language import core as language_core
|
||||||
from .. import types, builtins, ir, iodelay
|
from .. import types, builtins, ir
|
||||||
|
|
||||||
|
|
||||||
llvoid = ll.VoidType()
|
llvoid = ll.VoidType()
|
||||||
@ -787,6 +787,12 @@ class LLVMIRGenerator:
|
|||||||
elif insn.op == "at_mu":
|
elif insn.op == "at_mu":
|
||||||
time, = insn.operands
|
time, = insn.operands
|
||||||
return self.llbuilder.store(self.map(time), self.llbuiltin("now"))
|
return self.llbuilder.store(self.map(time), self.llbuiltin("now"))
|
||||||
|
elif insn.op == "delay_mu":
|
||||||
|
interval, = insn.operands
|
||||||
|
llnowptr = self.llbuiltin("now")
|
||||||
|
llnow = self.llbuilder.load(llnowptr)
|
||||||
|
lladjusted = self.llbuilder.add(llnow, self.map(interval))
|
||||||
|
return self.llbuilder.store(lladjusted, llnowptr)
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
@ -1062,6 +1068,8 @@ class LLVMIRGenerator:
|
|||||||
def process_Branch(self, insn):
|
def process_Branch(self, insn):
|
||||||
return self.llbuilder.branch(self.map(insn.target()))
|
return self.llbuilder.branch(self.map(insn.target()))
|
||||||
|
|
||||||
|
process_Delay = process_Branch
|
||||||
|
|
||||||
def process_BranchIf(self, insn):
|
def process_BranchIf(self, insn):
|
||||||
return self.llbuilder.cbranch(self.map(insn.condition()),
|
return self.llbuilder.cbranch(self.map(insn.condition()),
|
||||||
self.map(insn.if_true()), self.map(insn.if_false()))
|
self.map(insn.if_true()), self.map(insn.if_false()))
|
||||||
@ -1141,17 +1149,3 @@ class LLVMIRGenerator:
|
|||||||
self.llbuilder.branch(self.map(insn.cleanup()))
|
self.llbuilder.branch(self.map(insn.cleanup()))
|
||||||
|
|
||||||
return llexn
|
return llexn
|
||||||
|
|
||||||
def process_Delay(self, insn):
|
|
||||||
def map_delay(expr):
|
|
||||||
if isinstance(expr, iodelay.Const):
|
|
||||||
return ll.Constant(lli64, int(expr.value))
|
|
||||||
else:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
llnowptr = self.llbuiltin("now")
|
|
||||||
llnow = self.llbuilder.load(llnowptr)
|
|
||||||
lladjusted = self.llbuilder.add(llnow, map_delay(insn.expr))
|
|
||||||
self.llbuilder.store(lladjusted, llnowptr)
|
|
||||||
|
|
||||||
return self.llbuilder.branch(self.map(insn.target()))
|
|
||||||
|
@ -18,8 +18,8 @@ def g():
|
|||||||
print("E", now_mu())
|
print("E", now_mu())
|
||||||
|
|
||||||
# CHECK-L: A 0
|
# CHECK-L: A 0
|
||||||
|
# CHECK-L: C 0
|
||||||
# CHECK-L: B 2
|
# CHECK-L: B 2
|
||||||
# CHECK-L: C 2
|
|
||||||
# CHECK-L: D 2
|
# CHECK-L: D 2
|
||||||
# CHECK-L: E 4
|
# CHECK-L: E 4
|
||||||
g()
|
g()
|
||||||
|
@ -17,9 +17,9 @@ def g():
|
|||||||
#
|
#
|
||||||
print("E", now_mu())
|
print("E", now_mu())
|
||||||
|
|
||||||
|
# CHECK-L: A 0
|
||||||
# CHECK-L: C 0
|
# CHECK-L: C 0
|
||||||
# CHECK-L: A 2
|
# CHECK-L: D 2
|
||||||
# CHECK-L: B 3
|
# CHECK-L: B 3
|
||||||
# CHECK-L: D 3
|
|
||||||
# CHECK-L: E 4
|
# CHECK-L: E 4
|
||||||
g()
|
g()
|
||||||
|
Loading…
Reference in New Issue
Block a user