diff --git a/RELEASENOTES-1.4.docu b/RELEASENOTES-1.4.docu index 592e86d75..6945da14e 100644 --- a/RELEASENOTES-1.4.docu +++ b/RELEASENOTES-1.4.docu @@ -521,4 +521,20 @@ 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();
+      
+ 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 7c9923baa..c5bc22bce 100644 --- a/doc/1.4/language.md +++ b/doc/1.4/language.md @@ -3808,6 +3808,33 @@ 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. + +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: +``` +// 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/grammar_to_md.py b/grammar_to_md.py
index ba5770548..56a4cfe20 100644
--- a/grammar_to_md.py
+++ b/grammar_to_md.py
@@ -78,7 +78,8 @@
               'BCONST'         : 'binary-literal',
               'FCONST'         : 'float-literal',
               'ELLIPSIS'       : '"..."',
-              ''        : '<empty>'
+              ''        : '<empty>',
+              '_'              : '_'
              }
 
 for k in (reserved_idents
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/c_backend.py b/py/dml/c_backend.py
index 2cb4384e4..f01bf63b4 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)
@@ -3120,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)
@@ -3137,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 644e9d064..b1c540df2 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):
@@ -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)
@@ -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
 
@@ -1538,7 +1545,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:
@@ -2131,8 +2138,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
@@ -2340,21 +2347,31 @@ 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]]
     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 +2387,33 @@ def stmt_assign(stmt, location, scope):
                            + f'initializer: Expected {src_asts}, got 1'))
             return []
 
-        stmts = []
-        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(
-            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)]
+
+        lscope = Symtab(scope)
+        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
@@ -2399,53 +2430,66 @@ 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(map(sym_declaration, syms))
-        stmts.extend(
-            mkAssignStatement(
-                tgt.site, tgt, ExpressionInitializer(mkLocalVariable(tgt.site,
-                                                                     sym)))
-            for (tgt, sym) in zip(tgts, syms))
-        return stmts
+            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
 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
@@ -3601,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,
@@ -3885,7 +3929,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 = []
 
@@ -4025,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/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/ctree.py b/py/dml/ctree.py
index 24541c295..930d390b1 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',
@@ -126,6 +126,7 @@
     'mkEachIn', 'EachIn',
     'mkBoolConstant',
     'mkUndefined', 'Undefined',
+    'mkDiscardRef',
     'TraitParameter',
     'TraitSessionRef',
     'TraitHookRef',
@@ -599,8 +600,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 +1024,39 @@ 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)
+
+    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 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
@@ -1094,21 +1115,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
@@ -2444,8 +2456,8 @@ class AssignOp(BinOp):
     def __str__(self):
         return "%s = %s" % (self.lh, self.rh)
 
-    def discard(self):
-        return self.lh.write(self.rh)
+    def discard(self, explicit=False):
+        return self.lh.write(ExpressionInitializer(self.rh))
 
     def read(self):
         return '((%s), (%s))' % (self.discard(), self.lh.read())
@@ -2524,13 +2536,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 +2580,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 +2703,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 +2729,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:
@@ -2922,7 +2935,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)
 
@@ -2944,7 +2958,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]
@@ -3467,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.
 
@@ -4293,14 +4319,28 @@ 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)
 
+    @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:
@@ -4314,11 +4354,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 +4466,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 +4561,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 +4704,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 +4712,19 @@ def ctype(self):
         return self.expr.ctype()
     def read(self):
         return self.expr.read()
-    def discard(self): pass
+    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
+    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
 
@@ -4847,15 +4906,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
@@ -4883,21 +4962,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
@@ -4937,10 +5007,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}')
 
@@ -4979,8 +5050,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
@@ -5039,8 +5109,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/dmllex.py b/py/dml/dmllex.py
index 779269c1c..75336b7b3 100644
--- a/py/dml/dmllex.py
+++ b/py/dml/dmllex.py
@@ -21,7 +21,7 @@
     'AFTER', 'ASSERT', 'BITFIELDS', 'CALL', 'CAST', 'DEFINED', 'ERROR',
     'FOREACH', 'IN', 'IS', 'LAYOUT', 'LOCAL', 'LOG', 'SELECT',
     'SIZEOFTYPE', 'TYPEOF', 'UNDEFINED', 'VECT', '_WARNING', 'WHERE',
-    'EACH', 'SESSION', 'SEQUENCE',
+    'EACH', 'SESSION', 'SEQUENCE', '_',
 
     # ANSI C reserved words
     'AUTO', 'BREAK', 'CASE', 'CHAR', 'CONST', 'CONTINUE', 'DEFAULT',
diff --git a/py/dml/dmlparse.py b/py/dml/dmlparse.py
index 1d5bba12b..f2e9a9e3b 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
@@ -1754,7 +1754,7 @@ def expression_ident(t):
 
 @prod_dml14
 def expression_ident(t):
-    '''expression : objident
+    '''expression : objident_or_underscore
                   | DEFAULT'''
     t[0] = ast.variable(site(t), t[1])
 
@@ -2616,22 +2616,34 @@ def objident(t):
                 | REGISTER'''
     t[0] = t[1]
 
-def ident_rule(idents):
-    return 'ident : ' +  "\n| ".join(idents)
+@prod_dml14
+def objident_or_underscore(t):
+    '''objident_or_underscore : ident_or_underscore
+                              | REGISTER'''
+    t[0] = t[1]
+
+def ident_rule(name, idents):
+    return f'{name} : ' +  "\n| ".join(idents)
 
 # Most DML top-level keywords are also allowed as identifiers.
 
 @prod_dml12
-@lex.TOKEN(ident_rule(dmllex12.reserved_idents + (
-    'ID', 'EACH', 'SESSION', 'SEQUENCE')))
+@lex.TOKEN(ident_rule('ident', dmllex12.reserved_idents + (
+    'ID', 'EACH', 'SESSION', 'SEQUENCE', '_')))
 def ident(t):
     t[0] = t[1]
 
 @prod_dml14
-@lex.TOKEN(ident_rule(dmllex14.reserved_idents + ('ID',)))
+@lex.TOKEN(ident_rule('ident', dmllex14.reserved_idents + ('ID',)))
 def ident(t):
     t[0] = t[1]
 
+@prod_dml14
+@lex.TOKEN(ident_rule('ident_or_underscore',
+                      dmllex14.reserved_idents + ('ID', '_')))
+def ident_or_underscore(t):
+    t[0] = t[1]
+
 reserved_words_12 = [
     'CLASS', 'ENUM', 'NAMESPACE', 'PRIVATE', 'PROTECTED', 'PUBLIC',
     'RESTRICT', 'UNION', 'USING', 'VIRTUAL', 'VOLATILE']
@@ -2641,15 +2653,27 @@ def ident(t):
                                          'ASYNC', 'AWAIT', 'WITH']
 
 @prod_dml12
-@lex.TOKEN(ident_rule(reserved_words_12))
+@lex.TOKEN(ident_rule('ident', reserved_words_12))
 def reserved(t):
     raise ESYNTAX(site(t, 1), str(t[1]), "reserved word")
 
 @prod_dml14
-@lex.TOKEN(ident_rule(reserved_words_14))
+@lex.TOKEN(ident_rule('ident', reserved_words_14))
 def reserved(t):
     raise ESYNTAX(site(t, 1), str(t[1]), "reserved word")
 
+@prod_dml14
+@lex.TOKEN(ident_rule('ident_or_underscore', reserved_words_14))
+def reserved_(t):
+    raise ESYNTAX(site(t, 1), str(t[1]), "reserved word")
+
+@prod_dml14
+def ident_underscore(t):
+    '''ident : _'''
+    if compat.discard_ref_shadowing not in dml.globals.enabled_compat:
+        report(ESYNTAX(site(t), '_', "reserved identifier"))
+    t[0] = t[1]
+
 # Error handling
 @prod
 def error(t):
diff --git a/py/dml/expr.py b/py/dml/expr.py
index ebb0c5217..c467eeb29 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
@@ -128,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'''
@@ -139,10 +155,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 +178,15 @@ 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, 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
     appear as a subexpression of certain expressions.
@@ -202,11 +233,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
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()
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..3bc2c6e1c
--- /dev/null
+++ b/test/1.4/expressions/T_discard_ref.dml
@@ -0,0 +1,48 @@
+/*
+  © 2023 Intel Corporation
+  SPDX-License-Identifier: MPL-2.0
+*/
+dml 1.4;
+device test;
+
+/// DMLC-FLAG --no-compat=discard_ref_shadowing
+
+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;
+}
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;
+}