From 0041a7dacbbebd0248e5348bebebe4e03cd73979 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Wed, 18 Oct 2023 17:55:26 +0200 Subject: [PATCH 1/5] Port of RAII's `writable`/`addressable`/`c_lval` revamp --- py/dml/codegen.py | 4 +-- py/dml/ctree.py | 75 +++++++++++++++++++++++++++-------------------- py/dml/expr.py | 42 +++++++++++++++++++++----- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/py/dml/codegen.py b/py/dml/codegen.py index 644e9d064..39469eeb2 100644 --- a/py/dml/codegen.py +++ b/py/dml/codegen.py @@ -1148,7 +1148,7 @@ def expr_unop(tree, location, scope): elif op == 'post--': return mkPostDec(tree.site, rh) elif op == 'sizeof': if (compat.dml12_misc not in dml.globals.enabled_compat - and not isinstance(rh, ctree.LValue)): + and not rh.addressable): raise ERVAL(rh.site, 'sizeof') return codegen_sizeof(tree.site, rh) elif op == 'defined': return mkBoolConstant(tree.site, True) @@ -1538,7 +1538,7 @@ def eval_type(asttype, site, location, scope, extern=False, typename=None, etype = expr.node_type else: raise expr.exc() - elif (not isinstance(expr, ctree.LValue) + elif (not expr.addressable and compat.dml12_misc not in dml.globals.enabled_compat): raise ERVAL(expr.site, 'typeof') else: diff --git a/py/dml/ctree.py b/py/dml/ctree.py index 24541c295..681938363 100644 --- a/py/dml/ctree.py +++ b/py/dml/ctree.py @@ -1094,21 +1094,12 @@ def truncate_int_bits(value, signed, bits=64): return value & mask class LValue(Expression): - "Somewhere to read or write data" + """An expression whose C representation is always an LValue, whose address + is always safe to take, in the sense that the duration that address + remains valid is intuitively predictable by the user""" writable = True - - def write(self, source): - rt = realtype(self.ctype()) - if isinstance(rt, TEndianInt): - return (f'{rt.dmllib_fun("copy")}(&{self.read()},' - + f' {source.read()})') - return '%s = %s' % (self.read(), source.read()) - - @property - def is_stack_allocated(self): - '''Returns true only if it's known that writing to the lvalue will - write to stack-allocated data''' - return False + addressable = True + c_lval = True class IfExpr(Expression): priority = 30 @@ -2524,13 +2515,13 @@ def make_simple(cls, site, rh): TPtr(TVoid())], TVoid()))) if (compat.dml12_misc not in dml.globals.enabled_compat - and not isinstance(rh, LValue)): + and not rh.addressable): raise ERVAL(rh.site, '&') return AddressOf(site, rh) @property def is_pointer_to_stack_allocation(self): - return isinstance(self.rh, LValue) and self.rh.is_stack_allocated + return self.rh.is_stack_allocated def mkAddressOf(site, rh): if dml.globals.compat_dml12_int(site): @@ -2568,7 +2559,8 @@ def is_stack_allocated(self): @property def is_pointer_to_stack_allocation(self): - return isinstance(self.type, TArray) and self.is_stack_allocated + return (isinstance(safe_realtype_shallow(self.type), TArray) + and self.is_stack_allocated) mkDereference = Dereference.make @@ -2690,7 +2682,7 @@ def mkUnaryPlus(site, rh): rh, _ = promote_integer(rh, rhtype) else: raise ICE(site, "Unexpected arith argument to unary +") - if isinstance(rh, LValue): + if rh.addressable or rh.writable: # +x is a rvalue rh = mkRValue(rh) return rh @@ -2716,7 +2708,7 @@ def make_simple(cls, site, rh): rhtype = safe_realtype(rh.ctype()) if not isinstance(rhtype, (IntegerType, TPtr)): raise EINCTYPE(site, cls.op) - if not isinstance(rh, LValue): + if not rh.addressable: if isinstance(rh, BitSlice): hint = 'try %s= 1' % (cls.base_op[0],) else: @@ -4293,11 +4285,13 @@ def read(self): mkStaticVariable = StaticVariable -class StructMember(LValue): +class StructMember(Expression): priority = 160 explicit_type = True @auto_init def __init__(self, site, expr, sub, type, op): + # Write of StructMembers rely on them being C lvalues + assert not expr.writable or expr.c_lval assert_type(site, expr, Expression) assert_type(site, sub, str) @@ -4314,11 +4308,12 @@ def read(self): @property def is_stack_allocated(self): - return isinstance(self.expr, LValue) and self.expr.is_stack_allocated + return self.expr.is_stack_allocated @property def is_pointer_to_stack_allocation(self): - return isinstance(self.type, TArray) and self.is_stack_allocated + return (isinstance(safe_realtype_shallow(self.type), TArray) + and self.is_stack_allocated) def mkSubRef(site, expr, sub, op): if isinstance(expr, NodeRef): @@ -4425,18 +4420,28 @@ def is_stack_allocated(self): @property def is_pointer_to_stack_allocation(self): - return isinstance(self.type, TArray) and self.is_stack_allocated + return (isinstance(safe_realtype_shallow(self.type), TArray) + and self.is_stack_allocated) -class VectorRef(LValue): +class VectorRef(Expression): slots = ('type',) @auto_init def __init__(self, site, expr, idx): + assert not expr.writable or expr.c_lval self.type = realtype(self.expr.ctype()).base def read(self): return 'VGET(%s, %s)' % (self.expr.read(), self.idx.read()) - def write(self, source): - return "VSET(%s, %s, %s)" % (self.expr.read(), self.idx.read(), - source.read()) + # No need for write, VGET results in an lvalue + + @property + def writable(self): + return self.expr.writable + @property + def addressable(self): + return self.expr.addressable + @property + def c_lval(self): + return self.expr.c_lval def mkIndex(site, expr, idx): if isinstance(idx, NonValue): @@ -4510,7 +4515,7 @@ def read(self): @property def is_pointer_to_stack_allocation(self): - return (isinstance(self.type, TPtr) + return (isinstance(safe_realtype_shallow(self.type), TPtr) and self.expr.is_pointer_to_stack_allocation) def mkCast(site, expr, new_type): @@ -4653,7 +4658,6 @@ def mkCast(site, expr, new_type): class RValue(Expression): '''Wraps an lvalue to prohibit write. Useful when a composite expression is reduced down to a single variable.''' - writable = False @auto_init def __init__(self, site, expr): pass def __str__(self): @@ -4662,10 +4666,19 @@ def ctype(self): return self.expr.ctype() def read(self): return self.expr.read() - def discard(self): pass + def discard(self): + return self.expr.discard() + # Since addressable and readable are False this may only ever be leveraged + # by DMLC for optimization purposes + @property + def c_lval(self): + return self.expr.c_lval + @property + def is_pointer_to_stack_allocation(self): + return self.expr.is_pointer_to_stack_allocation def mkRValue(expr): - if isinstance(expr, LValue) or expr.writable: + if expr.addressable or expr.writable: return RValue(expr.site, expr) return expr diff --git a/py/dml/expr.py b/py/dml/expr.py index ebb0c5217..81cdb661e 100644 --- a/py/dml/expr.py +++ b/py/dml/expr.py @@ -109,11 +109,19 @@ class Expression(Code): # bitslicing. explicit_type = False - # Can the expression be assigned to? - # If writable is True, there is a method write() which returns a C - # expression to make the assignment. + # Can the expression be safely assigned to in DML? + # This implies write() can be safely used. writable = False + # Can the address of the expression be taken safely in DML? + # This implies c_lval, and typically implies writable. + addressable = False + + # Is the C representation of the expression an lvalue? + # If True, then the default implementation of write() must not be + # overridden. + c_lval = False + def __init__(self, site): assert not site or isinstance(site, Site) self.site = site @@ -139,10 +147,16 @@ def apply(self, inits, location, scope): 'Apply this expression as a function' return mkApplyInits(self.site, self, inits, location, scope) + @property + def is_stack_allocated(self): + '''Returns true only if it's known that the storage for the value that + this expression evaluates to is temporary to a method scope''' + return False + @property def is_pointer_to_stack_allocation(self): '''Returns True only if it's known that the expression is a pointer - to stack-allocated data''' + to storage that is temporary to a method scope''' return False def incref(self): @@ -156,6 +170,17 @@ def copy(self, site): return type(self)( site, *(getattr(self, name) for name in self.init_args[2:])) + # Return a (principally) void-typed C expression that write a source to the + # storage this expression represents + # This should only be called if either writable or c_lval is True + def write(self, source): + assert self.c_lval + rt = realtype(self.ctype()) + if isinstance(rt, TEndianInt): + return (f'{rt.dmllib_fun("copy")}(&{self.read()},' + + f' {source.read()})') + return '%s = %s' % (self.read(), source.read()) + class NonValue(Expression): '''An expression that is not really a value, but which may validly appear as a subexpression of certain expressions. @@ -202,11 +227,14 @@ def __str__(self): return self.str or self.cexpr def read(self): return self.cexpr - def write(self, source): - assert self.writable - return "%s = %s" % (self.cexpr, source.read()) @property def writable(self): + return self.c_lval + @property + def addressable(self): + return self.c_lval + @property + def c_lval(self): return self.type is not None mkLit = Lit From 74f9e3e2880d0d69bf347955caec13becbcb7001 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Tue, 31 Oct 2023 12:36:44 +0100 Subject: [PATCH 2/5] Port of RAII's `Expression.write`/`Initializer.assign_to` revamp --- py/dml/c_backend.py | 48 +++++++++-------- py/dml/codegen.py | 108 +++++++++++++++++++++---------------- py/dml/ctree.py | 128 ++++++++++++++++++++++++++++---------------- py/dml/expr.py | 10 ++-- py/dml/io_memory.py | 9 ++-- py/dml/serialize.py | 56 ++++++++++--------- py/dml/types.py | 15 ++++++ 7 files changed, 224 insertions(+), 150 deletions(-) diff --git a/py/dml/c_backend.py b/py/dml/c_backend.py index 2cb4384e4..2638afc6f 100644 --- a/py/dml/c_backend.py +++ b/py/dml/c_backend.py @@ -1837,7 +1837,7 @@ def generate_init_data_objs(device): start_function_definition( 'void _init_data_objs(%s *_dev)' % (crep.structtype(device),)) out('{\n', postindent = 1) - with crep.DeviceInstanceContext(): + with crep.DeviceInstanceContext(), allow_linemarks(): for node in device.initdata: # Usually, the initializer is constant, but we permit that it # depends on index. When the initializer is constant, we use a loop @@ -1859,25 +1859,26 @@ def generate_init_data_objs(device): # mainly meant to capture EIDXVAR; for other errors, the error will # normally re-appear when evaluating per instance except DMLError: - with allow_linemarks(): - for indices in node.all_indices(): - index_exprs = tuple(mkIntegerLiteral(node.site, i) - for i in indices) - nref = mkNodeRef(node.site, node, index_exprs) - try: - init = eval_initializer( - node.site, node._type, node.astinit, - Location(node.parent, index_exprs), - global_scope, True) - except DMLError as e: - report(e) - else: - markers = ([('store_writes_const_field', 'FALSE')] - if deep_const(node._type) else []) - coverity_markers(markers, init.site) - init.assign_to(nref, node._type) + for indices in node.all_indices(): + index_exprs = tuple(mkIntegerLiteral(node.site, i) + for i in indices) + nref = mkNodeRef(node.site, node, index_exprs) + try: + init = eval_initializer( + node.site, node._type, node.astinit, + Location(node.parent, index_exprs), + global_scope, True) + except DMLError as e: + report(e) + else: + markers = ([('store_writes_const_field', 'FALSE')] + if deep_const(node._type) else []) + coverity_markers(markers, init.site) + out(init.assign_to(nref.read(), node._type) + ';\n') else: index_exprs = () + if node.dimensions: + reset_line_directive() for (i, sz) in enumerate(node.dimsizes): var = 'i%d' % (i,) out(('for (int %s = 0; %s < %s; ++%s) {\n' @@ -1885,11 +1886,12 @@ def generate_init_data_objs(device): postindent=1) index_exprs += (mkLit(node.site, var, TInt(64, True)),) nref = mkNodeRef(node.site, node, index_exprs) - with allow_linemarks(): - markers = ([('store_writes_const_field', 'FALSE')] - if deep_const(node._type) else []) - coverity_markers(markers, init.site) - init.assign_to(nref, node._type) + markers = ([('store_writes_const_field', 'FALSE')] + if deep_const(node._type) else []) + coverity_markers(markers, init.site) + out(init.assign_to(nref.read(), node._type) + ';\n') + if node.dimensions: + reset_line_directive() for _ in range(node.dimensions): out('}\n', postindent=-1) out('}\n\n', preindent = -1) diff --git a/py/dml/codegen.py b/py/dml/codegen.py index 39469eeb2..051cf7593 100644 --- a/py/dml/codegen.py +++ b/py/dml/codegen.py @@ -704,9 +704,9 @@ def error_out_at_index(_i, exc, msg): site, val_expr, targets, error_out_at_index, f'deserialization of arguments to {self.method.name}') if self.args_type: - ctree.mkAssignStatement(site, out_expr, - ctree.ExpressionInitializer( - tmp_out_ref)).toc() + ctree.AssignStatement(site, out_expr, + ctree.ExpressionInitializer( + tmp_out_ref)).toc() @property def args_type(self): @@ -822,8 +822,8 @@ def error_out_at_index(_i, exc, msg): 'deserialization of arguments to a send_now') - ctree.mkAssignStatement(site, out_expr, - ctree.ExpressionInitializer(tmp_out_ref)).toc() + ctree.AssignStatement(site, out_expr, + ctree.ExpressionInitializer(tmp_out_ref)).toc() @property def args_type(self): @@ -2131,8 +2131,8 @@ def make_static_var(site, location, static_sym_type, name, init=None, with init_code: if deep_const(static_sym_type): coverity_marker('store_writes_const_field', 'FALSE') - init.assign_to(mkStaticVariable(site, static_sym), - static_sym_type) + out(init.assign_to(mkStaticVariable(site, static_sym).read(), + static_sym_type) + ';\n') c_init = init_code.buf else: c_init = None @@ -2354,7 +2354,6 @@ def stmt_assign(stmt, location, scope): else: method_tgts = tgts - # TODO support multiple assign sources. It should be generalized. method_invocation = try_codegen_invocation(site, src_asts, method_tgts, location, scope) if method_invocation: @@ -2370,19 +2369,26 @@ def stmt_assign(stmt, location, scope): + f'initializer: Expected {src_asts}, got 1')) return [] - stmts = [] lscope = Symtab(scope) + init_typ = tgts[-1].ctype() init = eval_initializer( - site, tgts[-1].ctype(), src_asts[0], location, scope, False) - - for (i, tgt) in enumerate(reversed(tgts[1:])): - name = 'tmp%d' % (i,) - sym = lscope.add_variable( - name, type=tgt.ctype(), site=tgt.site, init=init, stmt=True) - init = ExpressionInitializer(mkLocalVariable(tgt.site, sym)) - stmts.extend([sym_declaration(sym), - mkAssignStatement(tgt.site, tgt, init)]) - return stmts + [mkAssignStatement(tgts[0].site, tgts[0], init)] + tgts[-1].site, init_typ, src_asts[0], location, scope, False) + + if len(tgts) == 1: + return [mkAssignStatement(tgts[0].site, tgts[0], init)] + + sym = lscope.add_variable( + 'tmp', type=init_typ, site=init.site, init=init, + stmt=True) + init_expr = mkLocalVariable(init.site, sym) + stmts = [sym_declaration(sym)] + for tgt in reversed(tgts[1:]): + stmts.append(mkCopyData(tgt.site, init_expr, tgt)) + init_expr = (tgt if isinstance(tgt, NonValue) + else source_for_assignment(tgt.site, tgt.ctype(), + init_expr)) + stmts.append(mkCopyData(tgts[0].site, init_expr, tgts[0])) + return [mkCompound(site, stmts)] else: # Guaranteed by grammar assert tgt_ast.kind == 'assign_target_tuple' and len(tgts) > 1 @@ -2409,43 +2415,51 @@ def stmt_assign(stmt, location, scope): stmt=True) syms.append(sym) - stmts.extend(map(sym_declaration, syms)) + stmts.extend(sym_declaration(sym) for sym in syms) stmts.extend( - mkAssignStatement( - tgt.site, tgt, ExpressionInitializer(mkLocalVariable(tgt.site, - sym))) + AssignStatement( + tgt.site, tgt, + ExpressionInitializer(mkLocalVariable(tgt.site, sym))) for (tgt, sym) in zip(tgts, syms)) - return stmts + return [mkCompound(site, stmts)] @statement_dispatcher def stmt_assignop(stmt, location, scope): - (kind, site, tgt_ast, op, src_ast) = stmt + (_, site, tgt_ast, op, src_ast) = stmt tgt = codegen_expression(tgt_ast, location, scope) - if deep_const(tgt.ctype()): + if isinstance(tgt, ctree.InlinedParam): + raise EASSINL(tgt.site, tgt.name) + if not tgt.writable: + raise EASSIGN(site, tgt) + + ttype = tgt.ctype() + if deep_const(ttype): raise ECONST(tgt.site) - if isinstance(tgt, ctree.BitSlice): - # destructive hack - return stmt_assign( - ast.assign(site, ast.assign_target_chain(site, [tgt_ast]), - [ast.initializer_scalar( - site, - ast.binop(site, tgt_ast, op[:-1], src_ast))]), - location, scope) + src = codegen_expression(src_ast, location, scope) - ttype = tgt.ctype() - lscope = Symtab(scope) - sym = lscope.add_variable( - 'tmp', type = TPtr(ttype), site = tgt.site, - init = ExpressionInitializer(mkAddressOf(tgt.site, tgt)), stmt=True) - # Side-Effect Free representation of the tgt lvalue - tgt_sef = mkDereference(site, mkLocalVariable(tgt.site, sym)) - return [ - sym_declaration(sym), mkExpressionStatement( - site, - mkAssignOp(site, tgt_sef, arith_binops[op[:-1]]( - site, tgt_sef, src)))] + if tgt.addressable: + lscope = Symtab(scope) + tmp_tgt_sym = lscope.add_variable( + '_tmp_tgt', type = TPtr(ttype), site = tgt.site, + init = ExpressionInitializer(mkAddressOf(tgt.site, tgt)), + stmt=True) + # Side-Effect Free representation of the tgt lvalue + tgt = mkDereference(site, mkLocalVariable(tgt.site, tmp_tgt_sym)) + else: + # TODO Not ideal. This path is needed to deal with writable + # expressions that do not correspond to C lvalues; such as bit slices. + # The incurred repeated evaluation is painful. + tmp_tgt_sym = None + + assign_src = source_for_assignment(site, ttype, + arith_binops[op[:-1]](site, tgt, src)) + + return [mkCompound(site, + ([sym_declaration(tmp_tgt_sym)] if tmp_tgt_sym else []) + + [mkExpressionStatement(site, + ctree.AssignOp(site, tgt, assign_src))])] @statement_dispatcher def stmt_expression(stmt, location, scope): [expr] = stmt.args @@ -3885,7 +3899,7 @@ def prelude(): param = mkDereference(site, mkLit(site, name, TPtr(typ))) fnscope.add(ExpressionSymbol(name, param, site)) - code.append(mkAssignStatement(site, param, init)) + code.append(AssignStatement(site, param, init)) else: code = [] diff --git a/py/dml/ctree.py b/py/dml/ctree.py index 681938363..8a0ecdc57 100644 --- a/py/dml/ctree.py +++ b/py/dml/ctree.py @@ -66,7 +66,7 @@ 'mkVectorForeach', 'mkBreak', 'mkContinue', - 'mkAssignStatement', + 'mkAssignStatement', 'AssignStatement', 'mkCopyData', 'mkIfExpr', 'IfExpr', #'BinOp', @@ -599,8 +599,11 @@ def mkExpressionStatement(site, expr): def toc_constsafe_pointer_assignment(site, source, target, typ): target_val = mkDereference(site, Cast(site, mkLit(site, target, TPtr(void)), TPtr(typ))) - mkAssignStatement(site, target_val, - ExpressionInitializer(mkLit(site, source, typ))).toc() + + init = ExpressionInitializer( + source_for_assignment(site, typ, mkLit(site, source, typ))) + + return AssignStatement(site, target_val, init).toc() class After(Statement): @auto_init @@ -1020,22 +1023,32 @@ class AssignStatement(Statement): @auto_init def __init__(self, site, target, initializer): assert isinstance(initializer, Initializer) + def toc_stmt(self): self.linemark() - out('{\n', postindent=1) - self.toc_inline() - self.linemark() - out('}\n', preindent=-1) - def toc_inline(self): - self.linemark() - self.initializer.assign_to(self.target, self.target.ctype()) + out(self.target.write(self.initializer) + ';\n') + +def mkAssignStatement(site, target, init): + if isinstance(target, InlinedParam): + raise EASSINL(target.site, target.name) + if not target.writable: + raise EASSIGN(site, target) + + target_type = target.ctype() + + if deep_const(target_type): + raise ECONST(site) + + if isinstance(init, ExpressionInitializer): + init = ExpressionInitializer( + source_for_assignment(site, target_type, init.expr)) + + return AssignStatement(site, target, init) -mkAssignStatement = AssignStatement def mkCopyData(site, source, target): "Convert a copy statement to intermediate representation" - assignexpr = mkAssignOp(site, target, source) - return mkExpressionStatement(site, assignexpr) + return mkAssignStatement(site, target, ExpressionInitializer(source)) # # Expressions @@ -2436,7 +2449,7 @@ def __str__(self): return "%s = %s" % (self.lh, self.rh) def discard(self): - return self.lh.write(self.rh) + return self.lh.write(ExpressionInitializer(self.rh)) def read(self): return '((%s), (%s))' % (self.discard(), self.lh.read()) @@ -2914,7 +2927,8 @@ def writable(self): return self.expr.writable def write(self, source): - source_expr = source + assert isinstance(source, ExpressionInitializer) + source_expr = source.expr # if not self.size.constant or source.ctype() > self.type: # source = mkBitAnd(source, self.mask) @@ -2936,7 +2950,7 @@ def write(self, source): target_type = realtype(self.expr.ctype()) if target_type.is_int and target_type.is_endian: expr = mkCast(self.site, expr, target_type) - return self.expr.write(expr) + return self.expr.write(ExpressionInitializer(expr)) def mkBitSlice(site, expr, msb, lsb, bitorder): # lsb == None means that only one bit number was given (expr[i] @@ -4295,6 +4309,18 @@ def __init__(self, site, expr, sub, type, op): assert_type(site, expr, Expression) assert_type(site, sub, str) + @property + def writable(self): + return self.expr.writable + + @property + def addressable(self): + return self.expr.addressable + + @property + def c_lval(self): + return self.expr.c_lval + def __str__(self): s = str(self.expr) if self.expr.priority < self.priority: @@ -4860,15 +4886,35 @@ def assign_to(self, dest, typ): # be UB as long as the session variable hasn't been initialized # previously. site = self.expr.site - if deep_const(typ): - out('memcpy((void *)&%s, (%s){%s}, sizeof %s);\n' - % (dest.read(), - TArray(typ, mkIntegerLiteral(site, 1)).declaration(''), - mkCast(site, self.expr, typ).read(), - dest.read())) + rt = safe_realtype_shallow(typ) + # There is a reasonable implementation for this case (memcpy), but it + # never occurs today + assert not isinstance(typ, TArray) + if isinstance(rt, TEndianInt): + return (f'{rt.dmllib_fun("copy")}((void *)&{dest},' + + f' {self.expr.read()})') + elif deep_const(typ): + shallow_deconst_typ = safe_realtype_unconst(typ) + # a const-qualified ExternStruct can be leveraged by the user as a + # sign that there is some const-qualified member unknown to DMLC + if (isinstance(typ, TExternStruct) + or deep_const(shallow_deconst_typ)): + # Expression statement to delimit lifetime of compound literal + # TODO it's possible to improve the efficiency of this by not + # using a compound literal if self.expr is c_lval. However, + # this requires require strict cmp to ensure safety, and it's + # unclear if that path could ever be taken. + return ('({ memcpy((void *)&%s, (%s){%s}, sizeof(%s)); })' + % (dest, + TArray(typ, + mkIntegerLiteral(site, 1)).declaration(''), + mkCast(site, self.expr, typ).read(), + dest)) + else: + return (f'*({TPtr(shallow_deconst_typ).declaration("")})' + + f'&{dest} = {self.expr.read()}') else: - with disallow_linemarks(): - mkCopyData(site, self.expr, dest).toc() + return f'{dest} = {self.expr.read()}' class CompoundInitializer(Initializer): '''Initializer for a variable of struct or array type, using the @@ -4896,21 +4942,12 @@ def assign_to(self, dest, typ): '''output C statements to assign an lvalue''' # (void *) cast to avoid GCC erroring if the target type is (partially) # const-qualified. See ExpressionInitializer.assign_to - if isinstance(typ, TNamed): - out('memcpy((void *)&%s, &(%s)%s, sizeof %s);\n' % - (dest.read(), typ.declaration(''), self.read(), - dest.read())) - elif isinstance(typ, TArray): - out('memcpy((void *)%s, (%s)%s, sizeof %s);\n' - % (dest.read(), typ.declaration(''), - self.read(), dest.read())) - elif isinstance(typ, TStruct): - out('memcpy((void *)&%s, (%s){%s}, sizeof %s);\n' % ( - dest.read(), - TArray(typ, mkIntegerLiteral(self.site, 1)).declaration(''), - self.read(), dest.read())) + if isinstance(typ, (TNamed, TArray, TStruct)): + # Expression statement to delimit lifetime of compound literal + return ('({ memcpy((void *)&%s, &(%s)%s, sizeof(%s)); })' + % (dest, typ.declaration(''), self.read(), dest)) else: - raise ICE(self.site, 'strange type %s' % typ) + raise ICE(self.site, f'unexpected type for initializer: {typ}') class DesignatedStructInitializer(Initializer): '''Initializer for a variable of an extern-declared struct type, using @@ -4950,10 +4987,11 @@ def assign_to(self, dest, typ): if isinstance(typ, StructType): # (void *) cast to avoid GCC erroring if the target type is # (partially) const-qualified. See ExpressionInitializer.assign_to - out('memcpy((void *)&%s, (%s){%s}, sizeof %s);\n' % ( - dest.read(), - TArray(typ, mkIntegerLiteral(self.site, 1)).declaration(''), - self.read(), dest.read())) + return ('({ memcpy((void *)&%s, (%s){%s}, sizeof(%s)); })' + % (dest, + TArray(typ, + mkIntegerLiteral(self.site, 1)).declaration(''), + self.read(), dest)) else: raise ICE(self.site, f'unexpected type for initializer: {typ}') @@ -4992,8 +5030,7 @@ def assign_to(self, dest, typ): THook)) # (void *) cast to avoid GCC erroring if the target type is # (partially) const-qualified. See ExpressionInitializer.assign_to - out('memset((void *)&%s, 0, sizeof(%s));\n' - % (dest.read(), typ.declaration(''))) + return f'memset((void *)&{dest}, 0, sizeof({typ.declaration("")}))' class CompoundLiteral(Expression): @auto_init @@ -5052,8 +5089,7 @@ def toc(self): # zero-initialize VLAs self.type.print_declaration(self.name, unused = self.unused) site_linemark(self.init.site) - self.init.assign_to(mkLit(self.site, self.name, self.type), - self.type) + out(self.init.assign_to(self.name, self.type) + ';\n') else: self.type.print_declaration( self.name, init=self.init.read() if self.init else None, diff --git a/py/dml/expr.py b/py/dml/expr.py index 81cdb661e..81118c2ea 100644 --- a/py/dml/expr.py +++ b/py/dml/expr.py @@ -174,12 +174,10 @@ def copy(self, site): # storage this expression represents # This should only be called if either writable or c_lval is True def write(self, source): - assert self.c_lval - rt = realtype(self.ctype()) - if isinstance(rt, TEndianInt): - return (f'{rt.dmllib_fun("copy")}(&{self.read()},' - + f' {source.read()})') - return '%s = %s' % (self.read(), source.read()) + assert self.c_lval, repr(self) + # Wrap .read() in parantheses if its priority is less than that of & + dest = self.read() if self.priority >= 150 else f'({self.read()})' + return source.assign_to(dest, self.ctype()) class NonValue(Expression): '''An expression that is not really a value, but which may validly diff --git a/py/dml/io_memory.py b/py/dml/io_memory.py index 391c75c6e..81d682d91 100644 --- a/py/dml/io_memory.py +++ b/py/dml/io_memory.py @@ -221,7 +221,8 @@ def dim_sort_key(data): regvar, size.read())]) lines.append( ' %s;' % ( - size2.write(mkLit(site, 'bytes', TInt(64, False))))) + size2.write(ExpressionInitializer(mkLit(site, 'bytes', + TInt(64, False)))))) if partial: if bigendian: lines.extend([ @@ -246,7 +247,8 @@ def dim_sort_key(data): regvar, indices, memop.read(), bytepos_args), ' if (ret) return true;', ' %s;' % ( - value2.write(mkLit(site, 'val', TInt(64, False)))), + value2.write(ExpressionInitializer( + mkLit(site, 'val', TInt(64, False))))), ' return false;']) else: # Shifting/masking can normally be skipped in banks with @@ -272,7 +274,8 @@ def dim_sort_key(data): ' if (offset >= %s[last].offset' % (regvar,) + ' && offset < %s[last].offset + %s[last].size) {' % (regvar, regvar), - ' %s;' % (size2.write(mkIntegerLiteral(site, 0)),), + ' %s;' % (size2.write(ExpressionInitializer( + mkIntegerLiteral(site, 0))),), ' return false;', ' }']) lines.extend([ diff --git a/py/dml/serialize.py b/py/dml/serialize.py index 17de64f02..6a29ff0fc 100644 --- a/py/dml/serialize.py +++ b/py/dml/serialize.py @@ -116,8 +116,8 @@ def serialize(real_type, current_expr, target_expr): def construct_assign_apply(funname, intype): apply_expr = apply_c_fun(current_site, funname, [current_expr], attr_value_t) - return ctree.mkAssignStatement(current_site, target_expr, - ctree.ExpressionInitializer(apply_expr)) + return ctree.AssignStatement(current_site, target_expr, + ctree.ExpressionInitializer(apply_expr)) if real_type.is_int: if real_type.signed: funname = "SIM_make_attr_int64" @@ -133,7 +133,7 @@ def construct_assign_apply(funname, intype): [converted_arg], function_type) return ctree.mkCompound(current_site, - [ctree.mkAssignStatement( + [ctree.AssignStatement( current_site, target_expr, ctree.ExpressionInitializer( apply_expr))]) @@ -164,15 +164,15 @@ def construct_assign_apply(funname, intype): len(dimsizes)), elem_serializer], attr_value_t) - return ctree.mkAssignStatement(current_site, target_expr, - ctree.ExpressionInitializer(apply_expr)) + return ctree.AssignStatement(current_site, target_expr, + ctree.ExpressionInitializer(apply_expr)) elif isinstance(real_type, (TStruct, TVector)): apply_expr = apply_c_fun( current_site, lookup_serialize(real_type), [ctree.mkAddressOf(current_site, current_expr)], attr_value_t) - return ctree.mkAssignStatement(current_site, target_expr, - ctree.ExpressionInitializer(apply_expr)) + return ctree.AssignStatement(current_site, target_expr, + ctree.ExpressionInitializer(apply_expr)) elif isinstance(real_type, TTrait): id_infos = expr.mkLit(current_site, '_id_infos', TPtr(TNamed('_id_info_t', const = True))) @@ -180,8 +180,8 @@ def construct_assign_apply(funname, intype): TNamed("_identity_t"), ".") apply_expr = apply_c_fun(current_site, "_serialize_identity", [id_infos, identity_expr], attr_value_t) - return ctree.mkAssignStatement(current_site, target_expr, - ctree.ExpressionInitializer(apply_expr)) + return ctree.AssignStatement(current_site, target_expr, + ctree.ExpressionInitializer(apply_expr)) elif isinstance(real_type, THook): id_infos = expr.mkLit(current_site, '_hook_id_infos' if dml.globals.hooks @@ -189,8 +189,8 @@ def construct_assign_apply(funname, intype): TPtr(TNamed('_id_info_t', const = True))) apply_expr = apply_c_fun(current_site, "_serialize_identity", [id_infos, current_expr], attr_value_t) - return ctree.mkAssignStatement(current_site, target_expr, - ctree.ExpressionInitializer(apply_expr)) + return ctree.AssignStatement(current_site, target_expr, + ctree.ExpressionInitializer(apply_expr)) else: # Callers are responsible for checking that the type is serializeable, # which should be done with the mark_for_serialization function @@ -202,11 +202,12 @@ def construct_assign_apply(funname, intype): # with a given set_error_t and message. def deserialize(real_type, current_expr, target_expr, error_out): current_site = current_expr.site - def construct_assign_apply(attr_typ, intype): + def construct_assign_apply(attr_typ, intype, mod_apply_expr=lambda x: x): check_expr = apply_c_fun(current_site, 'SIM_attr_is_' + attr_typ, [current_expr], TBool()) - apply_expr = apply_c_fun(current_site, 'SIM_attr_' + attr_typ, - [current_expr], intype) + apply_expr = mod_apply_expr(apply_c_fun(current_site, + 'SIM_attr_' + attr_typ, + [current_expr], intype)) error_stmts = error_out('Sim_Set_Illegal_Type', 'expected ' + attr_typ) target = target_expr @@ -223,7 +224,7 @@ def construct_assign_apply(attr_typ, intype): return ctree.mkIf(current_site, check_expr, - ctree.mkAssignStatement( + ctree.AssignStatement( current_site, target, ctree.ExpressionInitializer(apply_expr)), ctree.mkCompound(current_site, error_stmts)) @@ -237,7 +238,7 @@ def addressof_target_unconst(): def construct_subcall(apply_expr): (sub_success_decl, sub_success_arg) = \ declare_variable(current_site, "_sub_success", set_error_t) - assign_stmt = ctree.mkAssignStatement( + assign_stmt = ctree.AssignStatement( current_site, sub_success_arg, ctree.ExpressionInitializer(apply_expr)) check_expr = ctree.mkLit(current_site, @@ -253,8 +254,13 @@ def construct_subcall(apply_expr): if real_type.is_int: if real_type.is_endian: - real_type = TInt(real_type.bits, real_type.signed) - return construct_assign_apply("integer", real_type) + def mod_apply_expr(expr): + return ctree.source_for_assignment(expr.site, real_type, expr) + else: + def mod_apply_expr(expr): + return expr + return construct_assign_apply("integer", TInt(64, True), + mod_apply_expr) elif isinstance(real_type, TBool): return construct_assign_apply("boolean", real_type) elif isinstance(real_type, TFloat): @@ -442,7 +448,7 @@ def serialize_sources_to_list(site, sources, out_attr): site, "SIM_alloc_attr_list", [ctree.mkIntegerConstant(site, size, False)], attr_value_t) - attr_assign_statement = ctree.mkAssignStatement( + attr_assign_statement = ctree.AssignStatement( site, out_attr, ctree.ExpressionInitializer(attr_alloc_expr)) imm_attr_decl, imm_attr_ref = declare_variable( site, "_imm_attr", attr_value_t) @@ -457,7 +463,7 @@ def serialize_sources_to_list(site, sources, out_attr): if typ is not None: sub_serialize = serialize(typ, source, imm_attr_ref) else: - sub_serialize = ctree.mkAssignStatement( + sub_serialize = ctree.AssignStatement( site, imm_attr_ref, ctree.ExpressionInitializer(source)) sim_attr_list_set_statement = call_c_fun( site, "SIM_attr_list_set_item", [ctree.mkAddressOf(site, out_attr), @@ -517,7 +523,7 @@ def deserialize_list_to_targets(site, val_attr, targets, error_out_at_index, index = ctree.mkIntegerConstant(site, i, False) sim_attr_list_item = apply_c_fun(site, "SIM_attr_list_item", [val_attr, index], attr_value_t) - imm_set = ctree.mkAssignStatement( + imm_set = ctree.AssignStatement( site, imm_attr_ref, ctree.ExpressionInitializer(sim_attr_list_item)) statements.append(imm_set) @@ -535,7 +541,7 @@ def sub_error_out(exc, msg): sub_deserialize = deserialize(typ, imm_attr_ref, target, sub_error_out) else: - sub_deserialize = ctree.mkAssignStatement( + sub_deserialize = ctree.AssignStatement( site, target, ctree.ExpressionInitializer(imm_attr_ref)) statements.append(sub_deserialize) else: @@ -620,9 +626,9 @@ def error_out_at_index(_i, exc, msg): deserialize_list_to_targets(site, in_arg, targets, error_out_at_index, f'deserialization of {real_type}') - ctree.mkAssignStatement(site, - ctree.mkDereference(site, out_arg), - ctree.ExpressionInitializer( + ctree.AssignStatement(site, + ctree.mkDereference(site, out_arg), + ctree.ExpressionInitializer( ctree.mkDereference( site, tmp_out_ref))).toc() diff --git a/py/dml/types.py b/py/dml/types.py index a329b2e58..a62abf1a7 100644 --- a/py/dml/types.py +++ b/py/dml/types.py @@ -12,6 +12,7 @@ 'realtype', 'safe_realtype_shallow', 'safe_realtype', + 'safe_realtype_unconst', 'conv_const', 'deep_const', 'type_union', @@ -155,6 +156,20 @@ def safe_realtype_shallow(t): except DMLUnknownType as e: raise ETYPE(e.type.declaration_site or None, e.type) +def safe_realtype_unconst(t0): + def sub(t): + if isinstance(t, (TArray, TVector)): + base = sub(t.base) + if t.const or base is not t.base: + t = t.clone() + t.const = False + t.base = base + elif t.const: + t = t.clone() + t.const = False + return t + return sub(safe_realtype(t0)) + def conv_const(const, t): if const and not t.const: t = t.clone() From 39698fe928395cb9f4679d83d62c52772202e581 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Thu, 2 Nov 2023 12:40:46 +0100 Subject: [PATCH 3/5] Discard reference -- SIMICS-21584 --- RELEASENOTES-1.4.docu | 10 +++ doc/1.4/language.md | 26 ++++++++ py/dml/c_backend.py | 14 +--- py/dml/codegen.py | 89 ++++++++++++++++++-------- py/dml/ctree.py | 38 ++++++++--- py/dml/expr.py | 12 +++- test/1.4/expressions/T_discard_ref.dml | 46 +++++++++++++ 7 files changed, 185 insertions(+), 50 deletions(-) create mode 100644 test/1.4/expressions/T_discard_ref.dml diff --git a/RELEASENOTES-1.4.docu b/RELEASENOTES-1.4.docu index 592e86d75..69d03299b 100644 --- a/RELEASENOTES-1.4.docu +++ b/RELEASENOTES-1.4.docu @@ -521,4 +521,14 @@ This fix is only enabled by default with Simics API version 7 or above. With version 6 or below it must be explicitly enabled by passing --no-compat=shared_logs_on_device to DMLC. + Added the discard reference + '_' — a non-value expression which may be used as an assign + target in order to explictly discard the result of an evaluated expression + or return value of a method call . + Example usage: +
+      _ = any_expression;
+      _ = throwing_method();
+      (_, x, _) = method_with_multiple_return_values();
+      
diff --git a/doc/1.4/language.md b/doc/1.4/language.md index 7c9923baa..ee0d74e0f 100644 --- a/doc/1.4/language.md +++ b/doc/1.4/language.md @@ -3808,6 +3808,32 @@ independent method callback(int i, void *aux) { } ``` +### The Discard Reference (`_`) +``` +_ +``` + +The discard reference *`_`* is an expression without any run-time representation +that may be used as the target of an assignment in order to explicitly discard +the result of an evaluated expression or return value of a method call. + +For backwards compatibility reasons, `_` is not a keyword, but instead behaves +more closely as a global identifier. What this means is that declared +identifiers (e.g. local variables) are allowed to shadow it by being named `_`. + +Example usage: +``` +// Evaluate an expression and explicitly discard its result. +// Can be relevant to e.g. suppress Coverity's CHECKED_RETURN checker +_ = nonthrowing_single_return_method(); + +// Calls to methods that throw or have multiple return values require a target +// for each return value. `_` can be used to discard return values not of +// interest. +_ = throwing_method(); +(_, x, _) = method_with_multiple_return_values(); +``` + ### New Expressions
diff --git a/py/dml/c_backend.py b/py/dml/c_backend.py
index 2638afc6f..f01bf63b4 100644
--- a/py/dml/c_backend.py
+++ b/py/dml/c_backend.py
@@ -3122,12 +3122,7 @@ def generate_startup_trait_calls(data, idxvars):
         ref = ObjTraitRef(site, node, trait, indices)
         out(f'_tref = {ref.read()};\n')
         for method in trait_methods:
-            outargs = [mkLit(method.site,
-                             ('*((%s) {0})'
-                              % ((TArray(t, mkIntegerLiteral(method.site, 1))
-                                  .declaration('')),)),
-                             t)
-                       for (_, t) in method.outp]
+            outargs = [mkDiscardRef(method.site) for _ in method.outp]
 
             method_ref = TraitMethodDirect(
                 method.site, mkLit(method.site, '_tref', TTrait(trait)), method)
@@ -3139,12 +3134,7 @@ def generate_startup_trait_calls(data, idxvars):
 def generate_startup_regular_call(method, idxvars):
     site = method.site
     indices = tuple(mkLit(site, idx, TInt(32, False)) for idx in idxvars)
-    outargs = [mkLit(site,
-                     ('*((%s) {0})'
-                      % ((TArray(t, mkIntegerLiteral(site, 1))
-                          .declaration('')),)),
-                     t)
-               for (_, t) in method.outp]
+    outargs = [mkDiscardRef(method.site) for _ in method.outp]
     # startup memoized methods can throw, which is ignored during startup.
     # Memoization of the throw then allows for the user to check whether
     # or not the method did throw during startup by calling the method
diff --git a/py/dml/codegen.py b/py/dml/codegen.py
index 051cf7593..b1c540df2 100644
--- a/py/dml/codegen.py
+++ b/py/dml/codegen.py
@@ -1207,6 +1207,13 @@ def expr_variable(tree, location, scope):
         if in_dev_tree:
             e = in_dev_tree
     if e is None:
+        # TODO/HACK: The discard ref is exposed like this to allow it to be as
+        # keyword-like as possible while still allowing it to be shadowed.
+        # Once we remove support for discard_ref_shadowing the discard ref
+        # should become a proper keyword and its codegen be done via dedicated
+        # dispatch
+        if name == '_' and tree.site.dml_version() != (1, 2):
+            return mkDiscardRef(tree.site)
         raise EIDENT(tree.site, name)
     return e
 
@@ -2340,14 +2347,25 @@ def try_codegen_invocation(site, init_ast, outargs, location, scope):
     else:
         return common_inline(site, meth_node, indices, inargs, outargs)
 
+def codegen_src_for_nonvalue_target(site, tgt, src_ast, location, scope):
+    if not tgt.writable:
+        raise EASSIGN(site, tgt)
+    if src_ast.kind != 'initializer_scalar':
+        raise EDATAINIT(tgt.site,
+                        f'{tgt} can only be used as the target '
+                        + 'of an assignment if its initializer is a '
+                        + 'simple expression or a return value of a '
+                        + 'method call')
+    return codegen_expression(src_ast.args[0], location, scope)
+
 @statement_dispatcher
 def stmt_assign(stmt, location, scope):
     (_, site, tgt_ast, src_asts) = stmt
     assert tgt_ast.kind in ('assign_target_chain', 'assign_target_tuple')
-    tgts = [codegen_expression(ast, location, scope)
+    tgts = [codegen_expression_maybe_nonvalue(ast, location, scope)
             for ast in tgt_ast.args[0]]
     for tgt in tgts:
-        if deep_const(tgt.ctype()):
+        if not isinstance(tgt, NonValue) and deep_const(tgt.ctype()):
             raise ECONST(tgt.site)
     if tgt_ast.kind == 'assign_target_chain':
         method_tgts = [tgts[0]]
@@ -2369,7 +2387,13 @@ def stmt_assign(stmt, location, scope):
                            + f'initializer: Expected {src_asts}, got 1'))
             return []
 
-        lscope = Symtab(scope)
+        if isinstance(tgts[-1], NonValue):
+            if len(tgts) != 1:
+                raise tgts[-1].exc()
+            expr = codegen_src_for_nonvalue_target(site, tgts[0], src_asts[0],
+                                                   location, scope)
+            return [mkCopyData(site, expr, tgts[0])]
+
         init_typ = tgts[-1].ctype()
         init = eval_initializer(
             tgts[-1].site, init_typ, src_asts[0], location, scope, False)
@@ -2377,6 +2401,7 @@ def stmt_assign(stmt, location, scope):
         if len(tgts) == 1:
             return [mkAssignStatement(tgts[0].site, tgts[0], init)]
 
+        lscope = Symtab(scope)
         sym = lscope.add_variable(
             'tmp', type=init_typ, site=init.site, init=init,
             stmt=True)
@@ -2405,22 +2430,27 @@ def stmt_assign(stmt, location, scope):
 
         stmts = []
         lscope = Symtab(scope)
-        syms = []
+        stmt_pairs = []
         for (i, (tgt, src_ast)) in enumerate(zip(tgts, src_asts)):
-            init = eval_initializer(site, tgt.ctype(), src_ast, location,
-                                    scope, False)
-            name = 'tmp%d' % (i,)
-            sym = lscope.add_variable(
-                    name, type=tgt.ctype(), site=tgt.site, init=init,
-                    stmt=True)
-            syms.append(sym)
-
-        stmts.extend(sym_declaration(sym) for sym in syms)
-        stmts.extend(
-            AssignStatement(
-                tgt.site, tgt,
-                ExpressionInitializer(mkLocalVariable(tgt.site, sym)))
-            for (tgt, sym) in zip(tgts, syms))
+            if isinstance(tgt, NonValue):
+                expr = codegen_src_for_nonvalue_target(site, tgt, src_ast,
+                                                       location, scope)
+                stmt_pairs.append((mkCopyData(tgt.site, expr, tgt), None))
+            else:
+                init = eval_initializer(site, tgt.ctype(), src_ast, location,
+                                        scope, False)
+                name = 'tmp%d' % (i,)
+                sym = lscope.add_variable(
+                        name, type=tgt.ctype(), site=tgt.site, init=init,
+                        stmt=True)
+                write = AssignStatement(
+                    tgt.site, tgt,
+                    ExpressionInitializer(mkLocalVariable(tgt.site, sym)))
+                stmt_pairs.append((sym_declaration(sym), write))
+
+        stmts.extend(first for (first, _) in stmt_pairs)
+        stmts.extend(second for (_, second) in stmt_pairs
+                     if second is not None)
         return [mkCompound(site, stmts)]
 
 @statement_dispatcher
@@ -3615,7 +3645,7 @@ def codegen_inline(site, meth_node, indices, inargs, outargs,
                                 parmtype if parmtype else arg.ctype(),
                                 meth_node.name)
                     for (arg, var, (parmname, parmtype)) in zip(
-                            outargs, outvars, meth_node.outp)] 
+                            outargs, outvars, meth_node.outp)]
             exit_handler = GotoExit_dml12()
             with exit_handler:
                 code = [codegen_statement(meth_node.astcode,
@@ -4039,15 +4069,20 @@ def copy_outarg(arg, var, parmname, parmtype, method_name):
     an exception. We would be able to skip the proxy variable for
     calls to non-throwing methods when arg.ctype() and parmtype are
     equivalent types, but we don't do this today.'''
-    argtype = arg.ctype()
-
-    if not argtype:
-        raise ICE(arg.site, "unknown expression type")
+    if isinstance(arg, NonValue):
+        if not arg.writable:
+            raise arg.exc()
     else:
-        ok, trunc, constviol = realtype(parmtype).canstore(realtype(argtype))
-        if not ok:
-            raise EARGT(arg.site, 'call', method_name,
-                         arg.ctype(), parmname, parmtype, 'output')
+        argtype = arg.ctype()
+
+        if not argtype:
+            raise ICE(arg.site, "unknown expression type")
+        else:
+            ok, trunc, constviol = realtype(parmtype).canstore(
+                realtype(argtype))
+            if not ok:
+                raise EARGT(arg.site, 'call', method_name,
+                             arg.ctype(), parmname, parmtype, 'output')
 
     return mkCopyData(var.site, var, arg)
 
diff --git a/py/dml/ctree.py b/py/dml/ctree.py
index 8a0ecdc57..930d390b1 100644
--- a/py/dml/ctree.py
+++ b/py/dml/ctree.py
@@ -126,6 +126,7 @@
     'mkEachIn', 'EachIn',
     'mkBoolConstant',
     'mkUndefined', 'Undefined',
+    'mkDiscardRef',
     'TraitParameter',
     'TraitSessionRef',
     'TraitHookRef',
@@ -1034,14 +1035,21 @@ def mkAssignStatement(site, target, init):
     if not target.writable:
         raise EASSIGN(site, target)
 
-    target_type = target.ctype()
+    if isinstance(target, NonValue):
+        if not isinstance(init, ExpressionInitializer):
+            raise EDATAINIT(target.site,
+                            f'{target} can only be used as the target of an '
+                            + 'assignment if its initializer is a simple '
+                            + 'expression or a return value of a method call')
+    else:
+        target_type = target.ctype()
 
-    if deep_const(target_type):
-        raise ECONST(site)
+        if deep_const(target_type):
+            raise ECONST(site)
 
-    if isinstance(init, ExpressionInitializer):
-        init = ExpressionInitializer(
-            source_for_assignment(site, target_type, init.expr))
+        if isinstance(init, ExpressionInitializer):
+            init = ExpressionInitializer(
+                source_for_assignment(site, target_type, init.expr))
 
     return AssignStatement(site, target, init)
 
@@ -2448,7 +2456,7 @@ class AssignOp(BinOp):
     def __str__(self):
         return "%s = %s" % (self.lh, self.rh)
 
-    def discard(self):
+    def discard(self, explicit=False):
         return self.lh.write(ExpressionInitializer(self.rh))
 
     def read(self):
@@ -3473,6 +3481,18 @@ def exc(self):
 
 mkUndefined = Undefined
 
+class DiscardRef(NonValue):
+    writable = True
+
+    def __str__(self):
+        return '_'
+
+    def write(self, source):
+        assert isinstance(source, ExpressionInitializer)
+        return source.expr.discard(explicit=True)
+
+mkDiscardRef = DiscardRef
+
 def endian_convert_expr(site, idx, endian, size):
     """Convert a bit index to little-endian (lsb=0) numbering.
 
@@ -4692,8 +4712,8 @@ def ctype(self):
         return self.expr.ctype()
     def read(self):
         return self.expr.read()
-    def discard(self):
-        return self.expr.discard()
+    def discard(self, explicit=False):
+        return self.expr.discard(explicit)
     # Since addressable and readable are False this may only ever be leveraged
     # by DMLC for optimization purposes
     @property
diff --git a/py/dml/expr.py b/py/dml/expr.py
index 81118c2ea..c467eeb29 100644
--- a/py/dml/expr.py
+++ b/py/dml/expr.py
@@ -136,8 +136,16 @@ def read(self):
         raise ICE(self.site, "can't read %r" % self)
 
     # Produce a C expression but don't worry about the value.
-    def discard(self):
-        return self.read()
+    def discard(self, explicit=False):
+        if not explicit or safe_realtype_shallow(self.ctype()).void:
+            return self.read()
+
+        if self.constant:
+            return '(void)0'
+        from .ctree import Cast
+        expr = (f'({self.read()})'
+                if self.priority < Cast.priority else self.read())
+        return f'(void){expr}'
 
     def ctype(self):
         '''The corresponding DML type of this expression'''
diff --git a/test/1.4/expressions/T_discard_ref.dml b/test/1.4/expressions/T_discard_ref.dml
new file mode 100644
index 000000000..64e433a97
--- /dev/null
+++ b/test/1.4/expressions/T_discard_ref.dml
@@ -0,0 +1,46 @@
+/*
+  © 2023 Intel Corporation
+  SPDX-License-Identifier: MPL-2.0
+*/
+dml 1.4;
+device test;
+
+header %{
+    #define FUNCLIKE_MACRO() 4
+    #define VARLIKE_MACRO ++counter
+
+    static int counter = 0;
+%}
+
+extern int FUNCLIKE_MACRO(void);
+extern int VARLIKE_MACRO;
+extern int counter;
+
+method t() -> (int) throws {
+    return 1;
+}
+method m2() -> (int, int) {
+    return (1, 2);
+}
+
+method init() {
+    local int x;
+    // Explicit discard guarantees GCC doesn't emit -Wunused by always
+    // void-casting, unless the expression is already void
+    _ = x;
+    _ = FUNCLIKE_MACRO();
+    // Explicit discard does generate C, which evaluates the initializer
+    assert counter == 0;
+    _ = VARLIKE_MACRO;
+    assert counter == 1;
+    try
+        _ = t();
+    catch assert false;
+    (x, _) = m2();
+    assert x == 1;
+    local int y;
+    // Tuple initializers retain the property of each expression being
+    // evaluated left-to-right
+    (_, y) = (x++, x);
+    assert y == 2;
+}

From 9c1b160307f0ba845e5b5b25efe54b4125d5947e Mon Sep 17 00:00:00 2001
From: Love Waern 
Date: Mon, 6 Nov 2023 13:14:27 +0100
Subject: [PATCH 4/5] Gate the ability to shadow `_` behind a compatibility
 feature

---
 RELEASENOTES-1.4.docu                         |  8 ++++-
 doc/1.4/language.md                           |  7 ++--
 lib/1.2/dml-builtins.dml                      |  1 +
 lib/1.4/dml-builtins.dml                      |  3 +-
 py/dml/compat.py                              |  7 ++++
 py/dml/dmlparse.py                            | 35 ++++++++++++++++++-
 .../T_discard_ref_shadowing_disabled.dml      | 27 ++++++++++++++
 .../T_discard_ref_shadowing_enabled.dml       | 24 +++++++++++++
 8 files changed, 106 insertions(+), 6 deletions(-)
 create mode 100644 test/1.4/legacy/T_discard_ref_shadowing_disabled.dml
 create mode 100644 test/1.4/legacy/T_discard_ref_shadowing_enabled.dml

diff --git a/RELEASENOTES-1.4.docu b/RELEASENOTES-1.4.docu
index 69d03299b..6945da14e 100644
--- a/RELEASENOTES-1.4.docu
+++ b/RELEASENOTES-1.4.docu
@@ -530,5 +530,11 @@
       _ = any_expression;
       _ = throwing_method();
       (_, x, _) = method_with_multiple_return_values();
-      
+ + For backwards compatibility, declared variables and object members are + still allowed to be named '_' with Simics API version 6 or below. + Any such declaration will shadow the discard reference — + i.e. make it unavailable within the scope that the declaration is + accessible. This compatibility feature can be disabled by passing + --no-compat=discard_ref_shadowing to DMLC. diff --git a/doc/1.4/language.md b/doc/1.4/language.md index ee0d74e0f..c5bc22bce 100644 --- a/doc/1.4/language.md +++ b/doc/1.4/language.md @@ -3817,9 +3817,10 @@ The discard reference *`_`* is an expression without any run-time representation that may be used as the target of an assignment in order to explicitly discard the result of an evaluated expression or return value of a method call. -For backwards compatibility reasons, `_` is not a keyword, but instead behaves -more closely as a global identifier. What this means is that declared -identifiers (e.g. local variables) are allowed to shadow it by being named `_`. +When the compatibility feature `discard_ref_shadowing` is enabled, `_` is not a +keyword, but instead behaves more closely as a global identifier. +What this means is that declared identifiers (e.g. local variables) are allowed +to shadow it by being named `_`. Example usage: ``` diff --git a/lib/1.2/dml-builtins.dml b/lib/1.2/dml-builtins.dml index 0b29285ea..a75c1120b 100644 --- a/lib/1.2/dml-builtins.dml +++ b/lib/1.2/dml-builtins.dml @@ -211,6 +211,7 @@ template device { parameter _compat_port_obj_param auto; parameter _compat_io_memory auto; parameter _compat_shared_logs_on_device auto; + parameter _compat_discard_ref_shadowing auto; parameter _compat_dml12_inline auto; parameter _compat_dml12_not auto; parameter _compat_dml12_goto auto; diff --git a/lib/1.4/dml-builtins.dml b/lib/1.4/dml-builtins.dml index 86c3b8031..884098f53 100644 --- a/lib/1.4/dml-builtins.dml +++ b/lib/1.4/dml-builtins.dml @@ -545,6 +545,7 @@ template device { param _compat_port_obj_param auto; param _compat_io_memory auto; param _compat_shared_logs_on_device auto; + param _compat_discard_ref_shadowing auto; param _compat_dml12_inline auto; param _compat_dml12_not auto; param _compat_dml12_goto auto; @@ -1848,7 +1849,7 @@ template bank is (object, shown_desc) { } shared method _num_registers() -> (uint32) { - local (const register *_, uint64 table_size) = _reginfo_table(); + local (const register *_table, uint64 table_size) = _reginfo_table(); return table_size; } diff --git a/py/dml/compat.py b/py/dml/compat.py index 3df8385ef..35c904ef6 100644 --- a/py/dml/compat.py +++ b/py/dml/compat.py @@ -117,6 +117,13 @@ class shared_logs_on_device(CompatFeature): short = "Make logs inside shared methods always log on the device object" last_api_version = api_6 +@feature +class discard_ref_shadowing(CompatFeature): + '''This compatibility feature allows declarations (within methods or + objects) to be named '_'. This will cause the discard reference `_` to be + inaccessible (*shadowed*) in all scopes with such a declaration.''' + short = "Allow declarations to shadow '_'" + last_api_version = api_6 @feature class dml12_inline(CompatFeature): diff --git a/py/dml/dmlparse.py b/py/dml/dmlparse.py index 1d5bba12b..f8730ba81 100644 --- a/py/dml/dmlparse.py +++ b/py/dml/dmlparse.py @@ -8,7 +8,7 @@ from .logging import * from .messages import * -from . import ast, logging +from . import ast, logging, compat import dml.globals from . import dmllex12 from . import dmllex14 @@ -176,6 +176,7 @@ def prod(f): @prod def device(t): 'dml : DEVICE objident SEMI maybe_bitorder device_statements' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.dml(site(t), t[2], t[5]) # Entry point for imported files @@ -290,6 +291,7 @@ def array_list(t): @prod def object_regarray(t): 'object : REGISTER objident array_list sizespec offsetspec maybe_istemplate object_spec' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.object_(site(t), t[2], 'register', t[3], t[4] + t[5] + t[6] + t[7]) @@ -306,6 +308,7 @@ def bitrangespec_empty(t): @prod_dml14 def object_field(t): 'object : FIELD objident array_list bitrangespec maybe_istemplate object_spec' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.object_(site(t), t[2], 'field', t[3], t[4] + t[5] + t[6]) def endian_translate_bit(expr, width, bitorder): @@ -454,11 +457,13 @@ def object3(t): | GROUP objident array_list maybe_istemplate object_spec | PORT objident array_list maybe_istemplate object_spec | IMPLEMENT objident array_list maybe_istemplate object_spec''' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.object_(site(t), t[2], t[1], t[3], t[4] + t[5]) @prod_dml14 def object_subdevice(t): '''object : SUBDEVICE objident array_list maybe_istemplate object_spec''' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.object_(site(t), t[2], t[1], t[3], t[4] + t[5]) @prod_dml12 @@ -582,6 +587,7 @@ def method_qualifiers_check(site, qualifiers, inp, outp, throws, default): def object_method(t): '''method : method_qualifiers METHOD objident method_params_typed maybe_default compound_statement''' name = t[3] + ident_enforce_not_discard_ref(site(t, 3), name) (inp, outp, throws) = t[4] body = t[6] (inp, outp) = method_qualifiers_check(site(t), t[1], inp, outp, throws, @@ -594,6 +600,7 @@ def object_method(t): def object_inline_method(t): '''method : INLINE METHOD objident method_params_maybe_untyped maybe_default compound_statement''' name = t[3] + ident_enforce_not_discard_ref(site(t, 3), name) (inp, outp, throws) = t[4] if all(typ for (_, asite, name, typ) in inp): # inline annotation would have no effect for fully typed methods. @@ -642,11 +649,13 @@ def arraydef2(t): @prod_dml14 def arraydef(t): '''arraydef : ident LT expression''' + ident_enforce_not_discard_ref(site(t, 1), t[1]) t[0] = (t[1], t[3]) @prod_dml14 def arraydef_implicit(t): '''arraydef : ident LT ELLIPSIS''' + ident_enforce_not_discard_ref(site(t, 1), t[1]) t[0] = (t[1], None) # Traits @@ -745,15 +754,18 @@ def trait_method(t): @prod def shared_method_abstract(t): '''shared_method : ident method_params_typed SEMI''' + ident_enforce_not_discard_ref(site(t, 1), t[1]) t[0] = (t[1], t[2], True, None, site(t, 3)) @prod def shared_method_default(t): '''shared_method : ident method_params_typed DEFAULT compound_statement''' + ident_enforce_not_discard_ref(site(t, 1), t[1]) t[0] = (t[1], t[2], True, t[4], lex_end_site(t, -1)) @prod def shared_method_final(t): '''shared_method : ident method_params_typed compound_statement''' + ident_enforce_not_discard_ref(site(t, 1), t[1]) t[0] = (t[1], t[2], False, t[3], lex_end_site(t, -1)) @prod_dml12 @@ -784,6 +796,7 @@ def template(t): @prod_dml14 def template(t): 'toplevel : TEMPLATE objident maybe_istemplate LBRACE template_stmts RBRACE' + ident_enforce_not_discard_ref(site(t, 2), t[2]) ises = [s for s in t[5] if s.kind == 'is'] shared_methods = [s for s in t[5] if s.kind == 'sharedmethod'] if ises and shared_methods: @@ -812,6 +825,7 @@ def impl_header(t): @prod def loggroup(t): 'toplevel : LOGGROUP ident SEMI' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.loggroup(site(t), t[2]) # constant/extern @@ -819,6 +833,7 @@ def loggroup(t): @prod def constant(t): 'toplevel : CONSTANT ident EQUALS expression SEMI' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.constant(site(t), t[2], t[4]) if logging.show_porting: report(PCONSTANT(site(t))) @@ -1019,6 +1034,7 @@ def object_else_if(t): @prod def object_parameter(t): '''parameter : param_ objident paramspec''' + ident_enforce_not_discard_ref(site(t, 2), t[2]) if logging.show_porting: if t[2] == 'hard_reset_value': report(PHARD_RESET_VALUE(site(t, 2))) @@ -1031,6 +1047,7 @@ def object_parameter(t): @prod_dml14 def object_typedparam(t): '''parameter : param_ objident COLON ctypedecl SEMI''' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.typedparam(site(t), site(t, 2), t[2], t[4]) @prod @@ -1207,6 +1224,7 @@ def cdecl_or_ident_decl(t): @prod_dml14 def cdecl_or_ident_inline(t): '''cdecl_or_ident : INLINE ident''' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.cdecl(site(t), t[2], None) # A C-like declaration with required identifier name @@ -1309,6 +1327,7 @@ def cdecl3(t): @prod_dml14 def cdecl3(t): 'cdecl3 : ident' + ident_enforce_not_discard_ref(site(t, 1), t[1]) t[0] = [t[1]] @prod @@ -2211,6 +2230,7 @@ def ident_list_nonempty(t): @prod_dml14 def ident_list_one(t): 'nonempty_ident_list : ident' + ident_enforce_not_discard_ref(site(t, 1), t[1]) t[0] = [(site(t, 1), t[1])] @prod_dml14 @@ -2226,6 +2246,7 @@ def statement_delay_hook(t): @prod_dml14 def statement_delay_hook_one_msg_param(t): 'statement_except_hashif : AFTER expression ARROW ident COLON expression SEMI %prec bind' + ident_enforce_not_discard_ref(site(t, 4), t[4]) t[0] = ast.afteronhook(site(t), t[2], [(site(t, 4), t[4])], t[6]) @@ -2350,11 +2371,13 @@ def hashselect(t): @prod def select(t): 'statement_except_hashif : hashselect ident IN LPAREN expression RPAREN WHERE LPAREN expression RPAREN statement hashelse statement' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.select(site(t), t[2], t[5], t[9], t[11], t[13]) @prod_dml12 def foreach(t): 'statement_except_hashif : FOREACH ident IN LPAREN expression RPAREN statement' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.foreach_dml12(site(t), t[2], t[5], t[7]) if logging.show_porting: report(PHASH(site(t))) @@ -2362,11 +2385,13 @@ def foreach(t): @prod_dml14 def foreach(t): 'statement_except_hashif : FOREACH ident IN LPAREN expression RPAREN statement' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.foreach(site(t), t[2], t[5], t[7]) @prod_dml14 def hashforeach(t): 'statement_except_hashif : HASHFOREACH ident IN LPAREN expression RPAREN statement' + ident_enforce_not_discard_ref(site(t, 2), t[2]) t[0] = ast.hashforeach(site(t), t[2], t[5], t[7]) @prod_dml12 @@ -2397,6 +2422,7 @@ def case_statement_default(t): @prod_dml14 def goto_statement(t): 'statement_except_hashif : GOTO ident SEMI' + ident_enforce_not_discard_ref(site(t, 2), t[2]) # Restricted goto should be implemented, see SIMICS-6130 report(ESYNTAX(site(t), 'goto', 'goto statements are not yet implemented in DML 1.4')) @@ -2573,6 +2599,7 @@ def simple_array_list(t): @prod_dml14 def hook_decl(t): '''hook_decl : HOOK LPAREN cdecl_list RPAREN ident simple_array_list SEMI''' + ident_enforce_not_discard_ref(site(t, 5), t[5]) cdecl_list_enforce_unnamed(t[3]) if t[6]: # Hook arrays are an internal feature, as their design depends on if we @@ -2632,6 +2659,12 @@ def ident(t): def ident(t): t[0] = t[1] +def ident_enforce_not_discard_ref(site, ident): + if (str(ident) == '_' + and site.dml_version() != (1, 2) + and compat.discard_ref_shadowing not in dml.globals.enabled_compat): + report(ESYNTAX(site, '_', "reserved identifier")) + reserved_words_12 = [ 'CLASS', 'ENUM', 'NAMESPACE', 'PRIVATE', 'PROTECTED', 'PUBLIC', 'RESTRICT', 'UNION', 'USING', 'VIRTUAL', 'VOLATILE'] diff --git a/test/1.4/legacy/T_discard_ref_shadowing_disabled.dml b/test/1.4/legacy/T_discard_ref_shadowing_disabled.dml new file mode 100644 index 000000000..e6d989521 --- /dev/null +++ b/test/1.4/legacy/T_discard_ref_shadowing_disabled.dml @@ -0,0 +1,27 @@ +/* + © 2023 Intel Corporation + SPDX-License-Identifier: MPL-2.0 +*/ +dml 1.4; + +device test; + +/// DMLC-FLAG --no-compat=discard_ref_shadowing + +/// ERROR ESYNTAX +constant _ = 1; + +group g is init { + /// ERROR ESYNTAX + param _ = 2; + method init() { + assert _ == 2; + } +} + +method init() { + assert _ == 1; + /// ERROR ESYNTAX + local int _ = 2; + assert _ == 2; +} diff --git a/test/1.4/legacy/T_discard_ref_shadowing_enabled.dml b/test/1.4/legacy/T_discard_ref_shadowing_enabled.dml new file mode 100644 index 000000000..476707882 --- /dev/null +++ b/test/1.4/legacy/T_discard_ref_shadowing_enabled.dml @@ -0,0 +1,24 @@ +/* + © 2023 Intel Corporation + SPDX-License-Identifier: MPL-2.0 +*/ +dml 1.4; + +device test; + +/// DMLC-FLAG --simics-api=6 + +constant _ = 1; + +group g is init { + param _ = 2; + method init() { + assert _ == 2; + } +} + +method init() { + assert _ == 1; + local int _ = 2; + assert _ == 2; +} From 8bb0cc29a7d75497c7da3e8dd293a1cfb16e680f Mon Sep 17 00:00:00 2001 From: Love Waern Date: Thu, 2 Nov 2023 14:32:50 +0100 Subject: [PATCH 5/5] Add DMLC option to not suppress GCC's `Wunused` for local variables -- SIMICS-21585 Implemented clumsily in a way that doesn't necessarily only affect local variables. This PR is more of a way to investigate the impact this has, and serve as a talking point. We likely want to report unused variables in DMLC directly instead. --- lib/1.4/dml-builtins.dml | 5 +---- py/dml/ctree.py | 3 ++- py/dml/dmlc.py | 12 +++++++++--- py/dml/globals.py | 3 +++ test/1.4/expressions/T_discard_ref.dml | 8 +++++--- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/1.4/dml-builtins.dml b/lib/1.4/dml-builtins.dml index 884098f53..64b4d2b6f 100644 --- a/lib/1.4/dml-builtins.dml +++ b/lib/1.4/dml-builtins.dml @@ -2809,7 +2809,7 @@ template register is (_conf_attribute, get, set, shown_desc, shared method _size() -> (int) { return this.bitsize / 8; } shared method _num_fields() -> (uint32) { local uint32 num_fields = 0; - foreach f in (fields) { + foreach _f in (fields) { num_fields++; } return num_fields; @@ -2992,7 +2992,6 @@ template register is (_conf_attribute, get, set, shown_desc, local uint64 unmapped_bits = unmapped & ~field_bits; local uint64 val = (this.val & default_access_bits & enabled_bytes); - local int r_lsb = _enabled_bytes_to_offset(enabled_bytes) * 8; for (local int f = 0; f < num_fields; f++) { local int f_lsb = fields[f].lsb; local int f_msb = f_lsb + fields[f].bitsize - 1; @@ -3081,8 +3080,6 @@ template register is (_conf_attribute, get, set, shown_desc, return; } - local int r_lsb = _enabled_bytes_to_offset(enabled_bytes) * 8; - local int r_msb = _enabled_bytes_to_size(enabled_bytes) * 8 - 1; for (local int f = 0; f < num_fields; f++) { local int f_lsb = fields[f].lsb; local int f_msb = f_lsb + fields[f].bitsize - 1; diff --git a/py/dml/ctree.py b/py/dml/ctree.py index 930d390b1..d1716f079 100644 --- a/py/dml/ctree.py +++ b/py/dml/ctree.py @@ -5146,7 +5146,8 @@ def sym_declaration(sym, unused=False): return None # This will prevent warnings from the C compiler - unused = unused or (refcount == 0) or sym.value.startswith("__") + unused = (unused or (refcount == 0 and dml.globals.suppress_wunused) + or sym.name.startswith("_") or sym.value.startswith("__")) return mkDeclaration(sym.site, sym.value, sym.type, sym.init, unused) diff --git a/py/dml/dmlc.py b/py/dml/dmlc.py index 1c5dad849..7e103eaac 100644 --- a/py/dml/dmlc.py +++ b/py/dml/dmlc.py @@ -455,9 +455,6 @@ def main(argv): default="0", help=('Limit the number of error messages to N')) - # - # - #
--no-compat=TAG
#
Disable a compatibility feature
parser.add_argument( @@ -468,6 +465,14 @@ def main(argv): '--help-no-compat', action=CompatHelpAction, help='List the available tags for --no-compat') + #
--no-suppress-wunused
+ #
Disable the automatic suppression of GCC warnings for unused local + # variables
+ parser.add_argument( + '--no-suppress-wunused', action='store_true', + help=("Disable the automatic suppression of GCC warnings for unused " + + "local variables")) + # # @@ -576,6 +581,7 @@ def main(argv): sys.exit(1) dml.globals.coverity = options.coverity + dml.globals.suppress_wunused = not options.no_suppress_wunused dml.globals.linemarks_enabled = not options.noline diff --git a/py/dml/globals.py b/py/dml/globals.py index 1f06bf2ca..b716d3389 100644 --- a/py/dml/globals.py +++ b/py/dml/globals.py @@ -106,3 +106,6 @@ def compat_dml12_int(site): # Relevant during C emission: indicates if linemarks should currently be generated linemarks = False + +# Insert UNUSED for user variable declarations when they are indeed unused +suppress_wunused = False diff --git a/test/1.4/expressions/T_discard_ref.dml b/test/1.4/expressions/T_discard_ref.dml index 64e433a97..b74c9c630 100644 --- a/test/1.4/expressions/T_discard_ref.dml +++ b/test/1.4/expressions/T_discard_ref.dml @@ -5,6 +5,8 @@ dml 1.4; device test; +/// DMLC-FLAG --no-suppress-wunused + header %{ #define FUNCLIKE_MACRO() 4 #define VARLIKE_MACRO ++counter @@ -24,10 +26,10 @@ method m2() -> (int, int) { } method init() { - local int x; + local int unused; // Explicit discard guarantees GCC doesn't emit -Wunused by always // void-casting, unless the expression is already void - _ = x; + _ = unused; _ = FUNCLIKE_MACRO(); // Explicit discard does generate C, which evaluates the initializer assert counter == 0; @@ -36,9 +38,9 @@ method init() { try _ = t(); catch assert false; + local (int x, int y); (x, _) = m2(); assert x == 1; - local int y; // Tuple initializers retain the property of each expression being // evaluated left-to-right (_, y) = (x++, x);