From 54eb78b1d16f90244c15e5989c0a2c8f4e24da59 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Tue, 17 Jan 2023 12:24:00 +0100 Subject: [PATCH 1/8] RAII Types and more Besides the obvious (RAII architecture, vectors, strings), this includes: 1. `cast(initializer, ...)` syntax, and rework of associated logic 3. As a result of the above, compound literals. `cast({1,2,3}, int[3])` is now supported. 4. `a += initializer` syntax, and rework of associated logic. This means complex method calls can be used as RHS of `+=`/`-=`/etc. 5. Reworked statements' `toc`/`toc_inline` family 6. ExpressionInitializer.assign_to and Expression.write rework 7. The addition of "addressable" and "c_lval" to Expression, and an associated rework of lvalue-ness 8. Strict `DMLType.cmp`. Old implementation is still used in a few places and is called `cmp_fuzzy` 9. The ability to hash DML Types in a way compatible with the new strict cmp. 10. `extern cident(blabla) as dmlident;`. 11. Improved deserialization logic for arrays; doesn't need a stack frame per dimension --- .gitignore | 1 + Makefile | 2 +- doc/1.4/language.md | 338 ++- include/simics/dmllib.h | 660 +++++- include/simics/dmlraiitypes.h | 1136 ++++++++++ lib/1.4/dml-builtins.dml | 8 + py/dml/c_backend.py | 451 ++-- py/dml/clone_test.py | 3 +- py/dml/codegen.py | 1025 +++++++-- py/dml/ctree.py | 1830 +++++++++++++++-- py/dml/ctree_test.py | 4 +- py/dml/dmlparse.py | 137 +- py/dml/expr.py | 95 +- py/dml/g_backend.py | 2 +- py/dml/globals.py | 5 + py/dml/io_memory.py | 9 +- py/dml/messages.py | 58 +- py/dml/serialize.py | 184 +- py/dml/structure.py | 29 +- py/dml/traits.py | 4 +- py/dml/types.py | 471 ++++- .../{T_WEXPERIMENTAL.dml => T_DEPRECATED.dml} | 2 +- test/1.4/errors/T_ECAST.dml | 4 +- test/1.4/errors/T_ESERIALIZE.dml | 4 +- test/1.4/errors/T_ESWITCH.dml | 6 - test/1.4/errors/T_EVOID.dml | 2 +- test/1.4/syntax/T_assign.dml | 23 + test/1.4/types/raii/T_after.cont.py | 22 + test/1.4/types/raii/T_after.dml | 106 + test/1.4/types/raii/T_after.py | 33 + test/1.4/types/raii/T_hooks_basic.dml | 147 ++ .../types/raii/T_hooks_checkpointing.cont.py | 4 + test/1.4/types/raii/T_hooks_checkpointing.dml | 155 ++ test/1.4/types/raii/T_hooks_checkpointing.py | 18 + test/1.4/types/raii/T_various.dml | 216 ++ test/1.4/types/raii/T_various.py | 11 + test/1.4/types/raii/hooks_common.dml | 115 ++ 37 files changed, 6440 insertions(+), 880 deletions(-) create mode 100644 include/simics/dmlraiitypes.h rename test/1.4/errors/{T_WEXPERIMENTAL.dml => T_DEPRECATED.dml} (83%) create mode 100644 test/1.4/types/raii/T_after.cont.py create mode 100644 test/1.4/types/raii/T_after.dml create mode 100644 test/1.4/types/raii/T_after.py create mode 100644 test/1.4/types/raii/T_hooks_basic.dml create mode 100644 test/1.4/types/raii/T_hooks_checkpointing.cont.py create mode 100644 test/1.4/types/raii/T_hooks_checkpointing.dml create mode 100644 test/1.4/types/raii/T_hooks_checkpointing.py create mode 100644 test/1.4/types/raii/T_various.dml create mode 100644 test/1.4/types/raii/T_various.py create mode 100644 test/1.4/types/raii/hooks_common.dml diff --git a/.gitignore b/.gitignore index 2f836aacf..36e183858 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ *.pyc +flycheck_* \ No newline at end of file diff --git a/Makefile b/Makefile index a4079e7eb..945f3ba2a 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ SCRIPTS := $(PYTHONPATH)/port_dml.py MPL_LICENSE := $(PYTHONPATH)/LICENSE BSD0_LICENSES := $(addsuffix /LICENSE,$(DMLLIB_DESTDIRS) $(OLD_DMLLIB_DESTDIRS_4_8) $(DMLLIB_DEST)/include/simics) -HFILES := $(DMLLIB_DEST)/include/simics/dmllib.h +HFILES := $(DMLLIB_DEST)/include/simics/dmllib.h $(DMLLIB_DEST)/include/simics/dmlraiitypes.h DMLC_BIN := $(OUT_PYFILES) $(OUT_GEN_PYFILES) $(HFILES) diff --git a/doc/1.4/language.md b/doc/1.4/language.md index 7c9923baa..904fb641d 100644 --- a/doc/1.4/language.md +++ b/doc/1.4/language.md @@ -1481,9 +1481,11 @@ deserialize for the purposes of checkpointing. This is important for the use of statement](#after-statements). All primitive non-pointer data types (integers, floating-point types, booleans, -etc.) are considered serializable, as is any struct, layout, or array type -consisting entirely of serializable types. [Template types](#templates-as-types) -and [hook reference types](#hook-declarations) are also considered serializable. +etc.) are considered serializable, as is any struct, layout, [vector](#vectors), +or array type consisting entirely of serializable types. The +[`string`](#strings) type (not to be confused with `char *`) is also considered +serializable, as are [template types](#templates-as-types) and [hook reference +types](#hook-declarations). Any type not fitting the above criteria is not considered serializable: in particular, any pointer type is not considered serializable, nor is any @@ -1491,6 +1493,305 @@ in particular, any pointer type is not considered serializable, nor is any impossible for the compiler to ensure it's aware of all members of the struct type. +### Resource-enriched (RAII) types + +_Resource-enriched types_ are types for which any value of such a type has +_associated resources beyond the simple storage needed for the value_, and that +these resources are bound to the lifetime of variable. +The DML compiler automatically manages the integrity of resources bound +to a value: they are duplicated if the value is copied to another location, +and are freed once the lifetime of the value expires (such as a variable going +out of scope, or a pointer allocated via [`new`](#new-expressions) being +[`delete`](#delete-statements)d.) + +Resource-enriched types are DML's application of Resource Acquisition Is +Initialization (RAII), a concept present in several other languages such as C++ +and Rust. + +As the integrity of values of resource-enriched types — and thus, their +safe usage — rely heavily on the DML compiler, usage of resource-enriched +types are more restricted compared to other types: + +* `sizeof` or `sizeoftype` may not be used to acquire the size of a + resource-enriched type. This is because the size of the underlying C type does + not reflect the associated resources of the type, and so any primitive memory + operation such as `memset` or `memcpy` are unsafe with values of + resource-enriched types. +* Values of resource-enriched types may not be passed as variadic arguments. + Variadic functions are never defined within DML, and so cannot manage + arguments of resource-enriched type correctly. +* Resource-enriched types may not be present in the signature of any [exported + method](#exported-methods). + Methods are exported for external use, but DML methods have an internal + calling convention when resource-enriched types are involved, and so it does + not make sense to export such methods. + +Any `struct` or array type containing a resource-enriched type is itself +considered resource-enriched. + +
+**Note:** For the time being, DML's support for resource-enriched types is +rather minimal. DML currently only supports two kinds of resource-enriched +types: strings, and vectors, with no way to define your own. In addition, +DML does not provide any operation to cheaply move resources from one value +of resource-enriched type to another. Further extensions to resource-enriched +types are currently under evaluation and experimentation. +
+ +#### Strings +``` +string +``` +DML strings are simultaneously length-counted and `NUL`-terminated dynamically +allocated strings. They have support for efficient mutation, making them +suitable for use as string builders. In particular, using `+=` to add *N* +characters to the end of a particular string is an O(*N*) (amortized) operation, +no matter the previous length of the string. + +`string` supports the use of string literals as initializers: +``` +local string s = "Hello world!"; +``` + +String values are copied by contents: +``` +local string s1 = "Hello world!"; +local string s2 = s1; +s2 += " Goodbye world"; // Does not affect s1. +assert s1 == "Hello world!" && s2 == "Hello world! Goodbye world"; +``` + +To construct a string from any arbitrary C string (rather than a string +literal), `mk_string` and `mk_string_f` may be used: +``` +method construct_guarded_string(const char *s) -> (string) throws { + if (strlen(s) > 512) + throw; + return mk_string(s); +} + +method stringify_int(int i) -> (string) { + return mk_string_f("%d", i); +} +``` + +Strings may be concatenated using `+`, and support all comparison operators +(which are based on alphabetical ordering). In addition, `+` and the comparison +operators support either operand being a string literal. + +The [`log` statement](#log-statements) has built-in support for values of string +type; the following is allowed: +``` +local string s = "Hello world"; +log info: "%s", s; +``` + +In addition to the operations described above, a string s also +supports the following operations: + +* _Indexing_:
s[idx]
+ + Retrieves the character at index idx within s. + idx must be non-negative, and less than the length of the string + (which does *not* include the final NUL character). Failure to uphold + this will result in a failed assertion at run-time. + + s[idx] is a writable expression as long as _`s`_ + is writable and not of const-qualified type. However, + s[idx] is never _addressable_, i.e. its address + cannot be taken using `&`. In order to retrieve a pointer within the buffer + of a string, use `.c_str()`. + +*
s.c_str()
+ Retrieves a `NUL`-terminated C string corresponding to _`s`_. + + The pointer retrieved from `.c_str()` will only remain valid as long as the + value referenced by s *remains alive* and *is not modified beyond + the mutation of preexisting elements*. In particular, for the pointer to be + valid it is necessary (but not sufficient) that the length of the string + remains unchanged. Any attempt to use the pointer past the point it is + invalidated is undefined behavior, and so `.c_str()` must be called again + to retrieve a new pointer. For example: + ``` + local string s = "I am a dtring"; + local char *s_ptr = s.c_str(); + assert s_ptr[7] == 'd'; // OK + s[7] = 's'; + assert s_ptr[7] == 's'; // OK + s += "ly string"; + assert s_ptr[7] == 's'; // BAD: undefined behavior + s_ptr = s.c_str(); + assert s_ptr[7] == 's'; // OK + s = mk_string_f("%sly string", s_ptr); // OK! + assert s_ptr[7] == 's'; // BAD: undefined behavior + ``` + + The type of the retrieved pointer will be `char *` if _`s`_ is both + writable and not of const-qualified type. Otherwise, the type of the + pointer will be `const char *`. If the former, then it is allowed to use the + pointer to mutate characters at indices less than the length of the string (it + is *not* allowed to mutate the final `NUL` character) as long as the pointer + remains valid as described above. Any character mutated this way will be + reflected in the DML string the pointer was retrieved from. + +*
s.len
+ Evaluates to the length of the string (which does *not* include the final + `NUL` character.) + + s.len is a writable expression as long as _`s`_ is writable + and not of const-qualified type. Writing to s.len will cause + _`s`_ to be resized to the specified length; truncating the string if the new + length is less than the previous one, and padding it with `NUL` characters + if the new length is greater than the previous one (these `NUL` characters + are at indices less than the length of the list, and so are allowed to be + indexed and/or overwritten.) +*
mk_vect_from_string(s)
+ Creates a `vect(char)` [vector](#vectors) from a string; its length is the + as `s`, and its elements are the characters of `s` in order (excluding the + final `NUL` character). + + Note that this does not consume or otherwise mutate _`s`_; it remains valid + and unchanged. + +Strings have a maximum length of 2147483647 characters. Exceeding this limit +is considered undefined behavior, but will with all likelihood result in a +failed run-time assertion. + +#### Vectors +
+vect(elem-type)
+
+DML vectors are dynamic arrays with support for efficient double-ended queue +operations — adding or removing an element from either end of a vector +is an O(1) (amortized) operation. These properties makes DML vectors suitable +for a large number of applications. + +`vect` supports the use of compound initializers: +``` +local vect(int) s = {1, 2, 3}; +s += {4, 5}; +``` + +Vector values are copied by contents: +``` +local vect(int) v1 = {1, 2, 3}; +local vect(int) v2 = s1; +v2 += {4, 5}; // Does not affect s1. +assert v1.len == 3 && s2.len == 5; +``` + +Vectors may be concatenated using `+` as long as they share the same +_`elem-type`_. Unlike [`string`s](#strings), vectors do not support any +comparison operator; element-by-element comparisons must be done by iterating +through the vector. Note also that `v + {6, 7}` is not valid, as compound +initializers are not valid expressions; instead, a cast must be used to create a +vector out of the initializer: `v + cast({6, 7}, vect(int))`. + +The [`foreach`](#foreach-statements) supports iteration on vectors: +``` +local vect(int) v = {1, 2, 3}; +foreach elem in (v) { + // The itered element is considered writable (but not addressable) + // as long as the vector or element type is not const-qualified + elem += 1; +} +assert v[0] == 2 && v[1] == 3 && v[2] == 4; +``` +`foreach` on vectors are restricted to addressable vector expressions — +i.e. those who may have their address taken. This restriction allows for safe +and predictable semantics while making as few assumptions as possible; if this +prevents the use of `foreach` for a particular use-case, then consider binding +the vector expression to variable. If all else fails, consider using a standard +`for`-loop instead. + +In addition to the operations described above, a vector v also +supports the following operations: + +* _Indexing_:
v[idx]
+ + Retrieves the element at index idx within s. + idx must be non-negative, and less than the length of the vector. + Failure to uphold this will result in a failed assertion at run-time. + + v[idx] is a writable expression as long as _`s`_ + is writable and not of const-qualified type. However, + v[idx] is never _addressable_, i.e. its address + cannot be taken using `&`. In order to retrieve a pointer within the buffer + of a vector, use `.c_buf()`. + +*
v.c_buf()
+ Retrieves a pointer to a continuous array of the vector's elements. Unlike + `string`'s `.c_str()`, `.c_buf()` may only be used if _`v`_ is writable and + not of const-qualified type. (This is because the underlying buffer of _`v`_ + may need to be reordered to make the elements continuous in memory.) + This also means that the type of the retrieved pointer will always be + elem-type * + + The pointer retrieved from `.c_buf()` will only remain valid as long as the + value referenced by s *remains alive* and *has not been modified + beyond the mutation of preexisting elements*. In particular, for the pointer + to be valid it is necessary (but not sufficient) that the length of vector + remains unchanged. Any attempt to use the pointer past the point it is + invalidated is undefined behavior, and so `.c_buf()` must be called again to + retrieve a new pointer. + + Note that although `.c_buf()` may mutate the vector it's used with, a call to + `.c_buf()` is guaranteed not to invalidate any pointer retrieved from a + previous call to `c_buf()` (not already invalidated). + ``` + local vect(int) v = {1, 2, 7}; + local int *v_ptr = v.c_buf(); + assert v_ptr[2] == 7; // OK + v.c_buf()[2] = 3; + assert v_ptr[2] == 3; // OK + v += {4, 5}; + assert v_ptr[2] == 3; // BAD: undefined behavior + v_ptr = v.c_buf(); + assert v_ptr[2] == 3; // OK + v = {1, 2, 3} + assert v_ptr[2] == 3; // BAD: undefined behavior + ``` + + writable and not of const-qualified type. Otherwise, the type of the + pointer will be `const char *`. If the former, then it is allowed to use the + pointer to mutate characters at indices less than the length of the string (it + is *not* allowed to mutate the final `NUL` character) as long as the pointer + remains valid as described above. + +*
v.push_back(item)
+ Adds _`item`_ to _`v`_ as its last element. +*
v.push_front(item)
+ Adds _`item`_ to _`v`_ as its first element. +*
v.pop_back(item)
+ Remove and return the last element of _`v`_. This requires the vector to be + non-empty. If that is not upheld, then an assertion will be failed at + run-time. +*
v.pop_front(item)
+ Remove and return the first element of _`v`_. This requires the vector to be + non-empty. If that is not upheld, then an assertion will be failed at + run-time. +*
v.insert(index, item)
+ Inserts _`item`_ at index _`index`_ within _`v`_. This operation is O(n) + unless _`index`_ is up to one element away from either end of the vector, in + which case it's O(1) amortized. +*
v.len
+ Evaluates to the length of the vector. + + v.len is a writable expression as long as _`v`_ is writable + and not of const-qualified type. Writing to v.len will cause + _`v`_ to be resized to the specified length; truncating the vector if the new + length is less than the previous one, and padding it with zero-initialized + elements if the new length is greater than the previous one. +*
mk_string_from_vect(v)
+ Creates a [`string`](#strings) from a `vect(char)`; its length will be + the same as `v`, and its characters will be the same as the elements of `v`. + Note that this does not consume or otherwise mutate _`v`_; it remains valid + and unchanged. + +Vectors have a maximum length of 2147483648 elements. Exceeding this limit is +considered undefined behavior, but will with all likelihood result in a failed +run-time assertion. + ## Methods
@@ -3040,6 +3341,7 @@ DML adds the following statements:
 target1 [= target2 = ...] = initializer;
 (target1, target2, ...) = initializer;
+target assignop initializer;
 
Assign values to targets according to an initializer. Unlike C, assignments are @@ -3071,6 +3373,25 @@ contents of variables through the following: (a, b) = (b, a) ``` +The third form constitues what is known in C as "compound assignment"; mutation +of the target according to a specified operator, i.e.: +``` +a += 1; +a -= 1; +a |= 1; +... +``` +Though the right-hand side permits arbitrary initializer syntax, the types that +compound assignment support typically restrict the initializer to be a simple +expression. The one exception to this are [`string`](#strings) and [vector +types](#vectors), which support the use of `+=` together with a string or +compound initializer, respectively: +``` +local (string s, vect(int) v) = ("I am", {1, 2}); +s += " a string"; +v += {3, 4}; +``` + ### Local Statements
 local type identifier [= initializer];
@@ -3587,8 +3908,9 @@ The `foreach` statement repeats its body (the
 The *`identifier`* is used to refer to the current element
 within the body.
 
-DML currently only supports `foreach` iteration on values of `sequence` types
-— which are created through [Each-In expressions](#each-in-expressions).
+DML currently only supports `foreach` iteration on values of [`vect`
+types](#vectors) or `sequence` types — the latter of which are created
+through [Each-In expressions](#each-in-expressions).
 
 The `break` statement can be used within a `foreach` loop to exit it.
 
@@ -3778,10 +4100,12 @@ will give a compile error unless it appears in one of the following contexts:
 ### Method References as Function Pointers
 It is possible to retrieve a function pointer for a method by using the prefix
 operator `&` with a reference to that method. The methods this is possible with
-are subject to the same restrictions as with the [`export` object
+are subject to most of the same restrictions as with the [`export` object
 statement](#export-declarations): it's not possible to retrieve a function
 pointer to any inline method, shared method, method that throws, method with
-more than one return argument, or method declared inside an object array.
+more than one return argument, or method declared inside an object array. Unlike
+`export`, `&` may be used with methods that have input parameters or return
+values of [resource-enriched type](#raii-types).
 
 For example, with the following method in DML:
 ```
diff --git a/include/simics/dmllib.h b/include/simics/dmllib.h
index 96f027454..2fda5b08c 100644
--- a/include/simics/dmllib.h
+++ b/include/simics/dmllib.h
@@ -24,6 +24,8 @@
 #include 
 #include 
 
+#include 
+
 // Copy bits from y given by mask into corresponding bits in x and return the
 // result
 static inline uint64 DML_combine_bits(uint64 x, uint64 y, uint64 mask)
@@ -150,6 +152,11 @@ DML_eq(uint64 a, uint64 b)
         return ((a ^ b) | ((b | a) >> 63)) == 0;
 }
 
+#define DML_SAFE_ASSIGN(dest, src) (({                                        \
+        typeof(dest) __tmp = src;                                             \
+        dest = __tmp;                                                         \
+    }))
+
 
 typedef struct {
     uint32 id;
@@ -1091,8 +1098,9 @@ _deserialize_simple_event_arguments(
             *out_args = NULL;
             return Sim_Set_Ok;
         } else {
+            // ZALLOC is critical for deserialization of values of RAII type
             void *temp_out_args =
-                args_size ? MM_MALLOC(args_size, uint8) : NULL;
+                args_size ? MM_ZALLOC(args_size, uint8) : NULL;
             set_error_t error = args_deserializer(*arguments_attr,
                                                   temp_out_args);
             if (error != Sim_Set_Ok) {
@@ -1219,14 +1227,16 @@ _deserialize_simple_event_data(
         dimsizes, dimensions, indices_attr, &temp_out.indices);
     if (error != Sim_Set_Ok) goto error;
 
-    error = _deserialize_simple_event_arguments(
-        args_size, args_deserializer, arguments_attr, &temp_out.args);
-    if (error != Sim_Set_Ok) goto error;
-
     error = _deserialize_simple_event_domains(
         id_info_ht, domains_attr, &temp_out.domains, &temp_out.no_domains);
     if (error != Sim_Set_Ok) goto error;
 
+    // Deserializing arguments last means we need not deal with destroying it
+    // in case of a later error
+    error = _deserialize_simple_event_arguments(
+        args_size, args_deserializer, arguments_attr, &temp_out.args);
+    if (error != Sim_Set_Ok) goto error;
+
     _simple_event_data_t *out_ptr = MM_MALLOC(1, _simple_event_data_t);
     *out_ptr = temp_out;
     *out = out_ptr;
@@ -1265,6 +1275,7 @@ _DML_create_simple_event_data(
 typedef struct {
     void (*callback)(conf_object_t *dev, const uint32 *indices,
                      const void *args);
+    void (*args_destructor)(void *args);
     _simple_event_data_t data;
 } _dml_immediate_after_queue_elem_t;
 
@@ -1321,11 +1332,13 @@ _DML_post_immediate_after(
     conf_object_t *dev,
     _dml_immediate_after_state_t *state,
     void (*callback)(conf_object_t *, const uint32 *, const void *),
+    void (*args_destructor)(void *),
     const uint32 *indices, uint32 dimensions, const void *args,
     size_t args_size, const _identity_t *domains, uint32 no_domains) {
     ASSERT(!state->deleted);
     _dml_immediate_after_queue_elem_t elem = {
         .callback = callback,
+        .args_destructor = args_destructor,
         .data = _DML_create_simple_event_data(
             indices, dimensions, args, args_size, domains, no_domains)
     };
@@ -1353,6 +1366,8 @@ _DML_cancel_immediate_afters(_dml_immediate_after_state_t *state,
             }
         }
         if (found) {
+            if (elem.args_destructor)
+                elem.args_destructor(elem.data.args);
             _free_simple_event_data(elem.data);
         } else {
             QADD(new_queue, elem);
@@ -1378,24 +1393,23 @@ _DML_cancel_immediate_afters(_dml_immediate_after_state_t *state,
 // Any after-on-hooks that are identical in respect to these three things
 // share the same set of after-on-hook info.
 typedef struct {
-    const char *callback_key;
     void (*callback)(conf_object_t *dev, const uint32 *indices,
                      const void *args, const void *msg);
-    _serializer_t args_serializer;
-    _deserializer_t args_deserializer;
+    void (*args_destructor)(void *);
     // Size of the data args_de/serializer works with
     uint32 args_size;
     // id of the object parent of the target callback method
     uint32 method_parent_id;
+    const char *callback_key;
+    _serializer_t args_serializer;
+    _deserializer_t args_deserializer;
 } _dml_after_on_hook_info_t;
 
 // An element of a hook queue. Currently, the only elements of a hook queue
 // are calls suspended from an after-on-hook, so that's what this represents.
 typedef struct {
-    void (*callback)(conf_object_t *dev, const uint32 *indices,
-                     const void *args, const void *msg);
+    const _dml_after_on_hook_info_t *info;
     _simple_event_data_t data;
-    const char *callback_key;
 } _dml_hook_queue_elem_t;
 
 // A hook queue, which may or may not have been detached.
@@ -1492,6 +1506,8 @@ _DML_resolve_hookref(void *dev, const _dml_hook_aux_info_t *hook_aux_infos,
 UNUSED static void
 _DML_free_hook_queue(_dml_hook_queue_t *q) {
     VFORT(*q, _dml_hook_queue_elem_t, elem) {
+        if (elem.info->args_destructor)
+            elem.info->args_destructor(elem.data.args);
         _free_simple_event_data(elem.data);
     }
     VFREE(*q);
@@ -1504,13 +1520,24 @@ _DML_attach_callback_to_hook(
     const _identity_t *domains, uint32 no_domains) {
     uint32 args_size = info->args_size;
     _dml_hook_queue_elem_t elem = {
-        .callback = info->callback,
-        .callback_key = info->callback_key,
+        .info = info,
         .data = _DML_create_simple_event_data(
-            indices, dimensions, args, args_size, domains, no_domains)};
+            indices, dimensions, args, args_size, domains, no_domains)
+    };
     VADD(hook->queue, elem);
 }
 
+
+#define _DML_SEND_HOOK(hookref, msg)                                          \
+    (({ _DML_send_hook(&_dev->obj, &_dev->_detached_hook_queue_stack,         \
+                       hookref, msg); }))
+
+#define _DML_SEND_HOOK_RAII(hookref, msg, destructor) (({                     \
+    void *__msg = (void *)(msg);                                              \
+    uint64 __resumed = _DML_SEND_HOOK(hookref, __msg);                        \
+    destructor(__msg);                                                        \
+    __resumed; }))
+
 UNUSED static uint64
 _DML_send_hook(conf_object_t *dev,
                _dml_detached_hook_queue_t **detached_queue_stack,
@@ -1533,7 +1560,7 @@ _DML_send_hook(conf_object_t *dev,
     VFORI(detached_queue, i) {
         _dml_hook_queue_elem_t elem = VGET(detached_queue, i);
         ++detached_queue_ref.processed_elems;
-        elem.callback(dev, elem.data.indices, elem.data.args, msg);
+        elem.info->callback(dev, elem.data.indices, elem.data.args, msg);
         _free_simple_event_data(elem.data);
     }
     if (detached_queue_ref.next) {
@@ -1582,6 +1609,8 @@ _DML_cancel_afters_in_hook_queue(
             }
         }
         if (found) {
+            if (elem.info->args_destructor)
+                elem.info->args_destructor(elem.data.args);
             _free_simple_event_data(elem.data);
         } else {
             VADD(new_queue, elem);
@@ -1664,22 +1693,22 @@ _DML_deserialize_hook_queue_elem_after(ht_str_table_t *callback_ht,
         goto error;
     }
 
-    error = _deserialize_simple_event_arguments(
-        callback_info->args_size, callback_info->args_deserializer,
-        &arguments_attr, &data.args);
+    error = _deserialize_simple_event_domains(
+        id_info_ht, &domains_attr, &data.domains, &data.no_domains);
     if (unlikely(error != Sim_Set_Ok)) {
         goto error;
     }
 
-    error = _deserialize_simple_event_domains(
-        id_info_ht, &domains_attr, &data.domains, &data.no_domains);
+    error = _deserialize_simple_event_arguments(
+        callback_info->args_size, callback_info->args_deserializer,
+        &arguments_attr, &data.args);
     if (unlikely(error != Sim_Set_Ok)) {
         goto error;
     }
 
-    e->callback = callback_info->callback;
+
+    e->info = callback_info;
     e->data = data;
-    e->callback_key = callback_info->callback_key;
     return Sim_Set_Ok;
   error:
     _free_simple_event_data(data);
@@ -1691,12 +1720,9 @@ UNUSED static attr_value_t
 _DML_serialize_hook_queue_elem_after(ht_str_table_t *callback_ht,
                                      const _id_info_t *id_infos,
                                      _dml_hook_queue_elem_t elem) {
-    _dml_after_on_hook_info_t *callback_info =
-        ht_lookup_str(callback_ht, elem.callback_key);
-    ASSERT(callback_info);
-    _id_info_t id_info = id_infos[callback_info->method_parent_id - 1];
+    _id_info_t id_info = id_infos[elem.info->method_parent_id - 1];
     attr_value_t callback_key_attr
-        = SIM_make_attr_string(callback_info->callback_key);
+        = SIM_make_attr_string(elem.info->callback_key);
 
     attr_value_t indices_attr = SIM_alloc_attr_list(id_info.dimensions);
     attr_value_t *indices_attr_list = SIM_attr_list(indices_attr);
@@ -1704,8 +1730,8 @@ _DML_serialize_hook_queue_elem_after(ht_str_table_t *callback_ht,
         indices_attr_list[i] = SIM_make_attr_uint64(elem.data.indices[i]);
     }
     attr_value_t arguments_attr =
-          callback_info->args_serializer
-        ? callback_info->args_serializer(elem.data.args)
+          elem.info->args_serializer
+        ? elem.info->args_serializer(elem.data.args)
         : SIM_make_attr_list(0);
 
     attr_value_t domains_attr = SIM_alloc_attr_list(elem.data.no_domains);
@@ -2149,7 +2175,8 @@ _DML_get_qname(_identity_t id, const _id_info_t *id_infos,
 
     uint32 indices[info.dimensions];
     uint32 index = id.encoded_index;
-    for (int32 i = info.dimensions - 1; i >= 0; --i) {
+    for (uint32 i_ = 0; i_ < info.dimensions; ++i_) {
+        uint32 i = info.dimensions - 1 - i_;
         indices[i] = index % info.dimsizes[i];
         index /= info.dimsizes[i];
     }
@@ -2937,6 +2964,362 @@ _callback_after_write(conf_object_t *bank,
                                   Callback_After_Write);
 }
 
+
+typedef void (*_raii_copier_t)(void *tgt, const void *src);
+typedef void (*_raii_destructor_t)(void *data);
+
+typedef struct {
+    const _raii_destructor_t *destructor_ref;
+    char data[] __attribute__((aligned));
+} _loose_allocation_t;
+
+typedef struct {
+    _raii_destructor_t destructor;
+    char data[] __attribute__((aligned));
+} _orphan_allocation_t;
+
+typedef struct {
+    _raii_destructor_t destructor;
+    void *data;
+} _scope_allocation_t;
+
+#define DML_NEW(t, count, destroy_ref) ({                           \
+    _loose_allocation_t *__alloc =                                  \
+        mm_zalloc(sizeof(_loose_allocation_t)                       \
+                  + sizeof(t)*(size_t)(count),                      \
+                  sizeof(t),                                        \
+                  #t " (DML allocated)",                            \
+                  __FILE__, __LINE__);                              \
+    __alloc->destructor_ref = destroy_ref;                          \
+    (typeof(t) *)__alloc->data; })
+
+#define DML_NEW_ORPHAN(t, destroy) (({                                \
+    _orphan_allocation_t *__alloc =                                   \
+        mm_zalloc(sizeof(_orphan_allocation_t)                        \
+                  + sizeof(t),                                        \
+                  sizeof(t),                                          \
+                  "orphan temporary (DML allocated)",                 \
+                  __FILE__, __LINE__);                                \
+    __alloc->destructor = destroy;                                    \
+    (typeof(t) *)__alloc->data; }))
+
+#define DML_RAII_SCOPE_LVAL(n, destructor, x) do {                            \
+        ASSERT_MSG(_scope_allocs_lens[n]                                      \
+                   < sizeof(_scope_ ## n ## _allocs)                          \
+                     /sizeof(_scope_allocation_t),                            \
+                   "RAII allocation buffer overrun. "                         \
+                   "Report this as a DMLC bug.");                             \
+        _scope_ ## n ## _allocs[_scope_allocs_lens[n]++] =                    \
+            (_scope_allocation_t) { destructor, (void *)&x };                 \
+    } while (0)
+
+
+#define DML_RAII_SCOPE_ORPHAN(n, destructor, x) (*({            \
+    typeof(x) *__alloc = DML_NEW_ORPHAN(typeof(x), destructor); \
+    memcpy(__alloc, (typeof(x)[1]) { x }, sizeof(*__alloc));    \
+    DML_RAII_SCOPE_LVAL(n, _DML_delete_orphan, *__alloc);       \
+    __alloc; }))
+
+#define DML_RAII_SCOPE_ARRAY_ORPHAN(n, destructor, x) (*({      \
+    typeof(x) *__alloc = DML_NEW_ORPHAN(typeof(x), destructor); \
+    memcpy(__alloc, x, sizeof(*__alloc));                       \
+    DML_RAII_SCOPE_LVAL(n, _DML_delete_orphan, *__alloc);       \
+    __alloc; }))
+
+#define DML_RAII_SESSION_ORPHAN(lval, destructor, x) (*({                     \
+    typeof(x) *__alloc = DML_NEW_ORPHAN(typeof(x), destructor);               \
+    memcpy(__alloc, (typeof(x)[1]) { x }, sizeof(*__alloc));                  \
+    lval = __alloc;                                                           \
+    __alloc; }))
+
+#define DML_RAII_SESSION_ARRAY_ORPHAN(lval, destructor, x) (*({               \
+    typeof(x) *__alloc = DML_NEW_ORPHAN(typeof(x), 1, destructor);            \
+    memcpy(__alloc, x, sizeof(*__alloc));                                     \
+    lval = (void *)__alloc;                                                   \
+    __alloc; }))
+
+#define DML_STATIC_ARRAY(t, x) (*({                                           \
+    static typeof(t) __array;                                                 \
+    memcpy(__array, x, sizeof(t));                                            \
+    &__array;}))
+
+#define DML_STATIC_ARRAY_CONSTSAFE(x) (*({                                    \
+    typeof(x) *__array = MM_MALLOC(1, typeof(x));                             \
+    memcpy(*__array, x, sizeof(x));                                           \
+    __array; }))
+
+#define DML_RAII_SCOPE_CLEANUP(n) do {                           \
+        _DML_cleanup_raii_scope(_scope_ ## n ## _allocs,         \
+                                _scope_allocs_lens[n]);    \
+        _scope_allocs_lens[n] = 0;                         \
+    } while (0)
+
+UNUSED static void _DML_cleanup_raii_scope(_scope_allocation_t *allocs,
+                                           uint16 len) {
+    for (uint16 i = 0; i < len; ++i) {
+        allocs[i].destructor(allocs[i].data);
+    }
+}
+
+
+#define DML_DELETE(alloc) (                                                   \
+    _DML_delete(alloc,                                                        \
+                (uintptr_t)_dml_raii_destructors,                             \
+                (uintptr_t)_dml_raii_destructor_ref_none,                     \
+                __FILE__,                                                     \
+                __LINE__))
+
+UNUSED static void _DML_delete(void *_alloc,
+                               uintptr_t _dml_raii_destructors,
+                               uintptr_t _dml_raii_destructor_ref_none,
+                               const char *filename,
+                               int lineno) {
+    if (!_alloc)
+        return;
+    _loose_allocation_t *alloc = (_loose_allocation_t *)(
+        (char *)_alloc - offsetof(_loose_allocation_t, data));
+    if ((uintptr_t)alloc->destructor_ref != _dml_raii_destructor_ref_none) {
+        if (likely((uintptr_t)alloc->destructor_ref >= _dml_raii_destructors
+                   && (uintptr_t)alloc->destructor_ref
+                      < _dml_raii_destructor_ref_none)) {
+            (*alloc->destructor_ref)(alloc->data);
+        } else {
+            _signal_critical_error("%s:%d: DML 'delete' statement used on "
+                                   "pointer %p, which has not been allocated "
+                                   "with 'new'! "
+                                   "Any pointer external to a particular DML "
+                                   "device model should be deallocated using "
+                                   "'MM_FREE'.",
+                                   filename, lineno, _alloc);
+            return;
+        }
+    }
+    MM_FREE(alloc);
+}
+
+UNUSED static void _DML_delete_orphan(void *_alloc) {
+    if (!_alloc)
+        return;
+    _orphan_allocation_t *alloc = (_orphan_allocation_t *)(
+        (char *)_alloc - offsetof(_orphan_allocation_t, data));
+    if (alloc->destructor) {
+        alloc->destructor(alloc->data);
+    }
+    MM_FREE(alloc);
+}
+
+UNUSED static void _dml_vect_free_raii(
+    size_t elem_size, _raii_destructor_t elem_raii_destructor,
+    _dml_vect_t v) {
+    if (elem_raii_destructor) {
+        for (uint32 i = 0; i < v.len; ++i) {
+            elem_raii_destructor(
+                v.elements + DML_VECT_INDEX(v, i)*elem_size);
+        }
+    }
+    _dml_vect_free(v);
+}
+
+UNUSED static void _dml_vect_resize_raii(
+    size_t elem_size, _raii_destructor_t elem_raii_destructor,
+    _dml_vect_t *v, uint32 new_len) {
+    ASSERT(new_len >= v->len || elem_raii_destructor);
+    for (uint32 i = new_len; i < v->len; ++i) {
+        elem_raii_destructor(
+            v->elements + DML_VECT_INDEX(*v, i)*elem_size);
+    }
+    _dml_vect_resize(elem_size, v, new_len, true);
+}
+
+UNUSED static void _dml_vect_clear_raii(
+    size_t elem_size, _raii_destructor_t elem_raii_destructor,
+    _dml_vect_t *v) {
+    for (uint32 i = 0; i < v->len; ++i) {
+        elem_raii_destructor(
+            v->elements + DML_VECT_INDEX(*v, i)*elem_size);
+    }
+    _dml_vect_clear(elem_size, v);
+}
+
+UNUSED static void _dml_vect_resize_destructive_raii(
+    size_t elem_size, _raii_destructor_t elem_raii_destructor,
+    _dml_vect_t *v, uint32 new_len) {
+    for (uint32 i = 0; i < v->len; ++i) {
+        elem_raii_destructor(
+            v->elements + DML_VECT_INDEX(*v, i)*elem_size);
+    }
+    _dml_vect_resize_destructive(elem_size, v, new_len);
+}
+
+
+// TODO consider moving elements to the start
+UNUSED static void _dml_vect_copy_raii(
+    size_t elem_size, _raii_copier_t elem_raii_copier,
+    _raii_destructor_t elem_raii_destructor,
+    _dml_vect_t *tgt, _dml_vect_t src) {
+    if (unlikely((uintptr_t)tgt->elements == (uintptr_t)src.elements)) {
+        ASSERT(tgt->size == src.size && tgt->len == src.len
+               && tgt->start == src.start);
+        return;
+    }
+    _dml_vect_resize_raii(elem_size, elem_raii_destructor, tgt, src.len);
+    for (uint32 i = 0; i < src.len; ++i) {
+        elem_raii_copier(
+            tgt->elements + DML_VECT_INDEX(*tgt, i)*elem_size,
+            src.elements + DML_VECT_INDEX(src, i)*elem_size);
+    }
+}
+
+UNUSED static _dml_vect_t
+_dml_vect_add(size_t elem_size, _dml_vect_t a, _dml_vect_t b) {
+    _dml_vect_append(elem_size, &a, b);
+    return a;
+}
+
+UNUSED static void _dml_vect_append_raii(
+    size_t elem_size, _raii_copier_t elem_raii_copier, _dml_vect_t *tgt,
+    _dml_vect_t src) {
+    uint32 tgt_start = tgt->len, add_len = src.len;
+    if (!add_len) return;
+    bool alias = tgt->elements == src.elements;
+    _dml_vect_resize(elem_size, tgt, tgt->len + src.len, true);
+    if (unlikely(alias))
+        src = *tgt;
+    for (uint32 i = 0; i < add_len; ++i) {
+        elem_raii_copier(tgt->elements
+                         + DML_VECT_INDEX(*tgt, tgt_start + i)*elem_size,
+                         src.elements + DML_VECT_INDEX(src, i)*elem_size);
+    }
+}
+
+UNUSED static _dml_vect_t
+_dml_vect_add_raii(size_t elem_size, _raii_copier_t elem_raii_copier,
+                   _dml_vect_t a, _dml_vect_t b) {
+    _dml_vect_append_raii(elem_size, elem_raii_copier, &a, b);
+    return a;
+}
+
+UNUSED static void _dml_vect_set_compound_init_raii(
+    size_t elem_size, _raii_destructor_t elem_raii_destructor,
+    _dml_vect_t *tgt, const void *src, uint32 no_elements) {
+    if (!no_elements)
+        _dml_vect_clear_raii(elem_size, elem_raii_destructor, tgt);
+
+    _dml_vect_resize_destructive_raii(elem_size, elem_raii_destructor,
+                                      tgt, no_elements);
+    memcpy(tgt->elements, src, no_elements*elem_size);
+}
+
+// TODO(RAII): Just replace usages with _dml_vect_append_array?
+UNUSED static inline void _dml_vect_append_compound_init_raii(
+    size_t elem_size, _dml_vect_t *tgt, const void *src, uint32 no_elements) {
+    _dml_vect_append_array(elem_size, tgt, src, no_elements);
+}
+
+// TODO(RAII): Not currently used.
+#define _DML_RAII_MOVED(x) (({                                                \
+            typeof(x) *__val = &(x);                                          \
+            typeof(x) __ret = *__val;                                         \
+            memset((void *)__val, 0, sizeof(x));                              \
+            __ret;                                                            \
+    }))
+
+#define _DML_RAII_DUPE(t, copier, x) (({                                      \
+            typeof(t) __ret = {0}, __val = x;                                 \
+            copier(&__ret, &__val);                                           \
+            __ret;                                                            \
+        }))
+
+#define _DML_RAII_ZERO_OUT(destroy, lval) (({                                 \
+        void *__pointer = (void *)&(lval);                                    \
+        destroy(__pointer);                                                   \
+        memset(__pointer, 0, sizeof(lval));                                   \
+    }))
+
+#define _DML_RAII_COPY_RVAL(copier, dest, src) (({                            \
+        copier((void *)&(dest), (typeof(dest)[1]){(src)});                    \
+    }))
+
+#define _DML_RAII_DESTROY_RVAL(destroy, expr) (({                             \
+        destroy((void *)(typeof(expr)[1]){(expr)});                           \
+    }))
+
+// No aliasing checks; not needed.
+// Note that src must be evaluated before tgt is destroyed
+#define _DML_RAII_LINEAR_MOVE_SIMPLE_RVAL(t, destroy, tgt, src) (({           \
+        typeof(t) __src = src;                                                \
+        typeof(t) *__dest = (typeof(t) *)&(tgt);                              \
+        destroy(__dest);                                                      \
+        *__dest = __src;                                                      \
+    }))
+
+// No aliasing checks; not needed.
+// Note that src must be evaluated before tgt is destroyed
+#define _DML_RAII_LINEAR_MOVE_SIMPLE(t, destroy, tgt, src) (({                \
+        typeof(t) *__src = (typeof(t) *)&(src);                               \
+        typeof(t) *__dest = (typeof(t) *)&(tgt);                              \
+        destroy(__dest);                                                      \
+        *__dest = *__src;                                                     \
+    }))
+
+
+#define _DML_RAII_LINEAR_MOVE_MEMCPY(destroy, tgt, src) (({                   \
+        const void *__src = &(src);                                           \
+        void *__dest = (void *)&(tgt);                                        \
+        destroy(__dest);                                                      \
+        memcpy(__dest, __src, sizeof(tgt));                                   \
+    }))
+
+#define _DML_RAII_LINEAR_MOVE_MEMCPY_RVAL(destroy, tgt, src) (({              \
+        typeof(tgt) __src = src;                                              \
+        void *__dest = (void *)&(tgt);                                        \
+        destroy((void *)__dest);                                              \
+        memcpy((void *)dest, &__src, sizeof(tgt));                            \
+    }))
+
+UNUSED static void
+_DML_vector_nonraii_elems_destructor(void *data) {
+    _dml_vect_free(*(_dml_vect_t *)data);
+}
+
+UNUSED static void
+_DML_string_destructor(void *data) {
+    _dml_string_free(*(_dml_string_t *)data);
+}
+
+UNUSED static void
+_DML_string_copier(void *tgt, const void *src) {
+    _dml_string_copy((_dml_string_t *)tgt, *(const _dml_string_t *)src);
+}
+
+UNUSED static _dml_string_t
+_dml_string_add(_dml_string_t a, _dml_string_t b) {
+    _dml_string_cat(&a, b);
+    return a;
+}
+
+UNUSED static _dml_string_t
+_dml_string_add_cstr_before(const char *a, _dml_string_t b) {
+    _dml_string_addstr_before(a, &b);
+    return b;
+}
+
+UNUSED static _dml_string_t
+_dml_string_add_cstr(_dml_string_t a, const char *b) {
+    _dml_string_addstr(&a, b);
+    return a;
+}
+
+UNUSED static int
+_dml_string_cmp(_dml_string_t a, _dml_string_t b) {
+    return strcmp(_dml_string_str(a), _dml_string_str(b));
+}
+
+UNUSED static int
+_dml_string_cmp_c_str(_dml_string_t a, const char *b) {
+    return strcmp(_dml_string_str(a), b);
+}
+
 UNUSED static set_error_t
 _set_device_member(attr_value_t val,
                    char *ptr,
@@ -3175,83 +3558,173 @@ _serialize_array(const void *data, size_t elem_size,
                                 total_elem_count, serialize_elem);
 }
 
-
-static set_error_t
-_deserialize_array_aux(attr_value_t val, uint8 *data, size_t elem_size,
-                       const uint32 *dimsizes, uint32 dims,
-                       size_t total_elem_count,
-                       _deserializer_t deserialize_elem,
-                       bool elems_are_bytes) {
-    uint32 len = *dimsizes;
-    ASSERT(len != 0);
-
-    // Allow the final dimension to be represented as data if elems_are_bytes
-    bool data_allowed = elems_are_bytes && dims == 1;
-    if (data_allowed && SIM_attr_is_data(val)) {
-        if (unlikely(SIM_attr_data_size(val) != len)) {
-            SIM_c_attribute_error(
-                "Invalid serialized value of byte array: expected data of %u "
-                "bytes, got %u", len, SIM_attr_data_size(val));
-            return Sim_Set_Illegal_Value;
-        }
-        memcpy(data, SIM_attr_data(val), len);
-        return Sim_Set_Ok;
-    } else if (unlikely(!SIM_attr_is_list(val))) {
-        if (data_allowed) {
-            SIM_attribute_error("Invalid serialized representation of byte "
-                                "array: not a list or data");
-        } else {
-            SIM_attribute_error("Invalid serialized representation of array: "
-                                "not a list");
+UNUSED static set_error_t
+_deserialize_array(attr_value_t in_val, void *out_data, size_t elem_size,
+                   const uint32 *dimsizes, uint32 dims,
+                   _deserializer_t deserialize_elem,
+                   _raii_destructor_t elem_raii_destructor,
+                   bool elems_are_bytes) {
+    uint32 outer_dims_lists = 1;
+    for (uint32 i = 0; i < dims - 1; ++i) {
+        outer_dims_lists *= dimsizes[i];
+    }
+    uint32 total_elem_count = outer_dims_lists*dimsizes[dims-1];
+    attr_value_t *lists = MM_MALLOC(outer_dims_lists, attr_value_t);
+    attr_value_t *lists_buf = MM_MALLOC(outer_dims_lists, attr_value_t);
+    *lists = in_val;
+    size_t no_lists = 1;
+    for (uint32 i = 0; i < dims - 1; ++i) {
+        uint32 frag_size = dimsizes[i];
+        size_t new_no_lists = no_lists * frag_size;
+        for (uint32 j = 0; j < no_lists; ++j) {
+            if (unlikely(!SIM_attr_is_list(lists[j]))) {
+                SIM_attribute_error("Invalid serialized representation of "
+                                    "array: not a list");
+                goto flatten_failure;
+            } else if (unlikely(SIM_attr_list_size(lists[j]) != frag_size)) {
+                SIM_c_attribute_error(
+                    "Invalid serialized representation of array: expected "
+                    "list of %u elements, got %u",
+                    frag_size, SIM_attr_list_size(lists[j]));
+                goto flatten_failure;
+            }
+            attr_value_t *subfrags = SIM_attr_list(lists[j]);
+            for (int k = 0; k < frag_size; ++k) {
+                lists_buf[j * frag_size + k] = subfrags[k];
+            }
         }
-        return Sim_Set_Illegal_Type;
-    } else if (unlikely(SIM_attr_list_size(val) != len)) {
-        SIM_c_attribute_error(
-            "Invalid serialized representation of %sarray: expected list of "
-            "%u elements, got %u", data_allowed ? "byte " : "", len,
-            SIM_attr_list_size(val));
+        attr_value_t *prev_lists = lists;
+        lists = lists_buf;
+        lists_buf = prev_lists;
+        no_lists = new_no_lists;
+    }
+    if (0) {
+      flatten_failure:
+        MM_FREE(lists);
+        MM_FREE(lists_buf);
         return Sim_Set_Illegal_Type;
     }
-    attr_value_t *items = SIM_attr_list(val);
-    size_t children_elem_count = total_elem_count/len;
-    for (uint32 i = 0; i < len; ++i) {
-        set_error_t error;
-        if (dims == 1) {
-            error = deserialize_elem(items[i], &data[i*elem_size]);
+    MM_FREE(lists_buf);
+    uint8 *tmp_out = elem_raii_destructor
+        ? MM_MALLOC(total_elem_count*elem_size, uint8)
+        : MM_ZALLOC(total_elem_count*elem_size, uint8);
+    set_error_t error = Sim_Set_Ok;
+    uint32 processed = 0;
+    uint32 last_dimsize = dimsizes[dims-1];
+    for (uint32 i = 0; i < no_lists; ++i) {
+        attr_value_t list = lists[i];
+        if (elems_are_bytes && SIM_attr_is_data(list)) {
+            if (unlikely(SIM_attr_data_size(list) != last_dimsize)) {
+                SIM_c_attribute_error(
+                    "Invalid serialized value of byte array: expected "
+                    "data of %u bytes, got %u", last_dimsize,
+                    SIM_attr_data_size(list));
+                error = Sim_Set_Illegal_Value;
+                goto elem_deserialization_failure;
+            }
+            memcpy(tmp_out + last_dimsize*i, SIM_attr_data(list),
+                   last_dimsize);
+            processed += last_dimsize;
+        } else if (likely(SIM_attr_is_list(list))) {
+            if (unlikely(SIM_attr_list_size(list) != last_dimsize)) {
+                SIM_c_attribute_error(
+                    "Invalid serialized representation of %sarray: "
+                    "expected list of %u elements, got %u",
+                    elems_are_bytes ? "byte " : "", last_dimsize,
+                    SIM_attr_list_size(list));
+                error = Sim_Set_Illegal_Type;
+                goto elem_deserialization_failure;
+            }
+            attr_value_t *items = SIM_attr_list(list);
+            for (uint32 j = 0; j < last_dimsize; ++j) {
+                error = deserialize_elem(
+                    items[j], &tmp_out[(last_dimsize*i + j)*elem_size]);
+                if (unlikely(error != Sim_Set_Ok))
+                    goto elem_deserialization_failure;
+                ++processed;
+            }
         } else {
-            error = _deserialize_array_aux(
-                items[i], &data[i*children_elem_count*elem_size], elem_size,
-                dimsizes + 1, dims - 1, children_elem_count, deserialize_elem,
-                elems_are_bytes);
+            SIM_attribute_error(
+                  elems_are_bytes
+                ? "Invalid serialized representation of byte array: not a "
+                  "list or data"
+                : "Invalid serialized representation of array: not a list");
+            error = Sim_Set_Illegal_Type;
+            goto elem_deserialization_failure;
         }
-        if (error != Sim_Set_Ok) {
-            return error;
+    }
+  elem_deserialization_failure:
+    MM_FREE(lists);
+    if (unlikely(error != Sim_Set_Ok)) {
+        if (elem_raii_destructor) {
+            for (uint32 i = 0; i < processed; ++i)
+                elem_raii_destructor(tmp_out + i*elem_size);
+        }
+    } else {
+        if (elem_raii_destructor) {
+            for (uint32 i = 0; i < total_elem_count; ++i)
+                elem_raii_destructor((char *)out_data + i*elem_size);
         }
+        memcpy(out_data, tmp_out, total_elem_count*elem_size);
     }
-    return Sim_Set_Ok;
+    MM_FREE(tmp_out);
+    return error;
 }
 
-UNUSED static set_error_t
-_deserialize_array(attr_value_t in_val, void *out_data, size_t elem_size,
-                   const uint32 *dimsizes, uint32 dims,
-                   _deserializer_t deserialize_elem, bool elems_are_bytes) {
-    ASSERT(dims > 0);
-    size_t total_elem_count = 1;
-    for (uint32 i = 0; i < dims; ++i) {
-        total_elem_count *= dimsizes[i];
+UNUSED static void _dml_vect_move_raii(
+    size_t elem_size, _raii_destructor_t elem_raii_destructor,
+    _dml_vect_t *tgt, _dml_vect_t src) {
+    if (unlikely((uintptr_t)tgt->elements == (uintptr_t)src.elements)) {
+        ASSERT(tgt->size == src.size && tgt->len == src.len
+               && tgt->start == src.start);
+        return;
     }
+    _dml_vect_free_raii(elem_size, elem_raii_destructor, *tgt);
+    *tgt = src;
+}
 
-    uint8 *temp_out = MM_MALLOC(total_elem_count * elem_size, uint8);
-    set_error_t error = _deserialize_array_aux(
-        in_val, temp_out, elem_size, dimsizes, dims, total_elem_count,
-        deserialize_elem, elems_are_bytes);
-    if (error == Sim_Set_Ok) {
-        memcpy(out_data, temp_out, total_elem_count*elem_size);
+UNUSED static attr_value_t
+_serialize_vector(_dml_vect_t v, size_t elem_size,
+                  _serializer_t serialize_elem) {
+    attr_value_t val = SIM_alloc_attr_list(v.len);
+    attr_value_t *items = SIM_attr_list(val);
+    for (uint32 i = 0; i < v.len; ++i) {
+        items[i] = serialize_elem(v.elements + DML_VECT_INDEX(v, i)*elem_size);
     }
-    MM_FREE(temp_out);
-    return error;
+    return val;
 }
 
+UNUSED static set_error_t
+_deserialize_vector(attr_value_t val, _dml_vect_t *tgt, size_t elem_size,
+                    _deserializer_t deserialize_elem,
+                    _raii_destructor_t elem_raii_destructor) {
+
+    if (unlikely(!SIM_attr_is_list(val))) {
+        SIM_attribute_error("Invalid serialized representation of vector: "
+                            "not a list");
+        return Sim_Set_Illegal_Type;
+    }
+    _dml_vect_t tmp_vect = {0};
+    _dml_vect_resize(elem_size, &tmp_vect, SIM_attr_list_size(val),
+                     elem_raii_destructor ? true : false);
+    ASSERT(tmp_vect.start == 0);
+    attr_value_t *items = SIM_attr_list(val);
+    for (uint32 i = 0; i < tmp_vect.len; ++i) {
+        set_error_t error = deserialize_elem(items[i],
+                                             tmp_vect.elements + i*elem_size);
+        if (error != Sim_Set_Ok) {
+            if (elem_raii_destructor) {
+                for (uint32 j = 0; j < i; ++j) {
+                    elem_raii_destructor(tmp_vect.elements + j*elem_size);
+                }
+            }
+            _dml_vect_free(tmp_vect);
+            return error;
+        }
+    }
+    _dml_vect_move_raii(elem_size, elem_raii_destructor, tgt, tmp_vect);
+    return Sim_Set_Ok;
+}
 
 // The internal format for ht is:
 // dict(trait_identifier -> dict(statement_idx -> bool))
@@ -3402,5 +3875,4 @@ UNUSED static void _DML_register_attributes(
         sb_free(&type);
     }
 }
-
 #endif
diff --git a/include/simics/dmlraiitypes.h b/include/simics/dmlraiitypes.h
new file mode 100644
index 000000000..b09f93b3d
--- /dev/null
+++ b/include/simics/dmlraiitypes.h
@@ -0,0 +1,1136 @@
+/*
+  © 2010-2023 Intel Corporation
+  SPDX-License-Identifier: 0BSD
+*/
+
+/* DML runtime utilities needed by the C code generated by dmlc */
+
+#ifndef SIMICS_DMLRAIITYPES_H
+#define SIMICS_DMLRAIITYPES_H
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+// Smallest power of two >= x, except _DML_BITCEIL(0) == 0
+#define _DML_BITCEIL(x) \
+    ((uint32)(x) <= 1 ? (uint32)(x) : (1 << (32 - __builtin_clz((x) - 1))))
+// Largest power of two <= x, except _DML_BITFLOOR(0) == 0
+#define _DML_BITFLOOR(x) \
+    ((uint32)(x) <= 1 ? (uint32)(x) : (1 << (31 - __builtin_clz(x))))
+
+// Variant of strbuf, but where 0-initialization is valid.
+// Invariants:
+// - size > len OR size == len == 0
+// - size > 0 implies s != NULL and s[len] == '\0'
+typedef struct {
+    char *s;
+    uint32 size;
+    uint32 len;
+} _dml_string_t;
+
+/* minimum allocation size */
+#define _DML_STRING_INITIAL_SIZE 32
+
+/* return string as C-string, always 0-terminated.
+   Only guaranteed valid as long as the argument is alive and
+   unmodified.
+   May be written to, but only at indices less than len
+   (the final NUL character may not be written to.) */
+UNUSED static inline char *
+_dml_string_str(_dml_string_t s) {
+    return s.s ? s.s : (char *)"";
+}
+
+#define DML_STRING_INBOUNDS(v, i)                           \
+    ASSERT_FMT((i) >= 0 && (i) < (v).len,                   \
+               "OOB index %u for DML string of length %u",  \
+               (unsigned) i, (v).len)
+
+#define DML_STRING_CHAR(str, i) (*({                                           \
+        _dml_string_t __string = (str);                                        \
+        uint32 __index = (i);                                                  \
+        DML_STRING_INBOUNDS(__string, __index);                                \
+        &__string.s[__index];                                                  \
+    }))
+
+UNUSED static inline void
+_dml_string_init(_dml_string_t *s) {
+	s->s = NULL;
+	s->size = 0;
+	s->len = 0;
+}
+
+UNUSED static void
+_dml_string_realloc(_dml_string_t *s, uint32 new_size) {
+    // new_size is a power of two or 0
+    ASSERT((new_size & (new_size - 1)) == 0);
+    if (unlikely(new_size == s->size)) {
+        return;
+    }
+    // TODO(RAII) This path is unused. Remove?
+    if (unlikely(new_size == 0)) {
+        MM_FREE(s->s);
+        s->s = NULL;
+        s->size = s->len = 0;
+        return;
+    }
+    s->s = MM_REALLOC_SZ(s->s, new_size, char);
+    s->size = new_size;
+}
+
+UNUSED static void
+_dml_string_realloc_for_len(_dml_string_t *s, uint32 new_len) {
+    ASSERT(s->len < s->size || (s->len == 0 && s->size == 0));
+    if (s->len == new_len) {
+        return;
+    }
+    uint32 new_size = s->size;
+    if ((s->size > _DML_STRING_INITIAL_SIZE && new_len + 1 < s->size/4)) {
+        new_size = MAX(_DML_STRING_INITIAL_SIZE, _DML_BITCEIL(new_len + 1)*2);
+    } else if (new_len + 1 > s->size) {
+        new_size = MAX(_DML_STRING_INITIAL_SIZE, _DML_BITCEIL(new_len + 1));
+    }
+    // This assert also catches if the buf grows to be larger than
+    // 2^32-1 elements
+    ASSERT(new_size >= new_len);
+    if (new_size != s->size)
+        _dml_string_realloc(s, new_size);
+}
+
+UNUSED static void
+_dml_string_resize(_dml_string_t *s, uint32 new_len) {
+    uint32 prev_len = s->len;
+    if (prev_len == new_len) return;
+    _dml_string_realloc_for_len(s, new_len);
+    s->len = new_len;
+    if (s->size) {
+        if (prev_len > new_len) {
+            s->s[new_len] = '\0';
+        } else {
+            memset(s->s + prev_len + 1, 0, new_len - prev_len);
+        }
+    }
+}
+
+UNUSED __attribute__ ((const)) static inline bool
+_dml_pointers_overlap(const void *const orig, const void *const possible_deriv,
+                      const size_t orig_size) {
+    return (uintptr_t)(orig) + orig_size > (uintptr_t)possible_deriv
+        && (uintptr_t)possible_deriv >= (uintptr_t)orig;
+}
+
+/* set a string to the contents of a C-string */
+UNUSED static void
+_dml_string_set(_dml_string_t *s, const char *str) {
+    if (unlikely(_dml_pointers_overlap(s->s, str, s->size))) {
+        uint32 new_len = s->len - (uint32)((uintptr_t)str - (uintptr_t)s->s);
+        if (new_len == s->len) return;
+        memmove(s->s, str, new_len + 1);
+        _dml_string_realloc_for_len(s, new_len);
+        s->len = new_len;
+        return;
+    }
+    uint32 len = strlen(str);
+    _dml_string_realloc_for_len(s, len);
+    if (s->size)
+        memcpy(s->s, str, len + 1);
+    s->len = len;
+}
+
+/* make a string empty */
+UNUSED static inline void
+_dml_string_clear(_dml_string_t *s)
+{
+    _dml_string_realloc_for_len(s, 0);
+    if (s->size)
+        s->s[0] = '\0';
+    s->len = 0;
+}
+
+/* set a string to the contents of another string */
+UNUSED static void
+_dml_string_copy(_dml_string_t *dst, _dml_string_t src) {
+    if (src.s == dst->s) {
+        ASSERT(dst->len == src.len && dst->size == src.size);
+        return;
+    }
+    if (src.len) {
+        _dml_string_realloc_for_len(dst, src.len);
+        memcpy(dst->s, src.s, src.len + 1);
+        dst->len = src.len;
+    } else {
+        _dml_string_clear(dst);
+    }
+}
+
+/* Free storage associated with a string, making it empty.
+   The string should not be used after calling this function. */
+UNUSED static void
+_dml_string_free(_dml_string_t s)
+{
+    MM_FREE(s.s);
+}
+
+UNUSED static inline void
+_dml_string_move(_dml_string_t *dst, _dml_string_t src) {
+    _dml_string_free(*dst);
+    *dst = src;
+}
+
+/* append s2 to s1 */
+UNUSED static void
+_dml_string_cat(_dml_string_t *s1, _dml_string_t s2)
+{
+    if (s2.len == 0) return;
+    if (unlikely(s1->s == s2.s)) {
+        ASSERT(s1->len == s2.len && s1->size == s2.size);
+        _dml_string_realloc_for_len(s1, 2*s2.len);
+        memcpy(s1->s + s2.len, s1->s, s2.len);
+        s1->len = 2*s2.len;
+        s1->s[s1->len] = '\0';
+    } else {
+        _dml_string_realloc_for_len(s1, s1->len + s2.len);
+        memcpy(s1->s + s1->len, s2.s, s2.len + 1);
+        s1->len += s2.len;
+    }
+}
+
+/* append a C-string to a strbuf */
+UNUSED static void
+_dml_string_addstr(_dml_string_t *s, const char *str)
+{
+    if (unlikely(_dml_pointers_overlap(s->s, str, s->size))) {
+        uint32 offset = (uintptr_t)str - (uintptr_t)s->s;
+        uint32 new_len = 2*s->len - offset;
+        if (new_len == s->len) return;
+        _dml_string_realloc_for_len(s, new_len);
+        memcpy(s->s + s->len, s->s + offset, s->len - offset);
+        s->s[new_len] = '\0';
+        s->len = new_len;
+        return;
+    }
+    uint32 len = strlen(str);
+    if (!len)
+        return;
+    _dml_string_realloc_for_len(s, s->len + len);
+    memcpy(s->s + s->len, str, len + 1);
+    s->len += len;
+}
+
+/* append a C-string to a strbuf */
+UNUSED static void
+_dml_string_addstr_before(const char *str, _dml_string_t *s)
+{
+    if (unlikely(_dml_pointers_overlap(s->s, str, s->size))) {
+        uint32 offset = (uintptr_t)str - (uintptr_t)s->s;
+        uint32 len = s->len - offset;
+        if (!len) return;
+        _dml_string_realloc_for_len(s, s->len + len);
+        memmove(s->s + len, s->s, s->len + 1);
+        memcpy(s->s, s->s + len + offset, len);
+        s->len += len;
+        return;
+    }
+    uint32 len = strlen(str);
+    if (!len) return;
+    _dml_string_realloc_for_len(s, s->len + len);
+    memmove(s->s + len, s->s, s->len + 1);
+    memcpy(s->s, str, len);
+    s->len += len;
+}
+
+/* append a counted string to a strbuf */
+UNUSED static void
+_dml_string_addmem(_dml_string_t *s, const char *str, uint32 len)
+{
+    if (!len)
+        return;
+    if (unlikely(_dml_pointers_overlap(s->s, str, s->size))) {
+        uint32 offset = (uintptr_t)str - (uintptr_t)s->s;
+        // + 1 as including the NUL byte is allowed
+        ASSERT(len <= s->len - offset + 1);
+        _dml_string_realloc_for_len(s, s->len + len);
+        memcpy(s->s + s->len, s->s + offset, len);
+        s->len += len;
+        s->s[s->len] = '\0';
+        return;
+    }
+    _dml_string_realloc_for_len(s, s->len + len);
+    memcpy(s->s + s->len, str, len);
+    s->len += len;
+    s->s[s->len] = '\0';
+}
+
+/* add a character to a string */
+UNUSED static void
+_dml_string_addc(_dml_string_t *s, char c)
+{
+    _dml_string_realloc_for_len(s, s->len + 1);
+    s->s[s->len++] = c;
+    s->s[s->len] = '\0';
+}
+
+/* add a character repeated a given number of times to a string */
+UNUSED static void
+_dml_string_addchars(_dml_string_t *s, char c, unsigned n)
+{
+    _dml_string_realloc_for_len(s, s->len + n);
+    memset(s->s + s->len, c, n);
+    s->len += n;
+    s->s[s->len] = '\0';
+}
+
+/* return a copy of a string */
+UNUSED static inline _dml_string_t
+_dml_string_dupe(_dml_string_t s)
+{
+    _dml_string_t new_s = {0};
+    _dml_string_cat(&new_s, s);
+    return new_s;
+}
+
+/* Delete at most n characters from position start.
+   If start is negative, count from the end. */
+UNUSED static void
+_dml_string_delete(_dml_string_t *s, int start, unsigned n)
+{
+    if (start < 0)
+        start = s->len + start;
+
+    /* Avoid deleting too many characters. */
+    if (start + n > s->len)
+        n = s->len - start;
+
+    if (s->len)
+        memmove(s->s + start, s->s + start + n,
+                s->len - (start + n) + 1);
+    _dml_string_realloc_for_len(s, s->len - n);
+    s->len -= n;
+}
+
+/* Insert n characters from str at index start (start <= s->len) */
+UNUSED static void
+_dml_string_insertmem(_dml_string_t *s, uint32 start, const char *str, uint32 n)
+{
+    ASSERT(start <= s->len);
+    if (!n) return;
+    bool overlap = _dml_pointers_overlap(s->s, str, s->size);
+    if (unlikely(overlap)) {
+        char *duped_str = MM_MALLOC(n, char);
+        memcpy(duped_str, str, n);
+        str = duped_str;
+    }
+    _dml_string_realloc_for_len(s, s->len + n);
+    memmove(s->s + start + n, s->s + start, s->len - start + 1);
+    memcpy(s->s + start, str, n);
+    s->len += n;
+    if (unlikely(overlap)) {
+        MM_FREE((char *)str);
+    }
+}
+
+/* Insert a zero-terminated string at index start (start <= s->len) */
+UNUSED static inline void
+_dml_string_insertstr(_dml_string_t *s, unsigned start, const char *str)
+{
+    _dml_string_insertmem(s, start, str, strlen(str));
+}
+
+/* return a fresh strbuf initialised to a copy of a C string */
+UNUSED static _dml_string_t
+_dml_string_new(const char *init)
+{
+    _dml_string_t s = {0};
+    _dml_string_set(&s, init);
+    return s;
+}
+
+
+/* return a fresh strbuf initialised to a copy of a counted C string */
+UNUSED static _dml_string_t
+_dml_string_new_counted(const char *init, uint32 size)
+{
+    _dml_string_t s = {0};
+    _dml_string_addmem(&s, init, size);
+    return s;
+}
+
+/* return allocated C-string, and clear strbuf */
+UNUSED static char *
+_dml_string_detach(_dml_string_t *s)
+{
+    char *detached = s->s;
+    if (s->size == 0) {
+        /* special case: returned malloced zero-sized string */
+        detached = MM_MALLOC(1, char);
+        *detached = 0;
+    }
+    _dml_string_init(s);
+    return detached;
+}
+
+/* output a string to a file. return number of characters written */
+UNUSED static int
+_dml_string_write(_dml_string_t s, FILE *f)
+{
+    return fwrite(_dml_string_str(s), 1, s.len, f);
+}
+
+/* append formatted text to string */
+UNUSED static void
+_dml_string_vaddfmt(_dml_string_t *s, const char *format, va_list va)
+{
+    va_list va2;
+    va_copy(va2, va);
+    int need = vsnprintf(NULL, 0, format, va2);
+    va_end(va2);
+    ASSERT(need >= 0);
+    // Inefficient, but avoids aliasing issues
+    char *tmp = MM_MALLOC(need + 1, char);
+    int need2 = vsnprintf(tmp, need + 1, format, va);
+    ASSERT(need == need2);
+    _dml_string_realloc_for_len(s, s->len + need);
+    if (s->size)
+        memcpy(s->s + s->len, tmp, need + 1);
+    s->len += need;
+    MM_FREE(tmp);
+}
+
+/* append formatted text to string */
+UNUSED PRINTF_FORMAT(2, 3) static void
+_dml_string_addfmt(_dml_string_t *s, const char *format, ...)
+{
+    va_list va;
+
+    va_start(va, format);
+    _dml_string_vaddfmt(s, format, va);
+    va_end(va);
+}
+
+/* return a fresh DML string initialised to a formatted string */
+UNUSED static _dml_string_t
+_dml_string_vnewf(const char *format, va_list va)
+{
+    va_list va2;
+    va_copy(va2, va);
+    int need = vsnprintf(NULL, 0, format, va2);
+    va_end(va2);
+    ASSERT(need >= 0);
+    _dml_string_t s = {0};
+    if (!need)
+        return s;
+    _dml_string_realloc_for_len(&s, need);
+    int need2 = vsnprintf(s.s, need + 1, format, va);
+    ASSERT(need == need2);
+    s.len = need;
+    return s;
+}
+
+/* return a fresh DML string initialised to a formatted string */
+UNUSED PRINTF_FORMAT(1, 2) static _dml_string_t
+_dml_string_newf(const char *format, ...)
+{
+    va_list va;
+    va_start(va, format);
+    _dml_string_t s = _dml_string_vnewf(format, va);
+    va_end(va);
+    return s;
+}
+
+
+/* assign formatted text to string */
+UNUSED static void
+_dml_string_vfmt(_dml_string_t *s, const char *format, va_list va)
+{
+    _dml_string_move(s, _dml_string_vnewf(format, va));
+}
+
+/* assign formatted text to string */
+UNUSED static void
+PRINTF_FORMAT(2, 3)
+_dml_string_fmt(_dml_string_t *s, const char *format, ...)
+{
+    va_list va;
+    va_start(va, format);
+    _dml_string_vfmt(s, format, va);
+    va_end(va);
+}
+
+/* Add character, escaped if not printable or if delimiter */
+UNUSED static void
+_dml_string_addesc(_dml_string_t *s, char c, char delim)
+{
+    static const char ctrlchars[] = {
+        0, 0, 0, 0, 0, 0, 0, 'a', 'b', 't', 'n', 'v', 'f', 'r'
+    };
+    unsigned char uc = c;
+    _dml_string_realloc_for_len(s, s->len + 4); /* room for "\377" */
+    /* Don't escape values in 0x80..0xff - it would severely reduce
+       readability of UTF-8 strings */
+    if (uc < 0x20 || uc == 0x7f) {
+        /* control char */
+        s->s[s->len++] = '\\';
+        if (uc < sizeof ctrlchars && ctrlchars[uc])
+            s->s[s->len++] = ctrlchars[uc];
+        else {
+            sprintf(s->s + s->len, "%03o", uc);
+            s->len += 3;
+        }
+    } else {
+        /* printable */
+        if (uc == delim || uc == '\\')
+            s->s[s->len++] = '\\';
+        s->s[s->len++] = uc;
+    }
+    s->s[s->len] = '\0';
+}
+
+/* Read a line from f into s, replacing previous contents if any, and
+   return true. If EOF was encountered before anything could be read,
+   return false. The contents of s are then undefined.
+   The buffer will not contain the terminating newline. The last line of
+   a stream will be read in even if not newline-terminated. */
+UNUSED static bool
+_dml_string_readline(_dml_string_t *s, FILE *f)
+{
+    /* BUG: This routine cannot read lines containing null bytes,
+       because fgets() does not tell us how many bytes it reads.
+
+       We could pad the buffer with nonzero bytes before calling
+       fgets() and detect the last null byte, but that would be
+       slow and complicate the code. Or we could do a simpler
+       solution based on getc_unlocked(), but we would have to
+       use plain getc() on Windows and that is quite slow. */
+
+    unsigned min_room = 128;
+    unsigned got = 0;
+    for (;;) {
+        unsigned room = s->len - got;
+        if (room < min_room) {
+            room = min_room;
+            _dml_string_resize(s, got + room);
+        }
+        if (!fgets(s->s + got, room, f)) {
+            if (got > 0)
+                break;
+            else
+                return false;
+        }
+        got += strlen(s->s + got);
+        if (got > 0 && s->s[got - 1] == '\n') {
+            got--;
+            break;
+        }
+    }
+    _dml_string_resize(s, got);
+    return true;
+}
+
+
+
+// Invariants:
+// - size is a power of two, or 0
+// - elements == NULL if and only if size == 0
+// - len <= size
+// - if size != 0 then start < size else start == 0
+typedef struct {
+    char *elements;
+    uint32 size;
+    uint32 start;
+    uint32 len;
+} _dml_vect_t;
+
+#define DML_VECT_INDEX(v, i) \
+    (((v).start + (i)) & ((v).size - 1))
+
+
+#define DML_VECT_ELEMENTS(t, v) \
+    ((typeof(t) *)(_dml_vect_elements(sizeof(t), &(v)))
+
+// TODO(RAII): consider removal
+#define DML_VECT_ELEMENTS_FALLBACK(t, v, duped) (({                           \
+        _dml_vect_t __vect = v;                                               \
+        (typeof(t) *)(__vect.start + __vect.len <= __vect.size                \
+                      ? __vect.elements + __vect.start*sizeof(t)              \
+                      : _dml_vect_elements(sizeof(t), &duped));               \
+    }))
+
+#define DML_VECT_INBOUNDS(v, i)                             \
+    ASSERT_FMT((i) >= 0 && (i) < (v).len,                   \
+               "OOB index %u for DML vector of length %u",  \
+               (unsigned) i, (v).len)
+
+#define DML_VECT_ELEM_UNSAFE(t, v, i)                       \
+    (((typeof(t) *)(v).elements)[DML_VECT_INDEX((v), (i))])
+
+#define DML_VECT_ELEM(t, v, i) (*({                                     \
+        _dml_vect_t __vect = v;                                         \
+        uint32 __index = i;                                             \
+        DML_VECT_INBOUNDS(__vect, __index);                             \
+        &DML_VECT_ELEM_UNSAFE(t, __vect, __index);                      \
+    }))
+
+#define DML_VECT_REMOVE(t, v, i) (({                                        \
+        _dml_vect_t *__vect = &(v);                                         \
+        uint32 __index = i;                                                 \
+        DML_VECT_INBOUNDS(*__vect, __index);                                \
+        typeof(t) __popped = DML_VECT_ELEM_UNSAFE(t, *__vect, __index);     \
+        _dml_vect_remove(sizeof(t), __vect, __index);                       \
+        __popped;                                                           \
+    }))
+
+#define DML_VECT_NEW_ELEM_AT(t, v, i) (*(typeof(t) *)({                     \
+        _dml_vect_t *__vect = &(v);                                         \
+        uint32 __index = i;                                                 \
+        ASSERT_FMT(__index >= 0 && __index <= __vect->len,                  \
+                   "OOB insertion index %u for DML vector of length %u",    \
+                   (unsigned) __index, __vect->len);                        \
+        _dml_vect_new_at(sizeof(t), __vect, __index);                       \
+    }))
+
+#define DML_VECT_NEW_ELEM_AT_BACK(t, v) (*({                                  \
+        _dml_vect_t *__vect = &(v);                                           \
+        _dml_vect_resize(sizeof(t), __vect, __vect->len + 1, false);          \
+        &DML_VECT_ELEM_UNSAFE(t, *__vect, __vect->len - 1);                   \
+    }))
+
+#define DML_VECT_NEW_ELEM_AT_FRONT(t, v) \
+    (DML_VECT_NEW_ELEM_AT(t, v, 0, e))
+
+#define DML_VECT_POP_BACK(t, v) (({                                           \
+        _dml_vect_t *__vect = &(v);                                           \
+        ASSERT_MSG(__vect->len >= 0, ".pop_back(): DML vector is empty");     \
+        typeof(t) __popped = DML_VECT_ELEM_UNSAFE(t, *__vect, vect->len - 1); \
+        _dml_vect_remove(sizeof(t), __vect, vect->len - 1);                   \
+        __popped;                                                             \
+    }))
+
+#define DML_VECT_POP_FRONT(t, v) (({                                          \
+        _dml_vect_t *__vect = &(v);                                           \
+        ASSERT_MSG(__vect->len > 0, ".pop_front(): DML vector is empty");     \
+        typeof(t) __popped = DML_VECT_ELEM_UNSAFE(t, *__vect, 0);             \
+        _dml_vect_remove(sizeof(t), __vect, 0);                               \
+        __popped;                                                             \
+    }))
+
+#define DML_VECT_INITIAL_ELEMS(elem_size)   \
+    MAX(1, _DML_BITFLOOR(32 / (elem_size)))
+
+
+UNUSED static void
+_dml_vect_realloc(size_t elem_size, _dml_vect_t *v, uint32 new_size) {
+    // new_size is a power of two or 0
+    ASSERT((new_size & (new_size - 1)) == 0);
+    // This assert also catches if the buf grows to be larger than
+    // 2^32-1 elements
+    ASSERT(new_size >= v->len);
+
+    if (unlikely(new_size == v->size)) return;
+
+    // TODO(RAII) This path is unused. Remove?
+    if (unlikely(new_size == 0)) {
+        MM_FREE(v->elements);
+        v->elements = NULL;
+        v->size = v->start = v->len = 0;
+        return;
+    }
+    if (new_size > v->size) {
+        ASSERT(new_size >= v->start + v->len);
+        v->elements = MM_REALLOC_SZ(v->elements, new_size*elem_size, char);
+        if (v->start + v->len > v->size)
+            memcpy(v->elements + v->size*elem_size,
+                   v->elements,
+                   (v->len - (v->size - v->start))*elem_size);
+    } else {
+        // If moving elements is necessary...
+        if (v->start + v->len > new_size) {
+            if (v->start < new_size) {
+                // No preexisting wraparound. Induce wraparound
+                memcpy(v->elements,
+                       v->elements + new_size*elem_size,
+                       (v->len - (new_size - v->start))*elem_size);
+            } else {
+                // Preexisting wraparound. Move start segment back.
+                uint32 start_segment_len = v->size - v->start;
+                memcpy(v->elements + (new_size - start_segment_len)*elem_size,
+                       v->elements + v->start*elem_size,
+                       start_segment_len*elem_size);
+                v->start = new_size - start_segment_len;
+            }
+        }
+        v->elements = MM_REALLOC_SZ(v->elements, new_size*elem_size, char);
+    }
+    v->size = new_size;
+}
+
+// Internal. Realtime O(n), amortized O(1)
+UNUSED static void
+_dml_vect_force(size_t elem_size, _dml_vect_t *v) {
+    if (v->len == 0) {
+        v->start = 0;
+    }
+    if (v->len < v->size/4 && v->size > DML_VECT_INITIAL_ELEMS(elem_size)) {
+        uint32 new_size;
+        if (v->len == 0) {
+            new_size = MAX(DML_VECT_INITIAL_ELEMS(elem_size), 2);
+        } else {
+            new_size = _DML_BITCEIL(v->len);
+            new_size = MAX(DML_VECT_INITIAL_ELEMS(elem_size), new_size*2);
+        }
+        _dml_vect_realloc(elem_size, v, new_size);
+    }
+}
+
+UNUSED static void
+_dml_vect_clear(size_t elem_size, _dml_vect_t *v) {
+    v->len = 0;
+    _dml_vect_force(elem_size, v);
+}
+
+// Realtime O(max(new_len, n)), amortized O(|new_len - n|)
+UNUSED static void
+_dml_vect_resize(size_t elem_size, _dml_vect_t *v, uint32 new_len,
+                 bool zero_init_new) {
+    uint32 prev_len = v->len;
+    ASSERT(prev_len <= v->size);
+    if (new_len == prev_len)
+        return;
+    v->len = new_len;
+    if (new_len <= v->size) {
+        _dml_vect_force(elem_size, v);
+    } else {
+        uint32 new_size = MAX(DML_VECT_INITIAL_ELEMS(elem_size),
+                              _DML_BITCEIL(new_len));
+        if (new_size != v->size)
+            _dml_vect_realloc(elem_size, v, new_size);
+    }
+    if (zero_init_new && new_len > prev_len) {
+        if (v->start + prev_len < v->size) {
+            uint32 mid_segment_len = MIN(new_len - prev_len,
+                                         v->size - v->start - prev_len);
+            memset(v->elements + (v->start + prev_len)*elem_size,
+                   0,
+                   mid_segment_len*elem_size);
+            if (mid_segment_len < new_len - prev_len) {
+                memset(v->elements,
+                       0,
+                       (new_len - prev_len - mid_segment_len)*elem_size);
+            }
+        } else {
+            memset(v->elements + (v->start + prev_len - v->size)*elem_size,
+                   0,
+                   (new_len - prev_len)*elem_size);
+        }
+    }
+}
+
+UNUSED static void
+_dml_vect_resize_destructive(
+    size_t elem_size, _dml_vect_t *v, uint32 new_len) {
+    v->start = 0;
+    _dml_vect_resize(elem_size, v, new_len, false);
+}
+
+// O(1) if i is 0 or 1 elements from either end, O(n) otherwise
+UNUSED static void
+_dml_vect_remove(size_t elem_size, _dml_vect_t *v, uint32 i) {
+    ASSERT(i < v->len);
+
+    // Special cases
+    if (i == v->len - 1) {
+        goto done;
+    } else if (i == v->len - 2) {
+        memcpy(v->elements + DML_VECT_INDEX(*v, i)*elem_size,
+               v->elements + DML_VECT_INDEX(*v, v->len - 1)*elem_size,
+               elem_size);
+        goto done;
+    }
+    switch (i) {
+    case 0:
+        v->start = (v->start + 1) & (v->size - 1);
+        goto done;
+    case 1:
+        memcpy(v->elements + DML_VECT_INDEX(*v, 1)*elem_size,
+               v->elements + v->start*elem_size,
+               elem_size);
+        v->start = (v->start + 1) & (v->size - 1);
+        goto done;
+    }
+
+    if (v->start + v->len > v->size) {
+        // If the vector is discontinuous, then whatever segment the index is
+        // in is moved.
+        if (v->start + i < v->size) {
+            memmove(v->elements + (v->start + 1)*elem_size,
+                    v->elements + v->start*elem_size,
+                    i*elem_size);
+            ++v->start;
+        } else {
+            uint32 index = v->start + i - v->size;
+            memmove(v->elements + index*elem_size,
+                    v->elements + (index + 1)*elem_size,
+                    (v->len - 1 - i)*elem_size);
+        }
+    } else {
+        // Otherwise, whatever end is closest to the index is moved
+        // TODO(RAII): consider always moving the back end in order to avoid
+        // contributing to future discontinuity
+        if (i < v->len/2) {
+            memmove(v->elements + (v->start + 1)*elem_size,
+                    v->elements + v->start*elem_size,
+                    i*elem_size);
+            v->start = (v->start + 1) & (v->size - 1);
+        } else {
+            memmove(v->elements + (v->start + i)*elem_size,
+                    v->elements + (v->start + i + 1)*elem_size,
+                    (v->len - 1 - i)*elem_size);
+        }
+    }
+  done:
+    --v->len;
+    _dml_vect_force(elem_size, v);
+}
+
+// O(1) amortized if i is 0 or 1 elements from either end, O(n) otherwise
+UNUSED static void *
+_dml_vect_new_at(size_t elem_size, _dml_vect_t *v, uint32 i) {
+    ASSERT(i <= v->len);
+    _dml_vect_resize(elem_size, v, v->len + 1, false);
+
+    // Special cases
+    if (i == v->len - 1) {
+        goto done;
+    } else if (i == v->len - 2) {
+        memcpy(v->elements + DML_VECT_INDEX(*v, v->len - 1)*elem_size,
+               v->elements + DML_VECT_INDEX(*v, i)*elem_size,
+               elem_size);
+        goto done;
+    }
+    switch (i) {
+    case 0:
+        v->start = (v->start - 1) & (v->size - 1);
+        goto done;
+    case 1:
+        memcpy(v->elements + v->start,
+               v->elements + DML_VECT_INDEX(*v, 1)*elem_size,
+               elem_size);
+        goto done;
+    }
+
+    if (v->start + v->len > v->size) {
+        // If the vector is discontinuous, then whatever segment the index is
+        // in is moved.
+        if (v->start + i < v->size) {
+            memmove(v->elements + (v->start - 1)*elem_size,
+                    v->elements + v->start*elem_size,
+                    i*elem_size);
+            --v->start;
+        } else {
+            uint32 index = v->start + i - v->size;
+            memmove(v->elements + (index + 1)*elem_size,
+                    v->elements + index*elem_size,
+                    (v->len - i - 1)*elem_size);
+        }
+    } else {
+        // Otherwise, whatever end is closest to the index is moved, if
+        // it can be done without incurring wrap-around
+        // TODO(RAII): consider always moving the back end in order to avoid
+        // contributing to future discontinuity
+        if (v->start && i < v->len/2) {
+            memmove(v->elements + (v->start - 1)*elem_size,
+                    v->elements + v->start*elem_size,
+                    i*elem_size);
+            --v->start;
+        } else {
+            memmove(v->elements + (v->start + i + 1)*elem_size,
+                    v->elements + (v->start + i)*elem_size,
+                    (v->len - 1 - i)*elem_size);
+        }
+    }
+  done:
+    return v->elements + DML_VECT_INDEX(*v, i)*elem_size;
+}
+
+// Amortized O(1)
+UNUSED static void
+_dml_vect_replace_with_back(size_t elem_size, _dml_vect_t *v, uint32 i) {
+    ASSERT(i < v->len);
+    if (i != v->len - 1) {
+        memcpy(v->elements + DML_VECT_INDEX(*v, i)*elem_size,
+               v->elements + DML_VECT_INDEX(*v, v->len - 1)*elem_size,
+               elem_size);
+    }
+    --v->len;
+    _dml_vect_force(elem_size, v);
+}
+
+// Amortized O(1)
+UNUSED static void
+_dml_vect_replace_with_front(size_t elem_size, _dml_vect_t *v, uint32 i) {
+    ASSERT(i < v->len);
+    if (i != 0) {
+        memcpy(v->elements + DML_VECT_INDEX(*v, i)*elem_size,
+               v->elements + v->start*elem_size,
+               elem_size);
+    }
+    ++v->start;
+    --v->len;
+    _dml_vect_force(elem_size, v);
+}
+
+// Realtime O(n), hopefully fast
+UNUSED static void
+_dml_vect_reinit_size(size_t elem_size, _dml_vect_t *v, size_t size) {
+    if (!v->size && !size)
+        return;
+    v->size = MAX(DML_VECT_INITIAL_ELEMS(elem_size), size);
+    v->elements = MM_REALLOC_SZ(v->elements, v->size*elem_size, char);
+    v->len = v->start = 0;
+}
+
+UNUSED static void *
+_dml_vect_elements(size_t elem_size, _dml_vect_t *v) {
+    // Fast path, O(1) realtime -- the vect is already continuous
+    if (v->start + v->len <= v->size) {
+        return v->elements + v->start*elem_size;
+    }
+
+    // Slow path, O(n) realtime. Reorder the vect into one continuous segment,
+    // offset 1/4 into the buf, thus leaving room for at least n/4 future
+    // elements at either end.
+    // This guarantees O(1) amortized behavior -- the slow path can only be
+    // encountered again if Omega(n) elements have been removed or added since.
+
+    // Grow the buffer if we're starting to run out of elements
+    if (v->len*2 > v->size) {
+        _dml_vect_realloc(elem_size, v, v->size*2);
+    }
+    uint32 offset = v->size/4;
+    if (v->start + v->len <= v->size) {
+        // The vect is continuous as a result of realloc.
+        // Move it to 1/4 through the buffer if not there already.
+        if (v->start != offset) {
+            // In this path overlap between the current vect segment and
+            // the location of the desired vect segment is guaranteed.
+            memmove(v->elements + offset*elem_size,
+                    v->elements + v->start*elem_size,
+                    v->len*elem_size);
+        }
+    } else {
+        // The vect is discontinuous. Only possible if realloc didn't happen,
+        // meaning v->len*2 <= v->size. This also implies v->start > v->len
+        uint32 start_segment_len = v->size - v->start;
+        uint32 end_segment_len = v->len - start_segment_len;
+
+        if (offset < end_segment_len) {
+            // Current end segment overlaps with desired vect segment.
+            // This implies (proof omitted): offset + v->len <= v->start
+            // SO: may memmove the end segment, then copy the start segment
+            memmove(v->elements + (offset + start_segment_len)*elem_size,
+                    v->elements,
+                    end_segment_len*elem_size);
+            memcpy(v->elements + offset*elem_size,
+                   v->elements + v->start*elem_size,
+                   start_segment_len*elem_size);
+        } else {
+            // Either no overlap, or current start segment overlaps with
+            // desired vect segment.
+            // We have that offset >= end_segment_len
+            // SO: may memmove the start segment, then copy the end segment
+            memmove(v->elements + offset*elem_size,
+                    v->elements + v->start*elem_size,
+                    start_segment_len);
+            memcpy(v->elements + (offset + start_segment_len)*elem_size,
+                   v->elements,
+                   end_segment_len*elem_size);
+        }
+    }
+    v->start = offset;
+    return v->elements + offset*elem_size;
+}
+
+
+// O(n)
+UNUSED static void
+_dml_vect_copy_to_index(size_t elem_size, _dml_vect_t *tgt, _dml_vect_t src,
+                        uint32 i) {
+    // Aliasing must be dealt with before this call!
+    ASSERT(src.elements != tgt->elements);
+    ASSERT(i + src.len <= tgt->len);
+
+    uint32 tgt_start = DML_VECT_INDEX(*tgt, i);
+    uint32 src_start_len = MIN(src.len, src.size - src.start);
+    uint32 tgt_start_len = MIN(src.len, tgt->size - tgt_start);
+
+    memcpy(tgt->elements + tgt_start*elem_size,
+           src.elements + src.start*elem_size,
+           MIN(src_start_len, tgt_start_len)*elem_size);
+    if (src_start_len < tgt_start_len) {
+        memcpy(tgt->elements + (tgt_start + src_start_len)*elem_size,
+               src.elements,
+               (tgt_start_len - src_start_len)*elem_size);
+    } else if (tgt_start_len < src_start_len) {
+        memcpy(tgt->elements,
+               src.elements + (src.start + tgt_start_len)*elem_size,
+               (src_start_len - tgt_start_len)*elem_size);
+    }
+
+    uint32 remainder_offset = MAX(src_start_len, tgt_start_len);
+    if (remainder_offset != src.len) {
+        memcpy(tgt->elements
+               + DML_VECT_INDEX(*tgt, i + remainder_offset)*elem_size,
+               src.elements + DML_VECT_INDEX(src, remainder_offset)*elem_size,
+               src.len - remainder_offset);
+    }
+}
+
+UNUSED static void _dml_vect_free(_dml_vect_t v) {
+    MM_FREE(v.elements);
+}
+
+// O(n)
+UNUSED static void
+_dml_vect_copy(size_t elem_size, _dml_vect_t *tgt, _dml_vect_t src) {
+    if (unlikely((uintptr_t)tgt->elements == (uintptr_t)src.elements)) {
+        ASSERT(tgt->size == src.size && tgt->len == src.len
+               && tgt->start == src.start);
+        return;
+    }
+    _dml_vect_reinit_size(elem_size, tgt, src.size);
+    tgt->len = src.len;
+    _dml_vect_copy_to_index(elem_size, tgt, src, 0);
+}
+
+// O(n)
+UNUSED static _dml_vect_t
+_dml_vect_dupe(size_t elem_size, _dml_vect_t src) {
+    _dml_vect_t v = {0};
+    _dml_vect_copy(elem_size, &v, src);
+    return v;
+}
+
+// O(n)
+UNUSED static void
+_dml_vect_append(size_t elem_size, _dml_vect_t *tgt, _dml_vect_t src) {
+    if (src.len == 0) return;
+    uint32 appendat_index = tgt->len;
+    if (unlikely((uintptr_t)tgt->elements == (uintptr_t)src.elements)) {
+        ASSERT(tgt->size == src.size && tgt->len == src.len
+               && tgt->start == src.start);
+        _dml_vect_resize(elem_size, tgt, tgt->len*2, false);
+        // TODO(RAII) Rewrite. I believe the logic to be correct, but it is
+        // horribly headache inducing
+        if (appendat_index*2 <= tgt->size - tgt->start) {
+            memcpy(tgt->elements + (tgt->start + appendat_index)*elem_size,
+                   tgt->elements + tgt->start*elem_size,
+                   appendat_index*elem_size);
+        } else if (appendat_index <= tgt->size - tgt->start) {
+            uint32 tgt_start_len = tgt->size - tgt->start - appendat_index;
+            if (tgt_start_len) {
+                memcpy(tgt->elements + (tgt->start + appendat_index)*elem_size,
+                       tgt->elements + tgt->start*elem_size,
+                       tgt_start_len*elem_size);
+            }
+            memcpy(tgt->elements,
+                   tgt->elements + (tgt->start + tgt_start_len)*elem_size,
+                   (appendat_index - tgt_start_len)*elem_size);
+        } else {
+            uint32 tgt_start_index = DML_VECT_INDEX(*tgt, appendat_index);
+            uint32 src_start_len = tgt->size - tgt->start;
+            memcpy(tgt->elements + tgt_start_index*elem_size,
+                   tgt->elements + tgt->start*elem_size,
+                   src_start_len*elem_size);
+            memcpy(tgt->elements + (tgt_start_index + src_start_len)*elem_size,
+                   tgt->elements,
+                   (appendat_index - src_start_len)*elem_size);
+        }
+    } else {
+        _dml_vect_resize(elem_size, tgt, tgt->len + src.len, false);
+        _dml_vect_copy_to_index(elem_size, tgt, src, appendat_index);
+    }
+}
+
+UNUSED static _dml_vect_t
+_dml_vect_from_array(size_t elem_size, const void *src, uint32 no_elements) {
+    _dml_vect_t v = {0};
+    _dml_vect_resize(elem_size, &v, no_elements, false);
+    if (v.size)
+        memcpy(v.elements, src, no_elements*elem_size);
+    return v;
+}
+
+UNUSED static void
+_dml_vect_set_array(size_t elem_size, _dml_vect_t *tgt, const void *src,
+                    uint32 no_elements) {
+    if (!no_elements) {
+        _dml_vect_clear(elem_size, tgt);
+        return;
+    }
+
+    bool overlap = _dml_pointers_overlap(tgt->elements, src,
+                                         tgt->size*elem_size);
+
+    if (unlikely(overlap)) {
+        uint8 *duped_src = MM_MALLOC(no_elements*elem_size, uint8);
+        memcpy(duped_src, src, no_elements*elem_size);
+        src = duped_src;
+    }
+
+    _dml_vect_resize_destructive(elem_size, tgt, no_elements);
+    memcpy(tgt->elements, src, no_elements*elem_size);
+
+    if (unlikely(overlap))
+        MM_FREE((uint8 *)src);
+}
+
+UNUSED static void
+_dml_vect_append_array(size_t elem_size, _dml_vect_t *tgt, const void *src,
+                       uint32 no_elements) {
+    if (!no_elements)
+        return;
+    bool overlap = _dml_pointers_overlap(tgt->elements, src,
+                                         tgt->size*elem_size);
+    if (unlikely(overlap)) {
+        uint8 *duped_src = MM_MALLOC(no_elements*elem_size, uint8);
+        memcpy(duped_src, src, no_elements*elem_size);
+        src = duped_src;
+    }
+    uint32 prev_len = tgt->len;
+    _dml_vect_resize(elem_size, tgt, tgt->len + no_elements, false);
+    uint32 copyat_start = DML_VECT_INDEX(*tgt, prev_len);
+    uint32 tgt_start_len = tgt->size - copyat_start;
+
+    memcpy(tgt->elements + copyat_start*elem_size, src,
+           MIN(no_elements, tgt_start_len)*elem_size);
+    if (tgt_start_len < no_elements) {
+        memcpy(tgt->elements, (uint8 *)src + tgt_start_len*elem_size,
+               (no_elements - tgt_start_len)*elem_size);
+    }
+
+    if (unlikely(overlap))
+        MM_FREE((void *)src);
+}
+
+UNUSED static _dml_vect_t
+_dml_vect_from_string(_dml_string_t s) {
+    if (!s.size) return (_dml_vect_t) {0};
+    uint32 new_size = _DML_BITCEIL(s.size);
+    if (s.size != new_size)
+        s.s = MM_REALLOC_SZ(s.s, new_size, char);
+    return (_dml_vect_t) {.elements = s.s, .size = new_size, .len = s.len};
+}
+
+UNUSED static _dml_string_t
+_dml_string_from_vect(_dml_vect_t v) {
+    if (!v.size) return (_dml_string_t) {0};
+    if (v.start) {
+        (void)_dml_vect_elements(sizeof(char), &v);
+        memmove(v.elements, v.elements + v.start, v.len);
+    }
+    return (_dml_string_t) { .s = v.elements, .size = v.size, .len = v.len };
+}
+
+#endif
diff --git a/lib/1.4/dml-builtins.dml b/lib/1.4/dml-builtins.dml
index 4725dfa23..e45bb8d73 100644
--- a/lib/1.4/dml-builtins.dml
+++ b/lib/1.4/dml-builtins.dml
@@ -161,6 +161,13 @@ extern void _callback_after_write(conf_object_t *bank,
 extern void _cancel_simple_events(conf_object_t *obj, _identity_t id);
 extern void _register_attributes(conf_object_t *obj, _identity_t id);
 
+
+// Operations on RAII types
+extern string _dml_string_new(const char *) as mk_string;
+extern string _dml_string_newf(const char *, ...) as mk_string_f;
+extern string _dml_string_from_vect(vect(char)) as mk_string_from_vect;
+extern string _dml_vect_from_string(string) as mk_vect_from_string;
+
 /**
 # Libraries and Built-ins
 
@@ -3615,6 +3622,7 @@ template init_val is init {
 
 extern const char *_DML_get_qname(_identity_t, const _id_info_t *,
                                   dml_qname_cache_t *, const char *);
+
 template _qname {
     shared method _qname() -> (const char *) {
         local _qname ref = this;
diff --git a/py/dml/c_backend.py b/py/dml/c_backend.py
index 42416697e..4299f53a0 100644
--- a/py/dml/c_backend.py
+++ b/py/dml/c_backend.py
@@ -28,6 +28,7 @@
 from .set import Set
 
 prototypes = []
+constants = []
 c_split_threshold = None
 
 log_object_t = TNamed("log_object_t")
@@ -142,6 +143,11 @@ def composite_ctype(node, unfiltered_members, label=None):
             members.append((v.value, v.type))
         members.append(('_immediate_after_state',
                         TPtr(TNamed('_dml_immediate_after_state_t'))))
+        if dml.globals.session_orphan_allocs:
+            t = TArray(TPtr(void),
+                       ctree.mkIntegerLiteral(
+                           node.site, dml.globals.session_orphan_allocs))
+            members.append(('_orphan_allocs', t))
         return composite_ctype(node,
                                members + [(crep.cname(sub), print_device_substruct(sub))
                                           for sub in node.get_components()],
@@ -365,9 +371,12 @@ def generate_hfile(device, headers, filename):
 
 def generate_protofile(device):
     linkage = 'extern' if c_split_threshold else 'static'
-    out('\n/* generated function prototypes */\n')
+    out('\n/* generated function and variable prototypes */\n')
     for proto in prototypes:
         out("%s %s UNUSED;\n" % (linkage, proto))
+    out('\n/* generated internal constants */\n')
+    for (proto, init) in constants:
+        out("static %s UNUSED = %s;\n" % (proto, init))
 
 def get_attr_fname(node, port, group_prefix):
     port_prefix = port.attrname() + '_' if port else ''
@@ -416,19 +425,19 @@ def generate_attr_setter(fname, node, port, dimsizes, cprefix, loopvars,
         out('attr_value_t attr%d = %s;\n' % (dim, list_item))
         valuevar = 'attr%d' % (dim,)
 
-    with NoFailure(node.site), crep.DeviceInstanceContext():
-        setcode = [
-            codegen_inline_byname(
-                node, port_indices + loopvars,
-                '_set_attribute' if dml.globals.dml_version == (1, 2)
-                else 'set_attribute',
-                [mkLit(node.site, valuevar, TNamed('attr_value_t'))],
-                [mkLit(node.site, '_status', TNamed('set_error_t'))],
-                node.site,
-                inhibit_copyin = not loopvars)]
-
-    code = mkCompound(None, declarations(fscope) + setcode)
-    code.toc_inline()
+    with MethodRAIIScope() as raii_scope, NoFailure(node.site), \
+         crep.DeviceInstanceContext():
+        setcode = codegen_inline_byname(
+            node, port_indices + loopvars,
+            '_set_attribute' if dml.globals.dml_version == (1, 2)
+            else 'set_attribute',
+            [mkLit(node.site, valuevar, TNamed('attr_value_t'))],
+            [mkLit(node.site, '_status', TNamed('set_error_t'))],
+            node.site,
+            inhibit_copyin = not loopvars)
+        code = declarations(fscope) + [setcode]
+
+    mkCompoundRAII(None, code, raii_scope).toc_inline()
     reset_line_directive()
     if dimsizes:
         # abort on first bad value
@@ -477,15 +486,16 @@ def generate_attr_getter(fname, node, port, dimsizes, cprefix, loopvars):
         out('attr_value_t %s;\n' % (next_valuevar.read()))
         valuevar = next_valuevar
 
-    with NoFailure(node.site), crep.DeviceInstanceContext():
+    with MethodRAIIScope() as raii_scope, NoFailure(node.site), \
+         crep.DeviceInstanceContext():
         getcode = codegen_inline_byname(
             node, port_indices + loopvars,
             '_get_attribute' if dml.globals.dml_version == (1, 2)
             else 'get_attribute',
             [], [valuevar], node.site)
-        code = mkCompound(node.site, declarations(fscope) + [getcode])
-        code.toc_inline()
-        reset_line_directive()
+        code = declarations(fscope) + [getcode]
+    mkCompoundRAII(node.site, code, raii_scope).toc_inline()
+    reset_line_directive()
 
     for depth, loopvar in reversed(list(enumerate(loopvars))):
         out('SIM_attr_list_set_item(&_val%d, %s, _val%d);\n'
@@ -740,7 +750,7 @@ def wrap_method(meth, wrapper_name, indices=()):
         assert meth.dimensions == len(indices)
         out(devstruct+' *_dev UNUSED = ('+devstruct+'*)_obj;\n')
         indices = tuple(mkIntegerLiteral(meth.site, i) for i in indices)
-    with crep.DeviceInstanceContext():
+    with UnusedMethodRAIIScope(), crep.DeviceInstanceContext():
         if retvar:
             decl = mkDeclaration(meth.site, retvar, rettype,
                                  init = get_initializer(meth.site, rettype,
@@ -1036,11 +1046,11 @@ def generate_simple_events(device):
 
         # If data is NULL, report it and use emergency indices/args
         if info.dimensions or info.args_type:
-            out('if (!data) {', postindent=1)
+            out('if (!data) {\n', postindent=1)
             out('const char *msg = "Failed deserialization of after event '
                 + 'data. Using emergency indices/arguments.";\n')
             out('VT_critical_error(msg, msg);\n')
-            out('}', preindent=-1)
+            out('}\n', preindent=-1)
 
         if info.dimensions > 0:
             out('const uint32 *_indices = data ? data->indices '
@@ -1067,6 +1077,25 @@ def generate_simple_events(device):
         out('}\n\n', preindent = -1)
         splitting_point()
 
+        if info.args_type and info.args_type.is_raii:
+            start_function_definition(
+                f'void {info.cident_destroy}'
+                + '(conf_object_t *_obj, lang_void *_data)')
+            out('{\n', postindent = 1)
+            out('_simple_event_data_t *data = '
+                + '(_simple_event_data_t *)_data;\n')
+            out('if (data) {\n', postindent=1)
+            args_decl = TPtr(info.args_type).declaration('_args')
+            out(f'{args_decl} = data->args;\n')
+            raii_info = get_raii_type_info(info.args_type)
+            out(f'{raii_info.read_destroy_lval("*_args")};\n')
+            out('_free_simple_event_data(*data);\n')
+            out('MM_FREE(data);\n')
+            out('}\n', preindent=-1)
+            out('}\n\n', preindent = -1)
+            splitting_point()
+
+
         if info.dimensions or info.args_type:
             start_function_definition(
                 f'attr_value_t {info.cident_get_value}'
@@ -1131,6 +1160,19 @@ def generate_after_on_hooks_artifacts(device):
         splitting_point()
 
         if info.has_serialized_args:
+            if info.args_type and info.args_type.is_raii:
+                start_function_definition(
+                    f'void {info.cident_args_destructor}('
+                    + 'void *_args)')
+                out('{\n', postindent = 1)
+                args_type_ptr = TPtr(info.args_type)
+                out(f'{args_type_ptr.declaration("args")} = _args;\n')
+                args_expr = mkDereference(site, mkLit(site, 'args',
+                                                      args_type_ptr))
+                info.generate_args_destructor(site, args_expr)
+                out('}\n\n', preindent = -1)
+                splitting_point()
+
             start_function_definition(
                 f'attr_value_t {info.cident_args_serializer}('
                 + 'const void *_args)')
@@ -1155,12 +1197,28 @@ def generate_after_on_hooks_artifacts(device):
             out('{\n', postindent = 1)
             out('set_error_t _success UNUSED = Sim_Set_Ok;\n')
 
+            cleanup = []
+            cleanup_on_failure = []
             if info.args_type:
-                out(f'{TPtr(info.args_type).declaration("out")} = _out;\n')
-                out_expr = mkDereference(site, mkLit(site, 'out',
-                                                     TPtr(info.args_type)))
+                raii_info = (get_raii_type_info(info.args_type)
+                             if info.args_type.is_raii else None)
+                malloc = mkLit(site,
+                               f'MM_{("Z" if raii_info is not None else "M")}'
+                               + f'ALLOC(1, {info.args_type.declaration("")})',
+                               TPtr(info.args_type))
+                (tmp_out_decl, tmp_out_ref) = serialize.declare_variable(
+                    site, "_tmp_out", TPtr(info.args_type), malloc)
+                tmp_out_decl.toc()
+                tmp_out_expr = mkDereference(site, tmp_out_ref)
+                cleanup_ref = ('(void *)'*deep_const(info.args_type)
+                               + '_tmp_out')
+                cleanup.append(ctree.mkInline(site,
+                                              f'MM_FREE({cleanup_ref});'))
+                if raii_info:
+                    cleanup_on_failure.append(ctree.mkInline(
+                        site, raii_info.read_destroy_lval('*_tmp_out') + ';'))
             else:
-                out_expr = None
+                tmp_out_expr = None
             def error_out(exc, msg):
                 stmts = []
                 stmts.append(mkInline(site, f'_success = {exc};'))
@@ -1171,10 +1229,28 @@ def error_out(exc, msg):
                 return stmts
             val_expr = mkLit(site, 'val', attr_value_t)
 
-            info.generate_args_deserializer(site, val_expr, out_expr,
+            info.generate_args_deserializer(site, val_expr, tmp_out_expr,
                                             error_out)
 
-            out('_exit:\n')
+            if info.args_type:
+                out_expr = ctree.mkDereference(
+                    site,
+                    ctree.mkCast(site, mkLit(site, '_out', TPtr(void)),
+                                 TPtr(info.args_type)))
+                src = OrphanWrap(site, tmp_out_expr)
+                ctree.AssignStatement(site, out_expr,
+                                      ctree.ExpressionInitializer(src)).toc()
+            if cleanup_on_failure:
+                out('if (false) {\n', postindent=1)
+                out('_exit:\n')
+                for stmt in cleanup_on_failure:
+                    stmt.toc()
+                out('}\n', preindent=-1)
+            else:
+                out('_exit:\n')
+
+            for stmt in cleanup:
+                stmt.toc()
             out('return _success;\n')
             out('}\n\n', preindent = -1)
             splitting_point()
@@ -1183,12 +1259,16 @@ def error_out(exc, msg):
 
     if dml.globals.after_on_hook_infos:
         init = '{\n%s\n}' % (',\n'.join(
-            '    {%s, %s, %s, %s, %s, %d}'
-            % ((string_literal(info.string_key), info.cident_callback)
+            '    {%s, %s, %s, %d, %s, %s, %s}'
+            % ((info.cident_callback,
+                info.cident_args_destructor
+                if info.args_type and info.args_type.is_raii else 'NULL',
+                f'sizeof({info.args_type.declaration("")})'
+                if info.args_type else '0',
+                info.parent.uniq,
+                string_literal(info.string_key))
                + ((info.cident_args_serializer, info.cident_args_deserializer)
-                  if info.has_serialized_args else ('NULL', 'NULL'))
-               + (f'sizeof({info.args_type.declaration("")})'
-                  if info.args_type else '0', info.parent.uniq))
+                  if info.has_serialized_args else ('NULL', 'NULL')))
             for info in dml.globals.after_on_hook_infos),)
         add_variable_declaration(
             'const _dml_after_on_hook_info_t _after_on_hook_infos[]', init)
@@ -1318,7 +1398,7 @@ def generate_register_events(device):
             out(('%s = SIM_register_event(%s, class, 0, %s, %s, %s, %s, '
                 + 'NULL);\n')
                 % (crep.get_evclass(key), string_literal(info.string_key),
-                   info.cident_callback, '_destroy_simple_event_data',
+                   info.cident_callback, info.cident_destroy,
                    info.cident_get_value, info.cident_set_value))
     out('}\n\n', preindent = -1)
     splitting_point()
@@ -1346,17 +1426,20 @@ def generate_reg_callback(meth, name):
     out('{\n', postindent = 1)
     out('%s *_dev = _obj;\n' % dev_t)
     scope = Symtab(global_scope)
-    fail = ReturnFailure(meth.site)
-    with fail, crep.DeviceInstanceContext():
-        inargs = [mkLit(meth.site, n, t) for n, t in meth.inp]
-        outargs = [mkLit(meth.site, "*" + n, t) for n, t in meth.outp]
-        code = [codegen_call(
-                meth.site, meth,
-                tuple(mkLit(meth.site, 'indices[%d]' % i, TInt(32, False))
-                      for i in range(meth.dimensions)),
-                inargs, outargs)]
-
-    code = mkCompound(meth.site, declarations(scope) + code + [fail.nofail()])
+    with UnusedMethodRAIIScope():
+        fail = ReturnFailure(meth.site)
+        with fail, crep.DeviceInstanceContext():
+            inargs = [mkLit(meth.site, n, t) for n, t in meth.inp]
+            outargs = [mkLit(meth.site, "*" + n, t) for n, t in meth.outp]
+            code = [codegen_call(
+                    meth.site, meth,
+                    tuple(mkLit(meth.site, 'indices[%d]' % i, TInt(32, False))
+                          for i in range(meth.dimensions)),
+                    inargs, outargs)]
+
+        code = mkCompound(meth.site,
+                          declarations(scope) + code + [fail.nofail()])
+
     code.toc()
     out('}\n', preindent = -1)
     out('\n')
@@ -1571,7 +1654,7 @@ def generate_initialize(device):
     # changed such that zero-initialization would not be valid.
     out('QINIT(_dev->_immediate_after_state->queue);\n')
 
-    with crep.DeviceInstanceContext():
+    with MethodRAIIScope() as raii_scope, crep.DeviceInstanceContext():
         if dml.globals.dml_version == (1, 2):
             # Functions called from init_object shouldn't throw any
             # exceptions. But we don't want to force them to insert try-catch
@@ -1584,9 +1667,11 @@ def generate_initialize(device):
                 hard_reset = codegen_call_byname(device.site, device, (),
                                                  'hard_reset', [], [])
 
-            mkCompound(device.site, [init, hard_reset]).toc()
+            code = [init, hard_reset]
         else:
-            codegen_inline_byname(device, (), '_init', [], [], device.site).toc()
+            code = [codegen_inline_byname(device, (), '_init', [], [],
+                                          device.site)]
+    mkCompoundRAII(device.site, code, raii_scope).toc()
 
     reset_line_directive()
     if dml.globals.api_version <= '6':
@@ -1598,9 +1683,47 @@ def generate_initialize(device):
 
 def generate_dealloc(device):
     start_function_definition(
-        f'void {crep.cname(device)}_dealloc(conf_object_t *dev)')
+        f'void {crep.cname(device)}_dealloc(conf_object_t *_obj)')
     out('{\n', postindent = 1)
-    out('MM_FREE(dev);\n')
+    out(crep.structtype(device) + ' *_dev = ('
+        + crep.structtype(device) + ' *)_obj;\n')
+    site = SimpleSite('generated dealloc function for device')
+    by_dims = {}
+    for node in device.get_recursive_components('session', 'saved'):
+        if node._type.is_raii:
+            by_dims.setdefault(node.dimsizes, []).append(node)
+    for dims in by_dims:
+        for i in range(len(dims)):
+            idxvar = '_i%d' % (i,)
+            out('for (uint32 %s = 0; %s < %d; %s++) {\n'
+                % (idxvar, idxvar, dims[i], idxvar), postindent=1)
+
+        indices = tuple(mkLit(site, f'_i{i}', TInt(32, False))
+                        for i in range(len(dims)))
+
+        for node in by_dims[dims]:
+            info = get_raii_type_info(node._type)
+            assert info
+            out(info.read_destroy_lval('_dev->'
+                                      + crep.cref_session(node, indices))
+                + ';\n')
+
+        for i in range(len(dims)):
+            out('}\n', preindent=-1)
+
+    for (sym, _) in dml.globals.static_vars:
+        if not sym.type.is_raii:
+            continue
+        info = get_raii_type_info(sym.type)
+        out(info.read_destroy_lval('_dev->'+sym.value) + ';\n')
+
+    allocs = dml.globals.session_orphan_allocs
+    if allocs:
+        out(f'for (uint32 i = 0; i < {allocs}; ++i)\n', postindent=1)
+        out('_DML_delete_orphan(_dev->_orphan_allocs[i]);\n',
+            postindent=-1)
+
+    out('MM_FREE(_dev);\n')
     out('}\n\n', preindent = -1)
 
 def generate_finalize(device):
@@ -1610,7 +1733,7 @@ def generate_finalize(device):
     out(crep.structtype(device) + ' *_dev UNUSED = ('
         + crep.structtype(device) + ' *)_obj;\n')
 
-    with crep.DeviceInstanceContext():
+    with MethodRAIIScope() as raii_scope, crep.DeviceInstanceContext():
         if dml.globals.dml_version == (1, 2):
             # Functions called from new_instance shouldn't throw any
             # exceptions.  But we don't want to force them to insert try-catch
@@ -1621,8 +1744,7 @@ def generate_finalize(device):
         else:
             code = codegen_inline_byname(device, (), '_post_init', [], [],
                                      device.site)
-    if not code.is_empty:
-        code.toc()
+    mkCompoundRAII(device.site, [code], raii_scope).toc_inline()
     out('}\n\n', preindent = -1)
     reset_line_directive()
 
@@ -1636,66 +1758,66 @@ def generate_deinit(device):
     out('_DML_execute_immediate_afters_now(_obj, '
         + '_dev->_immediate_after_state);\n')
 
-    with crep.DeviceInstanceContext():
-        # Cancel all events
-        events = device.get_recursive_components('event')
+    # Cancel all events
+    events = device.get_recursive_components('event')
 
-        by_dims = {}
-        for event in events:
-            by_dims.setdefault(event.dimsizes, []).append(event)
+    by_dims = {}
+    for event in events:
+        by_dims.setdefault(event.dimsizes, []).append(event)
 
-        for (dims, events) in by_dims.items():
-            for i in range(len(dims)):
-                out(f'for (uint32 _i{i} = 0; _i{i} < {dims[i]}; _i{i}++) {{\n',
-                    postindent=1)
+    for (dims, events) in by_dims.items():
+        for i in range(len(dims)):
+            out(f'for (uint32 _i{i} = 0; _i{i} < {dims[i]}; _i{i}++) {{\n',
+                postindent=1)
 
-            indices = tuple(mkLit(device.site, f'_i{i}', TInt(32, False))
-                            for i in range(len(dims)))
-            for event in events:
-                method = event.get_component('_cancel_all', 'method')
-                # Functions called from pre_delete_instance shouldn't throw
-                # any exceptions. But we don't want to force them to insert
-                # try-catch in the init method.
-                with LogFailure(device.site, event, indices):
-                    codegen_inline(device.site, method, indices, [], []).toc()
-            for i in range(len(dims)):
-                out('}\n', preindent=-1)
+        indices = tuple(mkLit(device.site, f'_i{i}', TInt(32, False))
+                        for i in range(len(dims)))
+        for event in events:
+            method = event.get_component('_cancel_all', 'method')
+            # Functions called from pre_delete_instance shouldn't throw
+            # any exceptions. But we don't want to force them to insert
+            # try-catch in the init method.
+            with crep.DeviceInstanceContext(), UnusedMethodRAIIScope(), \
+                 LogFailure(device.site, event, indices):
+                codegen_inline(device.site, method, indices, [], []).toc()
+        for i in range(len(dims)):
+            out('}\n', preindent=-1)
 
-        for key in dml.globals.after_delay_infos:
-            out(f'SIM_event_cancel_time(_obj, {crep.get_evclass(key)}, _obj, '
-                + '0, NULL);\n')
+    for key in dml.globals.after_delay_infos:
+        out(f'SIM_event_cancel_time(_obj, {crep.get_evclass(key)}, _obj, '
+            + '0, NULL);\n')
 
-        with LogFailure(device.site, device, ()):
-            code = codegen_inline_byname(device, (), 'destroy', [], [],
-                                         device.site)
-        if not code.is_empty:
-            code.toc()
+    # Cancel all pending afters on hooks
+    by_dims = {}
+    for hook in dml.globals.hooks:
+        by_dims.setdefault(hook.dimsizes, []).append(hook)
 
-        # Cancel all pending afters on hooks
-        by_dims = {}
-        for hook in dml.globals.hooks:
-            by_dims.setdefault(hook.dimsizes, []).append(hook)
+    for (dims, hooks) in by_dims.items():
+        for i in range(len(dims)):
+            out(f'for (uint32 _i{i} = 0; _i{i} < {dims[i]}; _i{i}++) {{\n',
+                postindent=1)
 
-        for (dims, hooks) in by_dims.items():
-            for i in range(len(dims)):
-                out(f'for (uint32 _i{i} = 0; _i{i} < {dims[i]}; _i{i}++) {{\n',
-                    postindent=1)
+        indices = tuple(mkLit(device.site, f'_i{i}', TInt(32, False))
+                        for i in range(len(dims)))
+        for hook in hooks:
+            out('_DML_free_hook_queue('
+                + f'&_dev->{crep.cref_hook(hook, indices)}.queue);\n')
+        for i in range(len(dims)):
+            out('}\n', preindent=-1)
 
-            indices = tuple(mkLit(device.site, f'_i{i}', TInt(32, False))
-                            for i in range(len(dims)))
-            for hook in hooks:
-                out('_DML_free_hook_queue('
-                    + f'&_dev->{crep.cref_hook(hook, indices)}.queue);\n')
-            for i in range(len(dims)):
-                out('}\n', preindent=-1)
+    with MethodRAIIScope() as raii_scope, crep.DeviceInstanceContext(), \
+         LogFailure(device.site, device, ()):
+        code = codegen_inline_byname(device, (), 'destroy', [], [],
+                                     device.site)
+    mkCompoundRAII(device.site, [code], raii_scope).toc()
 
-        # Execute all immediate afters posted by destruction code
-        out('_DML_execute_immediate_afters_now(_obj, '
-            + '_dev->_immediate_after_state);\n')
+    # Execute all immediate afters posted by destruction code
+    out('_DML_execute_immediate_afters_now(_obj, '
+        + '_dev->_immediate_after_state);\n')
 
-        # Free the tables used for log_once after all calls into device code
-        # are done
-        out('_free_table(&_dev->_subsequent_log_ht);\n')
+    # Free the tables used for log_once after all calls into device code
+    # are done
+    out('_free_table(&_dev->_subsequent_log_ht);\n')
 
     out('QFREE(_dev->_immediate_after_state->queue);\n')
     out('if (likely(!_dev->_immediate_after_state->posted)) {\n', postindent=1)
@@ -1717,11 +1839,12 @@ def generate_reset(device, hardness):
     out(crep.structtype(device) + ' *_dev UNUSED = ('
         + crep.structtype(device) + ' *)_obj;\n\n')
     scope = Symtab(global_scope)
-    with LogFailure(device.site, device, ()), crep.DeviceInstanceContext():
+    with MethodRAIIScope() as raii_scope, \
+        LogFailure(device.site, device, ()), crep.DeviceInstanceContext():
         code = codegen_call_byname(device.site, device, (),
                                    hardness+'_reset', [], [])
-    code = mkCompound(device.site, declarations(scope) + [code])
-    code.toc()
+    mkCompoundRAII(device.site, declarations(scope) + [code],
+                   raii_scope).toc_inline()
     out('}\n\n', preindent = -1)
     reset_line_directive()
 
@@ -1863,10 +1986,12 @@ def generate_init_data_objs(device):
             try:
                 # only data/method obj
                 assert not node.isindexed()
-                init = eval_initializer(
-                    node.site, node._type, node.astinit,
-                    Location(node.parent, static_indices(node)),
-                    global_scope, True)
+                with ctree.SessionRAIIScope() as raii_scope:
+                    init = eval_initializer(
+                        node.site, node._type, node.astinit,
+                        Location(node.parent, static_indices(node)),
+                        global_scope, True)
+                dml.globals.session_orphan_allocs += raii_scope.allocs
             # mainly meant to capture EIDXVAR; for other errors, the error will
             # normally re-appear when evaluating per instance
             except DMLError:
@@ -1875,10 +2000,12 @@ def generate_init_data_objs(device):
                                         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)
+                        with ctree.SessionRAIIScope() as raii_scope:
+                            init = eval_initializer(
+                                node.site, node._type, node.astinit,
+                                Location(node.parent, index_exprs),
+                                global_scope, True)
+                        dml.globals.session_orphan_allocs += raii_scope.allocs
                     except DMLError as e:
                         report(e)
                     else:
@@ -1886,7 +2013,7 @@ def generate_init_data_objs(device):
                             coverity_marker('store_writes_const_field',
                                             'FALSE',
                                             node.site)
-                        init.assign_to(nref, node._type)
+                        out(init.assign_to(nref.read(), node._type) + ';\n')
             else:
                 index_exprs = ()
                 for (i, sz) in enumerate(node.dimsizes):
@@ -1898,7 +2025,7 @@ def generate_init_data_objs(device):
                 nref = mkNodeRef(node.site, node, index_exprs)
                 if deep_const(node._type):
                     coverity_marker('store_writes_const_field', 'FALSE', node.site)
-                init.assign_to(nref, node._type)
+                out(init.assign_to(nref.read(), node._type) + ';\n')
                 for _ in range(node.dimensions):
                     out('}\n', postindent=-1)
     out('}\n\n', preindent = -1)
@@ -1997,27 +2124,38 @@ def generate_init(device, initcode, outprefix):
 
 def generate_static_trampoline(func):
     # static trampolines never need to be generated for independent methods
-    assert not func.independent
-    params = [("_obj", TPtr(TNamed("conf_object_t")))] + func.cparams[1:]
+    # with the niche exception of when
+    # 1. they are startup memoized
+    # 2. have a return value of RAII Type
+    assert not func.independent or (func.memoized and func.rettype.is_raii)
+    params = ([("_obj", TPtr(TNamed("conf_object_t")))] + func.cparams[1:]
+              if not func.independent else func.cparams)
     params_string = ('void' if not params
                      else ", ".join(t.declaration(n) for (n, t) in params))
     start_function_definition(func.rettype.declaration(
         "%s(%s)" % ("_trampoline" + func.get_cname(), params_string)))
     out("{\n", postindent=1)
-    out('ASSERT(_obj);\n')
-    out('ASSERT(SIM_object_class(_obj) == _dev_class);\n')
-    (name, typ) = func.cparams[0]
-    out("%s = (%s)_obj;\n" % (typ.declaration(name), typ.declaration("")))
+    if not func.independent:
+        out('ASSERT(_obj);\n')
+        out('ASSERT(SIM_object_class(_obj) == _dev_class);\n')
+        (name, typ) = func.cparams[0]
+        out("%s = (%s)_obj;\n" % (typ.declaration(name), typ.declaration("")))
     out("%s%s(%s);\n" % ("" if func.rettype.void
                          else func.rettype.declaration("result") + " = ",
                          func.get_cname(),
                          ", ".join(n for (n, t) in func.cparams)))
-    output_dml_state_change(name)
+    if not func.independent:
+        output_dml_state_change(name)
     if not func.rettype.void:
-        out("return result;\n")
+        ret = 'result'
+        if func.memoized and func.rettype.is_raii:
+            info = get_raii_type_info(func.rettype)
+            ret = info.read_dupe('result')
+        out(f'return {ret};\n')
     out("}\n", preindent=-1)
 
 def generate_extern_trampoline(exported_name, func):
+    assert not (func.independent and func.memoized and func.rettype.is_raii)
     params = (func.cparams if func.independent else
               [("_obj", TPtr(TNamed("conf_object_t")))] + func.cparams[1:])
     params_string = ('void' if not params
@@ -3104,34 +3242,24 @@ 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(site,
-                             ('*((%s) {0})'
-                              % ((TArray(t, mkIntegerLiteral(site, 1))
-                                  .declaration('')),)),
-                             t)
-                       for (_, t) in method.outp]
+            outargs = [mkDiscardRef(site) for _ in method.outp]
 
             method_ref = TraitMethodDirect(
                 site, mkLit(site, '_tref', TTrait(trait)), method)
-            with IgnoreFailure(site):
+            with UnusedMethodRAIIScope(), IgnoreFailure(site):
                 codegen_call_traitmethod(site, method_ref, [],
-                                         outargs) .toc()
+                                         outargs).toc()
     out('}\n', preindent=-1)
 
 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(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
     # again.
-    with IgnoreFailure(site):
+    with UnusedMethodRAIIScope(), IgnoreFailure(site):
         codegen_call(method.site, method, indices, [], outargs).toc()
 
 def generate_startup_calls_entry_function(devnode):
@@ -3177,6 +3305,48 @@ def generate_startup_calls_entry_function(devnode):
         generate_startup_call_loops(startups)
     out('}\n', preindent=-1)
 
+def generate_raii_artifacts():
+    destructor_array_items = []
+
+    def add_destructor_array_item(ref_name, destructor):
+        constants.append((
+            f'const _raii_destructor_t *const {ref_name}',
+            f'&_dml_raii_destructors[{len(destructor_array_items)}]'))
+        destructor_array_items.append(destructor)
+
+    add_destructor_array_item(StringTypeInfo.cident_destructor_array_item,
+                              StringTypeInfo.cident_destructor)
+    add_destructor_array_item(
+        VectorRAIITypeInfo.cident_destructor_array_item_nonraii_elems,
+        VectorRAIITypeInfo.cident_destructor_nonraii_elems)
+    for info in dml.globals.generated_raii_types.values():
+        if info.should_generate_destructor:
+            start_function_definition(
+                f'void {info.cident_destructor}(void *_data)')
+            out('{\n', postindent=1)
+            TPtr(info.type).print_declaration('data', '_data')
+            info.generate_destructor('data')
+            out('}\n', preindent=-1)
+            add_destructor_array_item(info.cident_destructor_array_item,
+                                      info.cident_destructor)
+
+        if info.should_generate_copier:
+            start_function_definition(
+                f'void {info.cident_copier}(void *_dest, const void *_src)')
+            out('{\n', postindent=1)
+            TPtr(info.type).print_declaration('dest', '_dest')
+            TPtr(conv_const(True, info.type)).print_declaration('src', '_src')
+            info.generate_copier('dest', 'src')
+            out('}\n', preindent=-1)
+
+    constants.append(('const _raii_destructor_t *const '
+                      + '_dml_raii_destructor_ref_none',
+                      f'_dml_raii_destructors + {len(destructor_array_items)}')
+                     )
+    add_variable_declaration('const _raii_destructor_t _dml_raii_destructors'
+                             + f'[{len(destructor_array_items)}]',
+                             '{%s}' % (', '.join(destructor_array_items),))
+
 
 class MultiFileOutput(FileOutput):
     def __init__(self, stem, header):
@@ -3284,20 +3454,20 @@ def generate_cfile_body(device, footers, full_module, filename_prefix):
     generate_initialize(device)
     generate_finalize(device)
     generate_deinit(device)
-    generate_dealloc(device)
     generate_events(device)
     generate_identity_data_decls()
     generate_object_vtables_array()
     generate_class_var_decl()
     generate_startup_calls_entry_function(device)
     generate_init_data_objs(device)
+    generate_dealloc(device)
     if dml.globals.dml_version == (1, 2):
         generate_reset(device, 'hard')
         generate_reset(device, 'soft')
 
     # These parameter values are output into static context, so make sure
     # the expressions do not use _dev
-    with crep.TypedParamContext():
+    with crep.TypedParamContext(), ctree.StaticRAIIScope():
         trait_param_values = {
             node: resolve_trait_param_values(node)
             for node in flatten_object_subtree(device)
@@ -3407,6 +3577,7 @@ def generate_cfile_body(device, footers, full_module, filename_prefix):
 
     generate_index_enumerations()
     generate_tuple_table()
+    generate_raii_artifacts()
 
     for c in footers:
         c.toc()
diff --git a/py/dml/clone_test.py b/py/dml/clone_test.py
index 0ef9a63f6..4e04973c8 100644
--- a/py/dml/clone_test.py
+++ b/py/dml/clone_test.py
@@ -21,13 +21,14 @@ def test(self):
                     dt.TFloat("a"),
                     dt.TArray(typ0, ctree.mkIntegerLiteral(0, 2)),
                     dt.TPtr(typ0),
-                    dt.TVector(typ0),
+                    dt.TVectorLegacy(typ0),
                     dt.TTrait(object()),
                     dt.TStruct({"name": types.TInt(32, False)}),
                     dt.TLayout("big-endian", {}),
                     dt.TFunction([], dt.TVoid()),
                     dt.TDevice("a")):
             typ_clone = typ.clone()
+
             self.assertEqual(
                 types.realtype(typ_clone).cmp(types.realtype(typ)), 0)
             self.assertEqual(
diff --git a/py/dml/codegen.py b/py/dml/codegen.py
index fbc25cf3a..854ee6e5f 100644
--- a/py/dml/codegen.py
+++ b/py/dml/codegen.py
@@ -2,7 +2,7 @@
 # SPDX-License-Identifier: MPL-2.0
 
 import re
-from abc import ABC, abstractmethod, abstractproperty
+from abc import ABC, ABCMeta, abstractmethod, abstractproperty
 import operator
 import contextlib
 from functools import reduce
@@ -48,6 +48,9 @@
     'CatchFailure',
     'ReturnFailure',
     'IgnoreFailure',
+    'get_raii_type_info',
+    'StringTypeInfo',
+    'VectorRAIITypeInfo',
 
     'c_rettype',
     'c_inargs',
@@ -100,14 +103,26 @@ class LoopContext:
     def __enter__(self):
         self.prev = LoopContext.current
         LoopContext.current = self
+        assert RAIIScope.scope_stack()
+        self.outermost_raii_scope = RAIIScope.scope_stack()[-1]
+        return self
     def __exit__(self, exc_type, exc_val, exc_tb):
         assert LoopContext.current is self
         LoopContext.current = self.prev
+    @abstractmethod
+    def break_(self, site): pass
+    @abstractmethod
+    def continue_(self, site): pass
 
 class CLoopContext(LoopContext):
     '''DML loop context corresponding to a C loop'''
     def break_(self, site):
-        return [mkBreak(site)]
+        return [codegen_raii_clear_up_to(site, self.outermost_raii_scope),
+                mkBreak(site)]
+
+    def continue_(self, site):
+        return [codegen_raii_clear_up_to(site, self.outermost_raii_scope),
+                mkContinue(site)]
 
 class NoLoopContext(LoopContext):
     '''DML loop context corresponding to an inlined method call.
@@ -116,6 +131,9 @@ class NoLoopContext(LoopContext):
     def break_(self, site):
         raise EBREAK(site)
 
+    def continue_(self, site):
+        raise ECONT(site)
+
 class GotoLoopContext(LoopContext):
     '''DML loop context not directly corresponding to a single C loop
     statement. Uses of `break` is codegen:d as a goto past the loop.'''
@@ -128,7 +146,11 @@ def __init__(self):
 
     def break_(self, site):
         self.used = True
-        return [mkGotoBreak(site, self.label)]
+        return [codegen_raii_clear_up_to(site, self.outermost_raii_scope),
+                mkGotoBreak(site, self.label)]
+
+    def continue_(self, site):
+        raise ECONTU(site)
 
 class Failure(ABC):
     '''Handle exceptions failure handling is supposed to handle the various kind of
@@ -138,6 +160,10 @@ class Failure(ABC):
     fail_stack = []
     def __init__(self, site):
         self.site = site
+        if RAIIScope.scope_stack():
+            self.outermost_raii_scope = RAIIScope.scope_stack()[-1]
+        else:
+            self.outermost_raii_scope = None
     def __enter__(self):
         self.fail_stack.append(self)
     def __exit__(self, exc_type, exc_val, exc_tb):
@@ -174,7 +200,12 @@ class ReturnFailure(Failure):
     '''Generate boolean return statements to signal success. False
     means success.'''
     def fail(self, site):
-        return mkReturn(site, mkBoolConstant(site, True))
+        assert self.outermost_raii_scope is not None
+        return mkCompound(site,
+                          [codegen_raii_clear_up_to(
+                              site,
+                              self.outermost_raii_scope),
+                           mkReturn(site, mkBoolConstant(site, True))])
     def nofail(self):
         '''Return code that is used to leave the method successfully'''
         return mkReturn(self.site, mkBoolConstant(self.site, False))
@@ -187,9 +218,13 @@ def __init__(self, site, method_node):
         self.label = None
         self.method = method_node
     def fail(self, site):
+        assert self.outermost_raii_scope is not None
         if not self.label:
             self.label = gensym('throw')
-        return mkThrow(site, self.label)
+        return mkCompound(site,
+                          [codegen_raii_clear_up_to(site,
+                                                    self.outermost_raii_scope),
+                            mkThrow(site, self.label)])
 
 class IgnoreFailure(Failure):
     '''Ignore exceptions'''
@@ -199,9 +234,14 @@ def fail(self, site):
 class ExitHandler(ABC):
     current = None
 
+    def __init__(self):
+        assert RAIIScope.scope_stack()
+        self.outermost_raii_scope = RAIIScope.scope_stack()[-1]
+
     def __enter__(self):
         self.prev = ExitHandler.current
         ExitHandler.current = self
+
     def __exit__(self, exc_type, exc_val, exc_tb):
         assert ExitHandler.current is self
         ExitHandler.current = self.prev
@@ -221,12 +261,24 @@ def __init__(self):
         self.used = False
         GotoExit.count += 1
         self.label = 'exit%d' % (self.count,)
+        super(GotoExit, self).__init__()
+
+def codegen_raii_clear_up_to(site, scope):
+    return mkRAIIScopeClears(site,
+                             RAIIScope.scope_stack()[scope.scope_stack_ix+1:])
+def codegen_raii_clear_up_to_and_including(site, scope):
+    return mkRAIIScopeClears(site,
+                             RAIIScope.scope_stack()[scope.scope_stack_ix:])
+
 
 class GotoExit_dml12(GotoExit):
     def codegen_exit(self, site, retvals):
         assert retvals is None
         self.used = True
-        return mkGoto(site, self.label)
+        return mkCompound(
+            site,
+            [codegen_raii_clear_up_to(site, self.outermost_raii_scope)]
+            + [mkGoto(site, self.label)])
 
 class GotoExit_dml14(GotoExit):
     def __init__(self, outvars):
@@ -240,12 +292,14 @@ def codegen_exit(self, site, retvals):
             site,
             [mkCopyData(site, val, out)
              for (out, val) in zip(self.outvars, retvals)]
-            + [mkReturnFromInline(site, self.label)])
+            + [codegen_raii_clear_up_to(site, self.outermost_raii_scope),
+               mkReturnFromInline(site, self.label)])
 
 class ReturnExit(ExitHandler):
     def __init__(self, outp, throws):
         self.outp = outp
         self.throws = throws
+        super(ReturnExit, self).__init__()
     def codegen_exit(self, site, retvals):
         assert retvals is not None, 'dml 1.2/1.4 mixup'
         return codegen_return(site, self.outp, self.throws, retvals)
@@ -253,10 +307,10 @@ def codegen_exit(self, site, retvals):
 def memoized_return_failure_leave(site, make_ref, failed):
     ran = make_ref('ran', TInt(8, True))
     threw = make_ref('threw', TBool())
-    stmts = [mkCopyData(site, mkIntegerLiteral(site, 1), ran),
-             mkCopyData(site, mkBoolConstant(site, failed), threw),
-             mkReturn(site, mkBoolConstant(site, failed))]
-    return stmts
+    return [mkCopyData(site, mkIntegerLiteral(site, 1), ran),
+            mkCopyData(site, mkBoolConstant(site, failed), threw),
+            mkRAIIScopeClears(site, RAIIScope.scope_stack()),
+            mkReturn(site, mkBoolConstant(site, failed))]
 
 class MemoizedReturnFailure(Failure):
     '''Generate boolean return statements to signal success. False
@@ -293,7 +347,7 @@ def codegen_exit(self, site, retvals):
         for ((name, typ), val) in zip(self.outp, retvals):
             target = self.make_ref(f'p_{name}', typ)
             stmts.append(mkCopyData(site, val, target))
-            targets.append(target)
+            targets.append(OrphanWrap(target.site, target))
         stmts.append(codegen_return(site, self.outp, self.throws, targets))
         return mkCompound(site, stmts)
 
@@ -364,20 +418,22 @@ def fail_handler(self):
                 if self.method.throws else NoFailure(self.method.site))
 
 def memoization_common_prelude(name, site, outp, throws, make_ref):
-    has_run_stmts = []
-    # Throwing is treated as a special kind of output parameter, stored through
-    # 'threw'. When 'ran' indicates the method has been called before to
-    # completion, 'threw' is retrieved to check whether the method threw or
-    # not. If it did, then the call completes by throwing again; otherwise, the
-    # cached return values are retrieved and returned.
-    if throws:
+    with UnusedMethodRAIIScope():
+        has_run_stmts = []
+        # Throwing is treated as a special kind of output parameter, stored
+        # through 'threw'. When 'ran' indicates the method has been called
+        # before to completion, 'threw' is retrieved to check whether the
+        # method threw or not. If it did, then the call completes by throwing
+        # again; otherwise, the cached return values are retrieved and
+        # returned.
+        if throws:
+            has_run_stmts.append(
+                mkIf(site, make_ref('threw', TBool()),
+                     mkReturn(site, mkBoolConstant(site, True))))
         has_run_stmts.append(
-            mkIf(site, make_ref('threw', TBool()),
-                 mkReturn(site, mkBoolConstant(site, True))))
-    has_run_stmts.append(
-        codegen_return(site, outp, throws,
-                       [make_ref(f'p_{pname}', ptype)
-                        for (pname, ptype) in outp]))
+            codegen_return(site, outp, throws,
+                           [OrphanWrap(site, make_ref(f'p_{pname}', ptype))
+                            for (pname, ptype) in outp]))
     # 'ran' is used to check whether the function has been called or not:
     # - 0:  never been called before. Set to -1, and execute the body.
     #       Before any return, cache the results, and set 'ran' to 1.
@@ -506,6 +562,12 @@ def cident_set_value(self):
         else:
             return '_simple_event_only_domains_set_value'
 
+    @property
+    def cident_destroy(self):
+        return (self.cident_prefix + 'destroy'
+                if self.args_type and self.args_type.is_raii
+                else '_destroy_simple_event_data')
+
 
 class AfterOnHookInfo(CheckpointedAfterInfo):
     def __init__(self, dimsizes, parent, typeseq_info, prim_key,
@@ -531,6 +593,10 @@ def generate_args_serializer(self, site, args_expr, out_expr): pass
     def generate_args_deserializer(self, site, val_expr, out_expr, error_out):
         pass
 
+    @abstractmethod
+    def generate_args_destructor(self, site, args_expr):
+        pass
+
     @abstractproperty
     def string_prim_key(self):
         '''The AfterOnHookInfo key for the primary component -- the target
@@ -559,10 +625,19 @@ def cident_args_deserializer(self):
         assert self.has_serialized_args
         return self.cident_prefix + 'args_deserializer'
 
+    @property
+    def cident_args_destructor(self):
+        assert self.has_serialized_args and self.args_type.is_raii
+        return self.cident_prefix + 'args_destructor'
+
 class ImmediateAfterInfo(AfterInfo):
     def __init__(self, key, dimsizes, uniq):
         self.uniq = uniq
         super().__init__(key, dimsizes)
+        self.args_raii_info = (get_raii_type_info(self.args_type)
+                               if (self.args_type
+                                   and self.args_type.is_raii)
+                               else None)
 
     @abstractmethod
     def generate_callback_call(self, indices_lit, args_lit): pass
@@ -597,7 +672,7 @@ def generate_callback_call(self, indices_lit, args_lit):
                         for i in range(self.method.dimensions))
         args = tuple(mkLit(site, f'{args_lit}->{pname}', ptype)
                      for (pname, ptype) in self.method.inp)
-        with LogFailure(site, self.method, indices), \
+        with UnusedMethodRAIIScope(), LogFailure(site, self.method, indices), \
              crep.DeviceInstanceContext():
             code = codegen_call(site, self.method, indices, args, ())
         code = mkCompound(site, [code])
@@ -606,13 +681,16 @@ def generate_callback_call(self, indices_lit, args_lit):
 class AfterDelayIntoSendNowInfo(AfterDelayInfo):
     def __init__(self, typeseq_info, uniq):
         super().__init__(typeseq_info, [], uniq)
-        self .typeseq_info = typeseq_info
+        self.typeseq_info = typeseq_info
         hookref_type = THook(typeseq_info.types, validated=True)
         self._args_type = (
             TStruct({'hookref': hookref_type,
                      'args': typeseq_info.struct},
                     label=f'_simple_event_{self.uniq}_args')
             if typeseq_info.types else hookref_type)
+        self.typeseq_info_struct_raii_info = (
+            get_raii_type_info(typeseq_info.struct)
+            if typeseq_info.types and typeseq_info.struct.is_raii else None)
 
     @property
     def args_type(self):
@@ -630,10 +708,13 @@ def generate_callback_call(self, indices_lit, args_lit):
         assert indices_lit is None
         has_args = bool(self.typeseq_info.types)
         hookref = f'{args_lit}->hookref' if has_args else f'*{args_lit}'
+        resolved_hookref = ('_DML_resolve_hookref(_dev, _hook_aux_infos, '
+                            + f'{hookref})')
         args = f'&{args_lit}->args' if has_args else 'NULL'
-        out('_DML_send_hook(&_dev->obj, &_dev->_detached_hook_queue_stack, '
-            + f'_DML_resolve_hookref(_dev, _hook_aux_infos, {hookref}), '
-            + f'{args});\n')
+        out(f'_DML_SEND_HOOK({resolved_hookref}, {args});\n')
+        if self.typeseq_info_struct_raii_info:
+            out(self.typeseq_info_struct_raii_info.read_destroy_lval(
+                f'{args_lit}->args') + ';\n')
 
 def get_after_delay(key):
     try:
@@ -644,7 +725,6 @@ def get_after_delay(key):
         dml.globals.after_delay_infos[key] = info
         return info
 
-
 class AfterOnHookIntoMethodInfo(AfterOnHookInfo):
     def __init__(self, typeseq_info, method, param_to_msg_comp):
         self.method = method
@@ -656,23 +736,36 @@ def __init__(self, typeseq_info, method, param_to_msg_comp):
                      if i not in param_to_msg_comp},
                     label=f'_after_on_hook_{self.uniq}_args')
             if len(self.method.inp) > len(param_to_msg_comp) else None)
+        self.args_raii_info = (get_raii_type_info(self._args_type)
+                               if self._args_type and self._args_type.is_raii
+                               else None)
 
     def generate_callback_call(self, indices_lit, args_lit, msg_lit):
         site = self.method.site
         indices = tuple(mkLit(site, f'{indices_lit}[{i}]', TInt(32, False))
                         for i in range(self.method.dimensions))
         args = tuple(
-            mkLit(site,
-                  f'{msg_lit}->comp{self.param_to_msg_comp[i]}'
-                  if i in self.param_to_msg_comp else f'{args_lit}->{pname}',
-                  ptype)
+                (lambda e: RAIIDupe(site, e) if ptype.is_raii else e)(
+                    mkLit(site, f'{msg_lit}->comp{self.param_to_msg_comp[i]}',
+                          ptype))
+                if i in self.param_to_msg_comp else
+                    mkLit(site, f'{args_lit}->{pname}', ptype)
             for (i, (pname, ptype)) in enumerate(self.method.inp))
-        with LogFailure(site, self.method, indices), \
+        with UnusedMethodRAIIScope(), LogFailure(site, self.method, indices), \
              crep.DeviceInstanceContext():
             code = codegen_call(site, self.method, indices, args, ())
         code = mkCompound(site, [code])
         code.toc()
 
+    def generate_args_destructor(self, site, args_expr):
+        for (i, (name, typ)) in enumerate(self.method.inp):
+            if i in self.param_to_msg_comp or not typ.is_raii:
+                continue
+            info = get_raii_type_info(typ)
+            out(info.read_destroy_lval(ctree.mkSubRef(site, args_expr, name,
+                                                     '.').read())
+                + ';\n')
+
     def generate_args_serializer(self, site, args_expr, out_expr):
         sources = tuple((ctree.mkSubRef(site, args_expr, name, "."),
                          safe_realtype(typ))
@@ -681,13 +774,7 @@ def generate_args_serializer(self, site, args_expr, out_expr):
         serialize.serialize_sources_to_list(site, sources, out_expr)
 
     def generate_args_deserializer(self, site, val_expr, out_expr, error_out):
-        if self.args_type:
-            tmp_out_decl, tmp_out_ref = serialize.declare_variable(
-                site, '_tmp_out', self.args_type)
-            tmp_out_decl.toc()
-        else:
-            tmp_out_ref = None
-        targets = tuple((ctree.mkSubRef(site, tmp_out_ref, name, "."),
+        targets = tuple((ctree.mkSubRef(site, out_expr, name, "."),
                          safe_realtype(typ))
                         if i not in self.param_to_msg_comp else None
                         for (i, (name, typ)) in enumerate(self.method.inp))
@@ -698,10 +785,6 @@ def error_out_at_index(_i, exc, msg):
         serialize.deserialize_list_to_targets(
             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()
 
     @property
     def args_type(self):
@@ -719,6 +802,11 @@ def string_prim_key(self):
 class AfterOnHookIntoSendNowInfo(AfterOnHookInfo):
     def __init__(self, typeseq_info, sendnow_typeseq_info, param_to_msg_comp):
         self.sendnow_typeseq_info = sendnow_typeseq_info
+        self.sendnow_typeseq_info_raii_info = (
+            get_raii_type_info(sendnow_typeseq_info.struct)
+            if (self.sendnow_typeseq_info.types
+                and self.sendnow_typeseq_info.struct.is_raii) else None)
+
         inp = [(f'comp{i}', typ)
                for (i, typ) in enumerate(sendnow_typeseq_info.types)]
         has_inner_args = len(inp) > len(param_to_msg_comp)
@@ -743,20 +831,41 @@ def generate_callback_call(self, indices_lit, args_lit, msg_lit):
         has_inner_args = (len(self.sendnow_typeseq_info.types)
                           > len(self.param_to_msg_comp))
         hookref = f'{args_lit}->hookref' if has_inner_args else f'*{args_lit}'
+        resolved_hookref = ('_DML_resolve_hookref(_dev, _hook_aux_infos, '
+                            + f'{hookref})')
+
+        msg_comps = ', '.join(
+            (lambda x: (get_raii_type_info(typ).read_dupe(x)
+                        if typ.is_raii else x))(
+             f'{msg_lit}->comp{self.param_to_msg_comp[i]}')
+            if i in self.param_to_msg_comp else
+            f'{args_lit}->args.comp{i}'
+            for (i, typ) in enumerate(self.sendnow_typeseq_info.types))
+        msg = (('&((%s_t){%s})'
+                % (self.sendnow_typeseq_info.struct.label, msg_comps))
+               if msg_comps else 'NULL')
+        if self.sendnow_typeseq_info_raii_info:
+            destructor = self.sendnow_typeseq_info_raii_info.cident_destructor
+            out(f'_DML_SEND_HOOK_RAII({resolved_hookref}, {msg}, '
+                + f'{destructor});\n')
+        else:
+            out(f'_DML_SEND_HOOK({resolved_hookref}, {msg});\n')
+
+    def generate_args_destructor(self, site, args_expr):
+        has_inner_args = (len(self.sendnow_typeseq_info.types)
+                          > len(self.param_to_msg_comp))
+        if not has_inner_args:
+            return
 
-        sendnow_msg_struct = self.sendnow_typeseq_info.struct
-        args = (('&(%s_t) {%s}'
-                % (sendnow_msg_struct.label,
-                   ', '.join(
-                       f'{msg_lit}->comp{self.param_to_msg_comp[i]}'
-                       if i in self.param_to_msg_comp else
-                       f'{args_lit}->args.comp{i}'
-                       for i in range(len(self.sendnow_typeseq_info.types)))))
-                if self.sendnow_typeseq_info.types else 'NULL')
+        inner_args = ctree.mkSubRef(site, args_expr, 'args', '.')
+        for (i, typ) in enumerate(self.sendnow_typeseq_info.types):
+            if i in self.param_to_msg_comp or not typ.is_raii:
+                continue
+            info = get_raii_type_info(typ)
+            out(info.read_destroy_lval(ctree.mkSubRef(site, inner_args,
+                                                     f'comp{i}', '.').read())
+                + ';\n')
 
-        out('_DML_send_hook(&_dev->obj, &_dev->_detached_hook_queue_stack, '
-            + f'_DML_resolve_hookref(_dev, _hook_aux_infos, {hookref}), '
-            + f'{args});\n')
 
     def generate_args_serializer(self, site, args_expr, out_expr):
         has_inner_args = (len(self.sendnow_typeseq_info.types)
@@ -783,11 +892,8 @@ def generate_args_serializer(self, site, args_expr, out_expr):
     def generate_args_deserializer(self, site, val_expr, out_expr, error_out):
         has_inner_args = (len(self.sendnow_typeseq_info.types)
                           > len(self.param_to_msg_comp))
-        tmp_out_decl, tmp_out_ref = serialize.declare_variable(
-            site, '_tmp_out', self.args_type)
-        tmp_out_decl.toc()
-        hookref = (ctree.mkSubRef(site, tmp_out_ref, 'hookref', '.')
-                   if has_inner_args else tmp_out_ref)
+        hookref = (ctree.mkSubRef(site, out_expr, 'hookref', '.')
+                   if has_inner_args else out_expr)
         targets = [(hookref,
                     safe_realtype(THook(self.sendnow_typeseq_info.types,
                                         validated=True)))]
@@ -806,7 +912,7 @@ def error_out_at_index(_i, exc, msg):
             'deserialization of arguments to a send_now')
 
         if has_inner_args:
-            inner_args = ctree.mkSubRef(site, tmp_out_ref, 'args', '.')
+            inner_args = ctree.mkSubRef(site, out_expr, 'args', '.')
             inner_args_targets = (
                 (ctree.mkSubRef(site, inner_args, f'comp{i}', '.'),
                  safe_realtype(typ))
@@ -816,10 +922,6 @@ def error_out_at_index(_i, exc, msg):
                 site, inner_args_val, inner_args_targets, error_out_at_index,
                 'deserialization of arguments to a send_now')
 
-
-        ctree.mkAssignStatement(site, out_expr,
-                                ctree.ExpressionInitializer(tmp_out_ref)).toc()
-
     @property
     def args_type(self):
         return self._args_type
@@ -835,10 +937,10 @@ def string_prim_key(self):
 class ImmediateAfterIntoMethodInfo(ImmediateAfterInfo):
     def __init__(self, method, uniq):
         self.method = method
-        super().__init__(method, method.dimsizes, uniq)
         self._args_type = (TStruct(dict(method.inp),
-                                   label=f'_immediate_after_{self.uniq}_args')
+                                   label=f'_immediate_after_{uniq}_args')
                            if method.inp else None)
+        super().__init__(method, method.dimsizes, uniq)
 
     @property
     def args_type(self):
@@ -854,22 +956,25 @@ def generate_callback_call(self, indices_lit, args_lit):
                         for i in range(self.method.dimensions))
         args = tuple(mkLit(site, f'{args_lit}->{pname}', ptype)
                      for (pname, ptype) in self.method.inp)
-        with LogFailure(site, self.method, indices), \
+        with UnusedMethodRAIIScope(), LogFailure(site, self.method, indices), \
              crep.DeviceInstanceContext():
             code = codegen_call(site, self.method, indices, args, ())
-        code = mkCompound(site, [code])
-        code.toc()
+        mkCompound(site, [code]).toc()
 
 class ImmediateAfterIntoSendNowInfo(ImmediateAfterInfo):
     def __init__(self, typeseq_info, uniq):
-        super().__init__(typeseq_info, [], uniq)
         self.typeseq_info = typeseq_info
         hookref_type = THook(typeseq_info.types, validated=True)
         self._args_type = (
             TStruct({'hookref': hookref_type,
                      'args': typeseq_info.struct},
-                    label=f'_immediate_after_{self.uniq}_args')
+                    label=f'_immediate_after_{uniq}_args')
             if typeseq_info.types else hookref_type)
+        self.typeseq_info_raii_info = (get_raii_type_info(typeseq_info.struct)
+                                       if (typeseq_info.types
+                                           and typeseq_info.struct.is_raii)
+                                       else None)
+        super().__init__(typeseq_info, [], uniq)
 
     @property
     def args_type(self):
@@ -884,9 +989,12 @@ def generate_callback_call(self, indices_lit, args_lit):
         has_args = bool(self.typeseq_info.types)
         hookref = f'{args_lit}->hookref' if has_args else f'*{args_lit}'
         args = f'&{args_lit}->args' if has_args else 'NULL'
-        out('_DML_send_hook(&_dev->obj, &_dev->_detached_hook_queue_stack, '
-            + f'_DML_resolve_hookref(_dev, _hook_aux_infos, {hookref}), '
-            + f'{args});\n')
+        resolved_hookref = ('_DML_resolve_hookref(_dev, _hook_aux_infos, '
+                            + f'{hookref})')
+        out(f'_DML_SEND_HOOK({resolved_hookref}, {args});\n')
+        if self.typeseq_info_raii_info:
+            out(self.typeseq_info_raii_info.read_destroy_lval(
+                f'{args_lit}->args') + ';\n')
 
 def get_immediate_after(key):
     try:
@@ -937,7 +1045,329 @@ def args_init(self):
         assert self.inargs
         return f'{{ {", ".join(inarg.read() for inarg in self.inargs)} }}'
 
-def declarations(scope):
+class RAIITypeInfo(metaclass=ABCMeta):
+    '''Information used to generate artifact corresponding to a unique RAII
+    type'''
+
+    @abstractproperty
+    def type(self): pass
+
+    @abstractproperty
+    def cident_destructor(self): pass
+
+    @abstractproperty
+    def cident_destructor_array_item(self): pass
+
+    @abstractproperty
+    def cident_copier(self): pass
+
+    # Return a void-typed C expression that consumes all resources
+    # associated with the expression's value
+    @abstractmethod
+    def read_destroy(self, expr): pass
+
+    # A variant of read_destroy specialized for when expr is an lvalue
+    # This can be important for when the RAII type this RAIITypeInfo is
+    # for is arbitrarily large
+    def read_destroy_lval(self, expr):
+        return self.read_destroy(expr)
+
+    # Return a void-typed C expression that copies src into tgt, replacing
+    # tgt's resources with a duplication of those of src.
+    @abstractmethod
+    def read_copy(self, tgt, src): pass
+
+    # A variant of read_copy specialized for when src is an lvalue
+    # This can be important for when the RAII type this RAIITypeInfo is
+    # for can be arbitrarily large
+    def read_copy_lval(self, tgt, src):
+        return self.read_copy(tgt, src)
+
+    # Return a void-typed C expression that moves src into tgt.
+    # tgt must be a C lval. src is consumed by this and must never be accessed
+    # again; including never destroyed. This means src must be orphan.
+    def read_linear_move(self, tgt, src):
+        return (f'_DML_RAII_LINEAR_MOVE_SIMPLE_RVAL('
+                + f'{self.type.declaration("")}, '
+                + f'{self.cident_destructor}, {tgt}, {src})')
+
+    # a specialization of read_linear_move to optimize the case when the src is
+    # a C lvalue.
+    # This can be important if the type this RAIITypeInfo is for can be
+    # arbitrarily large (like structs)
+    def read_linear_move_lval(self, tgt, src):
+        return (f'_DML_RAII_LINEAR_MOVE_SIMPLE({self.type.declaration("")}, '
+                + f'{self.cident_destructor}, {tgt}, {src})')
+
+    # Return a C expression that evaluates to a copy of expr with duplicated
+    # associated resources.
+    def read_dupe(self, expr):
+        t = self.type.declaration('')
+        return f'_DML_RAII_DUPE({t}, {self.cident_copier}, {expr})'
+
+class StringTypeInfo(RAIITypeInfo):
+    type = TString()
+    cident_destructor = '_DML_string_destructor'
+    cident_destructor_array_item = '_DML_string_destructor_ref'
+    cident_copier = '_DML_string_copier'
+
+    def read_destroy(self, expr):
+        return f'_dml_string_free({expr})'
+
+    def read_copy(self, tgt, src):
+        return f'_dml_string_copy((void *)&({tgt}), {src})'
+
+    def read_dupe(self, expr):
+        return f'_dml_string_dupe({expr})'
+
+class GeneratedRAIITypeInfo(RAIITypeInfo):
+    @abstractproperty
+    def cident_prefix(self):
+        '''A prefix for C identifiers used for the naming of artifacts
+        generated for the RAII type'''
+
+    @property
+    def cident_destructor(self):
+        return self.cident_prefix + 'destructor'
+
+    @property
+    def cident_destructor_array_item(self):
+        return self.cident_prefix + 'destructor_ref'
+
+    @property
+    def cident_copier(self):
+        return self.cident_prefix + 'copier'
+
+    @property
+    def cident_dupe(self):
+        return self.cident_prefix + 'dupe'
+
+    def read_destroy(self, expr):
+        return f'_DML_RAII_DESTROY_RVAL({self.cident_destructor}, {expr})'
+
+    def read_destroy_lval(self, expr):
+        return f'{self.cident_destructor}((void *)&({expr}))'
+
+    def read_copy(self, tgt, src):
+        return f'_DML_RAII_COPY_RVAL({self.cident_copier}, {tgt}, {src})'
+
+    def read_copy_lval(self, tgt, src):
+        return f'{self.cident_copier}((void *)&({tgt}), &({src}))'
+
+    should_generate_destructor = True
+    def generate_destructor(self, expr):
+        raise ICE(None, f"generate_destructor of {type(self).__name__} is abstract")
+
+    should_generate_copier = True
+    def generate_copier(self, tgt, src):
+        raise ICE(None, f"generate_copier of {type(self).__name__} is abstract")
+
+class StructRAIITypeInfo(GeneratedRAIITypeInfo):
+    def __init__(self, type):
+        assert isinstance(type, TStruct)
+        self._type = type
+        self.raii_members = [
+            (name, typ, info)
+            for (name, typ) in self.type.members.items()
+            if typ.is_raii
+            for info in (get_raii_type_info(typ),)]
+
+
+    @property
+    def type(self):
+        return self._type
+
+    @property
+    def cident_prefix(self):
+        return '_dml_raii_' + self.type.label + '_'
+
+    def read_linear_move_lval(self, tgt, src):
+        if deep_const(self.type):
+            return (f'_DML_RAII_LINEAR_MOVE_MEMCPY({self.cident_destructor}'
+                    + f', {tgt}, {src})')
+        return GeneratedRAIITypeInfo.read_linear_move_lval(self, tgt, src)
+
+    def read_linear_move(self, tgt, src):
+        if deep_const(self.type):
+            return ('_DML_RAII_LINEAR_MOVE_MEMCPY_RVAL('
+                    + f'{self.cident_destructor}, {tgt}, {src})')
+        return GeneratedRAIITypeInfo.read_linear_move(self, tgt, src)
+
+    def generate_destructor(self, expr):
+        for (name, _, info) in self.raii_members:
+            out(info.read_destroy_lval(f'{expr}->{name}') + ';\n')
+
+    def generate_copier(self, tgt, src):
+        site = logging.SimpleSite(self.cident_copier)
+        for (name, typ) in self.type.members.items():
+            out(ctree.ExpressionInitializer(
+                mkLit(site, f'{src}->{name}', typ)).assign_to(f'{tgt}->{name}',
+                                                              typ)
+                + ';\n')
+
+class ArrayRAIITypeInfo(GeneratedRAIITypeInfo):
+    count = 0
+    def __init__(self, type):
+        assert isinstance(type, TArray) and type.size.constant
+        self._type = type
+        self.base_info = get_raii_type_info(type.base)
+        self.uniq = ArrayRAIITypeInfo.count
+        ArrayRAIITypeInfo.count += 1
+
+    @property
+    def type(self):
+        return self._type
+
+    @property
+    def cident_prefix(self):
+        return f'_dml_raii_array_{self.uniq}_'
+
+    def generate_destructor(self, expr):
+        if not self.type.size.value:
+            return
+        out(f'for (uint32 _i = 0; _i < {self.type.size.read()}; ++_i)\n',
+            postindent=1)
+        out(self.base_info.read_destroy_lval(f'(*{expr})[_i]') + ';\n',
+            postindent=-1)
+
+    def generate_copier(self, tgt, src):
+        site = logging.SimpleSite(self.cident_copier)
+        out(f'for (uint32 _i = 0; _i < {self.type.size.read()}; ++_i) {{\n',
+            postindent=1)
+        out(ctree.ExpressionInitializer(
+            mkLit(site, f'(*{src})[_i]', self.type.base)).assign_to(
+                f'(*({tgt}))[_i]', self.type.base)
+            + ';\n')
+        out('}\n',preindent=-1)
+
+    def read_linear_move_lval(self, tgt, src):
+        return (f'_DML_RAII_LINEAR_MOVE_MEMCPY({self.cident_destructor}'
+                + f', {tgt}, {src})')
+
+    def read_linear_move(self, tgt, src):
+        # Any array expression should be an lvalue
+        raise ICE(None,
+                  'ArrayTypeInfo.read_linear_move called instead of '
+                  + 'read_linear_move_lval')
+
+    def read_dupe(self, expr):
+        raise ICE(None,
+                  'ArrayTypeInfo.read_dupe called. '
+                  + 'Arrays should never be duped!')
+
+class VectorRAIITypeInfo(GeneratedRAIITypeInfo):
+    cident_destructor_nonraii_elems = '_DML_vector_nonraii_elems_destructor'
+    cident_destructor_array_item_nonraii_elems = (
+        '_DML_vector_nonraii_elems_destructor_ref')
+    count = 0
+    def __init__(self, type):
+        assert isinstance(type, TVector)
+        self._type = type
+        self.base_info = (get_raii_type_info(type.base)
+                          if type.base.is_raii else None)
+        self.uniq = VectorRAIITypeInfo.count
+        VectorRAIITypeInfo.count += 1
+
+    @property
+    def type(self):
+        return self._type
+
+    @property
+    def cident_prefix(self):
+        return f'_dml_raii_vector_{self.uniq}_'
+
+    @property
+    def cident_destructor(self):
+        if self.base_info:
+            return super().cident_destructor
+        return self.cident_destructor_nonraii_elems
+
+    @property
+    def cident_destructor_array_item(self):
+        if self.base_info:
+            return super().cident_destructor_array_item
+        return self.cident_destructor_array_item_nonraii_elems
+
+    def read_destroy(self, expr):
+        if self.base_info:
+            return ('_dml_vect_free_raii(sizeof(%s), %s, %s)'
+                    % (self.type.base.declaration(""),
+                       self.base_info.cident_destructor,
+                       expr))
+        else:
+            return f'_dml_vect_free({expr})'
+
+    def read_destroy_lval(self, expr):
+        return self.read_destroy(expr)
+
+    def read_copy(self, tgt, src):
+        if not self.base_info:
+            return ('_dml_vect_copy(sizeof(%s), (_dml_vect_t *)&(%s), %s)'
+                    % (self.type.base.declaration(""), tgt, src))
+        return GeneratedRAIITypeInfo.read_copy(self, tgt, src)
+
+    def read_copy_lval(self, tgt, src):
+        if not self.base_info:
+            return ('_dml_vect_copy(sizeof(%s), (_dml_vect_t *)&(%s), %s)'
+                    % (self.type.base.declaration(""), tgt, src))
+        return GeneratedRAIITypeInfo.read_copy_lval(self, tgt, src)
+
+    @property
+    def should_generate_destructor(self):
+        return self.base_info is not None
+
+    def generate_destructor(self, expr):
+        assert self.should_generate_destructor
+        out(self.read_destroy_lval(f'*{expr}') + ';\n')
+
+    def generate_copier(self, tgt, src):
+        if self.base_info:
+            out(f'if (unlikely({tgt}->elements == {src}->elements)) return;\n')
+            out('_dml_vect_resize_raii(sizeof(%s), %s, %s, %s->len);\n'
+                % (self.type.base.declaration(''),
+                   self.base_info.cident_destructor,
+                   tgt,
+                   src))
+            def elem_of_vec(vec):
+                return ('DML_VECT_ELEM_UNSAFE(%s, %s, _i)'
+                        % (self.type.base.declaration(""), vec))
+            out(f'for (uint32 _i = 0; _i < {src}->len; ++_i)\n',
+                postindent=1)
+            out(self.base_info.read_copy_lval(elem_of_vec(f'*({tgt})'),
+                                             elem_of_vec(f'*({src})'))
+                + ';\n',
+                postindent=-1)
+        else:
+            out('_dml_vect_copy(sizeof(%s), %s, *%s);\n'
+                % (self.type.base.declaration(""), tgt, src))
+
+def get_raii_type_info(t):
+    uct = safe_realtype_unconst(t)
+    assert uct.is_raii
+    if isinstance(uct, TString):
+        return StringTypeInfo()
+    try:
+        return dml.globals.generated_raii_types[TypeKey(uct)]
+    except KeyError:
+        if isinstance(uct, TStruct):
+            if deep_const(uct):
+                raise ICE(t.declaration_site or None,
+                          ('Structs that simultaneously have const-qualified '
+                           + 'members and resource-enriched (RAII) members '
+                           + 'are not supported'))
+            info = StructRAIITypeInfo(uct)
+        elif isinstance(uct, TArray):
+            if not uct.size.constant:
+                raise EVLARAII(t.declaration_site, t.describe())
+            info = ArrayRAIITypeInfo(uct)
+        elif isinstance(uct, TVector):
+            info = VectorRAIITypeInfo(uct)
+        else:
+            assert False
+        dml.globals.generated_raii_types[TypeKey(uct)] = info
+        return info
+
+def declarations(scope, unscoped_raii=False):
     "Get all local declarations in a scope as a list of Declaration objects"
     decls = []
     for sym in scope.symbols():
@@ -946,7 +1376,7 @@ def declarations(scope):
             continue
         if sym.stmt:
             continue
-        decl = sym_declaration(sym)
+        decl = sym_declaration(sym, unscoped_raii=unscoped_raii)
         if decl:
             decls.append(decl)
 
@@ -1061,7 +1491,7 @@ def codegen_sizeof(site, expr):
     fun = mkLit(site, 'sizeof',
                 TFunction([], TNamed('size_t'),
                           varargs = True))
-    return Apply(site, fun, [expr], fun.ctype())
+    return Apply(site, fun, [expr], fun.ctype(), True)
 
 def flatten(x):
     '''Recursively flatten lists and tuples'''
@@ -1084,6 +1514,8 @@ def expr_unop(tree, location, scope):
         var = rh_ast.args[0]
         if var in typedefs and scope.lookup(var) is None:
             report(WSIZEOFTYPE(tree.site))
+            if safe_realtype_shallow(typedefs[var]).is_raii:
+                report(ESIZEOFRAII(tree.site))
             return codegen_sizeof(
                 tree.site, mkLit(tree.site, cident(var), None))
     try:
@@ -1142,8 +1574,10 @@ def expr_unop(tree, location, scope):
     elif op == 'post++':  return mkPostInc(tree.site, rh)
     elif op == 'post--':  return mkPostDec(tree.site, rh)
     elif op == 'sizeof':
-        if not dml.globals.compat_dml12 and not isinstance(rh, ctree.LValue):
+        if not dml.globals.compat_dml12 and not rh.addressable:
             raise ERVAL(rh.site, 'sizeof')
+        if safe_realtype_shallow(rh.ctype()).is_raii:
+            report(ESIZEOFRAII(tree.site))
         return codegen_sizeof(tree.site, rh)
     elif op == 'defined': return mkBoolConstant(tree.site, True)
     elif op == 'stringify':
@@ -1160,6 +1594,8 @@ def expr_typeop(tree, location, scope):
     (struct_defs, t) = eval_type(t, tree.site, location, scope)
     for (site, _) in struct_defs:
         report(EANONSTRUCT(site, "'sizeoftype' expression"))
+    if safe_realtype_shallow(t).is_raii:
+        report(ESIZEOFRAII(tree.site))
     return codegen_sizeof(tree.site, mkLit(tree.site, t.declaration(''), None))
 
 @expression_dispatcher
@@ -1312,24 +1748,14 @@ def expr_list(tree, location, scope):
 
 @expression_dispatcher
 def expr_cast(tree, location, scope):
-    [expr_ast, casttype] = tree.args
-    expr = codegen_expression_maybe_nonvalue(expr_ast, location, scope)
+    [init_ast, casttype] = tree.args
     (struct_defs, type) = eval_type(casttype, tree.site, location, scope)
     for (site, _) in struct_defs:
         report(EANONSTRUCT(site, "'cast' expression"))
 
-    if (dml.globals.compat_dml12 and dml.globals.api_version <= "6"
-        and isinstance(expr, InterfaceMethodRef)):
-        # Workaround for bug 24144
-        return mkLit(tree.site, "%s->%s" % (
-            expr.node_expr.read(), expr.method_name), type)
 
-    if isinstance(expr, NonValue) and (
-            not isinstance(expr, NodeRef)
-            or not isinstance(safe_realtype(type), TTrait)):
-        raise expr.exc()
-    else:
-        return mkCast(tree.site, expr, type)
+    return eval_initializer(tree.site, type, init_ast, location, scope, False,
+                            cast_init=True).as_expr(type)
 
 @expression_dispatcher
 def expr_undefined(tree, location, scope):
@@ -1429,6 +1855,8 @@ def fix_printf(fmt, args, argsites, site):
                                               TPtr(TNamed('char',
                                                           const=True)))),
                               [mkAddressOf(site, arg)])
+            elif isinstance(argtype, TString):
+                arg = mkStringCStrApply(site, arg)
 
         filtered_fmt += "%" + flags + width + precision + length + conversion
         filtered_args.append(arg)
@@ -1551,6 +1979,12 @@ def eval_type(asttype, site, location, scope, extern=False, typename=None,
                 msg_comp_types.append(msg_comp_type)
                 struct_defs.extend(msg_comp_struct_defs)
             etype = THook(msg_comp_types)
+        elif tag == 'vect':
+            (bsite, btype_ast) = info
+            (local_struct_defs, btype) = eval_type(
+                btype_ast, bsite, location, scope, extern)
+            struct_defs.extend(local_struct_defs)
+            etype = TVector(btype)
         else:
             raise ICE(site, "Strange type")
     elif isinstance(asttype[0], str):
@@ -1575,7 +2009,7 @@ def eval_type(asttype, site, location, scope, extern=False, typename=None,
         elif asttype[0] == 'vect':
             if etype.void:
                 raise EVOID(site)
-            etype = TVector(etype)
+            etype = TVectorLegacy(etype)
             asttype = asttype[1:]
         elif asttype[0] == 'array':
             if etype.void:
@@ -1780,7 +2214,8 @@ def mk_bitfield_compound_initializer_expr(site, etype, inits, location, scope,
 
     return source_for_assignment(site, etype, bitfields_expr)
 
-def eval_initializer(site, etype, astinit, location, scope, static):
+def eval_initializer(site, etype, astinit, location, scope, static,
+                     cast_init=False):
     """Deconstruct an AST for an initializer, and return a
        corresponding initializer object. Report EDATAINIT errors upon
        invalid initializers.
@@ -1798,12 +2233,22 @@ def eval_initializer(site, etype, astinit, location, scope, static):
     def do_eval(etype, astinit):
         shallow_real_etype = safe_realtype_shallow(etype)
         if astinit.kind == 'initializer_scalar':
-            expr = codegen_expression(astinit.args[0], location, scope)
-            if static and not expr.constant:
+            expr = codegen_expression_maybe_nonvalue(astinit.args[0], location,
+                                                     scope)
+            if (isinstance(expr, StringConstant)
+                and isinstance(shallow_real_etype, TString)):
+                expr = FromCString(astinit.site, expr)
+            elif cast_init:
+                assert not static
+                expr = mkCast(astinit.site, expr, etype)
+            elif isinstance(expr, NonValue):
+                raise expr.exc()
+            elif static and not expr.constant:
                 raise EDATAINIT(astinit.site, 'non-constant expression')
+            else:
+                expr = source_for_assignment(astinit.site, etype, expr)
 
-            return ExpressionInitializer(
-                source_for_assignment(astinit.site, etype, expr))
+            return ExpressionInitializer(expr)
         elif astinit.kind == 'initializer_designated_struct':
             (init_asts, allow_partial) = astinit.args
             if isinstance(shallow_real_etype, StructType):
@@ -1854,6 +2299,12 @@ def do_eval(etype, astinit):
                     for ((mn, mt), e) in zip(etype.members_qualified,
                                              init_asts)}
             return DesignatedStructInitializer(site, init)
+        elif isinstance(etype, TVector):
+            return ExpressionInitializer(
+                VectorCompoundLiteral(
+                    site,
+                    tuple(do_eval(etype.base, e) for e in init_asts),
+                    etype.base))
         elif etype.is_int and etype.is_bitfields:
             if len(etype.members) != len(init_asts):
                 raise EDATAINIT(site, 'mismatched number of fields')
@@ -1896,11 +2347,12 @@ def get_initializer(site, etype, astinit, location, scope):
         return ExpressionInitializer(mkBoolConstant(site, False))
     elif typ.is_float:
         return ExpressionInitializer(mkFloatConstant(site, 0.0))
-    elif isinstance(typ, (TStruct, TExternStruct, TArray, TTrait, THook)):
-        return MemsetInitializer(site)
+    elif isinstance(typ, (TStruct, TExternStruct, TArray, TTrait, THook,
+                          TString, TVector)):
+        return MemsetInitializer(site, ignore_raii=True)
     elif isinstance(typ, TPtr):
         return ExpressionInitializer(mkLit(site, 'NULL', typ))
-    elif isinstance(typ, TVector):
+    elif isinstance(typ, TVectorLegacy):
         return ExpressionInitializer(mkLit(site, 'VNULL', typ))
     elif isinstance(typ, TFunction):
         raise EVARTYPE(site, etype.describe())
@@ -1919,8 +2371,12 @@ def codegen_statements(trees, *args):
             report(e)
     return stmts
 
+
 def codegen_statement(tree, *args):
-    return mkCompound(tree.site, codegen_statements([tree], *args))
+    with RAIIScope() as raii_scope:
+        stmts = codegen_statements([tree], *args)
+
+    return mkCompoundRAII(tree.site, stmts, raii_scope)
 
 @statement_dispatcher
 def stmt_compound(stmt, location, scope):
@@ -1940,8 +2396,10 @@ def stmt_compound(stmt, location, scope):
                                          dmlparse.start_site(rh.site),
                                          ret.site))
     lscope = Symtab(scope)
-    statements = codegen_statements(stmt_asts, location, lscope)
-    return [mkCompound(stmt.site, declarations(lscope) + statements)]
+    with RAIIScope() as raii_scope:
+        statements = codegen_statements(stmt_asts, location, lscope)
+    return [mkCompoundRAII(stmt.site, declarations(lscope) + statements,
+                           raii_scope)]
 
 def check_shadowing(scope, name, site):
     if (dml.globals.dml_version == (1, 2)
@@ -1983,6 +2441,10 @@ def convert_decl(decl_ast):
             check_varname(stmt.site, name)
         (struct_decls, etype) = eval_type(asttype, stmt.site, location, scope)
         stmts.extend(mkStructDefinition(site, t) for (site, t) in struct_decls)
+        for (site, t) in struct_decls:
+            if t.is_raii:
+                raise EANONRAIISTRUCT(site)
+
         etype = etype.resolve()
         rt = safe_realtype_shallow(etype)
         if isinstance(rt, TArray) and not rt.size.constant and deep_const(rt):
@@ -2004,14 +2466,16 @@ def mk_sym(name, typ, mkunique=not dml.globals.debuggable):
         for (name, typ) in decls:
             sym = mk_sym(name, typ)
             tgt_typ = safe_realtype_shallow(typ)
-            if tgt_typ.const:
-                nonconst_typ = tgt_typ.clone()
-                nonconst_typ.const = False
+            if shallow_const(tgt_typ):
+                nonconst_typ = safe_realtype_unconst(tgt_typ)
                 tgt_sym = mk_sym('_tmp_' + name, nonconst_typ, True)
                 sym.init = ExpressionInitializer(mkLocalVariable(stmt.site,
                                                                  tgt_sym))
                 late_declared_syms.append(sym)
             else:
+                if tgt_typ.is_raii:
+                    sym.init = get_initializer(stmt.site, typ, None, location,
+                                               scope)
                 tgt_sym = sym
             syms_to_add.append(sym)
             tgt_syms.append(tgt_sym)
@@ -2023,9 +2487,9 @@ def mk_sym(name, typ, mkunique=not dml.globals.debuggable):
         if method_invocation is not None and stmt.site.dml_version != (1, 2):
             for sym in syms_to_add:
                 scope.add(sym)
-            stmts.extend(sym_declaration(sym, True) for sym in tgt_syms)
+            stmts.extend(sym_declaration(sym, unused=True) for sym in tgt_syms)
             stmts.append(method_invocation)
-            stmts.extend(sym_declaration(sym, True)
+            stmts.extend(sym_declaration(sym, unused=True)
                          for sym in late_declared_syms)
         else:
             if len(tgts) != 1:
@@ -2035,7 +2499,7 @@ def mk_sym(name, typ, mkunique=not dml.globals.debuggable):
                 sym.init = ExpressionInitializer(
                     codegen_expression(inits[0].args[0], location, scope))
                 scope.add(sym)
-                stmts.append(sym_declaration(sym, True))
+                stmts.append(sym_declaration(sym, unused=True))
     else:
         # Initializer evaluation and variable declarations are done in separate
         # passes in order to prevent the newly declared variables from being in
@@ -2080,8 +2544,9 @@ def stmt_session(stmt, location, scope):
         add_late_global_struct_defs(struct_decls)
         if init:
             try:
-                init = eval_initializer(
-                    stmt.site, etype, init, location, global_scope, True)
+                with ctree.SessionRAIIScope():
+                    init = eval_initializer(
+                        stmt.site, etype, init, location, global_scope, True)
             except DMLError as e:
                 report(e)
                 init = None
@@ -2122,8 +2587,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
@@ -2264,8 +2729,7 @@ def try_codegen_invocation(site, init_ast, outargs, location, scope):
     meth_expr = codegen_expression_maybe_nonvalue(meth_ast, location, scope)
     if (isinstance(meth_expr, NonValue)
         and not isinstance(meth_expr, (
-            TraitMethodRef, NodeRef, InterfaceMethodRef, HookSendNowRef,
-            HookSendRef))):
+            TraitMethodRef, NodeRef, InterfaceMethodRef, PseudoMethodRef))):
         raise meth_expr.exc()
     if isinstance(meth_expr, TraitMethodRef):
         if not meth_expr.throws and len(meth_expr.outp) <= 1:
@@ -2373,8 +2837,8 @@ def stmt_assign(stmt, location, scope):
                 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)]
+                          AssignStatement(tgt.site, tgt, init)])
+        return stmts + [AssignStatement(tgts[0].site, tgts[0], init)]
     else:
         # Guaranteed by grammar
         assert tgt_ast.kind == 'assign_target_tuple' and len(tgts) > 1
@@ -2403,7 +2867,7 @@ def stmt_assign(stmt, location, scope):
 
         stmts.extend(map(sym_declaration, syms))
         stmts.extend(
-            mkAssignStatement(
+            AssignStatement(
                 tgt.site, tgt, ExpressionInitializer(mkLocalVariable(tgt.site,
                                                                      sym)))
             for (tgt, sym) in zip(tgts, syms))
@@ -2411,32 +2875,84 @@ def stmt_assign(stmt, location, scope):
 
 @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()):
-        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)
+    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)
+
     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)))]
+    tmp_ret_sym = lscope.add_variable(
+        '_tmp_ret', type = ttype, site = src_ast.site,
+        stmt=True)
+
+    if ttype.is_raii:
+        tmp_ret_sym.init = get_initializer(site, ttype, None, location, scope)
+
+    method_invocation = try_codegen_invocation(
+        site, [src_ast], [mkLocalVariable(tgt.site, tmp_ret_sym)], location,
+        scope)
+    if method_invocation:
+        src = OrphanWrap(site, mkLocalVariable(tmp_ret_sym.site, tmp_ret_sym))
+    else:
+        src = (eval_initializer(site, ttype, src_ast, location, scope, False)
+               .as_expr(ttype))
+
+    operation = None
+    # TODO(RAII) this is somewhat hacky.
+    if op == '+=':
+        real_ttype = safe_realtype_shallow(ttype)
+        if isinstance(real_ttype, TString):
+            operation = lambda: [mkStringAppend(site, tgt, src)]
+        if isinstance(real_ttype, TVector):
+            operation = lambda: [mkVectorAppend(site, tgt, src)]
+
+    if operation is None:
+        if tgt.addressable:
+            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(RAII) Not ideal. This path is needed to deal with writable
+            # expressions that do not correspond to C lvalues; such as bit
+            # slices or .len of vectors and stirngs.
+            # But the potentially repeated evaluation is painful, (though
+            # that's not a regression compared to how bit slices used to be
+            # managed).
+            # TODO(RAII) Also consider if tgt.adressable should be relaxed to
+            # tgt.c_lval. Other than the considerations of whether that would
+            # be safe, it would also mean mean mkAddressOf can't be used,
+            # which has some ramifications as it has a few layers of expression
+            # adjustment logic to it.
+            tmp_tgt_sym = None
+        def operation():
+            tmp_tgt_decl = ([sym_declaration(tmp_tgt_sym)]
+                            if tmp_tgt_sym is not None else [])
+            assign_src = source_for_assignment(site, ttype,
+                                               arith_binops[op[:-1]](site,
+                                                                     tgt, src))
+
+            return (tmp_tgt_decl
+                    + [mkExpressionStatement(site,
+                                             ctree.AssignOp(site, tgt,
+                                                            assign_src))])
+
+
+    with RAIIScope() as raii_scope:
+        method_invocation = ([sym_declaration(tmp_ret_sym, unscoped_raii=True),
+                              method_invocation]
+                             if method_invocation is not None else [])
+        body = method_invocation + operation()
+    return [mkCompoundRAII(site, body, raii_scope)]
 
 @statement_dispatcher
 def stmt_expression(stmt, location, scope):
@@ -3043,11 +3559,11 @@ def stmt_select(stmt, location, scope):
                 if_chain = mkIf(cond.site, cond, stmt, if_chain)
             return [if_chain]
         raise lst.exc()
-    elif dml.globals.compat_dml12 and isinstance(lst.ctype(), TVector):
+    elif dml.globals.compat_dml12 and isinstance(lst.ctype(), TVectorLegacy):
         itervar = lookup_var(stmt.site, scope, itername)
         if not itervar:
             raise EIDENT(stmt.site, itername)
-        return [mkVectorForeach(stmt.site,
+        return [mkLegacyVectorForeach(stmt.site,
                                 lst, itervar,
                                 codegen_statement(stmt_ast, location, scope))]
     else:
@@ -3060,14 +3576,32 @@ def foreach_each_in(site, itername, trait, each_in,
     inner_scope.add_variable(
         itername, type=trait_type, site=site,
         init=ForeachSequence.itervar_initializer(site, trait))
-    context = GotoLoopContext()
-    with context:
-        inner_body = mkCompound(site, declarations(inner_scope)
-            + codegen_statements([body_ast], location, inner_scope))
+    with GotoLoopContext() as loopcontext, RAIIScope() as raii_scope:
+        body = (declarations(inner_scope)
+                + codegen_statements([body_ast], location, inner_scope))
+    inner_body = mkCompoundRAII(site, body, raii_scope)
 
-    break_label = context.label if context.used else None
+    break_label = loopcontext.label if loopcontext.used else None
     return [mkForeachSequence(site, trait, each_in, inner_body, break_label)]
 
+def foreach_vector(site, itername, vect_realtype, vect, body_ast, location,
+                   scope):
+    if not vect.addressable:
+        raise ERVAL(vect, 'foreach')
+
+    uniq = ForeachVector.count
+    ForeachVector.count += 1
+    inner_scope = Symtab(scope)
+    iter_typ = conv_const(vect_realtype.const, vect_realtype.base)
+    inner_scope.add(
+        ExpressionSymbol(itername,
+                         ForeachVectorIterRef(site, itername, uniq,
+                                              vect.writable, iter_typ),
+                         site))
+    with CLoopContext():
+        body = codegen_statement(body_ast, location, inner_scope)
+    return [mkForeachVector(site, vect, uniq, body)]
+
 @expression_dispatcher
 def expr_each_in(ast, location, scope):
     (traitname, node_ast) = ast.args
@@ -3091,13 +3625,14 @@ def stmt_foreach_dml12(stmt, location, scope):
                                      statement, location, scope)
 
     list_type = safe_realtype(lst.ctype())
-    if isinstance(list_type, TVector):
+    if isinstance(list_type, TVectorLegacy):
         itervar = lookup_var(stmt.site, scope, itername)
         if not itervar:
             raise EIDENT(lst, itername)
         with CLoopContext():
-            res = mkVectorForeach(stmt.site, lst, itervar,
-                                  codegen_statement(statement, location, scope))
+            res = mkLegacyVectorForeach(stmt.site, lst, itervar,
+                                        codegen_statement(statement, location,
+                                                          scope))
         return [res]
     else:
         raise ENLST(stmt.site, lst)
@@ -3113,6 +3648,9 @@ def stmt_foreach(stmt, location, scope):
             # .traitname was validated by safe_realtype()
             dml.globals.traits[list_type.traitname],
             lst, statement, location, scope)
+    elif isinstance(list_type, TVector):
+        return foreach_vector(
+            stmt.site, itername, list_type, lst, statement, location, scope)
     else:
         raise ENLST(stmt.site, lst)
 
@@ -3211,10 +3749,8 @@ def stmt_switch(stmt, location, scope):
             assert body_ast.kind == 'compound'
             [stmt_asts] = body_ast.args
             stmts = codegen_statements(stmt_asts, location, scope)
-            if (not stmts
-                or not isinstance(stmts[0], (ctree.Case, ctree.Default))):
-                raise ESWITCH(
-                    body_ast.site, "statement before first case label")
+            if not stmts:
+                raise ESWITCH(body_ast.site, "empty switch statement")
             defaults = [i for (i, sub) in enumerate(stmts)
                         if isinstance(sub, ctree.Default)]
             if len(defaults) > 1:
@@ -3256,13 +3792,10 @@ def stmt_switch(stmt, location, scope):
 
 @statement_dispatcher
 def stmt_continue(stmt, location, scope):
-    if (LoopContext.current is None
-        or isinstance(LoopContext.current, NoLoopContext)):
+    if LoopContext.current is None:
         raise ECONT(stmt.site)
-    elif isinstance(LoopContext.current, CLoopContext):
-        return [mkContinue(stmt.site)]
     else:
-        raise ECONTU(stmt.site)
+        return LoopContext.current.continue_(stmt.site)
 
 @statement_dispatcher
 def stmt_break(stmt, location, scope):
@@ -3308,7 +3841,8 @@ def mkcall_method(site, func, indices):
               else [mkLit(site, dev(site),
                           TDevice(crep.structtype(dml.globals.device)))])
     return lambda args: mkApply(
-        site, func.cfunc_expr(site), devarg + list(indices) + args)
+        site, func.cfunc_expr(site), devarg + list(indices) + args,
+        orphan=not func.memoized)
 
 def common_inline(site, method, indices, inargs, outargs):
     if not verify_args(site, method.inp, method.outp, inargs, outargs):
@@ -3341,7 +3875,8 @@ def common_inline(site, method, indices, inargs, outargs):
 
         return codegen_call_stmt(site, func.method.name,
                                  mkcall_method(site, func, indices),
-                                 inp, func.outp, func.throws, inargs, outargs)
+                                 inp, func.outp, func.throws, inargs, outargs,
+                                 move_rets=not func.memoized)
 
     if not method.independent:
         crep.require_dev(site)
@@ -3514,7 +4049,8 @@ def codegen_inline(site, meth_node, indices, inargs, outargs,
     # TODO: in python 3.10 we can use parentheses instead of \
     with RecursiveInlineGuard(site, meth_node),  \
          ErrorContext(meth_node, site),          \
-         contextlib.nullcontext() if meth_node.throws else NoFailure(site):
+         contextlib.nullcontext() if meth_node.throws else NoFailure(site), \
+         RAIIScope() as raii_scope:
         param_scope = MethodParamScope(global_scope)
         param_scope.add(meth_node.default_method.default_sym(indices))
 
@@ -3582,7 +4118,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,
@@ -3591,8 +4127,7 @@ def codegen_inline(site, meth_node, indices, inargs, outargs,
             if exit_handler.used:
                 code.append(mkLabel(site, exit_handler.label))
             code.extend(copyout)
-            body = mkCompound(site, declarations(param_scope) + code)
-            return mkInlinedMethod(site, meth_node, body)
+            code = declarations(param_scope) + code
         else:
             assert meth_node.astcode.kind == 'compound'
             [subs] = meth_node.astcode.args
@@ -3602,10 +4137,13 @@ def codegen_inline(site, meth_node, indices, inargs, outargs,
                 code = codegen_statements(subs, location, param_scope)
             if exit_handler.used:
                 code.append(mkLabel(site, exit_handler.label))
-            body = mkCompound(site, declarations(param_scope) + code)
-            if meth_node.outp and body.control_flow().fallthrough:
+            code = declarations(param_scope) + code
+            if (meth_node.outp
+                and mkCompound(site, code).control_flow().fallthrough):
                 report(ENORET(meth_node.astcode.site))
-            return mkInlinedMethod(site, meth_node, body)
+
+    return mkInlinedMethod(site, meth_node, mkCompoundRAII(site, code,
+                                                           raii_scope))
 
 def c_rettype(outp, throws):
     if throws:
@@ -3753,7 +4291,7 @@ def codegen_method_func(func):
     intercepted = intercepted_method(method)
     if intercepted:
         assert method.throws
-        with crep.DeviceInstanceContext():
+        with UnusedMethodRAIIScope(), crep.DeviceInstanceContext():
             return intercepted(
                 method.parent, indices,
                 [mkLit(method.site, n, t) for (n, t) in func.inp],
@@ -3793,12 +4331,31 @@ def codegen_return(site, outp, throws, retvals):
         # no fall-through
         return mkAssert(site, mkBoolConstant(site, False))
     if throws:
-        ret = mkReturn(site, mkBoolConstant(site, False))
+        def mk_ret_with_prelude(stmts):
+            return mkCompound(site,
+                              stmts
+                              + [mkReturn(site, mkBoolConstant(site, False))])
     elif outp:
         (_, t) = outp[0]
-        ret = mkReturn(site, retvals[0], t)
+        if t.is_raii:
+            def mk_ret_with_prelude(stmts):
+                scope = Symtab(global_scope)
+                sym = scope.add_variable(
+                    'ret', t, ExpressionInitializer(retvals[0]), site, True,
+                    True)
+                return mkCompound(
+                    site,
+                    [sym_declaration(sym, unscoped_raii=True)]
+                    + stmts
+                    + [mkReturn(site, mkLocalVariable(site, sym), t)])
+        else:
+            def mk_ret_with_prelude(stmts):
+                return mkCompound(site,
+                                  stmts + [mkReturn(site, retvals[0], t)])
     else:
-        ret = mkReturn(site, None)
+        def mk_ret_with_prelude(stmts):
+            return mkCompound(site,
+                              stmts + [mkReturn(site, None)])
     stmts = []
     return_first_outarg = bool(not throws and outp)
     for ((name, typ), val) in itertools.islice(
@@ -3810,13 +4367,14 @@ def codegen_return(site, outp, throws, retvals):
             assert val.rh.str == name
             continue
         stmts.append(mkCopyData(site, val, mkLit(site, "*%s" % (name,), typ)))
-    stmts.append(ret)
-    return mkCompound(site, stmts)
+    stmts.append(mkRAIIScopeClears(site, RAIIScope.scope_stack()))
+    return mk_ret_with_prelude(stmts)
 
 def codegen_method(site, inp, outp, throws, independent, memoization, ast,
                    default, location, fnscope, rbrace_site):
     with (crep.DeviceInstanceContext() if not independent
-          else contextlib.nullcontext()):
+          else contextlib.nullcontext()), \
+          MethodRAIIScope() as raii_scope:
         for (arg, etype) in inp:
             fnscope.add_variable(arg, type=etype, site=site, make_unique=False)
         initializers = [get_initializer(site, parmtype, None, None, None)
@@ -3838,18 +4396,18 @@ def prelude():
                             else ReturnExit(outp, throws))
 
         if ast.site.dml_version() == (1, 2):
+            body = []
             if throws:
                 # Declare and initialize one variable for each output
                 # parameter.  We cannot write to the output parameters
                 # directly, because they should be left untouched if an
                 # exception is thrown.
-                code = []
                 for ((varname, parmtype), init) in zip(outp, initializers):
                     sym = fnscope.add_variable(
                         varname, type=parmtype, init=init, make_unique=True,
                         site=ast.site)
                     sym.incref()
-                    code.append(sym_declaration(sym))
+                    body.append(sym_declaration(sym))
             else:
                 if outp:
                     # pass first out argument as return value
@@ -3858,23 +4416,27 @@ def prelude():
                                                init=initializers[0],
                                                make_unique=False)
                     sym.incref()
-                    code = [sym_declaration(sym)]
+                    body.append(sym_declaration(sym))
                     for ((name, typ), init) in zip(outp[1:], initializers[1:]):
                         # remaining output arguments pass-by-pointer
                         param = mkDereference(site,
                                               mkLit(site, name, TPtr(typ)))
                         fnscope.add(ExpressionSymbol(name, param, site))
-                        code.append(mkAssignStatement(site, param, init))
+                        body.append(AssignStatement(site, param, init))
                 else:
-                    code = []
+                    body = []
 
+            body.append(mkRAIIScopeDeclarations(site, raii_scope))
+            body.append(mkRAIIScopeBindLVals(
+                site, raii_scope,
+                tuple(lookup_var(site, fnscope, varname)
+                      for (varname, t) in inp if t.is_raii)))
             with fail_handler, exit_handler:
-                code.append(codegen_statement(ast, location, fnscope))
+                body.append(codegen_statement(ast, location, fnscope))
             if exit_handler.used:
-                code.append(mkLabel(site, exit_handler.label))
-            code.append(codegen_return(site, outp, throws, [
+                body.append(mkLabel(site, exit_handler.label))
+            body.append(codegen_return(site, outp, throws, [
                 lookup_var(site, fnscope, varname) for (varname, _) in outp]))
-            to_return = mkCompound(site, code)
         else:
             # manually deconstruct compound AST node, to make sure
             # top-level locals share scope with parameters
@@ -3882,16 +4444,18 @@ def prelude():
             [subs] = ast.args
             with fail_handler, exit_handler:
                 body = prelude()
+                body.append(mkRAIIScopeDeclarations(site, raii_scope))
+                body.append(mkRAIIScopeBindLVals(
+                    site, raii_scope,
+                    tuple(lookup_var(site, fnscope, varname)
+                          for (varname, t) in inp if t.is_raii)))
                 body.extend(codegen_statements(subs, location, fnscope))
-                code = mkCompound(site, body)
-                if code.control_flow().fallthrough:
+                if mkCompound(site, body).control_flow().fallthrough:
                     if outp:
                         report(ENORET(site))
                     else:
-                        code = mkCompound(site, body + [codegen_exit(site,
-                                                                     [])])
-            to_return = code
-        return to_return
+                        body.append(codegen_exit(site, []))
+    return mkCompound(site, body)
 
 # Keep track of methods that we need to generate code for
 def mark_method_referenced(func):
@@ -3960,7 +4524,8 @@ def mkcall(args):
         args = [coerce_if_eint(arg) for arg in args]
         return expr.call_expr(args, rettype)
     return codegen_call_stmt(site, str(expr), mkcall, expr.inp, expr.outp,
-                             expr.throws, inargs, outargs)
+                             expr.throws, inargs, outargs,
+                             move_rets=not expr.memoized)
 
 def codegen_call(site, meth_node, indices, inargs, outargs):
     '''Generate a call using a direct reference to the method node'''
@@ -3978,7 +4543,8 @@ def codegen_call(site, meth_node, indices, inargs, outargs):
     mark_method_referenced(func)
     return codegen_call_stmt(site, func.method.name,
                              mkcall_method(site, func, indices),
-                             func.inp, func.outp, func.throws, inargs, outargs)
+                             func.inp, func.outp, func.throws, inargs, outargs,
+                             move_rets=not func.memoized)
 
 def codegen_call_byname(site, node, indices, meth_name, inargs, outargs):
     '''Generate a call using the parent node and indices, plus the method
@@ -3991,7 +4557,7 @@ def codegen_call_byname(site, node, indices, meth_name, inargs, outargs):
         raise UnknownMethod(node, meth_name)
     return codegen_call(site, meth_node, indices, inargs, outargs)
 
-def copy_outarg(arg, var, parmname, parmtype, method_name):
+def copy_outarg(arg, var, parmname, parmtype, method_name, move_outvar=True):
     '''Type-check the output argument 'arg', and create a local
     variable with that type in scope 'callscope'. The address of this
     variable will be passed as an output argument to the C function
@@ -4002,17 +4568,22 @@ 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()
 
-    return mkCopyData(var.site, var, arg)
+        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,
+                      OrphanWrap(var.site, var) if move_outvar else var,
+                      arg)
 
 def add_proxy_outvar(site, parmname, parmtype, callscope):
     varname = parmname
@@ -4020,7 +4591,8 @@ def add_proxy_outvar(site, parmname, parmtype, callscope):
     sym = callscope.add_variable(varname, type=parmtype, init=varinit, site=site)
     return mkLocalVariable(site, sym)
 
-def codegen_call_stmt(site, name, mkcall, inp, outp, throws, inargs, outargs):
+def codegen_call_stmt(site, name, mkcall, inp, outp, throws, inargs, outargs,
+                      move_rets=True):
     '''Generate a statement for calling a method'''
     if len(outargs) != len(outp):
         raise ICE(site, "wrong number of outargs")
@@ -4033,23 +4605,28 @@ def codegen_call_stmt(site, name, mkcall, inp, outp, throws, inargs, outargs):
     # an uint8 variable is passed in an uint32 output parameter.
     postcode = []
     outargs_conv = []
-    for (arg, (parmname, parmtype)) in zip(
-            outargs[return_first_outarg:], outp[return_first_outarg:]):
-        # It would make sense to pass output arguments directly, but
-        # the mechanisms to detect whether this is safe are
-        # broken. See bug 21900.
-        # if (isinstance(arg, (
-        #         Variable, ctree.Dereference, ctree.ArrayRef, ctree.SubRef))
-        #     and TPtr(parmtype).canstore(TPtr(arg.ctype()))):
-        #     outargs_conv.append(mkAddressOf(arg.site, arg))
-        # else:
-        var = add_proxy_outvar(site, '_ret_' + parmname, parmtype,
-                               callscope)
-        outargs_conv.append(mkAddressOf(var.site, var))
-        postcode.append(copy_outarg(arg, var, parmname, parmtype, name))
+    # RAII Scope should never need to be leveraged
+    with UnusedMethodRAIIScope():
+        for (arg, (parmname, parmtype)) in zip(
+                outargs[return_first_outarg:], outp[return_first_outarg:]):
+            # It would make sense to pass output arguments directly, but
+            # the mechanisms to detect whether this is safe are
+            # broken. See bug 21900.
+            # if (isinstance(arg, (
+            #         Variable, ctree.Dereference, ctree.ArrayRef,
+            #         ctree.SubRef))
+            #     and TPtr(parmtype).canstore(TPtr(arg.ctype()))):
+            #     outargs_conv.append(mkAddressOf(arg.site, arg))
+            # else:
+            var = add_proxy_outvar(site, '_ret_' + parmname, parmtype,
+                                   callscope)
+            outargs_conv.append(mkAddressOf(var.site, var))
+            postcode.append(copy_outarg(arg, var, parmname, parmtype, name,
+                                        move_outvar=move_rets))
 
     typecheck_inargs(site, inargs, inp, 'method')
     call_expr = mkcall(list(inargs) + outargs_conv)
+    assert call_expr.orphan == move_rets
 
     if throws:
         if not Failure.fail_stack[-1].allowed:
@@ -4061,6 +4638,8 @@ def codegen_call_stmt(site, name, mkcall, inp, outp, throws, inargs, outargs):
         else:
             call_stmt = mkExpressionStatement(site, call_expr)
 
-    return mkCompound(site, declarations(callscope) + [call_stmt] + postcode)
+    return mkCompound(site,
+                      declarations(callscope, unscoped_raii=True)
+                      + [call_stmt] + postcode)
 
 ctree.codegen_call_expr = codegen_call_expr
diff --git a/py/dml/ctree.py b/py/dml/ctree.py
index 5d9fad091..9f525ecbe 100644
--- a/py/dml/ctree.py
+++ b/py/dml/ctree.py
@@ -20,7 +20,8 @@
 from .types import *
 from .expr import *
 from .expr_util import *
-from .slotsmeta import auto_init
+from .slotsmeta import auto_init, SlotsMeta
+from .set import Set
 from . import dmlparse, output
 import dml.globals
 # set from codegen.py
@@ -31,11 +32,11 @@
     'source_for_assignment',
 
     'Location',
-
+    'RAIIScope', 'MethodRAIIScope', 'UnusedMethodRAIIScope',
     'ExpressionSymbol',
     'LiteralSymbol',
 
-    'mkCompound',
+    'mkCompound', 'mkCompoundRAII',
     'mkNull', 'Null',
     'mkLabel',
     'mkUnrolledLoop',
@@ -59,15 +60,26 @@
     'mkDoWhile',
     'mkFor',
     'mkForeachSequence', 'ForeachSequence',
+    'mkForeachVector', 'ForeachVector', 'ForeachVectorIterRef',
     'mkSwitch',
     'mkSubsequentCases',
     'mkCase',
     'mkDefault',
-    'mkVectorForeach',
+    'mkLegacyVectorForeach',
     'mkBreak',
     'mkContinue',
-    'mkAssignStatement',
+    'mkAssignStatement', 'AssignStatement',
     'mkCopyData',
+    'mkRAIIScopeClears',
+    'mkRAIIScopeDeclarations',
+    'mkRAIIScopeBindLVals',
+    'RAIIDupe',
+    'mkDiscardRef', 'DiscardRef',
+    'mkStringAppend',
+    'mkVectorAppend',
+    'mkStringCStrRef', 'mkStringCStrApply',
+    'FromCString',
+    'PseudoMethodRef',
     'mkIfExpr', 'IfExpr',
     #'BinOp',
     #'Test',
@@ -157,6 +169,7 @@
     'mkStructDefinition',
     'mkDeclaration',
     'mkCText',
+    'CompoundLiteral', 'VectorCompoundLiteral',
     'Initializer', 'ExpressionInitializer', 'CompoundInitializer',
     'DesignatedStructInitializer', 'MemsetInitializer',
 
@@ -290,9 +303,15 @@ class Statement(Code):
     def __init__(self, site):
         self.site = site
         self.context = ErrorContext.current()
+    # Emit a single C statement
     @abc.abstractmethod
-    def toc(self): pass
-    def toc_inline(self): return self.toc()
+    def toc_stmt(self): pass
+    # Emit any number of C statements, but without any guarantee that they are
+    # in a dedicated C block
+    def toc(self): self.toc_stmt()
+    # Emit any number of C statements, with the guarantee that they are in a
+    # dedicated C block
+    def toc_inline(self): self.toc()
 
     def control_flow(self):
         '''Rudimentary control flow analysis: Return a ControlFlow object
@@ -314,19 +333,33 @@ def control_flow(self):
         example, see test/1.4/errors/T_ENORET_throw_after_break.dml.'''
         return ControlFlow(fallthrough=True)
 
+
 class Compound(Statement):
     @auto_init
     def __init__(self, site, substatements):
         assert isinstance(substatements, list)
 
+    def toc_stmt(self):
+        if all(sub.is_empty for sub in self.substatements):
+            if self.site:
+                self.linemark()
+            out(';\n')
+        else:
+            out('{\n', postindent = 1)
+            self.toc_inline()
+            out('}\n', preindent = -1)
+
     def toc(self):
-        out('{\n', postindent = 1)
-        self.toc_inline()
-        out('}\n', preindent = -1)
+        if any(sub.is_declaration for sub in self.substatements):
+            self.toc_stmt()
+        else:
+            self.toc_inline()
 
     def toc_inline(self):
         for substatement in self.substatements:
-            substatement.toc()
+            if not substatement.is_empty:
+                substatement.toc()
+
 
     def control_flow(self):
         acc = ControlFlow(fallthrough=True)
@@ -337,6 +370,18 @@ def control_flow(self):
                 return acc
         return acc
 
+    @property
+    def is_empty(self):
+        return all(sub.is_empty for sub in self.substatements)
+
+def mkCompoundRAII(site, statements, raii_scope):
+    assert raii_scope.completed
+    body = (([mkRAIIScopeDeclarations(site, raii_scope)]
+             if isinstance(raii_scope, MethodRAIIScope) else [])
+            + statements
+            + [mkRAIIScopeClears(site, [raii_scope])])
+    return mkCompound(site, body)
+
 def mkCompound(site, statements):
     "Create a simplified Compound() from a list of ctree.Statement"
     collapsed = []
@@ -356,9 +401,9 @@ def mkCompound(site, statements):
 
 class Null(Statement):
     is_empty = True
-    def toc_inline(self):
-        pass
     def toc(self):
+        pass
+    def toc_stmt(self):
         if self.site:
             self.linemark()
         out(';\n')
@@ -370,7 +415,7 @@ def __init__(self, site, label, unused=False):
         Statement.__init__(self, site)
         self.label = label
         self.unused = unused
-    def toc(self):
+    def toc_stmt(self):
         out('%s: %s;\n' % (self.label, 'UNUSED'*self.unused), preindent = -1,
             postindent = 1)
 
@@ -380,14 +425,15 @@ class UnrolledLoop(Statement):
     @auto_init
     def __init__(self, site, substatements, break_label):
         assert isinstance(substatements, list)
+        assert all(not sub.is_declaration for sub in substatements)
 
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         out('{\n', postindent = 1)
         self.toc_inline()
         out('}\n', preindent = -1)
 
-    def toc_inline(self):
+    def toc(self):
         for substatement in self.substatements:
             substatement.toc()
         if self.break_label is not None:
@@ -404,7 +450,7 @@ def control_flow(self):
 class Goto(Statement):
     @auto_init
     def __init__(self, site, label): pass
-    def toc(self):
+    def toc_stmt(self):
         out('goto %s;\n' % (self.label,))
 
     def control_flow(self):
@@ -435,7 +481,11 @@ class TryCatch(Statement):
     block with a catch label, to which Throw statements inside will go.'''
     @auto_init
     def __init__(self, site, label, tryblock, catchblock): pass
-    def toc_inline(self):
+    def toc_stmt(self):
+        out('{\n', postindent = 1)
+        self.toc()
+        out('}\n', preindent = -1)
+    def toc(self):
         self.tryblock.toc()
 
         if (dml.globals.dml_version != (1, 2)
@@ -449,10 +499,6 @@ def toc_inline(self):
             out('%s: ;\n' % (self.label,), postindent=1)
             self.catchblock.toc_inline()
             out('}\n', preindent=-1)
-    def toc(self):
-        out('{\n', postindent = 1)
-        self.toc_inline()
-        out('}\n', preindent = -1)
     def control_flow(self):
         tryflow = self.tryblock.control_flow()
         if not tryflow.throw:
@@ -469,7 +515,7 @@ def mkTryCatch(site, label, tryblock, catchblock):
 class Inline(Statement):
     @auto_init
     def __init__(self, site, str): pass
-    def toc(self):
+    def toc_stmt(self):
         out(self.str + '\n')
 
 mkInline = Inline
@@ -478,6 +524,8 @@ class InlinedMethod(Statement):
     '''Wraps the body of an inlined method, to protect it from analysis'''
     @auto_init
     def __init__(self, site, method, body): pass
+    def toc_stmt(self):
+        self.body.toc_stmt()
     def toc(self):
         self.body.toc()
     def toc_inline(self):
@@ -490,7 +538,7 @@ def control_flow(self):
 class Comment(Statement):
     @auto_init
     def __init__(self, site, str): pass
-    def toc(self):
+    def toc_stmt(self):
         # self.linemark()
         out('/* %s */\n' % self.str)
 
@@ -499,7 +547,7 @@ def toc(self):
 class Assert(Statement):
     @auto_init
     def __init__(self, site, expr): pass
-    def toc(self):
+    def toc_stmt(self):
         out('DML_ASSERT("%s", %d, %s);\n'
             % (quote_filename(self.site.filename()),
                self.site.lineno, self.expr.read()))
@@ -513,7 +561,7 @@ def mkAssert(site, expr):
 class Return(Statement):
     @auto_init
     def __init__(self, site, expr): pass
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         if self.expr is None:
             out('return;\n')
@@ -530,8 +578,9 @@ def mkReturn(site, expr, rettype=None):
 class Delete(Statement):
     @auto_init
     def __init__(self, site, expr): pass
-    def toc(self):
-        out('MM_FREE(%s);\n' % self.expr.read())
+    def toc_stmt(self):
+        self.linemark()
+        out(f'DML_DELETE({self.expr.read()});\n')
 
 def mkDelete(site, expr):
     return Delete(site, expr)
@@ -539,7 +588,7 @@ def mkDelete(site, expr):
 class ExpressionStatement(Statement):
     @auto_init
     def __init__(self, site, expr): pass
-    def toc(self):
+    def toc_stmt(self):
         #if not self.site:
         #    print 'NOSITE', str(self), repr(self)
         self.linemark()
@@ -556,15 +605,19 @@ 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)),
+        ignore_raii=True)
+
+    return AssignStatement(site, target_val, init).toc()
 
 class After(Statement):
     @auto_init
     def __init__(self, site, unit, delay, domains, info, indices,
                  args_init):
         crep.require_dev(site)
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         objarg = '&_dev->obj'
         out(f'if (SIM_object_clock({objarg}) == NULL)\n', postindent=1)
@@ -622,7 +675,7 @@ class AfterOnHook(Statement):
     def __init__(self, site, domains, hookref_expr, info, indices,
                  args_init):
         crep.require_dev(site)
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         hookref = resolve_hookref(self.hookref_expr)
         indices = ('(const uint32 []) {%s}'
@@ -636,18 +689,20 @@ def toc(self):
         domains = ('(const _identity_t []) {%s}'
                    % (', '.join(domain.read() for domain in self.domains),)
                    if self.domains else 'NULL')
-        out(f'_DML_attach_callback_to_hook({hookref}, '
+        out(f'{{ _DML_attach_callback_to_hook({hookref}, '
             + f'&_after_on_hook_infos[{self.info.uniq}], {indices}, '
             + f'{len(self.indices)}, {args}, {domains}, '
-            + f'{len(self.domains)});\n')
+            + f'{len(self.domains)}); }}\n')
 
 mkAfterOnHook = AfterOnHook
 
 class ImmediateAfter(Statement):
+    slots = ('args_raii_info',)
     @auto_init
     def __init__(self, site, domains, info, indices, args_init):
         crep.require_dev(site)
-    def toc(self):
+
+    def toc_stmt(self):
         self.linemark()
         indices = ('(const uint32 []) {%s}'
                    % (', '.join(i.read() for i in self.indices),)
@@ -660,13 +715,17 @@ def toc(self):
             args_size = f'sizeof({self.info.args_type.declaration("")})'
         else:
             (args, args_size) = ('NULL', '0')
+
+        args_destructor = (self.info.args_raii_info.cident_destructor
+                           if self.info.args_raii_info else 'NULL')
         domains = ('(const _identity_t []) {%s}'
                    % (', '.join(domain.read() for domain in self.domains),)
                    if self.domains else 'NULL')
-        out('_DML_post_immediate_after('
+        out('{ _DML_post_immediate_after('
             + '&_dev->obj, _dev->_immediate_after_state, '
-            + f'{self.info.cident_callback}, {indices}, {len(self.indices)}, '
-            + f'{args}, {args_size}, {domains}, {len(self.domains)});\n')
+            + f'{self.info.cident_callback}, {args_destructor}, {indices}, '
+            + f'{len(self.indices)}, {args}, {args_size}, {domains}, '
+            + f'{len(self.domains)}); }}\n')
 
 mkImmediateAfter = ImmediateAfter
 
@@ -676,7 +735,7 @@ def __init__(self, site, cond, truebranch, falsebranch):
         assert_type(site, cond.ctype(), TBool)
         assert_type(site, truebranch, Statement)
         assert_type(site, falsebranch, (Statement, type(None)))
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         out('if ('+self.cond.read()+') {\n', postindent = 1)
         self.truebranch.toc_inline()
@@ -684,7 +743,10 @@ def toc(self):
             out('} else ', preindent = -1)
             if dml.globals.linemarks:
                 out('\n')
-            self.falsebranch.toc()
+            # TODO(RAII): from what I (lwaern) can tell, this is the ONE USE of
+            # toc_stmt that is needed across dmlc. If we can work around it, we
+            # may scrap toc_stmt.
+            self.falsebranch.toc_stmt()
         elif self.falsebranch:
             out('} else {\n', preindent = -1, postindent = 1)
             self.falsebranch.toc_inline()
@@ -714,7 +776,7 @@ class While(Statement):
     def __init__(self, site, cond, stmt):
         assert_type(site, cond.ctype(), TBool)
         assert_type(site, stmt, Statement)
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         # out('/* %s */\n' % repr(self))
         out('while ('+self.cond.read()+') {\n', postindent = 1)
@@ -742,7 +804,7 @@ class DoWhile(Statement):
     def __init__(self, site, cond, stmt):
         assert_type(site, cond.ctype(), TBool)
         assert_type(site, stmt, Statement)
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         # out('/* %s */\n' % repr(self))
         out('do {\n', postindent = 1)
@@ -765,7 +827,7 @@ class For(Statement):
     def __init__(self, site, pres, cond, posts, stmt):
         assert_type(site, cond.ctype(), TBool)
         assert_type(site, stmt, Statement)
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
 
         out('for (%s; %s; ' % (", ".join(pre.discard()
@@ -821,7 +883,7 @@ def itervar_initializer(site, trait):
 
     @auto_init
     def __init__(self, site, trait, each_in_expr, body, break_label): pass
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         out('{\n', postindent=1)
         self.linemark()
@@ -851,12 +913,55 @@ def control_flow(self):
 
 mkForeachSequence = ForeachSequence
 
+
+class ForeachVectorIterRef(Expression):
+    explicit_type = True
+    priority = dml.expr.Apply.priority
+    c_lval = True
+    @auto_init
+    def __init__(self, site, itername, uniq, writable, type): pass
+
+    def __str__(self):
+        return self.itername
+
+    def read(self):
+        t = self.type.declaration('')
+        return f'DML_VECT_ELEM({t}, *_{self.uniq}_vect, _{self.uniq}_vect_idx)'
+
+class ForeachVector(Statement):
+    count = 0
+
+    @auto_init
+    def __init__(self, site, vect, uniq, body):
+        assert self.vect.c_lval
+    def toc_stmt(self):
+        self.linemark()
+        out('{\n', postindent=1)
+        self.linemark()
+        const = ' const'*safe_realtype_shallow(self.vect.ctype()).const
+        out(f'_dml_vect_t{const} *_{self.uniq}_vect = '
+            + f'&({self.vect.read()});\n')
+        self.linemark()
+        out(f'for (uint32 _{self.uniq}_vect_idx = 0; '
+            + f'_{self.uniq}_vect_idx < _{self.uniq}_vect->len; '
+            + f'++_{self.uniq}_vect_idx) {{\n', postindent=1)
+        self.body.toc_inline()
+        out('}\n', preindent=-1)
+        out('}\n', preindent=-1)
+
+    def control_flow(self):
+        bodyflow = self.body.control_flow()
+        # fallthrough is possible if the vector is empty
+        return bodyflow.replace(fallthrough=True, br=False)
+
+mkForeachVector = ForeachVector
+
 class Switch(Statement):
     @auto_init
     def __init__(self, site, expr, stmt):
         assert_type(site, expr, Expression)
         assert_type(site, stmt, Statement)
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         # out('/* %s */\n' % repr(self))
         out('switch ('+self.expr.read()+') {\n', postindent = 1)
@@ -892,7 +997,7 @@ class SubsequentCases(Statement):
     @auto_init
     def __init__(self, site, cases, has_default):
         assert len(self.cases) > 0
-    def toc(self):
+    def toc_stmt(self):
         for (i, case) in enumerate(self.cases):
             assert isinstance(case, (Case, Default))
             site_linemark(case.site)
@@ -908,7 +1013,7 @@ def toc(self):
 class Case(Statement):
     @auto_init
     def __init__(self, site, expr): pass
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         out('case %s: ;\n' % self.expr.read(), preindent = -1, postindent = 1)
 
@@ -917,17 +1022,17 @@ def toc(self):
 class Default(Statement):
     @auto_init
     def __init__(self, site): pass
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
         out('default: ;\n', preindent = -1, postindent = 1)
 
 mkDefault = Default
 
-class VectorForeach(Statement):
+class LegacyVectorForeach(Statement):
     @auto_init
     def __init__(self, site, vect, var, stmt): pass
 
-    def toc(self):
+    def toc_stmt(self):
         out('VFOREACH(%s, %s) {\n' % (self.vect.read(), self.var.read()),
             postindent = 1)
         self.stmt.toc_inline()
@@ -937,11 +1042,11 @@ def control_flow(self):
         flow = self.stmt.control_flow()
         return flow.replace(fallthrough=flow.fallthrough or flow.br, br=False)
 
-def mkVectorForeach(site, vect, var, stmt):
-    return VectorForeach(site, vect, var, stmt)
+def mkLegacyVectorForeach(site, vect, var, stmt):
+    return LegacyVectorForeach(site, vect, var, stmt)
 
 class Break(Statement):
-    def toc(self):
+    def toc_stmt(self):
         out('break;\n')
     def control_flow(self):
         return ControlFlow(br=True)
@@ -949,7 +1054,7 @@ def control_flow(self):
 mkBreak = Break
 
 class Continue(Statement):
-    def toc(self):
+    def toc_stmt(self):
         out('continue;\n')
     def control_flow(self):
         return ControlFlow()
@@ -960,19 +1065,39 @@ class AssignStatement(Statement):
     @auto_init
     def __init__(self, site, target, initializer):
         assert isinstance(initializer, Initializer)
-    def toc(self):
-        out('{\n', postindent=1)
-        self.toc_inline()
-        out('}\n', preindent=-1)
-    def toc_inline(self):
-        self.initializer.assign_to(self.target, self.target.ctype())
+    def toc_stmt(self):
+        self.linemark()
+        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 ICE(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)
 
-mkAssignStatement = AssignStatement
+        if isinstance(init, ExpressionInitializer):
+            init = ExpressionInitializer(
+                source_for_assignment(site, target_type, init.expr),
+                init.ignore_raii)
 
-def mkCopyData(site, source, target):
+    return AssignStatement(site, target, init)
+
+def mkCopyData(site, source, target, ignore_raii=False):
     "Convert a copy statement to intermediate representation"
-    assignexpr = mkAssignOp(site, target, source)
-    return mkExpressionStatement(site, assignexpr)
+    return mkAssignStatement(site, target,
+                             ExpressionInitializer(source, ignore_raii))
 
 #
 # Expressions
@@ -1015,7 +1140,8 @@ def as_int(e):
         target_type = TInt(64, True)
     if t.is_endian:
         (fun, funtype) = t.get_load_fun()
-        e = dml.expr.Apply(e.site, mkLit(e.site, fun, funtype), (e,), funtype)
+        e = dml.expr.Apply(e.site, mkLit(e.site, fun, funtype), (e,), funtype,
+                           True)
         if not compatible_types(realtype(e.ctype()), target_type):
             e = mkCast(e.site, e, target_type)
         return e
@@ -1030,27 +1156,28 @@ def truncate_int_bits(value, signed, bits=64):
     else:
         return value & mask
 
+class PseudoMethodRef(NonValue):
+    '''Nonvalue expressions with 'apply' that are not method references'''
+
+    def apply(self, inits, location, scope):
+        raise ICE(self.site, f'apply not implemented for {type(self)}')
+
 class LValue(Expression):
     "Somewhere to read or write data"
     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
+    slots = ('orphan',)
     @auto_init
-    def __init__(self, site, cond, texpr, fexpr, type): pass
+    def __init__(self, site, cond, texpr, fexpr, type):
+        self.orphan = texpr.orphan and fexpr.orphan
+        if texpr.orphan != fexpr.orphan:
+            self.texpr = mkAdoptedOrphan(texpr.site, texpr)
+            self.fexpr = mkAdoptedOrphan(fexpr.site, fexpr)
+
     def __str__(self):
         return '%s ? %s : %s' % (self.cond, self.texpr, self.fexpr)
     def read(self):
@@ -1103,7 +1230,7 @@ def mkIfExpr(site, cond, texpr, fexpr):
                 (texpr, fexpr, utype) = usual_int_conv(
                     texpr, ttype, fexpr, ftype)
         else:
-            if not compatible_types(ttype, ftype):
+            if not compatible_types_fuzzy(ttype, ftype):
                 raise EBINOP(site, ':', texpr, fexpr)
             # TODO: in C, the rules are more complex,
             # but our type system is too primitive to cover that
@@ -1240,8 +1367,10 @@ def make(cls, site, lh, rh):
         lhtype = realtype(lh.ctype())
         rhtype = realtype(rh.ctype())
 
-        if (lhtype.is_arith and rhtype.is_arith
-            and lh.constant and rh.constant):
+        if ((lhtype.is_arith and rhtype.is_arith
+             and lh.constant and rh.constant)
+            or (isinstance(lh, StringConstant)
+                and isinstance(rh, StringConstant))):
             return mkBoolConstant(site, cls.eval_const(lh.value, rh.value))
         if lhtype.is_int:
             lh = as_int(lh)
@@ -1272,8 +1401,13 @@ def make(cls, site, lh, rh):
         if ((lhtype.is_arith and rhtype.is_arith)
             or (isinstance(lhtype, (TPtr, TArray))
                 and isinstance(rhtype, (TPtr, TArray))
-                and compatible_types(lhtype.base, rhtype.base))):
+                and compatible_types_fuzzy(lhtype.base, rhtype.base))):
             return cls.make_simple(site, lh, rh)
+        if ((isinstance(lh, StringConstant)
+             or isinstance(lhtype, TString))
+            and (isinstance(rh, StringConstant)
+                 or isinstance (rhtype, TString))):
+            return mkStringCmp(site, lh, rh, cls.op)
         raise EILLCOMP(site, lh, lhtype, rh, rhtype)
 
     @classmethod
@@ -1405,6 +1539,58 @@ def make_simple(site, lh, rh):
             return mkBoolConstant(site, lh.value == rh.value)
         return Equals_dml12(site, lh, rh)
 
+class StringCmp(Expression):
+    priority = 70
+    type = TBool()
+
+    @auto_init
+    def __init__(self, site, lh, rh, op): pass
+
+    def __str__(self):
+        lh = str(self.lh)
+        rh = str(self.rh)
+        if self.lh.priority <= self.priority:
+            lh = '('+lh+')'
+        if self.rh.priority <= self.priority:
+            rh = '('+rh+')'
+        return lh + ' ' + self.op + ' ' + rh
+
+    def read(self):
+        return (f'_dml_string_cmp({self.lh.read()}, {self.rh.read()}) '
+                + f'{self.op} 0')
+
+class StringCmpC(Expression):
+    priority = 70
+    type = TBool()
+
+    @auto_init
+    def __init__(self, site, lh, rh, op): pass
+
+    def __str__(self):
+        lh = str(self.lh)
+        rh = str(self.rh)
+        if self.lh.priority <= self.priority:
+            lh = '('+lh+')'
+        if self.rh.priority <= self.priority:
+            rh = '('+rh+')'
+        return lh + ' ' + self.op + ' ' + rh
+
+    def read(self):
+        return (f'_dml_string_cmp_c_str({self.lh.read()}, {self.rh.read()}) '
+                + f'{self.op} 0')
+
+def mkStringCmp(site, lh, rh, op):
+    if isinstance(lh, StringConstant):
+        op = {'<': '>', '>':'<'}.get(op[1], op[1]) + op[1:]
+        rh_conv = mkAdoptedOrphan(rh.site, rh)
+        return StringCmpC(site, rh_conv, lh, op)
+    elif isinstance(rh, StringConstant):
+        lh_conv = mkAdoptedOrphan(rh.site, lh)
+        return StringCmpC(site, lh_conv, rh, op)
+    lh_conv = mkAdoptedOrphan(lh.site, lh_conv)
+    rh_conv = mkAdoptedOrphan(rh.site, rh_conv)
+    return StringCmp(site, lh_conv, rh_conv, op)
+
 class Equals(BinOp):
     priority = 70
     type = TBool()
@@ -1477,7 +1663,7 @@ def make(cls, site, lh, rh):
         if ((lhtype.is_arith and rhtype.is_arith)
             or (isinstance(lhtype, (TPtr, TArray))
                 and isinstance(rhtype, (TPtr, TArray))
-                and compatible_types(lhtype, rhtype))
+                and compatible_types_fuzzy(lhtype, rhtype))
             or (isinstance(lhtype, TBool) and isinstance(rhtype, TBool))):
             return Equals(site, lh, rh)
 
@@ -1488,6 +1674,11 @@ def make(cls, site, lh, rh):
         if (isinstance(lhtype, THook) and isinstance(rhtype, THook)
             and lhtype.cmp(rhtype) == 0):
             return IdentityEq(site, lh, rh)
+        if ((isinstance(lh, StringConstant)
+             or isinstance(lhtype, TString))
+            and (isinstance(rh, StringConstant)
+                 or isinstance (rhtype, TString))):
+            return mkStringCmp(site, lh, rh, '==')
 
         raise EILLCOMP(site, lh, lhtype, rh, rhtype)
 
@@ -2202,6 +2393,138 @@ def make_simple(site, lh, rh):
 
         return Add_dml12(site, lh, rh)
 
+class StringAdd(Orphan):
+    priority = dml.expr.Apply.priority
+
+    type = TString()
+
+    # Implemented by INHERITING ownership of first argument, but only BORROWING
+    # second argument. Meaning the second argument is never duped, and, if
+    # orphan, must be adopted.
+    def __init__(self, site, lh, rh):
+        self.site = site
+        self.lh = lh
+        self.rh = mkAdoptedOrphan(rh.site, rh)
+
+    def __str__(self):
+        lh = str(self.lh)
+        rh = str(self.rh)
+        if self.lh.priority <= Add.priority:
+            lh = '('+lh+')'
+        if self.rh.priority <= Add.priority:
+            rh = '('+rh+')'
+        return lh + ' + ' + rh
+
+    def read(self):
+        lh = self.lh.read()
+        if not self.lh.orphan:
+            lh = f'_dml_string_dupe({lh})'
+        return f'_dml_string_add({lh}, {self.rh.read()})'
+
+class StringAddCStrAfter(Orphan):
+    priority = dml.expr.Apply.priority
+
+    type = TString()
+
+    @auto_init
+    def __init__(self, site, lh, rh): pass
+
+    def __str__(self):
+        lh = str(self.lh)
+        rh = str(self.rh)
+        if self.lh.priority <= Add.priority:
+            lh = '('+lh+')'
+        if self.rh.priority <= Add.priority:
+            rh = '('+rh+')'
+        return lh + ' + ' + rh
+
+    def read(self):
+        lh = self.lh.read()
+        if not self.lh.orphan:
+            lh = f'_dml_string_dupe({lh})'
+        return f'_dml_string_add_cstr({lh}, {self.rh.read()})'
+
+class StringAddCStrBefore(Orphan):
+    priority = dml.expr.Apply.priority
+
+    type = TString()
+
+    @auto_init
+    def __init__(self, site, lh, rh): pass
+
+    def __str__(self):
+        lh = str(self.lh)
+        rh = str(self.rh)
+        if self.lh.priority <= Add.priority:
+            lh = '('+lh+')'
+        if self.rh.priority <= Add.priority:
+            rh = '('+rh+')'
+        return lh + ' + ' + rh
+
+    def read(self):
+        rh = self.rh.read()
+        if not self.rh.orphan:
+            rh = f'_dml_string_dupe({rh})'
+        return f'_dml_string_add_cstr_before({self.lh.read()}, {rh})'
+
+class StringCStrRef(PseudoMethodRef):
+    @auto_init
+    def __init__(self, site, expr): pass
+
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, 'c_str')
+
+    def apply(self, inits, location, scope):
+        if inits:
+            raise EARG(self.site, '.c_str')
+        return mkStringCStrApply(self.site, self.expr)
+
+mkStringCStrRef = StringCStrRef
+
+class StringCStrApply(Expression):
+    priority = dml.expr.Apply.priority
+
+    slots = ('type', 'constant', 'value')
+
+    @auto_init
+    def __init__(self, site, expr):
+        assert not expr.orphan
+        self.type = TPtr(
+            TNamed('char',
+                   const=(not expr.writable
+                          or safe_realtype_shallow(expr.ctype()).const)))
+        self.constant = expr.constant
+        self.value = expr.value if expr.constant else None
+
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, 'c_str()')
+
+    def read(self):
+        return f'_dml_string_str({self.expr.read()})'
+
+def mkStringCStrApply(site, expr):
+    if expr.orphan:
+        expr = mkAdoptedOrphan(expr.site, expr)
+    return StringCStrApply(site, expr)
+
+class FromCString(Orphan):
+    priority = dml.expr.Apply.priority
+
+    type = TString()
+
+    slots = ('constant', 'value')
+
+    @auto_init
+    def __init__(self, site, expr):
+        self.constant = expr.constant
+        self.value = expr.value if expr.constant else None
+
+    def __str__(self):
+        return f'cast({self.expr}, string)'
+
+    def read(self):
+        return f'_dml_string_new({self.expr.read()})'
+
 class Add(ArithBinOp):
     priority = 110
     op = '+'
@@ -2216,6 +2539,29 @@ def make_simple(site, lh, rh):
             return StringConstant(site, lh.value + rh.value)
         lhtype = realtype(lh.ctype())
         rhtype = realtype(rh.ctype())
+        if isinstance(lhtype, TString) and isinstance(rh, StringConstant):
+            if (isinstance(lh, StringAddCStrAfter)
+                and isinstance(lh.rh, StringConstant)):
+                (lh, rh) = (lh.lh,
+                            mkStringConstant(rh.site, lh.rh.value + rh.value))
+
+            return StringAddCStrAfter(site, lh, rh)
+        elif isinstance(lh, StringConstant) and isinstance(rhtype, TString):
+            if (isinstance(rh, StringAddCStrBefore)
+                and isinstance(rh.lh, StringConstant)):
+                (lh, rh) = (mkStringConstant(lh.site, lh.value + rh.lh.value),
+                            rh.rh)
+            return StringAddCStrBefore(site, lh, rh)
+
+        if isinstance(lhtype, TString) and isinstance(rhtype, TString):
+            return StringAdd(site, lh, rh)
+
+        if (isinstance(lhtype, TVector)
+            and (safe_realtype_unconst(lhtype).cmp(
+                    safe_realtype_unconst(rhtype))
+                 == 0)):
+            return VectorAdd(site, lh, rh)
+
         # ECSADD should always be emitted when the operand types are equivalent
         # to char pointers/arrays -- even including when the operands are
         # explicitly typed as int8 pointers/arrays
@@ -2377,7 +2723,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())
@@ -2394,7 +2740,6 @@ def mkAssignOp(site, target, source):
         raise EASSINL(target.site, target.name)
     if not target.writable:
         raise EASSIGN(site, target)
-
     target_type = target.ctype()
 
     source = source_for_assignment(site, target_type, source)
@@ -2455,13 +2800,14 @@ def make_simple(cls, site, rh):
                     TFunction([TPtr(TNamed('conf_object_t')),
                                TPtr(TVoid())],
                               TVoid())))
-        if not dml.globals.compat_dml12 and not isinstance(rh, LValue):
+        if not dml.globals.compat_dml12 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.writable and self.rh.is_stack_allocated
 
 def mkAddressOf(site, rh):
     if dml.globals.compat_dml12_int(site):
@@ -2621,7 +2967,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
@@ -2647,7 +2993,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:
@@ -2660,9 +3006,10 @@ def make_simple(cls, site, rh):
             (result, signed) = promote_integer(result, rhtype)
             return result
 
+    @classmethod
     @property
-    def op(self):
-        return self.base_op
+    def op(cls):
+        return cls.base_op
 
     @property
     def is_pointer_to_stack_allocation(self):
@@ -2853,7 +3200,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)
 
@@ -2875,7 +3223,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]
@@ -2923,7 +3271,8 @@ def mkBitSlice(site, expr, msb, lsb, bitorder):
 class TraitMethodApplyIndirect(Expression):
     '''The C expression of a trait method call'''
     @auto_init
-    def __init__(self, site, traitref, methname, independent, inargs, type):
+    def __init__(self, site, traitref, methname, independent, memoized,
+                 inargs, type):
         if not independent:
             crep.require_dev(site)
 
@@ -2931,6 +3280,10 @@ def __str__(self):
         return '%s.%s(%s)' % (self.traitref, self.methname,
                               ', '.join(map(str, self.inargs)))
 
+    @property
+    def orphan(self):
+        return not self.memoized
+
     def read(self):
         infix_independent = 'INDEPENDENT_' if self.independent else ''
         suffix_noarg = '' if self.inargs else '0'
@@ -2953,6 +3306,10 @@ def __init__(self, site, traitref, methodref, inargs, type):
     def __str__(self):
         return '%s(%s)' % (self.methodref, ', '.join(map(str, self.inargs)))
 
+    @property
+    def orphan(self):
+        return not self.methodref.memoized
+
     def read(self):
         return "%s(%s)" % (
             self.methodref.cname(),
@@ -2963,7 +3320,7 @@ class New(Expression):
     priority = 160 # f()
     slots = ('type',)
     @auto_init
-    def __init__(self, site, newtype, count):
+    def __init__(self, site, newtype, count, raii_info):
         self.type = TPtr(newtype)
     def __str__(self):
         if self.count:
@@ -2971,16 +3328,21 @@ def __str__(self):
         else:
             return 'new %s' % self.newtype
     def read(self):
-        t = self.newtype.declaration('')
-        if self.count:
-            return 'MM_ZALLOC(%s, %s)' % (self.count.read(), t)
-        else:
-            return 'MM_ZALLOC(1, %s)' % (t)
+        destructor = (self.raii_info.cident_destructor_array_item
+                      if self.raii_info else '_dml_raii_destructor_ref_none')
+        count = self.count.read() if self.count else '1'
+        return (f'DML_NEW({self.newtype.declaration("")}, {count}, '
+                +f'{destructor})')
 
 def mkNew(site, newtype, count = None):
     if count:
         count = as_int(count)
-    return New(site, newtype, count)
+    if newtype.is_raii:
+        from .codegen import get_raii_type_info # TODO(RAII) imports...
+        info = get_raii_type_info(newtype)
+    else:
+        info = None
+    return New(site, newtype, count, info)
 
 class ListItems(metaclass=abc.ABCMeta):
     '''A series of consecutive list elements, where each list element
@@ -3337,7 +3699,9 @@ def ctype(self):
                               self.value.rettype))
 
     def read(self):
-        prefix = '_trampoline' * (not self.value.independent)
+        prefix = '_trampoline' * (not self.value.independent
+                                  or (self.value.memoized
+                                      and self.value.rettype.is_raii))
         return f'(&{prefix}{self.value.get_cname()})'
 
 
@@ -3398,6 +3762,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()
+
+mkDiscardRef = DiscardRef
+
 def endian_convert_expr(site, idx, endian, size):
     """Convert a bit index to little-endian (lsb=0) numbering.
 
@@ -3570,8 +3946,12 @@ def mkTraitUpcast(site, sub, parent):
         if typ.trait is parent:
             return sub
         elif parent in typ.trait.ancestors:
+            if isinstance(sub, ObjTraitRef):
+                return ObjTraitRef(site, sub.node, parent, sub.indices)
             return TraitUpcast(site, sub, parent)
         elif parent is dml.globals.object_trait:
+            if isinstance(sub, ObjTraitRef):
+                return ObjTraitRef(site, sub.node, parent, sub.indices)
             return TraitObjectCast(site, sub)
     raise ETEMPLATEUPCAST(site, typ, parent.type())
 
@@ -3646,6 +4026,8 @@ def outp(self): pass
     def throws(self): pass
     @abc.abstractproperty
     def independent(self): pass
+    @abc.abstractproperty
+    def memoized(self): pass
 
     def apply(self, inits, location, scope):
         '''Return expression for application as a function'''
@@ -3688,6 +4070,10 @@ def throws(self):
     def independent(self):
         return self.methodref.independent
 
+    @property
+    def memoized(self):
+        return self.methodref.memoized
+
     def call_expr(self, inargs, rettype):
         return TraitMethodApplyDirect(self.site, self.traitref,
                                       self.methodref, inargs, rettype)
@@ -3699,7 +4085,7 @@ class TraitMethodIndirect(TraitMethodRef):
     it needs to be called.'''
     @auto_init
     def __init__(self, site, traitref, methname, inp, outp, throws,
-                 independent): pass
+                 independent, memoized): pass
 
     def __str__(self):
         return "%s.%s" % (str(self.traitref), self.methname)
@@ -3707,7 +4093,7 @@ def __str__(self):
     def call_expr(self, inargs, rettype):
         return TraitMethodApplyIndirect(self.site, self.traitref,
                                         self.methname, self.independent,
-                                        inargs, rettype)
+                                        self.memoized, inargs, rettype)
 
 class TraitHookArrayRef(NonValueArrayRef):
     @auto_init
@@ -4067,7 +4453,7 @@ def read(self):
 
 mkHookSuspended = HookSuspended
 
-class HookSendNowRef(NonValue):
+class HookSendNowRef(PseudoMethodRef):
     '''Reference to the send_now pseudomethod of a hook'''
     @auto_init
     def __init__(self, site, hookref_expr): pass
@@ -4087,33 +4473,39 @@ def apply(self, inits, location, scope):
 
 class HookSendNowApply(Expression):
     '''Application of the send_now pseudomethod with valid arguments'''
-    slots = ('msg_struct',)
+    slots = ('msg_struct', 'msg_struct_info')
     type = TInt(64, False)
     priority = dml.expr.Apply.priority
     @auto_init
     def __init__(self, site, hookref_expr, args):
         crep.require_dev(site)
         msg_types = safe_realtype(hookref_expr.ctype()).msg_types
-        from .codegen import get_type_sequence_info
+        from .codegen import get_type_sequence_info, get_raii_type_info
         self.msg_struct = get_type_sequence_info(msg_types,
                                                  create_new=True).struct
+        self.msg_struct_info = (get_raii_type_info(self.msg_struct)
+                                if self.msg_struct and self.msg_struct.is_raii
+                                else None)
+
 
     def __str__(self):
         return '%s.send_now(%s)' % (self.hookref_expr,
                                     ', '.join(str(e) for e in self.args))
     def read(self):
-        msg = (('&(%s_t) {%s}'
+        hookref = resolve_hookref(self.hookref_expr)
+        msg = (('&((%s_t) {%s})'
                 % (self.msg_struct.label,
                    ', '.join(arg.read() for arg in self.args)))
                if self.args else 'NULL')
-        hookref = resolve_hookref(self.hookref_expr)
-
-        return ('_DML_send_hook(&_dev->obj, '
-                + f'&_dev->_detached_hook_queue_stack, {hookref}, {msg})')
+        if self.msg_struct_info:
+            return (f'_DML_SEND_HOOK_RAII({hookref}, {msg}, '
+                    + f'{self.msg_struct_info.cident_destructor})')
+        else:
+            return f'_DML_SEND_HOOK({hookref}, {msg})'
 
 mkHookSendNowApply = HookSendNowApply
 
-class HookSendRef(NonValue):
+class HookSendRef(PseudoMethodRef):
     '''Reference to the send pseudomethod of a hook'''
     @auto_init
     def __init__(self, site, hookref_expr): pass
@@ -4157,11 +4549,15 @@ def read(self):
                     % (TArray(self.info.args_type,
                               mkIntegerLiteral(self.site, 1)).declaration(''),
                        self.hookref_expr.read()))
+
+        args_destructor = (self.info.args_raii_info.cident_destructor
+                           if self.info.args_raii_info else 'NULL')
+
         args_size = f'sizeof({self.info.args_type.declaration("")})'
-        return ('_DML_post_immediate_after('
+        return ('(({ _DML_post_immediate_after('
                 + '&_dev->obj, _dev->_immediate_after_state, '
-                + f'{self.info.cident_callback}, NULL, 0, {args}, '
-                + f'{args_size}, NULL, 0)')
+                + f'{self.info.cident_callback}, {args_destructor}, NULL, 0, '
+                + f'{args}, {args_size}, NULL, 0); }}))')
 
 mkHookSendApply = HookSendApply
 
@@ -4182,6 +4578,8 @@ def read(self):
         else:
             s = self.sym.name
         return s
+    def discard(self):
+        return f'(void){self.read()}'
     def incref(self):
         self.sym.incref()
     def decref(self):
@@ -4210,14 +4608,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):
+        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:
@@ -4231,7 +4643,7 @@ def read(self):
 
     @property
     def is_stack_allocated(self):
-        return isinstance(self.expr, LValue) and self.expr.is_stack_allocated
+        return self.expr.writable and self.expr.is_stack_allocated
 
     @property
     def is_pointer_to_stack_allocation(self):
@@ -4270,12 +4682,14 @@ def mkSubRef(site, expr, sub, op):
         basetype = real_etype.base
         real_basetype = safe_realtype(basetype)
         baseexpr = mkDereference(site, expr)
+        def structmember_expr(): return expr
     else:
         if op == '->':
             raise ENOPTR(site, expr)
         basetype = etype
         real_basetype = safe_realtype(etype)
         baseexpr = expr
+        def structmember_expr(): return mkAdoptedOrphan(expr.site, expr)
 
     real_basetype = real_basetype.resolve()
 
@@ -4283,7 +4697,7 @@ def mkSubRef(site, expr, sub, op):
         typ = real_basetype.get_member_qualified(sub)
         if not typ:
             raise EMEMBER(site, baseexpr, sub)
-        return StructMember(site, expr, sub, typ, op)
+        return StructMember(site, structmember_expr(), sub, typ, op)
     elif real_basetype.is_int and real_basetype.is_bitfields:
         member = real_basetype.members.get(sub)
         if member is None:
@@ -4318,16 +4732,119 @@ def mkSubRef(site, expr, sub, op):
             return mkHookSendNowRef(site, baseexpr)
         elif sub == 'suspended':
             return mkHookSuspended(site, baseexpr)
-
+    elif isinstance(basetype, TString):
+        if sub == 'c_str':
+            return mkStringCStrRef(site, baseexpr)
+        if sub == 'len':
+            return mkStringLen(site, baseexpr)
+    elif isinstance(basetype, TVector):
+        if sub == 'c_buf':
+            return mkVectorCBuf(site, baseexpr)
+        if sub == 'len':
+            return mkVectorLen(site, baseexpr)
+        if sub == 'push_back':
+            return mkVectorPushBack(site, baseexpr)
+        if sub == 'pop_back':
+            return mkVectorPopBack(site, baseexpr)
+        if sub == 'push_front':
+            return mkVectorPushFront(site, baseexpr)
+        if sub == 'pop_front':
+            return mkVectorPopFront(site, baseexpr)
+        if sub == 'insert':
+            return mkVectorInsert(site, baseexpr)
+        if sub == 'remove':
+            return mkVectorRemove(site, baseexpr)
     raise ENOSTRUCT(site, expr)
 
-class ArrayRef(LValue):
+class StringLen(Expression):
+    priority = StructMember.priority
+
     slots = ('type',)
-    priority = 160
-    explicit_type = True
+
+    @auto_init
+    def __init__(self, site, expr):
+        assert not expr.orphan
+        assert not expr.writable or expr.c_lval
+        self.type = TInt(32, False,
+                         const=safe_realtype_shallow(self.expr.ctype()).const)
+
+
+    def __str__(self):
+        expr = (f'({self.expr})'
+                if self.expr.priority < StructMember.priority
+                else str(self.expr))
+        return f'{expr}.len'
+
+    @property
+    def writable(self):
+        return self.expr.writable
+
+    def read(self):
+        s = self.expr.read()
+        if self.expr.priority < self.priority:
+            s = '(' + s + ')'
+        return s + '.len'
+
+    def write(self, source):
+        assert isinstance(source, ExpressionInitializer)
+        return (f'_dml_string_resize(&({self.expr.read()}), '
+                + f'{source.expr.read()})')
+
+def mkStringLen(site, expr):
+    return StringLen(site, mkAdoptedOrphan(expr.site, expr))
+
+class VectorLen(Expression):
+    priority = StructMember.priority
+
+    slots = ('type',)
+
+    @auto_init
+    def __init__(self, site, expr):
+        assert not expr.orphan
+        assert not expr.writable or expr.c_lval
+        self.type = TInt(32, False, const=deep_const(self.expr.ctype()))
+
+    def __str__(self):
+        expr = (f'({self.expr})'
+                if self.expr.priority < StructMember.priority
+                else str(self.expr))
+        return f'{expr}.len'
+
+    def read(self):
+        s = self.expr.read()
+        if self.expr.priority < self.priority:
+            s = '(' + s + ')'
+        return s + '.len'
+
+    @property
+    def writable(self):
+        return self.expr.writable
+
+    def write(self, source):
+        assert isinstance(source, ExpressionInitializer)
+        base = realtype_shallow(self.expr.ctype()).base
+        if base.is_raii:
+            from .codegen import get_raii_type_info
+            info = get_raii_type_info(base)
+            return ('_dml_vect_resize_raii(sizeof(%s), %s, (void *)&(%s), %s)'
+                    % (base.declaration(''), info.cident_destructor,
+                       self.expr.read(), source.expr.read()))
+        else:
+            return ('_dml_vect_resize(sizeof(%s), (void *)&(%s), %s, true)'
+                    % (base.declaration(''), self.expr.read(),
+                       source.expr.read()))
+
+def mkVectorLen(site, expr):
+    return VectorLen(site, mkAdoptedOrphan(expr.site, expr))
+
+class ArrayRef(LValue):
+    slots = ('type',)
+    priority = 160
+    explicit_type = True
     @auto_init
     def __init__(self, site, expr, idx):
-        self.type = realtype_shallow(expr.ctype()).base
+        expr_type = realtype_shallow(expr.ctype())
+        self.type = conv_const(expr_type.const, expr_type.base)
     def __str__(self):
         return '%s[%s]' % (self.expr, self.idx)
     def read(self):
@@ -4344,16 +4861,63 @@ def is_stack_allocated(self):
     def is_pointer_to_stack_allocation(self):
         return isinstance(self.type, TArray) and self.is_stack_allocated
 
-class VectorRef(LValue):
+class LegacyVectorRef(LValue):
     slots = ('type',)
     @auto_init
     def __init__(self, site, expr, idx):
         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
+
+class StringCharRef(Expression):
+    slots = ('type',)
+    priority = dml.expr.Apply.priority
+    c_lval = True
+    @auto_init
+    def __init__(self, site, expr, idx):
+        self.type = TNamed(
+            'char', const=(safe_realtype(self.expr.ctype()).const))
+
+    @property
+    def writable(self):
+        return self.expr.writable
+
+    def read(self):
+        return f'DML_STRING_CHAR({self.expr.read()}, {self.idx.read()})'
+
+def mkStringCharRef(site, expr, idx):
+    return StringCharRef(site, mkAdoptedOrphan(expr.site, expr), idx)
+
+
+# Not considered addressable, as the address of an elem is very easily
+# invalidated.
+# Users have to use .c_buf() instead to acknowledge that possibility.
+class VectorRef(Expression):
+    slots = ('type',)
+    priority = dml.expr.Apply.priority
+    explicit_type=True
+    c_lval = True
+
+    @auto_init
+    def __init__(self, site, expr, idx):
+        typ = safe_realtype_shallow(self.expr.ctype())
+        self.type = conv_const(typ.const, typ.base)
+
+    @property
+    def writable(self):
+        return self.expr.writable
+
+    def read(self):
+        base_typ = safe_realtype_shallow(self.expr.ctype()).base
+        return ('DML_VECT_ELEM(%s, %s, %s)'
+                % (base_typ.declaration(''), self.expr.read(),
+                   self.idx.read()))
+
+def mkVectorRef(site, expr, idx):
+    if idx.constant and idx.value < 0:
+        raise EOOB(expr)
+    return VectorRef(site, mkAdoptedOrphan(expr.site, expr), idx)
 
 def mkIndex(site, expr, idx):
     if isinstance(idx, NonValue):
@@ -4403,20 +4967,30 @@ def mkIndex(site, expr, idx):
     if typ.is_int:
         return mkBitSlice(site, expr, idx, None, None)
 
+    expr = mkAdoptedOrphan(expr.site, expr)
+
     if isinstance(typ, (TArray, TPtr)):
         return ArrayRef(site, expr, idx)
 
     if isinstance(typ, (TVector)):
         return VectorRef(site, expr, idx)
 
+    if isinstance(typ, (TVectorLegacy)):
+        return LegacyVectorRef(site, expr, idx)
+
+    if isinstance(typ, TString):
+        return mkStringCharRef(site, expr, idx)
+
     raise ENARRAY(expr)
 
 class Cast(Expression):
     "A C type cast"
     priority = 140
     explicit_type = True
+    slots = ('orphan',)
     @auto_init
-    def __init__(self, site, expr, type): pass
+    def __init__(self, site, expr, type):
+        self.orphan = expr.orphan
     def __str__(self):
         return 'cast(%s, %s)' % (self.expr, self.type.declaration(""))
     def read(self):
@@ -4440,20 +5014,32 @@ def mkCast(site, expr, new_type):
             raise ETEMPLATEUPCAST(site, "object", new_type)
         else:
             return mkTraitUpcast(site, expr, real.trait)
+
+    if (dml.globals.compat_dml12 and dml.globals.api_version <= "6"
+        and isinstance(expr, InterfaceMethodRef)):
+        # Workaround for bug 24144
+        return mkLit(site, "%s->%s" % (
+            expr.node_expr.read(), expr.method_name), new_type)
+    if isinstance(expr, NonValue):
+        raise expr.exc()
     old_type = safe_realtype(expr.ctype())
     if (dml.globals.compat_dml12_int(site)
-        and (isinstance(old_type, (TStruct, TVector))
-             or isinstance(real, (TStruct, TVector)))):
+        and (isinstance(old_type, (TStruct, TVectorLegacy))
+             or isinstance(real, (TStruct, TVectorLegacy)))):
         # these casts are permitted by C only if old and new are
         # the same type, which is useless
         return Cast(site, expr, new_type)
+    if old_type.cmp(real) == 0:
+        if (old_type.is_int
+            and not old_type.is_endian
+            and dml.globals.compat_dml12_int(expr.site)):
+            # 1.2 integer expressions often lie about their actual type,
+            # and require a "redundant" cast! Why yes, this IS horrid!
+            return Cast(site, expr, new_type)
+        return mkRValue(expr)
     if isinstance(real, TStruct):
-        if isinstance(old_type, TStruct) and old_type.label == real.label:
-            return expr
         raise ECAST(site, expr, new_type)
     if isinstance(real, TExternStruct):
-        if isinstance(old_type, TExternStruct) and old_type.id == real.id:
-            return expr
         raise ECAST(site, expr, new_type)
     if isinstance(real, (TVoid, TArray, TVector, TTraitList, TFunction)):
         raise ECAST(site, expr, new_type)
@@ -4463,7 +5049,7 @@ def mkCast(site, expr, new_type):
         expr = as_int(expr)
         old_type = safe_realtype(expr.ctype())
     if real.is_int and not real.is_endian:
-        if isinstance(expr, IntegerConstant):
+        if old_type.is_int and expr.constant:
             value = truncate_int_bits(expr.value, real.signed, real.bits)
             if dml.globals.compat_dml12_int(site):
                 return IntegerConstant_dml12(site, value, real)
@@ -4474,8 +5060,8 @@ def mkCast(site, expr, new_type):
         # Shorten redundant chains of integer casts. Avoids insane C
         # output for expressions like a+b+c+d.
         if (isinstance(expr, Cast)
-            and isinstance(expr.type, TInt)
-            and expr.type.bits >= real.bits):
+            and isinstance(old_type, TInt)
+            and old_type.bits >= real.bits):
             # (uint64)(int64)x -> (uint64)x
             expr = expr.expr
             old_type = safe_realtype(expr.ctype())
@@ -4511,9 +5097,7 @@ def mkCast(site, expr, new_type):
             return expr
     elif real.is_int and real.is_endian:
         old_type = safe_realtype(expr.ctype())
-        if real.cmp(old_type) == 0:
-            return expr
-        elif old_type.is_arith or isinstance(old_type, TPtr):
+        if old_type.is_arith or isinstance(old_type, TPtr):
             return mkApply(
                 expr.site,
                 mkLit(expr.site, *real.get_store_fun()),
@@ -4570,7 +5154,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):
@@ -4579,10 +5162,29 @@ def ctype(self):
         return self.expr.ctype()
     def read(self):
         return self.expr.read()
-    def discard(self): pass
+    @property
+    def c_lval(self):
+        return self.expr.c_lval()
+    @property
+    def explicit_type(self):
+        return self.expr.explicit_type
+    @property
+    def type(self):
+        assert self.explicit_type
+        return self.expr.type
+    @property
+    def orphan(self):
+        return self.expr.orphan
+    # TODO(RAII) This used to be simply be `pass`, which SCREAMS incorrect.
+    # But it makes me wonder why it was defined like that to begin with.
+    def discard(self):
+        return self.expr.discard()
+    @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
 
@@ -4733,23 +5335,44 @@ class Initializer(object):
     being initialized."""
     __slots__ = ()
 
+    @abc.abstractproperty
+    def site(self): pass
+
 class ExpressionInitializer(Initializer):
-    __slots__ = ('expr',)
-    def __init__(self, expr):
+    __slots__ = ('expr', 'ignore_raii')
+    def __init__(self, expr, ignore_raii=False):
         assert isinstance(expr, Expression)
         self.expr = expr
+        self.ignore_raii = ignore_raii
     def __str__(self):
         return "%s" % self.expr
     def __repr__(self):
-        return "ExpressionInitializer(%r)" % self.expr
+        return f'ExpressionInitializer({self.expr!r}, {self.ignore_raii})'
+    @property
+    def site(self):
+        return self.expr.site
     def incref(self):
         self.expr.incref()
     def decref(self):
         self.expr.decref()
     def read(self):
+        realt = safe_realtype_shallow(self.expr.ctype())
+        if (not self.ignore_raii
+            and realt.is_raii
+            and not isinstance(realt, TArray)
+            and not self.expr.orphan):
+            from .codegen import get_raii_type_info
+            return get_raii_type_info(self.expr.ctype()).read_dupe(
+                self.expr.read())
+
         return self.expr.read()
     def as_expr(self, typ):
-        return source_for_assignment(self.expr.site, typ, self.expr)
+        expr = source_for_assignment(self.expr.site, typ, self.expr)
+        return (RAIIDupe(expr.site, expr)
+                if (not self.ignore_raii and not self.expr.orphan
+                    and typ.is_raii)
+                else expr)
+
     def assign_to(self, dest, typ):
         # Assigns to (partially) const-qualified targets can happen as part of
         # initializing (partially) const-qualified session variables. To allow
@@ -4758,15 +5381,59 @@ def assign_to(self, dest, typ):
         # Since session variables are allocated on the heap, this should *not*
         # be UB as long as the session variable hasn't been initialized
         # previously.
+        typ = realtype(typ)
         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()))
+        if not self.ignore_raii and typ.is_raii:
+            # TODO(RAII) this logic should likely be decentralized
+            from .codegen import get_raii_type_info, VectorRAIITypeInfo
+            info = get_raii_type_info(typ)
+            if isinstance(self.expr, FromCString):
+                return (f'_dml_string_set((void *)&({dest}), '
+                        + f'{self.expr.expr.read()})')
+            if isinstance(self.expr, VectorCompoundLiteral):
+                assert isinstance(info, VectorRAIITypeInfo)
+                sizeof = f'sizeof({info.type.base.declaration("")})'
+                tgt = f'(void *)&({dest})'
+                src = self.expr.as_array_literal().read()
+                length = f'{len(self.expr.inits)}ULL'
+                if not info.base_info:
+                    return (f'(({{ _dml_vect_set_array({sizeof}, {tgt}, {src}, '
+                            + f'{length}); }}))')
+                destructor = info.base_info.cident_destructor
+                return (f'(({{ _dml_vect_set_compound_init_raii({sizeof}, '
+                        + f'{destructor}, {tgt}, {src}, {length}); }}))')
+            if self.expr.orphan:
+                if isinstance(self.expr, RAIIDupe):
+                    return (info.read_copy_lval if self.expr.expr.c_lval else
+                            info.read_copy)(dest, self.expr.expr.read())
+                outp = (info.read_linear_move_lval if self.expr.c_lval else
+                        info.read_linear_move)(dest, self.expr.read())
+                if (isinstance(self.expr, CompoundLiteral)
+                    and isinstance(safe_realtype_shallow(self.expr.ctype()),
+                                   TArray)):
+                    outp = f'(({{ {outp}; }}))'
+                return outp
+            return (info.read_copy_lval
+                    if self.expr.c_lval else info.read_copy)(dest,
+                                                             self.expr.read())
+
+        elif isinstance(typ, TEndianInt):
+            return (f'{typ.dmllib_fun("copy")}((void *)&{dest},'
+                    + f' {self.expr.read()})')
+        elif deep_const(typ):
+            shallow_deconst_typ = safe_realtype_unconst(typ)
+            if (deep_const(shallow_deconst_typ)
+                or isinstance(typ, (TExternStruct, TArray))):
+                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:
-            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
@@ -4789,24 +5456,33 @@ def decref(self):
     def read(self):
         return '{' + ", ".join(i.read() for i in self.init) + '}'
     def as_expr(self, typ):
+        if isinstance(safe_realtype_shallow(typ), TArray):
+            return ArrayCompoundLiteral(self.site, self, typ)
         return CompoundLiteral(self.site, self, typ)
     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()))
+        if typ.is_raii:
+            from .codegen import get_raii_type_info
+            info = get_raii_type_info(typ)
+            outp = info.read_linear_move_lval(
+                dest, f'(({typ.declaration("")}){self.read()})')
+            if isinstance(safe_realtype_shallow(typ), TArray):
+                outp = f'(({{ {outp}; }}))'
+            return outp
+        elif isinstance(typ, TNamed):
+            return ('memcpy((void *)&%s, &(%s)%s, sizeof %s)' %
+                (dest, typ.declaration(''), self.read(),
+                 dest))
         elif isinstance(typ, TArray):
-            out('memcpy((void *)%s, (%s)%s, sizeof %s);\n'
-                % (dest.read(), typ.declaration(''),
-                   self.read(), dest.read()))
+            return ('memcpy((void *)%s, (%s)%s, sizeof %s)'
+                % (dest, typ.declaration(''), self.read(), dest))
         elif isinstance(typ, TStruct):
-            out('memcpy((void *)&%s, (%s){%s}, sizeof %s);\n' % (
-                dest.read(),
+            return 'memcpy((void *)&%s, (%s){%s}, sizeof %s)' % (
+                dest,
                 TArray(typ, mkIntegerLiteral(self.site, 1)).declaration(''),
-                self.read(), dest.read()))
+                self.read(), dest)
         else:
             raise ICE(self.site, 'strange type %s' % typ)
 
@@ -4843,15 +5519,25 @@ def read(self):
     def as_expr(self, typ):
         return CompoundLiteral(self.site, self, typ)
     def assign_to(self, dest, typ):
-        '''output C statements to assign an lvalue'''
+        '''return a C string for a void expression to assign an lvalue'''
         typ = safe_realtype(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()))
+            if typ.is_raii:
+                from .codegen import get_raii_type_info
+                info = get_raii_type_info(typ)
+                return info.read_linear_move_lval(
+                    dest,
+                    f'(({typ.declaration.declaration("")}){self.read()})')
+            else:
+                # (void *) cast to avoid GCC erroring if the target type is
+                # (partially) const-qualified.
+                # See ExpressionInitializer.assign_to
+                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}')
 
@@ -4868,9 +5554,10 @@ class MemsetInitializer(Initializer):
 
     This initializer may only be used for struct or array initializers.
     '''
-    __slots__ = ('site',)
-    def __init__(self, site):
+    __slots__ = ('site','ignore_raii')
+    def __init__(self, site, ignore_raii=False):
         self.site = site
+        self.ignore_raii = ignore_raii
     def __str__(self):
         return self.read()
     def __repr__(self):
@@ -4882,18 +5569,25 @@ def decref(self):
     def read(self):
         return '{0}'
     def as_expr(self, typ):
+        if isinstance(safe_realtype_shallow(typ), TArray):
+            return ArrayCompoundLiteral(self.site, self, typ)
         return CompoundLiteral(self.site, self, typ)
     def assign_to(self, dest, typ):
         '''output C statements to assign an lvalue'''
         assert isinstance(safe_realtype(typ),
                           (TExternStruct, TStruct, TArray, TEndianInt, TTrait,
-                           THook))
+                           THook, TString))
+
+        if not self.ignore_raii and typ.is_raii:
+            from .codegen import get_raii_type_info
+            info = get_raii_type_info(typ)
+            return f'_DML_RAII_ZERO_OUT({info.cident_destructor}, {dest})'
+
         # (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):
+class CompoundLiteral(Orphan):
     @auto_init
     def __init__(self, site, init, type):
         assert isinstance(init, (CompoundInitializer,
@@ -4902,7 +5596,102 @@ def __init__(self, site, init, type):
     def __str__(self):
         return 'cast(%s, %s)' % (self.init, self.type)
     def read(self):
-        return f'({self.type.declaration("")}) {self.init.read()}'
+        return f'(({self.type.declaration("")}){self.init.read()})'
+
+class ArrayCompoundLiteral(Expression):
+    slots = ('read_adopted',)
+    # TODO(RAII) writable?
+    addressable = True
+    c_lval = True
+    @auto_init
+    def __init__(self, site, init, type):
+        assert isinstance(init, (CompoundInitializer, MemsetInitializer))
+        self.read_adopted = RAIIScope.reserve_orphan_adoption(
+            site, CompoundLiteral(site, init, type))
+    def __str__(self):
+        return 'cast(%s, %s)' % (self.init, self.type)
+    def read(self):
+        return self.read_adopted()
+
+class VectorCompoundLiteral(Orphan):
+    '''Initializer for a variable of vector type, using the
+    {value1, value2, ...}  syntax as in C'''
+    priority = Cast.priority
+    slots = ('type',)
+    @auto_init
+    def __init__(self, site, inits, basetype):
+        self.type = TVector(basetype)
+
+    def __str__(self):
+        return ('cast({%s}, vect(%s))'
+                % (','.join(str(e) for e in self.inits),
+                   str(self.basetype)))
+
+    def read(self):
+        if self.inits:
+            # The statement expression limits the lifetime of the compound
+            # literal, cause the compiler won't be able to tell otherwise
+            # that it needn't consume the stack space.
+            return ('(({ _dml_vect_from_array(sizeof(%s), %s, %s); }))'
+                    % (self.basetype.declaration(''),
+                       self.as_array_literal().read(),
+                       len(self.inits)))
+        else:
+            return '((const _dml_vect_t){0})'
+
+    def as_array_literal(self):
+        if self.inits:
+            count = mkIntegerLiteral(self.site, len(self.inits))
+            const_basetype = conv_const(True, self.basetype)
+            return CompoundLiteral(
+                self.site, CompoundInitializer(self.site, self.inits),
+                TArray(const_basetype, count))
+        else:
+            return NullConstant(self.site)
+
+class RAIIDupe(Orphan):
+    priority = dml.expr.Apply.priority
+    slots = ('info',)
+    @auto_init
+    def __init__(self, site, expr):
+        assert expr.ctype().is_raii
+        assert not isinstance(safe_realtype_shallow(expr.ctype()), TArray)
+        from .codegen import get_raii_type_info
+        self.info = get_raii_type_info(expr.ctype())
+
+    def read(self):
+        return self.info.read_dupe(self.expr.read())
+
+    def ctype(self):
+        return self.expr.ctype()
+
+    def discard(self):
+        return self.expr.discard()
+
+class AdoptedOrphan(Expression):
+    priority = dml.expr.Apply.priority
+    slots = ('read_adopted', 'constant', 'value')
+    c_lval = True
+    @auto_init
+    def __init__(self, site, expr):
+        assert expr.orphan
+        self.read_adopted = RAIIScope.reserve_orphan_adoption(site, expr)
+        self.constant = expr.constant
+        self.value = expr.value if expr.constant else None
+
+    def __str__(self):
+        return str(self.expr)
+
+    def ctype(self):
+        return self.expr.ctype()
+
+    def read(self):
+        return self.read_adopted()
+
+def mkAdoptedOrphan(site, expr):
+    if expr.orphan and expr.ctype().is_raii:
+        return AdoptedOrphan(site, expr)
+    return expr
 
 class StructDefinition(Statement):
     """A C struct definition appearing in a local scope, like
@@ -4911,14 +5700,16 @@ class StructDefinition(Statement):
     is preceded by a StructDefinition."""
     @auto_init
     def __init__(self, site, structtype): pass
-    def toc(self):
+    def toc_stmt(self):
         self.structtype.resolve().print_struct_definition()
 mkStructDefinition = StructDefinition
 
 class Declaration(Statement):
     "A variable declaration"
     is_declaration = True
-    def __init__(self, site, name, type, init = None, unused = False):
+    slots = ('toc_raii_bind',)
+    def __init__(self, site, name, type, init = None, unused = False,
+                 unscoped_raii = False):
         assert site
         self.site = site
         self.name = name
@@ -4930,24 +5721,682 @@ def __init__(self, site, name, type, init = None, unused = False):
         if name.startswith("__"):
             assert unused == True
         self.unused = unused
+        self.unscoped_raii = unscoped_raii
+        if type.is_raii and not unscoped_raii:
+            self.toc_raii_bind = RAIIScope.reserve_bind_lval(
+                site, mkLit(site, self.name, type))
+        else:
+            self.toc_raii_bind = None
 
-    def toc(self):
+    def toc_stmt(self):
         self.linemark()
 
         if (isinstance(self.init, MemsetInitializer)
-            and not deep_const(self.type)):
+            and not deep_const(self.type)
+            and not self.type.is_raii):
             # ducks a potential GCC warning, and also serves to
             # zero-initialize VLAs
             self.type.print_declaration(self.name, unused = self.unused)
-            self.init.assign_to(mkLit(self.site, self.name, self.type),
-                                self.type)
+            self.linemark()
+            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,
                 unused=self.unused)
+        if self.toc_raii_bind is not None:
+            self.toc_raii_bind()
 
 mkDeclaration = Declaration
 
+class RAIIScope(metaclass=SlotsMeta):
+    slots = ('top_scope', 'allocs', 'scope_stack_ix',)
+    def __init__(self):
+        self.top_scope = None
+        self.scope_stack_ix = None
+        self.allocs = 0
+
+    @staticmethod
+    def scope_stack():
+        return TopRAIIScope.active.scope_stack
+
+    def __enter__(self):
+        assert TopRAIIScope.active is not None
+        self.top_scope = TopRAIIScope.active
+        TopRAIIScope.active.push_scope(self)
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        assert TopRAIIScope.active is self.top_scope
+        top = self.top_scope.pop_scope()
+        assert top is self
+
+    @staticmethod
+    def reserve_orphan_adoption(site, expr):
+        assert TopRAIIScope.active is not None
+        return TopRAIIScope.active.reserve_orphan_adoption(
+            site, expr, RAIIScope.scope_stack()[-1])
+
+    @staticmethod
+    def reserve_bind_lval(site, expr):
+        return RAIIScope.reserve_bind_lvals(site, (expr,))
+
+    @staticmethod
+    def reserve_bind_lvals(site, exprs):
+        assert TopRAIIScope.active
+        return TopRAIIScope.active.reserve_bind_lvals(
+            site, exprs, RAIIScope.scope_stack()[-1])
+
+    def clear_scope(self, site):
+        assert self.top_scope is not None
+        self.top_scope.clear_subscope(site, self)
+
+    @property
+    def used(self):
+        return self.allocs > 0
+
+    @property
+    def completed(self):
+        return self.top_scope is not None and self.scope_stack_ix is None
+
+class TopRAIIScope(RAIIScope):
+    active = None
+
+    slots = ('prev_active',)
+
+    @auto_init
+    def __init__(self):
+        self.prev_active = None
+
+    def __enter__(self):
+        self.prev_active = TopRAIIScope.active
+        TopRAIIScope.active = self
+        return RAIIScope.__enter__(self)
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        RAIIScope.__exit__(self, exc_type, exc_val, exc_tb)
+        assert TopRAIIScope.active is self
+        TopRAIIScope.active = self.prev_active
+
+    @property
+    def scope_stack(self):
+        raise ICE(None,
+                  f'{type(self).__name__}.scope_stack not supported')
+
+    def clear_subscope(self, site, scope):
+        raise ICE(site,
+                  f'{type(self).__name__}.clear_subscope() not supported')
+
+    def reserve_bind_lval(self, site, lval, scope):
+        raise ICE(site,
+                  f'{type(self).__name__}.reserve_bind_lval() not supported')
+
+    def reserve_bind_lvals(self, site, exprs, scope):
+        raise ICE(site,
+                  f'{type(self).__name__}.reserve_bind_lvals() not supported')
+
+    def reserve_orphan_adoption(self, site, expr, scope):
+        raise ICE(site,
+                  (f'{type(self).__name__}.reserve_orphan_adoption() '
+                   + 'not supported'))
+
+    def push_scope(self, scope):
+        if scope is not self:
+            raise ICE(None, f'{type(self).__name__}: subscopes not supported')
+        self.scope_stack_ix = 0
+
+    def pop_scope(self):
+        assert self.scope_stack_ix == 0
+        self.scope_stack_ix = None
+        return self
+
+
+class MethodRAIIScope(TopRAIIScope):
+    slots = ('scopes', 'scope_stack', 'used_scopes')
+
+    @auto_init
+    def __init__(self):
+        self.scopes = Set()
+        self.scope_stack = []
+        self.used_scopes = None
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        TopRAIIScope.__exit__(self, exc_type, exc_val, exc_tb)
+        self.used_scopes = {scope: i
+                            for (i, scope) in enumerate(filter(
+                                    lambda x: x.used, self.scopes))}
+
+    def reserve_bind_lvals(self, site, exprs, subscope):
+        assert self.used_scopes is None and subscope in self.scopes
+        from .codegen import get_raii_type_info
+        binds = [(expr, get_raii_type_info(expr.ctype()).cident_destructor)
+                 for expr in exprs]
+        subscope.allocs += len(binds)
+        def toc_stmt():
+            assert self.used_scopes is not None
+            for (expr, destructor) in binds:
+                site_linemark(site)
+                out(f'DML_RAII_SCOPE_LVAL({self.used_scopes[subscope]}, '
+                    + f'{destructor}, {expr.read()});\n')
+        return toc_stmt
+
+    def clear_subscope(self, site, scope):
+        assert self.used_scopes is None or scope in self.used_scopes
+        site_linemark(site)
+        out(f'DML_RAII_SCOPE_CLEANUP({self.used_scopes[scope]});\n')
+
+    def reserve_orphan_adoption(self, site, expr, subscope):
+        assert self.used_scopes is None and subscope in self.scopes
+        subscope.allocs += 1
+        typ = safe_realtype_shallow(expr.ctype())
+        if typ.is_raii:
+            from .codegen import get_raii_type_info
+            destructor = get_raii_type_info(typ).cident_destructor
+        else:
+            assert isinstance(typ, TArray)
+            destructor = 'NULL'
+
+        array_prefix = 'ARRAY_'*(isinstance(typ, TArray))
+
+        def read():
+            assert self.used_scopes is not None
+            return (f'DML_RAII_SCOPE_{array_prefix}ORPHAN('
+                    + f'{self.used_scopes[subscope]}, {destructor}, '
+                    + f'{expr.read()})')
+        return read
+
+    def push_scope(self, scope):
+        assert self.used_scopes is None
+        assert scope.scope_stack_ix is None
+        scope.scope_stack_ix = len(self.scope_stack)
+        self.scope_stack.append(scope)
+        self.scopes.add(scope)
+
+    def pop_scope(self):
+        assert self.used_scopes is None
+        scope = self.scope_stack.pop()
+        scope.scope_stack_ix = None
+        return scope
+
+class UnusedMethodRAIIScope(MethodRAIIScope):
+    def reserve_orphan_adoption(self, site, expr, subscope):
+        raise ICE(site, 'orphan adopted in UnusedMethodRAIIScope')
+
+    def reserve_bind_lvals(self, site, exprs, subscope):
+        raise ICE(site, 'lval bound in UnusedMethodRAIIScope')
+
+class StaticRAIIScope(TopRAIIScope):
+    def reserve_orphan_adoption(self, site, expr, subscope):
+        assert subscope is self
+        self.allocs += 1
+        typ = safe_realtype_shallow(expr.ctype())
+        assert typ.is_raii or isinstance(typ, TArray)
+        def read():
+            if isinstance(typ, TArray):
+                unconst_typ = safe_realtype_unconst(typ)
+                if deep_const(unconst_typ):
+                    return f'DML_STATIC_ARRAY_CONSTSAFE({expr.read()})'
+                else:
+                    return (f'DML_STATIC_ARRAY({unconst_typ.declaration("")}, '
+                            + f'{expr.read()})')
+
+            return expr.read()
+        return read
+
+
+    @property
+    def scope_stack(self):
+        return [self]
+
+class SessionRAIIScope(TopRAIIScope):
+    def reserve_orphan_adoption(self, site, expr, subscope):
+        assert subscope is self
+        ix = self.allocs
+        self.allocs += 1
+        typ = safe_realtype_shallow(expr.ctype())
+        if typ.is_raii:
+            from .codegen import get_raii_type_info
+            destructor = get_raii_type_info(typ).cident_destructor
+        else:
+            assert isinstance(typ, TArray)
+            destructor = 'NULL'
+        array_prefix = 'ARRAY_'*isinstance(typ, TArray)
+        def read():
+            return (f'DML_RAII_SESSION_{array_prefix}ORPHAN('
+                    + f'_dev->_orphan_allocs[{ix}], {destructor}, '
+                    + f'{expr.read()})')
+
+        return read
+
+    @property
+    def scope_stack(self):
+        return [self]
+
+class RAIIScopeDeclarations(Statement):
+    @auto_init
+    def __init__(self, site, methodscope):
+        assert isinstance(methodscope, MethodRAIIScope)
+
+    @property
+    def is_declaration(self):
+        return bool(self.methodscope.used_scopes is None
+                    or self.methodscope.used_scopes)
+
+    @property
+    def is_empty(self):
+        return bool(self.methodscope.used_scopes is not None
+                    and not self.methodscope.used_scopes)
+
+    def toc_stmt(self):
+        raise ICE(self.site, 'RAIIScopeDeclarations.toc_stmt: nonsensical')
+
+    def toc(self):
+        for (scope, c_uniq) in self.methodscope.used_scopes.items():
+            self.linemark()
+            out(f'_scope_allocation_t _scope_{c_uniq}_allocs'
+                + f'[{scope.allocs}] UNUSED;\n')
+        if self.methodscope.used_scopes:
+            self.linemark()
+            out('uint16 _scope_allocs_lens'
+                + f'[{len(self.methodscope.used_scopes)}] UNUSED = {{0}};\n')
+
+mkRAIIScopeDeclarations = RAIIScopeDeclarations
+
+class RAIIScopeBindLVals(Statement):
+    slots = ('toc_bind_lvals',)
+    @auto_init
+    def __init__(self, site, scope, lvals):
+        self.toc_bind_lvals = TopRAIIScope.active.reserve_bind_lvals(
+            site, lvals, scope)
+
+    def toc_stmt(self):
+        out('{\n', postindent = 1)
+        self.toc_inline()
+        out('}\n', preindent = -1)
+
+    def toc(self):
+        self.toc_bind_lvals()
+
+    @property
+    def is_empty(self):
+        return not self.lvals
+
+mkRAIIScopeBindLVals = RAIIScopeBindLVals
+
+class RAIIScopeClears(Statement):
+    @auto_init
+    def __init__(self, site, scopes): pass
+
+    def toc_stmt(self):
+        out('{\n', postindent = 1)
+        self.toc_inline()
+        out('}\n', preindent = -1)
+
+    def toc(self):
+        for scope in self.scopes:
+            if scope.used:
+                scope.clear_scope(self.site)
+
+    @property
+    def is_empty(self):
+        return all(scope.completed and not scope.used
+                   for scope in self.scopes)
+
+def mkRAIIScopeClears(site, scopes):
+    return RAIIScopeClears(site, [scope for scope in scopes
+                                  if scope.used or not scope.completed])
+
+class StringAppend(Statement):
+    @auto_init
+    def __init__(self, site, tgt, src):
+        assert not src.orphan
+        assert tgt.c_lval
+
+    def toc_stmt(self):
+        self.linemark()
+        out(f'_dml_string_cat(&({self.tgt.read()}), {self.src.read()});\n')
+
+class StringCAppend(Statement):
+    @auto_init
+    def __init__(self, site, tgt, src):
+        assert not src.orphan
+        assert tgt.c_lval
+
+    def toc_stmt(self):
+        self.linemark()
+        out(f'_dml_string_addstr(&({self.tgt.read()}), {self.src.read()});\n')
+
+def mkStringAppend(site, tgt, src):
+    if not tgt.writable:
+        raise ERVAL(tgt, '+=')
+    elif deep_const(tgt.ctype()):
+        raise ENCONST(site)
+    if isinstance(src, FromCString):
+        return StringCAppend(site, tgt, src.expr)
+    return StringAppend(site, tgt, mkAdoptedOrphan(site, src))
+
+class VectorAppend(Statement):
+    slots = ('basetyp_info',)
+    @auto_init
+    def __init__(self, site, basetyp, tgt, src):
+        assert not src.orphan
+        assert tgt.c_lval
+        from .codegen import get_raii_type_info
+        self.basetyp_info = (get_raii_type_info(basetyp)
+                             if basetyp.is_raii else None)
+
+    def toc_stmt(self):
+        self.linemark()
+        if self.basetyp_info:
+            out('_dml_vect_append_raii(sizeof(%s), %s, &(%s), %s)'
+                % (self.basetyp.declaration(''),
+                   self.basetyp_info.cident_copier, self.tgt.read(),
+                   self.src.read()))
+        else:
+            out('_dml_vect_append(sizeof(%s), &(%s), %s);\n'
+                % (self.basetyp.declaration(''), self.tgt.read(),
+                   self.src.read()))
+
+class VectorCompoundInitAppend(Statement):
+    @auto_init
+    def __init__(self, site, basetype, tgt, src, length):
+        assert tgt.c_lval
+
+    def toc_stmt(self):
+        self.linemark()
+        # Limit the lifetime of the compound literal
+        out('{ _dml_vect_append_array(sizeof(%s), &(%s), %s, %dULL); }\n'
+            % (self.basetype.declaration(''), self.tgt.read(), self.src.read(),
+               self.length))
+
+def mkVectorAppend(site, tgt, src):
+    basetype = safe_realtype_shallow(tgt.ctype()).base
+    if not tgt.writable:
+        raise ERVAL(tgt, '+=')
+    elif deep_const(tgt.ctype()):
+        raise ENCONST(site)
+
+    if isinstance(src, VectorCompoundLiteral):
+        if len(src.inits) == 0:
+            return mkNull(site)
+        elif len(src.inits) == 1:
+            return VectorPushApply(site, 'back', tgt,
+                                   src.inits[0].as_expr(basetype))
+        else:
+            return VectorCompoundInitAppend(site, basetype, tgt,
+                                            src.as_array_literal(),
+                                            len(src.inits))
+
+    return VectorAppend(site, basetype, tgt, mkAdoptedOrphan(site, src))
+
+def str_expr_pseudomethod(expr, sub):
+    expr = (f'({expr})'
+            if expr.priority < StructMember.priority
+            else str(expr))
+    return f'{expr}.{sub}'
+
+class VectorCBufRef(PseudoMethodRef):
+    @auto_init
+    def __init__(self, site, expr):
+        pass
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, 'c_buf')
+    def apply(self, inits, location, scope):
+        expr = self.expr
+        if inits:
+            raise EARG(self.site, '.c_buf')
+        # No deep_const is intentional, as it's only shallow const that results
+        # in invalid generated C.
+        # TODO(RAII): Though perhaps deep_const should be used anyway...
+        if safe_realtype_shallow(expr.ctype()).const:
+            raise ECONST(self.site)
+        if not expr.writable:
+            raise ERVAL(self.expr.site, '.c_buf()')
+
+        base_const = deep_const(expr.ctype())
+
+        return VectorCBufApply(self.site, expr, base_const)
+
+class VectorCBufApply(Expression):
+    priority = dml.expr.Apply.priority
+
+    slots = ('basetype',)
+
+    @auto_init
+    def __init__(self, site, expr, base_const):
+        typ = safe_realtype_shallow(expr.ctype())
+        assert not expr.orphan and expr.c_lval and not typ.const
+        self.basetype = conv_const(base_const, typ.base)
+
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, 'c_buf()')
+
+    def read(self):
+        return (f'DML_VECT_ELEMENTS({self.basetype.declaration("")}, '
+                + f'{self.expr.read()})')
+
+    def ctype(self):
+        return TPtr(self.basetype)
+
+mkVectorCBuf = VectorCBufRef
+
+class VectorPopRef(PseudoMethodRef):
+    @auto_init
+    def __init__(self, site, direction, expr):
+        pass
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, f'pop_{self.direction}')
+    def apply(self, inits, location, scope):
+        if inits:
+            raise EARG(self.site, f'.pop_{self.direction}')
+        if not self.expr.writable:
+            raise ERVAL(self.site, f'.pop_{self.direction}()')
+        if deep_const(self.expr.ctype()):
+            raise ECONST(self.site)
+        # Writable orphans are not impossible in principle
+        expr = mkAdoptedOrphan(self.site, self.expr)
+        return VectorPopApply(self.site, self.direction, expr)
+
+class VectorPopApply(Orphan):
+    priority = StructMember.priority
+
+    slots = ('basetype',)
+
+    @auto_init
+    def __init__(self, site, direction, expr):
+        assert not expr.orphan
+        typ = safe_realtype_shallow(expr.ctype())
+        self.basetype = typ.base
+
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, f'pop_{self.direction}()')
+
+    def read(self):
+        t = self.basetype.declaration('')
+        return (f'DML_VECT_POP_{self.direction.upper()}({t}, '
+                + f'{self.expr.read()})')
+
+    def ctype(self):
+        return self.basetype
+
+def mkVectorPopBack(site, expr):
+    return VectorPopRef(site, 'back', expr)
+
+def mkVectorPopFront(site, expr):
+    return VectorPopRef(site, 'front', expr)
+
+class VectorPushRef(PseudoMethodRef):
+    @auto_init
+    def __init__(self, site, direction, expr):
+        pass
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, f'push_{self.direction}')
+    def apply(self, inits, location, scope):
+        if not self.expr.writable:
+            raise ERVAL(self.site, f'.push_{self.direction}(...)')
+        if deep_const(self.expr.ctype()):
+            raise ECONST(self.site)
+        basetype = safe_realtype_shallow(self.expr.ctype()).base
+        [item] = typecheck_inarg_inits(
+            self.site, inits, [('item', basetype)], location, scope,
+            f'push_{self.direction}')
+        # Writable orphans are not impossible in principle
+        expr = mkAdoptedOrphan(self.site, self.expr)
+        return VectorPushApply(self.site, self.direction, expr, item)
+
+class VectorPushApply(Expression):
+    priority = AssignOp.priority
+
+    type = void
+
+    slots = ('basetype',)
+
+    @auto_init
+    def __init__(self, site, direction, expr, item):
+        assert not expr.orphan
+        typ = safe_realtype_shallow(expr.ctype())
+        self.basetype = typ.base
+
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr,
+                                     f'push_{self.direction}({self.item})')
+
+    def read(self):
+        t = self.basetype.declaration('')
+        return ('DML_SAFE_ASSIGN('
+                + f'DML_VECT_NEW_ELEM_AT_{self.direction.upper()}({t}, '
+                + f'{self.expr.read()}), {self.item.read()})')
+
+def mkVectorPushBack(site, expr):
+    return VectorPushRef(site, 'back', expr)
+
+def mkVectorPushFront(site, expr):
+    return VectorPushRef(site, 'front', expr)
+
+class VectorInsertRef(PseudoMethodRef):
+    @auto_init
+    def __init__(self, site, expr):
+        pass
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, 'insert')
+    def apply(self, inits, location, scope):
+        if not self.expr.writable:
+            raise ERVAL(site, '.insert(...)')
+        if deep_const(self.expr.ctype()):
+            raise ECONST(self.site)
+        basetype = safe_realtype_shallow(self.expr.ctype()).base
+        [index, item] = typecheck_inarg_inits(
+            self.site, inits, [('index', TInt(32, False)),
+                               ('item', basetype)],
+            location, scope, 'insert')
+        if index.constant and index.value < 0:
+            raise EOOB(index)
+        # Writable orphans are not impossible in principle
+        expr = mkAdoptedOrphan(self.expr.site, self.expr)
+        return VectorInsertApply(self.site, basetype, expr, index, item)
+
+mkVectorInsert = VectorInsertRef
+
+class VectorInsertApply(Expression):
+    priority = AssignOp.priority
+
+    type = void
+
+    @auto_init
+    def __init__(self, site, basetype, expr, index, item):
+        assert not expr.orphan
+
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr,
+                                     f'insert({self.index}, {self.item})')
+    def read(self):
+        t = self.basetype.declaration('')
+        return (f'DML_SAFE_ASSIGN(DML_VECT_NEW_ELEM_AT({t}, '
+                + f'{self.expr.read()}, {self.index.read()}), '
+                + f'{self.item.read()})')
+
+class VectorRemoveRef(PseudoMethodRef):
+    @auto_init
+    def __init__(self, site, expr):
+        pass
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr, 'remove')
+    def apply(self, inits, location, scope):
+        if not self.expr.writable:
+            raise ERVAL(self.site, '.remove(...)')
+        if deep_const(self.expr.ctype()):
+            raise ECONST(self.site)
+        basetype = safe_realtype_shallow(self.expr.ctype()).base
+        [index] = typecheck_inarg_inits(
+            self.site, inits, [('index', TInt(32, False))],
+            location, scope, 'remove')
+        if index.constant and index.value < 0:
+            raise EOOB(index)
+        # Writable orphans are not impossible in principle
+        expr = mkAdoptedOrphan(self.expr.site, self.expr)
+        return VectorRemoveApply(self.site, basetype, expr, index)
+
+mkVectorRemove = VectorRemoveRef
+
+class VectorRemoveApply(Orphan):
+    priority = dml.expr.Apply.priority
+
+    @auto_init
+    def __init__(self, site, basetype, expr, index):
+        assert not expr.orphan
+
+    def __str__(self):
+        return str_expr_pseudomethod(self.expr,
+                                     f'remove({self.index})')
+    def read(self):
+        t = self.basetype.declaration('')
+        return (f'DML_VECT_REMOVE({t}, {self.vect.read()}, '
+                + f'{self.index.read()})')
+
+    def ctype(self):
+        return self.basetype
+
+class VectorAdd(Orphan):
+    priority = dml.expr.Apply.priority
+
+    slots = ('base_type', 'info')
+
+    # Implemented by INHERITING ownership of first argument, but only BORROWING
+    # second argument. Meaning the second argument is never duped, and, if
+    # orphan, must be adopted.
+    def __init__(self, site, lh, rh):
+        self.site = site
+        self.lh = lh
+        self.rh = mkAdoptedOrphan(rh.site, rh)
+         # TODO(RAII) imports...
+        from .codegen import get_raii_type_info, VectorRAIITypeInfo
+        self.base_type = safe_realtype_unconst(lh.ctype()).base
+        self.info = get_raii_type_info(self.ctype())
+        assert isinstance(self.info, VectorRAIITypeInfo)
+
+    def __str__(self):
+        lh = str(self.lh)
+        rh = str(self.rh)
+        if self.lh.priority <= Add.priority:
+            lh = '('+lh+')'
+        if self.rh.priority <= Add.priority:
+            rh = '('+rh+')'
+        return lh + ' + ' + rh
+
+    def ctype(self):
+        return TVector(self.base_type)
+
+    def read(self):
+        lh = self.lh.read()
+        if not self.lh.orphan:
+            lh = self.info.read_dupe(lh)
+        sizeof = f'sizeof({self.base_type.declaration("")})'
+        if self.info.base_info is None:
+            return f'_dml_vect_add({sizeof}, {lh}, {self.rh.read()})'
+        else:
+            copier = self.info.base_info.cident_copier
+            return (f'_dml_vect_add_raii({sizeof}, {copier}, {lh}, '
+                    + f'{self.rh.read()})')
+
 def possible_side_effect(init):
     """Return True if this expression might have some side effect
     This means that the expression has to be evaluated exactly once."""
@@ -4967,7 +6416,7 @@ def possible_side_effect(init):
         return False
     return True
 
-def sym_declaration(sym, unused=False):
+def sym_declaration(sym, unused=False, unscoped_raii=False):
     assert not isinstance(sym, symtab.StaticSymbol)
     refcount = sym.refcount()
     if not sym.stmt and refcount == 0 and not possible_side_effect(sym.init):
@@ -4980,7 +6429,7 @@ def sym_declaration(sym, unused=False):
     unused = unused or (refcount == 0) or sym.value.startswith("__")
 
     return mkDeclaration(sym.site, sym.value, sym.type,
-                         sym.init, unused)
+                         sym.init, unused, unscoped_raii)
 
 ###
 
@@ -5049,5 +6498,6 @@ def log_statement(site, node, indices, logtype, level, groups, fmt, *args):
                 groups,
                 mkStringConstant(site, fmt) ] +
               list(args),
-              fun.ctype())
+              fun.ctype(),
+              True)
     return mkExpressionStatement(site, x)
diff --git a/py/dml/ctree_test.py b/py/dml/ctree_test.py
index f5200a979..39d09b4ed 100644
--- a/py/dml/ctree_test.py
+++ b/py/dml/ctree_test.py
@@ -40,13 +40,13 @@ def test(self):
 class DummyStatement(ctree.Statement):
     def __repr__(self):
         return 'D'
-    def toc(self): pass
+    def toc_stmt(self): pass
 
 class DummyDecl(ctree.Statement):
     is_declaration = 1
     def __repr__(self):
         return 'decl'
-    def toc(self): pass
+    def toc_stmt(self): pass
 
 class Test_mkcompound(unittest.TestCase):
     def test(self):
diff --git a/py/dml/dmlparse.py b/py/dml/dmlparse.py
index 8ce2a9643..34b5bf25b 100644
--- a/py/dml/dmlparse.py
+++ b/py/dml/dmlparse.py
@@ -826,12 +826,23 @@ def constant(t):
 @prod_dml12
 def extern(t):
     'toplevel : EXTERN cdecl_or_ident SEMI'
-    t[0] = ast.extern(site(t), t[2])
+    t[0] = ast.extern(site(t), t[2], None)
 
 @prod_dml14
 def extern(t):
-    'toplevel : EXTERN cdecl SEMI'
-    t[0] = ast.extern(site(t), t[2])
+    'toplevel : EXTERN cdecl maybe_extern_as SEMI'
+    t[0] = ast.extern(site(t), t[2], t[3])
+
+
+@prod_dml14
+def maybe_extern_as_no(t):
+    'maybe_extern_as : '
+    t[0] = None
+
+@prod_dml14
+def maybe_extern_as_yes(t):
+    'maybe_extern_as : AS ident'
+    t[0] = t[2]
 
 @prod
 def typedef(t):
@@ -1269,6 +1280,14 @@ def basetype_hook(t):
     cdecl_list_enforce_unnamed(t[3])
     t[0] = ('hook', t[3])
 
+@prod_dml14
+def basetype_vect(t):
+    '''basetype : VECT LPAREN cdecl RPAREN'''
+    (_, site, name, typ) = t[3]
+    if name:
+        report(ESYNTAX(site, name, None))
+    t[0] = ('vect', (site, typ))
+
 @prod
 def cdecl2(t):
     'cdecl2 : cdecl3'
@@ -1285,12 +1304,13 @@ def cdecl2_ptr(t):
     t[0] = ['pointer'] + t[2]
 
 @prod_dml14
-def cdecl2_vect(t):
+def cdecl2_legacy_vect(t):
     'cdecl2 : VECT cdecl2'
     # vect is actually experimental in 1.2 as well, but we will probably
     # defensively keep it the way it is, because it's used a lot.
     # TODO: improve how vect works in 1.4, and make it public (US325)
-    report(WEXPERIMENTAL(site(t), 'vect types'))
+    report(WDEPRECATED(site(t),
+                       "vect.h vector type. Use 'vect(BASETYPE)' instead."))
     t[0] = ['vect'] + t[2]
 
 @prod_dml12
@@ -1586,25 +1606,34 @@ def expression_assign(t):
     'expression : expression EQUALS expression'
     t[0] = ast.set(site(t, 2), t[1], t[3])
 
-@prod
-def assignop(t):
-    '''assignop : expression PLUSEQUAL expression
-                | expression MINUSEQUAL expression
-                | expression TIMESEQUAL expression
-                | expression DIVEQUAL expression
-                | expression MODEQUAL expression
-                | expression BOREQUAL expression
-                | expression BANDEQUAL expression
-                | expression BXOREQUAL expression
-                | expression LSHIFTEQUAL expression
-                | expression RSHIFTEQUAL expression'''
-    t[0] = ast.assignop(site(t, 2), t[1], t[2], t[3])
-
 @prod_dml12
 def expression_assignop(t):
-    '''expression : assignop'''
-    (tgt, op, src) = t[1].args
-    t[0] = ast.set(t[1].site, tgt, ast.binop(t[1].site, tgt, op[:-1], src))
+    '''expression : expression PLUSEQUAL   expression
+                  | expression MINUSEQUAL  expression
+                  | expression TIMESEQUAL  expression
+                  | expression DIVEQUAL    expression
+                  | expression MODEQUAL    expression
+                  | expression BOREQUAL    expression
+                  | expression BANDEQUAL   expression
+                  | expression BXOREQUAL   expression
+                  | expression LSHIFTEQUAL expression
+                  | expression RSHIFTEQUAL expression'''
+    t[0] = ast.set(site(t, 2), t[1],
+                   ast.binop(t[1].site, t[1], t[2][:-1], t[3]))
+
+@prod_dml14
+def assignop(t):
+    '''assignop : expression PLUSEQUAL   single_initializer
+                | expression MINUSEQUAL  single_initializer
+                | expression TIMESEQUAL  single_initializer
+                | expression DIVEQUAL    single_initializer
+                | expression MODEQUAL    single_initializer
+                | expression BOREQUAL    single_initializer
+                | expression BANDEQUAL   single_initializer
+                | expression BXOREQUAL   single_initializer
+                | expression LSHIFTEQUAL single_initializer
+                | expression RSHIFTEQUAL single_initializer'''
+    t[0] = ast.assignop(site(t, 2), t[1], t[2], t[3])
 
 @prod
 def expression_conditional(t):
@@ -1640,10 +1669,21 @@ def expression_binary_operator(t):
 
 # TODO: proper C-cast
 
-@prod
+@prod_dml12
+def expression_cast(t):
+    'expression : CAST LPAREN expression COMMA cdecl RPAREN'
+    (_, psite, name, typ) = t[5]
+    if name:
+        report(ESYNTAX(psite, name, ''))
+    t[0] = ast.cast(site(t), ast.initializer_scalar(site(t, 3), t[3]), typ)
+
+@prod_dml14
 def expression_cast(t):
-    'expression : CAST LPAREN expression COMMA ctypedecl RPAREN'
-    t[0] = ast.cast(site(t), t[3], t[5])
+    'expression : CAST LPAREN single_initializer COMMA cdecl RPAREN'
+    (_, psite, name, typ) = t[5]
+    if name:
+        report(ESYNTAX(psite, name, ''))
+    t[0] = ast.cast(site(t), t[3], typ)
 
 # TODO: proper C-sizeof
 
@@ -2130,37 +2170,32 @@ def statement_switch(t):
 
 @prod_dml14
 def statement_switch(t):
-    'statement_except_hashif : SWITCH LPAREN expression RPAREN LBRACE stmt_or_case_list RBRACE'
+    'statement_except_hashif : SWITCH LPAREN expression RPAREN LBRACE case_blocks_list RBRACE'
     stmts = t[6]
     t[0] = ast.switch(site(t), t[3], ast.compound(site(t, 5), stmts))
 
 @prod_dml14
-def stmt_or_case(t):
-    '''stmt_or_case : statement_except_hashif
-                    | cond_case_statement
-                    | case_statement'''
-    t[0] = t[1]
-
-@prod_dml14
-def switch_hashif(t):
-    'cond_case_statement : HASHIF LPAREN expression RPAREN LBRACE stmt_or_case_list RBRACE %prec LOWEST_PREC'
-    t[0] = ast.hashif(site(t), t[3], ast.compound(site(t, 5), t[6]), None)
+def case_blocks_list_empty(t):
+    'case_blocks_list : '
+    t[0] = []
 
 @prod_dml14
-def switch_hashifelse(t):
-    'cond_case_statement : HASHIF LPAREN expression RPAREN LBRACE stmt_or_case_list RBRACE HASHELSE LBRACE stmt_or_case_list RBRACE'
-    t[0] = ast.hashif(site(t), t[3], ast.compound(site(t, 5), t[6]),
-                      ast.compound(site(t, 9), t[10]))
+def case_blocks_list_case(t):
+    'case_blocks_list : case_statement statement_except_hashif_list case_blocks_list'
+    t[0] = ([t[1]] + ([ast.compound(site(t, 2), t[2])] if t[2] else [])) + t[3]
 
 @prod_dml14
-def stmt_or_case_list_empty(t):
-    'stmt_or_case_list : '
-    t[0] = []
+def case_blocks_list_hashif(t):
+    'case_blocks_list : HASHIF LPAREN expression RPAREN LBRACE case_blocks_list RBRACE case_blocks_list %prec LOWEST_PREC'
+    stmt = ast.hashif(site(t), t[3], ast.compound(site(t, 5), t[6]), None)
+    t[0] = [stmt] + t[8]
 
 @prod_dml14
-def stmt_or_case_list_stmt(t):
-    'stmt_or_case_list : stmt_or_case_list stmt_or_case'
-    t[0] = t[1] + [t[2]]
+def case_blocks_list_hashifelse(t):
+    'case_blocks_list : HASHIF LPAREN expression RPAREN LBRACE case_blocks_list RBRACE HASHELSE LBRACE case_blocks_list RBRACE case_blocks_list'
+    stmt = ast.hashif(site(t), t[3], ast.compound(site(t, 5), t[6]),
+                      ast.compound(site(t, 9), t[10]))
+    t[0] = [stmt] + t[12]
 
 # Delete is an expression in C++, not a statement, but we don't care.
 @prod
@@ -2482,6 +2517,16 @@ def statement_list_2(t):
     'statement_list : statement_list statement'
     t[0] = t[1] + [t[2]]
 
+@prod_dml14
+def statement_except_hashif_list_1(t):
+    'statement_except_hashif_list : '
+    t[0] = []
+
+@prod_dml14
+def statement_except_hashif_list_2(t):
+    'statement_except_hashif_list : statement_except_hashif_list statement_except_hashif'
+    t[0] = t[1] + [t[2]]
+
 # local
 
 @prod_dml12
diff --git a/py/dml/expr.py b/py/dml/expr.py
index 4f0ed704a..43e1bde62 100644
--- a/py/dml/expr.py
+++ b/py/dml/expr.py
@@ -18,6 +18,7 @@
     'NonValueArrayRef',
     'mkLit', 'Lit',
     'mkApply', 'mkApplyInits', 'Apply',
+    'Orphan', 'OrphanWrap',
     'mkNullConstant', 'NullConstant',
     'StaticIndex',
     'typecheck_inargs',
@@ -173,11 +174,29 @@ class Expression(Code):
     # bitslicing.
     explicit_type = False
 
-    # Can the expression be assigned to?
+    # An expression is considered orphan if it evaluates to a value of an
+    # object that is never accessed again past the particular evaluation of the
+    # expression. This is typically known by virtue of the C representation of
+    # the expression not being an lvalue; for example, the return value of a
+    # function call can never be accessed save by the function call itself.
+    # Orphanhood is only important for the RAII architecture. This means that
+    # there are some expressions that *could* be considered orphans, and yet
+    # are not, as they cannot be of RAII type and so orphanhood status is
+    # irrelevant. Integer literals are an example of this.
+    orphan = False
+
+    # Can the expression be assigned to in DML?
     # If writable is True, there is a method write() which returns a C
     # expression to make the assignment.
     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?
+    c_lval = False
+
     def __init__(self, site):
         assert not site or isinstance(site, Site)
         self.site = site
@@ -193,7 +212,14 @@ def read(self):
 
     # Produce a C expression but don't worry about the value.
     def discard(self):
-        return self.read()
+        if self.orphan and self.ctype().is_raii:
+            from .codegen import get_raii_type_info
+            # TODO(RAII) oh i dislike this. I'd rather discard() produced a statement
+            return get_raii_type_info(self.ctype()).read_destroy(self.read())
+        elif self.constant:
+            return '(void)0'
+        else:
+            return self.read()
 
     def ctype(self):
         '''The corresponding DML type of this expression'''
@@ -203,6 +229,15 @@ 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 writing to this expression will
+           write to stack-allocated data.
+           This may only be called if the expression is writable or
+           addressable'''
+        assert self.writable or self.addressable
+        return False
+
     @property
     def is_pointer_to_stack_allocation(self):
         '''Returns True only if it's known that the expression is a pointer
@@ -220,6 +255,11 @@ def copy(self, site):
         return type(self)(
             site, *(getattr(self, name) for name in self.init_args[2:]))
 
+    def write(self, source):
+        assert self.c_lval
+        return source.assign_to(self.read(), self.ctype())
+
+
 class NonValue(Expression):
     '''An expression that is not really a value, but which may validly
     appear as a subexpression of certain expressions.
@@ -268,10 +308,13 @@ def read(self):
         return self.cexpr
     def write(self, source):
         assert self.writable
-        return "%s = %s" % (self.cexpr, source.read())
+        return source.assign_to(self.cexpr, self.type)
     @property
     def writable(self):
         return self.type is not None
+    @property
+    def c_lval(self):
+        return self.type is not None
 
 mkLit = Lit
 
@@ -292,6 +335,32 @@ def copy(self, site):
 
 mkNullConstant = NullConstant
 
+class Orphan(Expression):
+    """Expressions that evaluate to a value that is allocated on the stack, but
+    not belonging to any local variable. Archetypical example are function
+    applications."""
+    orphan = True
+
+class OrphanWrap(Orphan):
+    @auto_init
+    def __init__(self, site, expr): pass
+
+    def ctype(self):
+        return self.expr.ctype()
+
+    @property
+    def c_lval(self):
+        return self.expr.c_lval
+
+    def __str__(self):
+        return str(self.expr)
+
+    def read(self):
+        return self.expr.read()
+
+    def discard(self):
+        return self.expr.discard()
+
 def typecheck_inargs(site, args, inp, kind="function", known_arglen=None):
     arglen = len(args) if known_arglen is None else known_arglen
     if arglen != len(inp):
@@ -399,16 +468,22 @@ def typecheck_inarg_inits(site, inits, inp, location, scope,
             if init.kind != 'initializer_scalar':
                 raise ESYNTAX(init.site, '{',
                               'variadic arguments must be simple expressions')
-            args.append(coerce_if_eint(codegen_expression(init.args[0],
-                                                          location, scope)))
+            arg = codegen_expression(init.args[0], location, scope)
+            if arg.ctype().is_raii:
+                is_string = isinstance(safe_realtype_shallow(arg.ctype()),
+                                       TString)
+                raise ERAIIVARARG(arg.site, is_string)
+            args.append(coerce_if_eint(arg))
 
     return args
 
 class Apply(Expression):
+    # An Apply expression is always orphan except for the application of
+    # memoized methods.
     priority = 160
     explicit_type = True
     @auto_init
-    def __init__(self, site, fun, args, funtype):
+    def __init__(self, site, fun, args, funtype, orphan):
         pass
     def ctype(self):
         return self.funtype.output_type
@@ -419,7 +494,7 @@ def read(self):
         return (self.fun.read() +
                 '(' + ", ".join(e.read() for e in self.args) + ')')
 
-def mkApplyInits(site, fun, inits, location, scope):
+def mkApplyInits(site, fun, inits, location, scope, orphan=True):
     '''Apply a C function to initializers'''
     funtype = fun.ctype()
 
@@ -442,9 +517,9 @@ def mkApplyInits(site, fun, inits, location, scope):
         [(str(i + 1), t) for (i, t) in enumerate(funtype.input_types)],
         location, scope, 'function', funtype.varargs)
 
-    return Apply(site, fun, args, funtype)
+    return Apply(site, fun, args, funtype, orphan)
 
-def mkApply(site, fun, args):
+def mkApply(site, fun, args, orphan=True):
     '''Apply a C function'''
     funtype = fun.ctype()
 
@@ -485,7 +560,7 @@ def mkApply(site, fun, args):
     else:
         args = [coerce_if_eint(arg) for arg in args[:known_arglen]]
     args.extend(coerced_varargs)
-    return Apply(site, fun, args, funtype)
+    return Apply(site, fun, args, funtype, orphan)
 
 class StaticIndex(NonValue):
     """A reference to the index variable of a containing object array,
diff --git a/py/dml/g_backend.py b/py/dml/g_backend.py
index e1ce7b783..fb6e3cd40 100644
--- a/py/dml/g_backend.py
+++ b/py/dml/g_backend.py
@@ -48,7 +48,7 @@ def enc(expr):
         return (str(expr),)
 
     try:
-        with crep.DeviceInstanceContext():
+        with crep.DeviceInstanceContext(), ctree.StaticRAIIScope():
             expr = node.get_expr(tuple(
                 mkLit(node.site, dollar(node.site) + idxvar, types.TInt(32, False))
                 for idxvar in node.parent.idxvars()))
diff --git a/py/dml/globals.py b/py/dml/globals.py
index 8fd5592c1..3711934c3 100644
--- a/py/dml/globals.py
+++ b/py/dml/globals.py
@@ -58,6 +58,9 @@
 # types.TypeSequence -> codegen.TypeSequenceInfo
 type_sequence_infos = {}
 
+# TypeKey -> GeneratedRAIITypeInfo
+generated_raii_types = {}
+
 # 1.4 style integer operations in 1.2, --strict-dml12-int
 strict_int_flag = None
 def compat_dml12_int(site):
@@ -101,3 +104,5 @@ def compat_dml12_int(site):
 build_confidentiality = 0
 
 linemarks = False
+
+session_orphan_allocs = 0
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/messages.py b/py/dml/messages.py
index 22ad1c369..c8e660fdb 100644
--- a/py/dml/messages.py
+++ b/py/dml/messages.py
@@ -410,7 +410,8 @@ class EEXTERN(DMLError):
 
 class EEXPORT(DMLError):
     """Can only export non-inline, non-shared, non-throwing methods declared
-    outside object arrays."""
+    outside object arrays, that do not have any input parameter or return value
+    of resource-enriched (RAII) type."""
     fmt = "cannot export this method"
     version = "1.4"
 
@@ -1369,10 +1370,10 @@ class ECONSTP(DMLError):
 
 class ECONST(DMLError):
     """
-    The lvalue that is assigned to is declared as a `const` and
-    thus can't be assigned to.
+    Attempted modification (e.g. assignment) of a lvalue that is declared
+    as a `const` and thus can't be modified.
     """
-    fmt = "assignment to constant"
+    fmt = "modification of constant"
     def __init__(self, site):
         DMLError.__init__(self, site);
 
@@ -1585,6 +1586,24 @@ class EVLACONST(DMLError):
     fmt = ("variable length array declared with (partially) const-qualified "
            + "type")
 
+class EVLARAII(DMLError):
+    """
+    Variable length arrays may not have a resource-enriched (RAII) type as
+    base type. Use `vect` instead.
+    """
+    fmt = ("variable length array declared with resource-enriched (RAII) type:"
+           + " %s. Workaround: use a vector ('vect') instead, and resize it "
+           + "to the desired length.")
+
+class EANONRAIISTRUCT(DMLError):
+    """
+    Anonymous structs declared within methods may not have any member of
+    resource-enriched (RAII) type.
+    """
+    fmt = ("method-local anonymous struct declared that has a member of "
+           + "resource-enriched (RAII) type")
+
+
 class EIDENTSIZEOF(DMLError):
     """
     A variant of the EIDENT message exclusive to usages of `sizeof`: it is
@@ -1607,6 +1626,37 @@ class ELOGGROUPS(DMLError):
     fmt = ("Too many loggroup declarations. A maximum of 63 log groups (61 "
            + "excluding builtins) may be declared per device.")
 
+class ERAIIVARARG(DMLError):
+    """
+    Values of resource-enriched (RAII) type cannot be passed as variadic
+    arguments.
+
+    This error typically occurs as the result of trying to pass a value of
+    type `string` to a `printf`-like function, which is a mistake to begin
+    with: `.c_str()` should be used to convert the `string` to `char *`.
+    """
+    fmt = ("values of resource-enriched (RAII) type cannot be passed as "
+           + "variadic arguments%s")
+    def __init__(self, site, is_string):
+        hint = ("\nif you are trying to pass a string to a 'printf'-like "
+                + "function, use '.c_str()' to convert the string to 'char *'"
+                )*is_string
+
+        DMLError.__init__(self, site, hint)
+
+class ESIZEOFRAII(DMLError):
+    """
+    The 'sizeof'/'sizeoftype' operator cannot be used to retrieve the size of
+    a resource-enriched (RAII) type. This is to prevent primitive memory
+    manipulation with values of such types, as such manipulation is always
+    unsafe.
+    """
+    fmt = ("'sizeof'/'sizeoftype' cannot be used with resource-enriched "
+           + "(RAII) types.\n"
+           + "Don't attempt to work around this by using an integer literal "
+           + "instead: any primitive memory manipulation like memcpy is "
+           + "unsafe when involving values of resource-enriched types.")
+
 #
 # WARNINGS (keep these as few as possible)
 #
diff --git a/py/dml/serialize.py b/py/dml/serialize.py
index 17de64f02..397a580cf 100644
--- a/py/dml/serialize.py
+++ b/py/dml/serialize.py
@@ -90,7 +90,6 @@ def prepare_array_de_serialization(site, t):
     while isinstance(base, TArray):
         dims.append(base.size)
         base = base.base
-
     sizeof_base = expr.mkLit(site, f"sizeof({base.declaration('')})",
                              TNamed('size_t'))
     dimsizes_lit = ('(const uint32 []) { %s }'
@@ -113,10 +112,12 @@ def call_c_fun(site, fun, args):
 # to target_expr
 def serialize(real_type, current_expr, target_expr):
     current_site = current_expr.site
-    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,
+    def construct_assign_apply(funname):
+        return construct_assign_apply_on_expr(funname, current_expr)
+    def construct_assign_apply_on_expr(funname, custom_expr):
+        apply_expr = apply_c_fun(current_site, funname, [custom_expr],
+                                 attr_value_t)
+        return ctree.AssignStatement(current_site, target_expr,
                                        ctree.ExpressionInitializer(apply_expr))
     if real_type.is_int:
         if real_type.signed:
@@ -131,18 +132,25 @@ def construct_assign_apply(funname, intype):
                                                funname,
                                                function_type),
                                     [converted_arg],
-                                    function_type)
+                                    function_type,
+                                    True)
             return ctree.mkCompound(current_site,
-                                    [ctree.mkAssignStatement(
+                                    [ctree.AssignStatement(
                                         current_site, target_expr,
                                         ctree.ExpressionInitializer(
                                             apply_expr))])
         else:
-            return construct_assign_apply(funname, real_type)
+            return construct_assign_apply(funname)
     elif isinstance(real_type, TBool):
-        return construct_assign_apply("SIM_make_attr_boolean", real_type)
+        return construct_assign_apply("SIM_make_attr_boolean")
     elif isinstance(real_type, TFloat):
-        return construct_assign_apply("SIM_make_attr_floating", real_type)
+        return construct_assign_apply("SIM_make_attr_floating")
+    elif isinstance(real_type, TString):
+        c_string = apply_c_fun(current_site, "_dml_string_str", [current_expr],
+                               TPtr(TNamed('char')))
+        return construct_assign_apply_on_expr("SIM_make_attr_string",
+                                              c_string)
+
     elif isinstance(real_type, TArray):
         (base, dimsizes, sizeof_base,
          dimsizes_expr) = prepare_array_de_serialization(current_site,
@@ -164,15 +172,28 @@ def construct_assign_apply(funname, intype):
                                                          len(dimsizes)),
                                   elem_serializer],
                                  attr_value_t)
-        return ctree.mkAssignStatement(current_site, target_expr,
+        return ctree.AssignStatement(current_site, target_expr,
                                        ctree.ExpressionInitializer(apply_expr))
-
-    elif isinstance(real_type, (TStruct, TVector)):
+    elif isinstance(real_type, TVector):
+        base = real_type.base
+        sizeof_base = expr.mkLit(current_site,
+                                 f"sizeof({base.declaration('')})",
+                                 TNamed('size_t'))
+        elem_serializer = expr.mkLit(current_site, lookup_serialize(base),
+                                     TPtr(serializer_t))
+        apply_expr = apply_c_fun(current_site, '_serialize_vector',
+                                 [expr.OrphanWrap(current_site, current_expr),
+                                  sizeof_base,
+                                  elem_serializer],
+                                 attr_value_t)
+        return ctree.AssignStatement(current_site, target_expr,
+                                     ctree.ExpressionInitializer(apply_expr))
+    elif isinstance(real_type, TStruct):
         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))
+            [ctree.AddressOf(current_site, current_expr)], attr_value_t)
+        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,7 +201,7 @@ 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,
+        return ctree.AssignStatement(current_site, target_expr,
                                        ctree.ExpressionInitializer(apply_expr))
     elif isinstance(real_type, THook):
         id_infos = expr.mkLit(current_site,
@@ -189,7 +210,7 @@ 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,
+        return ctree.AssignStatement(current_site, target_expr,
                                        ctree.ExpressionInitializer(apply_expr))
     else:
         # Callers are responsible for checking that the type is serializeable,
@@ -202,11 +223,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 +245,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 +259,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,18 +275,38 @@ 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):
         return construct_assign_apply("floating", real_type)
+    elif isinstance(real_type, TString):
+        def mod_apply_expr(expr):
+            return apply_c_fun(expr.site, "_dml_string_new", [expr], TString())
+        return construct_assign_apply("string",
+                                      TPtr(TNamed('char', const=True)),
+                                      mod_apply_expr)
     elif isinstance(real_type, TArray):
         (base, dimsizes, sizeof_base,
          dimsizes_expr) = prepare_array_de_serialization(current_site,
                                                          real_type)
         elem_deserializer = expr.mkLit(current_site, lookup_deserialize(base),
                                        TPtr(deserializer_t))
+
+        if base.is_raii:
+            from .codegen import get_raii_type_info
+            raii_destructor = get_raii_type_info(base).cident_destructor
+        else:
+            raii_destructor = "NULL"
+        raii_destructor = expr.mkLit(current_site, raii_destructor,
+                                     TPtr(TFunction([TPtr(void)], void)))
+
         # elems_are_bytes informs if the final dimension may either be
         # deserialized as a list or a data attribute value.
         # This is true for all integer types of width 8 bits
@@ -275,9 +317,29 @@ def construct_subcall(apply_expr):
             [current_expr, addressof_target_unconst(),
              sizeof_base, dimsizes_expr,
              ctree.mkIntegerLiteral(current_site, len(dimsizes)),
-             elem_deserializer, elems_are_bytes], set_error_t)
+             elem_deserializer, raii_destructor, elems_are_bytes], set_error_t)
         return construct_subcall(apply_expr)
-    elif isinstance(real_type, (TStruct, TVector)):
+    elif isinstance(real_type, TVector):
+        base = real_type.base
+        sizeof_base = expr.mkLit(current_site,
+                                 f"sizeof({base.declaration('')})",
+                                 TNamed('size_t'))
+        elem_deserializer = expr.mkLit(current_site, lookup_deserialize(base),
+                                       TPtr(deserializer_t))
+        if base.is_raii:
+            from .codegen import get_raii_type_info
+            raii_destructor = get_raii_type_info(base).cident_destructor
+        else:
+            raii_destructor = "NULL"
+        raii_destructor = expr.mkLit(current_site, raii_destructor,
+                                     TPtr(TFunction([TPtr(void)], void)))
+
+        apply_expr = apply_c_fun(
+            current_site, '_deserialize_vector',
+            [current_expr, addressof_target_unconst(),
+             sizeof_base, elem_deserializer, raii_destructor], set_error_t)
+        return construct_subcall(apply_expr)
+    elif isinstance(real_type, TStruct):
         apply_expr = apply_c_fun(
             current_site, lookup_deserialize(real_type),
             [current_expr, addressof_target_unconst()],
@@ -345,6 +407,8 @@ def map_dmltype_to_attrtype(site, dmltype):
         return 'b'
     if isinstance(real_type, TFloat):
         return 'f'
+    if isinstance(real_type, TString):
+        return 's'
     if isinstance(real_type, TStruct):
         return '[%s]' % "".join([map_dmltype_to_attrtype(site, mt)
                                  for mt in real_type.members.values()])
@@ -357,9 +421,8 @@ def map_dmltype_to_attrtype(site, dmltype):
         return '[%s{%s}]' % (arr_attr_type, arr_length) + or_data
     if isinstance(real_type, (TTrait, THook)):
         return '[s[i*]]'
-    # TODO should be implemented
-    #if isinstance(real_type, TVector):
-        # return '[%s*]' % (map_dmltype_to_attrtype(site, real_type.base))
+    if isinstance(real_type, TVector):
+        return f'[{map_dmltype_to_attrtype(site, real_type.base)}*]'
     raise ICE(site, 'unserializable type: %r' % (dmltype,))
 
 def mark_for_serialization(site, dmltype):
@@ -375,13 +438,15 @@ def mark_for_serialization(site, dmltype):
         if not real_type.size.constant:
             raise messages.ESERIALIZE(site, dmltype)
         mark_for_serialization(site, real_type.base)
+    elif isinstance(real_type, TVector):
+        mark_for_serialization(site, real_type.base)
     elif isinstance(real_type, TTrait):
         dml.globals.serialized_traits.add(real_type.trait)
     elif isinstance(real_type, THook):
         real_type.validate(dmltype.declaration_site or site)
         from .codegen import get_type_sequence_info
         get_type_sequence_info(real_type.msg_types, create_new=True)
-    elif not isinstance(real_type, (IntegerType, TBool, TFloat)):
+    elif not isinstance(real_type, (IntegerType, TBool, TFloat, TString)):
         raise messages.ESERIALIZE(site, dmltype)
 
 # generate a part of the function name from a description of the dmltype
@@ -416,6 +481,8 @@ def type_signature(dmltype, is_for_serialization):
         return 'B'
     if isinstance(dmltype, TFloat):
         return {'double': 'Fd', 'float': 'Fs'}[dmltype.name]
+    if isinstance(dmltype, TString):
+        return 'STR'
     if isinstance(dmltype, TStruct):
         return 'S' + dmltype.label
     if isinstance(dmltype, TArray):
@@ -442,7 +509,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 +524,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),
@@ -493,10 +560,8 @@ def generate_serialize(real_type):
                         safe_realtype(typ))
                        for (name, typ) in real_type.members.items())
             serialize_sources_to_list(site, sources, out_arg)
-        elif isinstance(real_type, TVector):
-            raise ICE(site, "TODO: serialize vector")
         elif isinstance(real_type, (IntegerType, TBool, TFloat, TTrait,
-                                    TArray, THook)):
+                                    TArray, THook, TString, TVector)):
             serialize(real_type,
                       ctree.mkDereference(site, in_arg),
                       out_arg).toc()
@@ -517,7 +582,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 +600,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:
@@ -592,6 +657,7 @@ def generate_deserialize(real_type):
 
     func_code = output.StrOutput()
     with func_code:
+        cleanup_on_failure = []
         cleanup = []
         output.out(function_decl + " {\n", postindent = 1)
         out_arg_decl.toc()
@@ -605,13 +671,22 @@ def error_out(exc, msg):
             stmts.append(ctree.mkInline(site, 'goto _exit;'))
             return stmts
         if isinstance(real_type, TStruct):
+            from .codegen import get_raii_type_info
+            raii_info = (get_raii_type_info(real_type)
+                         if real_type.is_raii else None)
+            malloc = expr.mkLit(site,
+                           f'MM_{("Z" if raii_info is not None else "M")}ALLOC'
+                           + f'(1, {real_type.declaration("")})',
+                           TPtr(real_type))
             (tmp_out_decl, tmp_out_ref) = declare_variable(
-                site, "_tmp_out", TPtr(real_type),
-                ctree.mkNew(site, real_type))
-            cleanup_ref = (tmp_out_ref if not deep_const(real_type)
-                           else ctree.mkCast(site, tmp_out_ref, TPtr(void)))
-            cleanup.append(ctree.mkDelete(site, cleanup_ref))
+                site, "_tmp_out", TPtr(real_type), malloc)
             tmp_out_decl.toc()
+            cleanup_ref = '(void *)'*deep_const(real_type) + '_tmp_out'
+            if raii_info is not None:
+                cleanup_on_failure.append(ctree.mkInline(
+                    site, raii_info.read_destroy_lval('*_tmp_out') + ';'))
+
+            cleanup.append(ctree.mkInline(site, f'MM_FREE({cleanup_ref});'))
             targets = tuple((ctree.mkSubRef(site, tmp_out_ref, name, "->"),
                              conv_const(real_type.const, safe_realtype(typ)))
                             for (name, typ) in real_type.members.items())
@@ -620,23 +695,26 @@ 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.mkDereference(
-                                            site, tmp_out_ref))).toc()
-
-        elif isinstance(real_type, TVector):
-            raise ICE(site, "TODO: serialize vector")
-        elif isinstance(real_type, (IntegerType, TBool, TFloat, TTrait,
-                                    TArray, THook)):
+            dest = ctree.mkDereference(site, out_arg)
+            src = expr.OrphanWrap(site, ctree.mkDereference(site, tmp_out_ref))
+            ctree.AssignStatement(site, dest, ctree.ExpressionInitializer(src)
+                                  ).toc()
+
+        elif isinstance(real_type, (IntegerType, TBool, TFloat, TTrait, TArray,
+                                    THook, TString, TVector)):
             deserialize(real_type,
                         in_arg,
                         ctree.mkDereference(site, out_arg),
                         error_out).toc()
         else:
             assert False
+        if cleanup_on_failure:
+            output.out("if (false) {\n", postindent=1)
         output.out("_exit:\n")
+        for stmt in cleanup_on_failure:
+            stmt.toc()
+        if cleanup_on_failure:
+            output.out("}\n", preindent=-1)
         for stmt in cleanup:
             stmt.toc()
         output.out("return _success;\n")
diff --git a/py/dml/structure.py b/py/dml/structure.py
index 81364662d..94bed4c39 100644
--- a/py/dml/structure.py
+++ b/py/dml/structure.py
@@ -73,7 +73,11 @@ def mkglobals(stmts):
     by_name = {}
     assert not global_scope.symbols()
     for stmt in stmts:
-        if stmt.kind in ['extern', 'extern_typedef', 'dml_typedef']:
+        if stmt.kind == 'extern':
+            ((_, _, cname, _), name) = stmt.args
+            if name is None:
+                name = cname
+        elif stmt.kind in {'extern_typedef', 'dml_typedef'}:
             ((_, _, name, _),) = stmt.args
         else:
             name = stmt.args[0]
@@ -150,7 +154,9 @@ def mkglobals(stmts):
                     trait_body = None
                 templates[name] = (stmt.site, template_body, trait_body)
             elif stmt[0] == 'extern':
-                (_, esite, (_, site, name, typ)) = stmt
+                (_, esite, (_, site, cname, typ), dmlname) = stmt
+                if dmlname is None:
+                    dmlname = cname
                 if typ is None:
                     # guaranteed by grammar
                     assert dml.globals.dml_version == (1, 2)
@@ -164,8 +170,8 @@ def mkglobals(stmts):
                         allow_void=site.dml_version() == (1, 2))
                     # any substructs are converted to anonymous extern structs
                     assert not struct_defs
-                new_symbols.append(LiteralSymbol(name, typ, site))
-                externs.append((name, site, typ))
+                new_symbols.append(LiteralSymbol(dmlname, typ, site, cname))
+                externs.append((cname, site, typ))
             elif stmt[0] == 'extern_typedef':
                 (_, site, (_, _, name, typ)) = stmt
                 assert not typedefs.get(name, None)
@@ -240,7 +246,7 @@ def check_named_types(t):
         t.resolve()
         for (mn, mt) in t.members.items():
             check_named_types(mt)
-    elif isinstance(t, (TPtr, TVector, TArray)):
+    elif isinstance(t, (TPtr, TVectorLegacy, TVector, TArray)):
         check_named_types(t.base)
     elif isinstance(t, TFunction):
         for pt in t.input_types:
@@ -252,7 +258,7 @@ def check_named_types(t):
     elif isinstance(t, THook):
         for msg_t in t.msg_types:
             check_named_types(msg_t)
-    elif isinstance(t, (TVoid, IntegerType, TBool, TFloat, TTrait)):
+    elif isinstance(t, (TVoid, IntegerType, TBool, TFloat, TTrait, TString)):
         pass
     else:
         raise ICE(t.declaration_site, "unknown type %r" % t)
@@ -302,13 +308,13 @@ def type_deps(t, include_structs, expanded_typedefs):
         return deps
     elif isinstance(t, TArray):
         return type_deps(t.base, True, expanded_typedefs)
-    elif isinstance(t, (TPtr, TVector)):
+    elif isinstance(t, (TPtr, TVectorLegacy, TVector)):
         return type_deps(t.base, False, expanded_typedefs)
     elif isinstance(t, TFunction):
         return ([dep for pt in t.input_types
                  for dep in type_deps(pt, False, expanded_typedefs)]
                 + type_deps(t.output_type, False, expanded_typedefs))
-    elif isinstance(t, (IntegerType, TVoid, TBool, TFloat, TTrait)):
+    elif isinstance(t, (IntegerType, TVoid, TBool, TFloat, TTrait, TString)):
         return []
     elif isinstance(t, TExternStruct):
         # extern structs are assumed to be self-contained
@@ -626,6 +632,8 @@ def is_default(decl):
 
 def typecheck_method_override(m1, m2):
     '''check that m1 can override m2'''
+    # TODO(RAII) the usage of cmp instead of cmp_fuzzy may break old code,
+    # though I doubt it
     assert m1.kind == m2.kind == 'method'
     (_, (inp1, outp1, throws1, qualifiers1, _), _, _, _) \
         = m1.args
@@ -1864,6 +1872,7 @@ def mkobj2(obj, obj_specs, params, each_stmts):
             with ExitStack() as stack:
                 stack.enter_context(ErrorContext(param, None))
                 stack.enter_context(crep.DeviceInstanceContext())
+                stack.enter_context(ctree.StaticRAIIScope())
                 try:
                     try:
                         # Evaluate statically, because it triggers caching
@@ -1932,6 +1941,10 @@ def mkobj2(obj, obj_specs, params, each_stmts):
             if method.throws or len(method.outp) > 1:
                 report(EEXPORT(method.site, export.site))
                 continue
+            if any(t.is_raii
+                   for (_, t) in itertools.chain(method.inp, method.outp)):
+                report(EEXPORT(method.site, export.site))
+                continue
             func = method_instance(method)
             mark_method_referenced(func)
             mark_method_exported(func, name, export.site)
diff --git a/py/dml/traits.py b/py/dml/traits.py
index cb5695a45..265c386eb 100644
--- a/py/dml/traits.py
+++ b/py/dml/traits.py
@@ -783,10 +783,10 @@ def lookup(self, name, expr, site):
                     expr = TraitUpcast(site, expr, impl.vtable_trait)
                 return TraitMethodDirect(site, expr, impl)
         if name in self.vtable_methods:
-            (_, inp, outp, throws, independent, _, _) = \
+            (_, inp, outp, throws, independent, _, memoized) = \
                 self.vtable_methods[name]
             return TraitMethodIndirect(site, expr, name, inp, outp, throws,
-                                       independent)
+                                       independent, memoized)
         if name in self.vtable_params:
             (_, ptype) = self.vtable_params[name]
             return TraitParameter(site, expr, name, ptype)
diff --git a/py/dml/types.py b/py/dml/types.py
index 481b751e2..ab8e978bd 100644
--- a/py/dml/types.py
+++ b/py/dml/types.py
@@ -12,10 +12,14 @@
     'realtype',
     'safe_realtype_shallow',
     'safe_realtype',
+    'safe_realtype_unconst',
     'conv_const',
+    'shallow_const',
     'deep_const',
+    'TypeKey',
     'type_union',
     'compatible_types',
+    'compatible_types_fuzzy',
     'typedefs',
     'global_type_declaration_order',
     'global_anonymous_structs',
@@ -35,7 +39,7 @@
     'TFloat',
     'TArray',
     'TPtr',
-    'TVector',
+    'TVectorLegacy',
     'TTrait',
     'TTraitList',
     'StructType',
@@ -44,6 +48,8 @@
     'TLayout',
     'TFunction',
     'THook',
+    'TString',
+    'TVector',
     'cident',
     'void',
 )
@@ -51,6 +57,7 @@
 import sys
 import re
 from itertools import *
+from enum import Enum
 
 from .env import is_windows
 from .output import out
@@ -126,6 +133,10 @@ def realtype(t):
         t2 = realtype(t.base)
         if t2 != t:
             return TArray(t2, t.size, t.const)
+    elif isinstance(t, TVectorLegacy):
+        t2 = realtype(t.base)
+        if t2 != t:
+            return TVectorLegacy(t2, t.const)
     elif isinstance(t, TVector):
         t2 = realtype(t.base)
         if t2 != t:
@@ -160,13 +171,34 @@ def conv_const(const, t):
         t.const = True
     return t
 
+def safe_realtype_unconst(t0):
+    def sub(t):
+        if isinstance(t, (TArray, TVector, TVectorLegacy)):
+            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 shallow_const(t):
+    t = safe_realtype_shallow(t)
+    while not t.const and isinstance(t, (TArray, TVector, TVectorLegacy)):
+        t = safe_realtype_shallow(t.base)
+
+    return t.const
+
 def deep_const(origt):
     subtypes = [origt]
     while subtypes:
         st = safe_realtype_shallow(subtypes.pop())
         if st.const:
             return True
-        if isinstance(st, (TArray, TVector)):
+        if isinstance(st, (TArray, TVector, TVectorLegacy)):
             subtypes.append(st.base)
         elif isinstance(st, StructType):
             subtypes.extend(st.members.values())
@@ -192,7 +224,26 @@ def __eq__(self, other):
                         in zip(self.types, other.types)))
 
     def __hash__(self):
-        return hash(tuple(type(elem) for elem in self.types))
+        return hash(tuple(elem.hashed() for elem in self.types))
+
+class TypeKey:
+    '''A wrapper around a DML type with equality and hashing based around cmp.
+    Meant to be used as keys for dictionaries.
+    '''
+    def __init__(self, type, consider_quals=True):
+        self.type = type
+        self.consider_quals=consider_quals
+
+
+    def __eq__(self, other):
+        if not (isinstance(other, TypeKey)
+                and self.consider_quals == other.consider_quals):
+            return NotImplemented
+
+        return self.type.cmp(other.type, self.consider_quals) == 0
+
+    def __hash__(self):
+        return self.type.hashed(self.consider_quals)
 
 class DMLType(metaclass=abc.ABCMeta):
     '''The type of a value (expression or declaration) in DML. One DML
@@ -222,26 +273,60 @@ def sizeof(self):
         '''Return size, or None if not known'''
         return None
 
-    def cmp(self, other):
-        """Compare this type to another.
+    def hashed(self, consider_quals=True):
+        '''Hash the DML type in a way compatible with cmp. I.e.
+           a.cmp(b, consider_quals) == 0
+           implies a.hashed(consider_quals) == b.hashed(consider_quals)'''
+        return hash((type(self), self.const if consider_quals else None))
 
-        Return 0 if the types are equivalent,
-        Return NotImplemented otherwise.
+    # TODO(RAII) I think consider_quals is useless, but it's a tricky question.
+    # Existing usages of cmp, cmp_fuzzy, and cmp + safe_realtype_unconst should
+    # be reevaluated
+    def cmp(self, other, consider_quals=True):
+        """Strict type compatibility.
+
+        Return 0 if the types are run-time compatible,
+        Return NotImplemented otherwise
+
+        "Run-time compatibility" has two minimal criteria:
+        1. The C representation of the type MUST be compatible, in a C sense
+        (modulo const qualifiers when consider_quals is False.)
+        2. A value of one type can be treated by DMLC as though it were of the
+        other type without any additional risk of undefined behavior or invalid
+        generated C.
+        For example, all trait reference types share the same C representation,
+        and so satisfy (1), but trait reference types for different traits do
+        not share vtables; trying to use a vtable for one trait with an
+        incompatible reference would result in undefined behavior, and so do
+        not satisfy (2).
 
-        The exact meaning of this is somewhat fuzzy. The
-        method is used for three purposes:
+
+        The method is used for three purposes:
 
         1. in TPtr.canstore(), to judge whether pointer target types
         are compatible.
         2. in ctree, to compare how large values different numerical
         types can hold
-        3. when judging whether a method override is allowed, as an inaccurate
-        replacement of TPtr(self).canstore(TPtr(other))[0]
+        3. when judging whether a method override is allowed
 
         See bug 21900 for further discussions.
 
         """
-        return NotImplemented
+        return (0 if (isinstance(other, type(self))
+                      and (not consider_quals or self.const == other.const))
+                else NotImplemented)
+
+    def cmp_fuzzy(self, other, consider_quals=False):
+        """Compare this type to another.
+
+        Return 0 if the types are pretty much equivalent,
+        Return NotImplemented otherwise.
+
+        As implied, the exact meaning of this is fuzzy. It mostly relaxes
+        criteria (1) of 'cmp'); for example, TPtr(void).cmp(TPtr(TBool())) is
+        allowed to return 0, as is TPtr(TBool()).cmp(TArray(TBool())).
+        """
+        return self.cmp(other, consider_quals)
 
     def canstore(self, other):
         """Can a variable of this type store a value of another type.
@@ -254,7 +339,7 @@ def canstore(self, other):
         The correctness of the return value can not be trusted; see
         bug 21900 for further discussions.
         """
-        return (self.cmp(other) == 0, False, False)
+        return (self.cmp(other, False) == 0, False, False)
 
     @abc.abstractmethod
     def clone(self):
@@ -288,6 +373,21 @@ def resolve(self):
            return self"""
         return self
 
+    @property
+    def is_raii(self):
+        return False
+
+class DMLTypeRAII(DMLType):
+    __slots__ = ()
+    @property
+    def is_raii(self):
+        return True
+
+    def resolve(self):
+        from .codegen import get_raii_type_info
+        _ = get_raii_type_info(self)
+        return self
+
 class TVoid(DMLType):
     __slots__ = ()
     void = True
@@ -301,8 +401,6 @@ def declaration(self, var):
         return 'void ' + self.const_str + ' ' + var
     def clone(self):
         return TVoid()
-    def cmp(self, other):
-        return 0 if isinstance(realtype(other), TVoid) else NotImplemented
 
 class TUnknown(DMLType):
     '''A type unknown to DML. Typically used for a generic C macro
@@ -335,8 +433,6 @@ def describe(self):
         return 'pointer to %s' % self.name
     def key(self):
         return 'device'
-    def cmp(self, other):
-        return 0 if isinstance(realtype(other), TDevice) else NotImplemented
     def canstore(self, other):
         constviol = False
         if not self.const and other.const:
@@ -389,8 +485,10 @@ def describe(self):
         return self.c
     def key(self):
         raise ICE(self.declaration_site, 'need realtype before key')
-    def cmp(self, other):
+    def cmp(self, other, consider_quals=True):
         assert False, 'need realtype before cmp'
+    def hashed(self, consider_quals=True):
+        assert False, 'need realtype before hashed'
 
     def clone(self):
         return TNamed(self.c, self.const)
@@ -398,6 +496,10 @@ def clone(self):
     def declaration(self, var):
         return cident(self.c) + ' ' + self.const_str + var
 
+    @property
+    def is_raii(self):
+        return safe_realtype_shallow(self).is_raii
+
 class TBool(DMLType):
     __slots__ = ()
     def __init__(self):
@@ -409,10 +511,6 @@ def describe(self):
         return 'bool'
     def declaration(self, var):
         return 'bool ' + self.const_str + var
-    def cmp(self, other):
-        if isinstance(other, TBool):
-            return 0
-        return NotImplemented
 
     def canstore(self, other):
         constviol = False
@@ -467,9 +565,19 @@ def get_member_qualified(self, member):
             t = (conv_const(self.const, t[0]),) + t[1:]
         return t
 
-    def cmp(self, other):
+    def hashed(self, consider_quals=True):
+        byte_order = self.byte_order if self.is_endian else None
+        return hash((IntegerType, self.const if consider_quals else None,
+                     self.bits, self.signed, byte_order))
+
+    def cmp(self, other, consider_quals=True):
+        if consider_quals and (self.const != other.const):
+            return NotImplemented
         if not other.is_int:
             return NotImplemented
+        if (isinstance(self, TLong) != isinstance(other, TLong)
+            or isinstance (self, TSize) != isinstance(other, TSize)):
+            return NotImplemented
         if self.is_endian:
             if not other.is_endian:
                 return NotImplemented
@@ -477,9 +585,20 @@ def cmp(self, other):
                 return NotImplemented
         elif other.is_endian:
             return NotImplemented
-        if isinstance(self, TLong) != isinstance(other, TLong):
+        return (0 if (self.bits, self.signed) == (other.bits, other.signed)
+                else NotImplemented)
+
+    def cmp_fuzzy(self, other, consider_quals=False):
+        if consider_quals and (self.const != other.const):
+            return NotImplemented
+        if not other.is_int:
             return NotImplemented
-        if isinstance(self, TSize) != isinstance(other, TSize):
+        if self.is_endian:
+            if not other.is_endian:
+                return NotImplemented
+            if self.byte_order != other.byte_order:
+                return NotImplemented
+        elif other.is_endian:
             return NotImplemented
         if (dml.globals.dml_version == (1, 2)
             and not dml.globals.strict_int_flag):
@@ -488,6 +607,7 @@ def cmp(self, other):
         else:
             return (0 if (self.bits, self.signed) == (other.bits, other.signed)
                     else NotImplemented)
+
     # This is the most restrictive canstore definition for
     # IntegerTypes, if this is overridden then it should be
     # because we want to be less restrictive
@@ -552,8 +672,6 @@ def canstore(self, other):
         other = realtype(other)
         if other.is_int:
             trunc = (other.bits > self.bits)
-            if dml.globals.compat_dml12 and isinstance(other, TBool):
-                return (False, False, constviol)
             return (True, trunc, constviol)
         if other.is_float and not self.is_bitfields:
             return (True, True, constviol)
@@ -586,6 +704,10 @@ def __repr__(self):
     def clone(self):
         return TLong(self.signed, self.const)
 
+    def hashed(self, consider_quals=True):
+        return hash((TLong, self.const if consider_quals else None,
+                     self.signed))
+
     def declaration(self, var):
         decl = 'long ' + var
         return decl if self.signed else 'unsigned ' + decl
@@ -605,6 +727,10 @@ def __repr__(self):
     def clone(self):
         return TSize(self.signed, self.const)
 
+    def hashed(self, consider_quals=True):
+        return hash((TSize, self.const if consider_quals else None,
+                     self.signed))
+
     def declaration(self, var):
         return ('ssize_t ' if self.signed else 'size_t ') + var
 
@@ -613,7 +739,7 @@ class TEndianInt(IntegerType):
     Corresponds to the (u)?intX_[be|le] family of types defined in
     dmllib.h
     '''
-    __slots__ = ('byte_order')
+    __slots__ = ('byte_order',)
     def __init__(self, bits, signed, byte_order, members = None, const = False):
         IntegerType.__init__(self, bits, signed, members, const)
         if (bits % 8 != 0):
@@ -682,7 +808,9 @@ def __repr__(self):
         return '%s(%r,%r)' % (self.__class__.__name__, self.name, self.const)
     def describe(self):
         return self.name
-    def cmp(self, other):
+    def cmp(self, other, consider_quals=True):
+        if consider_quals and (self.const != other.const):
+            return NotImplemented
         if other.is_float and self.name == other.name:
             return 0
         return NotImplemented
@@ -717,7 +845,7 @@ def key(self):
                 % (conv_const(self.const, self.base).key(),
                    self.size.value))
     def describe(self):
-        return 'array of size %s of %s' % (self.size.read(),
+        return 'array of size %s of %s' % (str(self.size),
                                            self.base.describe())
     def declaration(self, var):
         return self.base.declaration(self.const_str + var
@@ -728,6 +856,13 @@ def print_declaration(self, var, init = None, unused = False):
         assert not init or self.size.constant
         DMLType.print_declaration(self, var, init, unused)
 
+    def hashed(self, consider_quals=True):
+        cconst = conv_const if consider_quals else lambda c, t: t
+        size = self.size.value if self.size.constant else self.size
+        return hash((TArray,
+                     size,
+                     cconst(self.const, self.base).hashed(consider_quals)))
+
     def sizeof(self):
         if not self.size.constant:
             # variable-sized array, sizeof is not known
@@ -736,14 +871,31 @@ def sizeof(self):
         if elt_size == None:
             return None
         return self.size.value * elt_size
-    def cmp(self, other):
-        if dml.globals.compat_dml12:
+    def cmp(self, other, consider_quals=True):
+        if not isinstance(other, TArray):
+            return NotImplemented
+        if not (self.size is other.size
+                or (self.size.constant and other.size.constant
+                    and self.size.value == other.size.value)):
+            return NotImplemented
+        cconst = conv_const if consider_quals else lambda c, t: t
+        return cconst(self.const, self.base).cmp(
+            cconst(other.const, other.base), consider_quals)
+
+    def cmp_fuzzy(self, other, consider_quals=False):
+        cconst = conv_const if consider_quals else lambda c, t: t
+        if not dml.globals.compat_dml12:
             if isinstance(other, (TArray, TPtr)):
-                return self.base.cmp(other.base)
+                return cconst(self.const, self.base).cmp(
+                    cconst(other.const and isinstance(other, TArray),
+                           other.base),
+                    consider_quals)
         elif isinstance(other, (TPtr, TArray)):
             if self.base.void or other.base.void:
                 return 0
-            if self.base.cmp(other.base) == 0:
+            if (cconst(self.const, self.base).cmp(
+                    cconst(other.const and isinstance(other, TArray),
+                           other.base), consider_quals) == 0):
                 return 0
         return NotImplemented
     def canstore(self, other):
@@ -754,6 +906,10 @@ def resolve(self):
         self.base.resolve()
         return self
 
+    @property
+    def is_raii(self):
+        return self.base.is_raii
+
 class TPtr(DMLType):
     __slots__ = ('base',)
     def __init__(self, base, const = False):
@@ -767,34 +923,54 @@ def key(self):
         return f'{self.const_str}pointer({self.base.key()})'
     def describe(self):
         return 'pointer to %s' % (self.base.describe())
-    def cmp(self, other):
-        if dml.globals.compat_dml12:
-            if isinstance(other, TPtr):
+    def cmp_fuzzy(self, other, consider_quals=False):
+        cconst = conv_const if consider_quals else lambda c, t: t
+        if not dml.globals.compat_dml12:
+            if isinstance(other, (TArray, TPtr)):
                 # Can only compare for voidness or equality
                 if self.base.void or other.base.void:
                     return 0
-                if self.base.cmp(other.base) == 0:
-                    return 0
+
+                return self.base.cmp(
+                    cconst(other.const and isinstance(other, TArray),
+                           other.base),
+                    consider_quals)
         elif isinstance(other, (TPtr, TArray)):
             if self.base.void or other.base.void:
                 return 0
-            if self.base.cmp(other.base) == 0:
+            if (self.base.cmp(
+                    cconst(other.const and isinstance(other, TArray),
+                           other.base), consider_quals) == 0):
                 return 0
         return NotImplemented
 
+    def cmp(self, other, consider_quals=True):
+        if DMLType.cmp(self, other, consider_quals) != 0:
+            return NotImplemented
+        return self.base.cmp(other.base, consider_quals)
+
     def canstore(self, other):
         ok = False
         trunc = False
         constviol = False
         if isinstance(other, (TPtr, TArray)):
+            constviol = (not shallow_const(self.base)
+                         and shallow_const(other.base))
             if self.base.void or other.base.void:
                 ok = True
             else:
-                if not self.base.const and other.base.const:
-                    constviol = True
-                ok = (self.base.cmp(other.base) == 0)
+                # TODO(RAII) this means that in 1.4 you can't do things like
+                # assign int* to unsigned*
+                unconst_self_base = safe_realtype_unconst(self.base)
+                unconst_other_base = safe_realtype_unconst(other.base)
+
+                ok = ((unconst_self_base.cmp_fuzzy
+                       if dml.globals.compat_dml12_int
+                       else unconst_self_base.cmp)(unconst_other_base)
+                      == 0)
         elif isinstance(other, TFunction):
-            ok = True
+            ok = safe_realtype_unconst(self.base).cmp(other) == 0
+        # TODO(RAII) gate this behind dml.globals.dml_version == (1, 2)?
         if self.base.void and isinstance(other, TDevice):
             ok = True
         #dbg('TPtr.canstore %r %r => %r' % (self, other, ok))
@@ -813,7 +989,7 @@ def resolve(self):
         self.base.resolve()
         return self
 
-class TVector(DMLType):
+class TVectorLegacy(DMLType):
     __slots__ = ('base',)
     def __init__(self, base, const = False):
         DMLType.__init__(self, const)
@@ -821,21 +997,28 @@ def __init__(self, base, const = False):
             raise DMLTypeError("Null base")
         self.base = base
     def __repr__(self):
-        return "TVector(%r,%r)" % (self.base, self.const)
+        return "TVectorLegacy(%r,%r)" % (self.base, self.const)
     def key(self):
-        return f'{self.const_str}vector({self.base.key()})'
+        return f'{self.const_str}vectorlegacy({self.base.key()})'
     def describe(self):
-        return 'vector of %s' % self.base.describe()
-    def cmp(self, other):
-        if isinstance(other, TVector):
+        return '1.2 vector of %s' % self.base.describe()
+    def cmp(self, other, consider_quals=True):
+        if not isinstance(other, TVectorLegacy):
+            return NotImplemented
+        cconst = conv_const if consider_quals else lambda c, t: t
+        return cconst(self.const, self.base).cmp(
+            cconst(other.const, other.base), consider_quals)
+    def cmp_fuzzy(self, other, consider_quals=False):
+        cconst = conv_const if consider_quals else lambda c, t: t
+        if isinstance(other, TVectorLegacy):
             # Can only compare for voidness or equality
             if self.base.void or other.base.void:
                 return 0
-            if self.base.cmp(other.base) == 0:
-                return 0
+            return cconst(self.const, self.base).cmp(
+                cconst(other.const, other.base), consider_quals)
         return NotImplemented
     def clone(self):
-        return TVector(self.base, self.const)
+        return TVectorLegacy(self.base, self.const)
     def declaration(self, var):
         s = self.base.declaration('')
         return 'VECT(%s) %s%s' % (s, self.const_str, var)
@@ -855,14 +1038,18 @@ def __repr__(self):
     def clone(self):
         return TTrait(self.trait)
 
-    def cmp(self, other):
-        if isinstance(other, TTrait) and self.trait is other.trait:
-            return 0
-        else:
-            return NotImplemented
+    def cmp(self, other, consider_quals=True):
+        return (0 if (DMLType.cmp(self, other, consider_quals) == 0
+                      and self.trait is other.trait)
+                else NotImplemented)
 
     def key(self):
         return f'{self.const_str}trait({self.trait.name})'
+
+    def hashed(self, consider_quals=True):
+        return hash((TTrait, self.const if consider_quals else None,
+                     self.trait))
+
     def describe(self):
         return 'trait ' + self.trait.name
 
@@ -882,11 +1069,14 @@ def __repr__(self):
     def clone(self):
         return TTraitList(self.traitname, self.const)
 
-    def cmp(self, other):
-        if isinstance(other, TTraitList) and self.traitname == other.traitname:
-            return 0
-        else:
-            return NotImplemented
+    def hashed(self, consider_quals=True):
+        return hash((TTraitList, self.const if consider_quals else None,
+                     self.traitname))
+
+    def cmp(self, other, consider_quals=True):
+        return (0 if (DMLType.cmp(self, other, consider_quals) == 0
+                      and self.traitname == other.traitname)
+                else NotImplemented)
 
     def key(self):
         return f'{self.const_str}sequence({self.traitname})'
@@ -901,12 +1091,15 @@ def declaration(self, var):
         # information is discarded.
         return '_each_in_t %s' % (var,)
 
+is_raii_inprogress = object()
+
 class StructType(DMLType):
     '''common superclass for DML-defined structs and extern structs'''
-    __slots__ = ('members',)
+    __slots__ = ('members', '_raii')
     def __init__(self, members, const):
         super(StructType, self).__init__(const)
         self.members = members
+        self._raii = None
 
     @property
     def members_qualified(self):
@@ -917,6 +1110,24 @@ def get_member_qualified(self, member):
         t = self.members.get(member)
         return t if t is None else conv_const(self.const, t)
 
+    @property
+    def is_raii(self):
+        if self._raii is not None:
+            assert self._raii is not is_raii_inprogress
+            return self._raii
+
+        self._raii = is_raii_inprogress
+
+        self.resolve()
+
+        for typ in self.members.values():
+            if typ.is_raii:
+                self._raii = True
+                return True
+
+        self._raii = False
+        return False
+
 class TExternStruct(StructType):
     '''A struct-like type defined by code outside DMLC's control.
     'members' is the potential right operands of binary '.',
@@ -955,10 +1166,15 @@ def declaration(self, var):
             raise EANONEXT(self.declaration_site)
         return "%s %s%s" % (self.typename, self.const_str, var)
 
-    def cmp(self, other):
-        if isinstance(other, TExternStruct) and self.id == other.id:
-            return 0
-        return NotImplemented
+    def hashed(self, consider_quals=True):
+        return hash((TExternStruct,
+                     self.const if consider_quals else None,
+                     self.id))
+
+    def cmp(self, other, consider_quals=True):
+        return (0 if (DMLType.cmp(self, other, consider_quals) == 0
+                      and self.id == other.id)
+                else NotImplemented)
 
     def clone(self):
         return TExternStruct(self.members, self.id, self.typename, self.const)
@@ -1007,10 +1223,14 @@ def print_struct_definition(self):
             t.print_declaration(n)
         out("};\n", preindent = -1)
 
-    def cmp(self, other):
-        if isinstance(other, TStruct) and self.label == other.label:
-            return 0
-        return NotImplemented
+    def hashed(self, consider_quals=True):
+        return hash((TStruct, self.const if consider_quals else None,
+                     self.label))
+
+    def cmp(self, other, consider_quals=True):
+        return (0 if (DMLType.cmp(self, other, consider_quals) == 0
+                      and self.label == other.label)
+                else NotImplemented)
 
     def clone(self):
         return TStruct(self.members, self.label, self.const)
@@ -1137,13 +1357,34 @@ def describe(self):
         return ('function(%s) returning %s'
                 % (inparams, self.output_type.describe()))
 
-    def cmp(self, other):
+    def hashed(self, consider_quals=True):
+        return hash((TFunction,
+                     tuple(typ.hashed(consider_quals)
+                           for typ in self.input_types),
+                     self.output_type.hashed(consider_quals),
+                     self.varargs))
+
+    def cmp_fuzzy(self, other, consider_quals=False):
+        if (isinstance(other, TFunction)
+            and len(self.input_types) == len(other.input_types)
+            and all(arg1.cmp_fuzzy(arg2, consider_quals) == 0
+                    for (arg1, arg2)
+                    in zip(self.input_types, other.input_types))
+            and self.output_type.cmp_fuzzy(other.output_type,
+                                           consider_quals) == 0
+            and self.varargs == other.varargs):
+            return 0
+        return NotImplemented
+
+    def cmp(self, other, consider_quals=True):
         if (isinstance(other, TFunction)
             and len(self.input_types) == len(other.input_types)
-            and all(arg1.cmp(arg2) == 0
-                    for (arg1, arg2) in zip(self.input_types,
-                                            other.input_types))
-            and self.output_type.cmp(other.output_type) == 0
+            and all(safe_realtype_unconst(arg1).cmp(
+                        safe_realtype_unconst(arg2), consider_quals) == 0
+                    for (arg1, arg2)
+                    in zip(self.input_types, other.input_types))
+            and safe_realtype_unconst(self.output_type).cmp(
+                safe_realtype_unconst(other.output_type), consider_quals) == 0
             and self.varargs == other.varargs):
             return 0
         return NotImplemented
@@ -1179,10 +1420,10 @@ def __repr__(self):
     def clone(self):
         return THook(self.msg_types, self.validated, self.const)
 
-    def cmp(self, other):
+    def cmp(self, other, consider_quals=True):
         if (isinstance(other, THook)
             and len(self.msg_types) == len(other.msg_types)
-            and all(own_comp.cmp(other_comp) == 0
+            and all(own_comp.cmp(other_comp, consider_quals) == 0
                     for (own_comp, other_comp) in zip(self.msg_types,
                                                       other.msg_types))):
             return 0
@@ -1210,6 +1451,60 @@ def validate(self, fallback_site):
                     raise EHOOKTYPE(self.declaration_site or fallback_site,
                                     typ, e.clarification) from e
 
+class TString(DMLTypeRAII):
+    __slots__ = ()
+    def __repr__(self):
+        return "TString(%r)" % (self.const,)
+    def describe(self):
+        return 'string'
+    def cmp(self, other, consider_quals=True):
+        return DMLType.cmp(self, other, consider_quals)
+    def clone(self):
+        return TString(self.const)
+    def declaration(self, var):
+        return f'_dml_string_t {self.const_str}{var}'
+
+class TVector(DMLTypeRAII):
+    __slots__ = ('base',)
+    def __init__(self, base, const = False):
+        DMLType.__init__(self, const)
+        if not base:
+            raise DMLTypeError("Null base")
+        self.base = base
+    def __repr__(self):
+        return "TVector(%r,%r)" % (self.base, self.const)
+    def key(self):
+        return f'vector({conv_const(self.const, self.base).key()})'
+    def describe(self):
+        return f'vector of {self.base.describe()}'
+    def __str__(self):
+        return f'vect({self.base})'
+    def cmp_fuzzy(self, other, consider_quals=False):
+        if isinstance(other, TVector):
+            cconst = conv_const if consider_quals else lambda c, t: t
+            # Can only compare for voidness or equality
+            if self.base.void or other.base.void:
+                return 0
+            if cconst(self.const, self.base).cmp(
+                    cconst(other.const, other.base), consider_quals) == 0:
+                return 0
+        return NotImplemented
+    def cmp(self, other, consider_quals=True):
+        if isinstance(other, TVector):
+            cconst = conv_const if consider_quals else lambda c, t: t
+            # Can only compare for voidness or equality
+            if cconst(self.const, self.base).cmp(
+                    cconst(other.const, other.base), consider_quals) == 0:
+                return 0
+        return NotImplemented
+    def hashed(self, consider_quals=True):
+        cconst = conv_const if consider_quals else lambda c, t: t
+        return hash((TVector, cconst(self.const,
+                                     self.base).hashed(consider_quals)))
+    def clone(self):
+        return TVector(self.base, self.const)
+    def declaration(self, var):
+        return f'_dml_vect_t {self.const_str}{var}'
 
 intre = re.compile('(u?)int([1-5][0-9]?|6[0-4]?|[789])(_be_t|_le_t)?$')
 def parse_type(typename):
@@ -1235,6 +1530,8 @@ def parse_type(typename):
         return TInt(64, True)
     elif typename == 'uinteger_t' and dml.globals.api_version < '7':
         return TInt(64, False)
+    elif typename == 'string':
+        return TString()
     else:
         return TNamed(typename)
 
@@ -1249,11 +1546,21 @@ def type_union(type1, type2):
 def compatible_types(type1, type2):
     # This function intends to verify that two DML types are
     # compatible in the sense defined by the C spec, possibly with
-    # some DML-specific restrictions added. TODO: DMLType.cmp is only
-    # a rough approximation of this; we should write tests and
-    # either repair cmp or rewrite the logic from scratch.
+    # some DML-specific restrictions added.
     return type1.cmp(type2) == 0
 
+# TODO(RAII) can we be rid of this and cmp_fuzzy?
+def compatible_types_fuzzy(type1, type2):
+    # This function intends to verify that two DML types are
+    # compatible in the sense defined by the C spec, possibly with
+    # some DML-specific restrictions added.
+    # DMLType.cmp_fuzzy is only a very rough approximation of this,
+    # meant to suite usages such as type-checking the ternary
+    # operator.
+    # Any use of .cmp_fuzzy or compatible_type_fuzzy should be considered
+    # a HACK.
+    return type1.cmp_fuzzy(type2) == 0
+
 void = TVoid()
 # These are the named types used.  This includes both "imported"
 # typedefs for types declared in C header files, and types defined in
diff --git a/test/1.4/errors/T_WEXPERIMENTAL.dml b/test/1.4/errors/T_DEPRECATED.dml
similarity index 83%
rename from test/1.4/errors/T_WEXPERIMENTAL.dml
rename to test/1.4/errors/T_DEPRECATED.dml
index 1ea72bc2a..463ff62d6 100644
--- a/test/1.4/errors/T_WEXPERIMENTAL.dml
+++ b/test/1.4/errors/T_DEPRECATED.dml
@@ -8,5 +8,5 @@ dml 1.4;
 
 device test;
 
-/// WARNING WEXPERIMENTAL
+/// WARNING WDEPRECATED
 session int vect x;
diff --git a/test/1.4/errors/T_ECAST.dml b/test/1.4/errors/T_ECAST.dml
index f07c6d4a9..dd4d3cd3e 100644
--- a/test/1.4/errors/T_ECAST.dml
+++ b/test/1.4/errors/T_ECAST.dml
@@ -8,7 +8,7 @@ device test;
 
 typedef struct { uint32 x; } s_t;
 typedef layout "little-endian" { uint32 x; } l_t;
-/// WARNING WEXPERIMENTAL
+/// WARNING WDEPRECATED
 typedef int vect v_t;
 typedef int a_t[1];
 typedef void f_t(void);
@@ -44,8 +44,6 @@ method init() {
     /// ERROR ECAST
     cast(i, v_t);
     /// ERROR ECAST
-    cast(v, v_t);
-    /// ERROR ECAST
     cast(l, uint32);
     // no error!
     cast(a, uint32);
diff --git a/test/1.4/errors/T_ESERIALIZE.dml b/test/1.4/errors/T_ESERIALIZE.dml
index 0a63142ba..9aed60b61 100644
--- a/test/1.4/errors/T_ESERIALIZE.dml
+++ b/test/1.4/errors/T_ESERIALIZE.dml
@@ -33,8 +33,8 @@ typedef void (*f)();
 /// ERROR ESERIALIZE
 saved f f_ptr;
 
-// TODO: vectors should be serializable
-/// WARNING WEXPERIMENTAL
+// Legacy vectors should not be serializable
+/// WARNING WDEPRECATED
 typedef int vect int_vect;
 /// ERROR ESERIALIZE
 saved int_vect x;
diff --git a/test/1.4/errors/T_ESWITCH.dml b/test/1.4/errors/T_ESWITCH.dml
index b87c186c7..ecca31404 100644
--- a/test/1.4/errors/T_ESWITCH.dml
+++ b/test/1.4/errors/T_ESWITCH.dml
@@ -11,12 +11,6 @@ method init() {
         /// ERROR ESWITCH
     {
     }
-    switch (1)
-        // must start with a case
-        /// ERROR ESWITCH
-    {
-        ;
-    }
     switch (1) {
     default:
     #if (true) {
diff --git a/test/1.4/errors/T_EVOID.dml b/test/1.4/errors/T_EVOID.dml
index a08076063..8e816581f 100644
--- a/test/1.4/errors/T_EVOID.dml
+++ b/test/1.4/errors/T_EVOID.dml
@@ -35,7 +35,7 @@ extern typedef const void ext_void_t;
 
 /// ERROR EVOID
 session void
-/// WARNING WEXPERIMENTAL
+/// WARNING WDEPRECATED
     vect v;
 
 // this is allowed in 1.2, and even used by dml-builtins, unclear why
diff --git a/test/1.4/syntax/T_assign.dml b/test/1.4/syntax/T_assign.dml
index d8588e216..1eb5f7764 100644
--- a/test/1.4/syntax/T_assign.dml
+++ b/test/1.4/syntax/T_assign.dml
@@ -6,6 +6,16 @@ dml 1.4;
 
 device test;
 
+method m(int i) -> (int) throws {
+    if (i > 4) throw;
+    return i;
+}
+
+method ms(int i) -> (string) throws {
+    if (i > 4) throw;
+    return mk_string_f("%d", i);
+}
+
 method init() {
     local int i;
 
@@ -55,6 +65,19 @@ method init() {
     c[++j] += ++i;
     assert c[1] == 4 && j == 1 && i == 4;
 
+    // += with method calls
+    i = 0;
+    try {
+        i += m(i + 4);
+        assert i == 4;
+    } catch assert false;
+    try {
+        i += m(i + 4);
+        assert false;
+    } catch {
+        assert i == 4;
+    }
+
     // multiple simultaneous assignment
     i = 1;
     j = 0;
diff --git a/test/1.4/types/raii/T_after.cont.py b/test/1.4/types/raii/T_after.cont.py
new file mode 100644
index 000000000..55b759f2f
--- /dev/null
+++ b/test/1.4/types/raii/T_after.cont.py
@@ -0,0 +1,22 @@
+# © 2023 Intel Corporation
+# SPDX-License-Identifier: MPL-2.0
+
+import stest
+
+obj = conf.obj
+SIM_continue(99999)
+stest.expect_equal(obj.trigger, [[0, 0], [0, 0]])
+stest.expect_equal(obj.trigger_hook, [0, 0])
+stest.expect_equal(obj.single_operator, 0)
+stest.expect_equal(obj.multi_operator, [[0, 0], [0, 0]])
+stest.expect_equal(obj.hook_operator, [[0, 0], [0, 0]])
+SIM_continue(2)
+stest.expect_equal(obj.trigger, [[0, 0], [0, 4]])
+stest.expect_equal(obj.trigger_hook, [0, 2])
+stest.expect_equal(obj.single_operator, 5)
+stest.expect_equal(obj.multi_operator, [[0, 5], [3, 0]])
+stest.expect_equal(obj.hook_operator, [[0, 5], [3, 0]])
+SIM_continue(99998)
+stest.expect_equal(obj.single_operator, 5)
+SIM_continue(2)
+stest.expect_equal(obj.single_operator, 3)
diff --git a/test/1.4/types/raii/T_after.dml b/test/1.4/types/raii/T_after.dml
new file mode 100644
index 000000000..1c111dfe6
--- /dev/null
+++ b/test/1.4/types/raii/T_after.dml
@@ -0,0 +1,106 @@
+/*
+  © 2021-2023 Intel Corporation
+  SPDX-License-Identifier: MPL-2.0
+*/
+dml 1.4;
+device test;
+
+import "simics/simulator/callbacks.dml";
+
+template trigger_attr is pseudo_attr {
+    param type = "i";
+    session int val;
+    method inc() default {
+        this.val++;
+    }
+}
+
+attribute trigger[i < 2][j < 2] is trigger_attr {
+    method set(attr_value_t val) throws {
+        after 0.1 s:
+          trigger[SIM_attr_integer(val)][SIM_attr_integer(val)].inc();
+    }
+    method get() -> (attr_value_t) {
+        return SIM_make_attr_uint64(val);
+    }
+}
+
+attribute trigger_hook[i < 2] is (trigger_attr, post_init) {
+    hook() h;
+
+    method post_init() {
+        if (!SIM_is_restoring_state(dev.obj)) {
+            after h: inc();
+        }
+    }
+
+    method inc() {
+        default();
+        after h: inc();
+    }
+
+    method set(attr_value_t val) throws {
+        after 0.1 s: trigger_hook[SIM_attr_integer(val)].h.send_now();
+    }
+    method get() -> (attr_value_t) {
+        return SIM_make_attr_uint64(val);
+    }
+}
+
+attribute operate is (write_only_attr) {
+    param type = "n";
+    method set(attr_value_t val) throws {
+        local vect(int) c = cast({3}, vect(int)) + cast({5}, vect(int));
+        after 0.1 s: single_operator.modify(c, 1);
+        after 0.1 s: multi_operator[0][1].modify(c, 1);
+        after 0.1 s: multi_operator[1][0].modify(c, 0);
+        after 200000 cycles: single_operator.modify(c, 0);
+
+        after 0.1 s: hook_operator[0][1].h.send_now(c, 1);
+        after 0.1 s: hook_operator[1][0].h.send_now(c, 0);
+    }
+}
+
+template operator is (int64_attr) {
+    method modify(vect(int) chunk, int ix) default {
+        this.val = chunk[ix];
+    }
+}
+
+attribute multi_operator[i < 2][j < 2] is (operator);
+attribute single_operator is (operator);
+
+attribute hook_operator[i < 2][j < 2] is (operator, post_init) {
+    hook(vect(int), int) h;
+
+    method post_init() {
+        if (!SIM_is_restoring_state(dev.obj)) {
+            after h -> (c, i): modify(c, i);
+        }
+    }
+
+    method modify(vect(int) chunk, int ix) {
+        default(chunk, ix);
+        after h -> (c, i): modify(c, i);
+    }
+}
+
+// Test methods with const-qualified parameters
+
+typedef layout "little-endian" {
+    const uint32 x[2];
+} l_t;
+
+attribute constig_res[i < 2] is uint64_attr;
+
+method constig(const uint64 i, const l_t l) {
+    constig_res[0].val = i;
+    constig_res[1].val = l.x[0] << 32 | l.x[1];
+}
+
+attribute trigger_constig is (write_only_attr) {
+    param type = "n";
+    method set(attr_value_t val) throws {
+        after 0.1 s: constig(4, {{7, 11}});
+    }
+}
diff --git a/test/1.4/types/raii/T_after.py b/test/1.4/types/raii/T_after.py
new file mode 100644
index 000000000..617d6983b
--- /dev/null
+++ b/test/1.4/types/raii/T_after.py
@@ -0,0 +1,33 @@
+# © 2023 Intel Corporation
+# SPDX-License-Identifier: MPL-2.0
+
+from os.path import join
+import subprocess
+from simicsutils.host import batch_suffix
+import stest
+
+cpu = SIM_create_object("clock", "clock", [["freq_mhz", 1]])
+obj.queue = cpu
+
+obj.trigger_constig = None
+SIM_continue(99999)
+stest.expect_equal(obj.constig_res, [0, 0])
+SIM_continue(2)
+stest.expect_equal(obj.constig_res, [4, 7 << 32 | 11])
+
+obj.single_operator = 0
+obj.multi_operator = [[0, 0], [0, 0]]
+
+obj.trigger = [[1, 1], [1, 1]]
+obj.trigger_hook = [1, 1]
+obj.operate = None
+
+SIM_write_configuration_to_file("checkpointing.chkp", Sim_Save_Nobundle)
+
+subprocess.check_call(
+    [f'{conf.sim.project}/bin/simics{batch_suffix()}'] +
+    ["--batch-mode", "--quiet", "--no-copyright", "--dump-core", "--werror",
+     '--project', conf.sim.project,
+     "-L", scratchdir,
+     "-c", "checkpointing.chkp",
+     "-p", join(basedir, "T_after.cont.py")])
diff --git a/test/1.4/types/raii/T_hooks_basic.dml b/test/1.4/types/raii/T_hooks_basic.dml
new file mode 100644
index 000000000..87403d3a7
--- /dev/null
+++ b/test/1.4/types/raii/T_hooks_basic.dml
@@ -0,0 +1,147 @@
+/*
+  © 2023 Intel Corporation
+  SPDX-License-Identifier: MPL-2.0
+*/
+dml 1.4;
+
+device test;
+
+/// DMLC-FLAG --enable-features-for-internal-testing-dont-use-this
+/// WARNING WEXPERIMENTAL hooks_common.dml
+
+import "hooks_common.dml";
+
+template hookset_test is hookset {
+    shared method testhooks();
+    method testhooks() {
+        local uint64 resumed;
+        count = storage = storage_indexed = 0;
+        last_i_indexed = last_j_indexed = -1;
+
+        assert h0.suspended == 0;
+        resumed = h0.send_now();
+        assert resumed == 0;
+
+        after h0: no_params();
+        assert count == 0 && h0.suspended == 1;
+        resumed = h0.send_now();
+        assert h0.suspended == 0 && resumed == 1 && count == 1;
+        resumed = h0.send_now();
+        assert resumed == 0;
+
+        local vect(int) v = {7};
+        after h0: no_params();
+        after h0: one_serializable_param(v);
+        assert h0.suspended == 2 && count == 1 && storage == 0;
+        resumed = h0.send_now();
+        assert h0.suspended == 0 && resumed == 2 && count == 2 && storage == 7;
+        count = storage = 0;
+
+        local int i = 5;
+        after h1 -> p: one_param(p);
+        assert h1.suspended == 1 && i == 5;
+        resumed = h1.send_now(&i);
+        assert h1.suspended == 0 && resumed == 1 && i == 6;
+        v = {-5};
+        after h1 -> p: one_serializable_param(v);
+        assert h1.suspended == 1 && storage == 0 && i == 6;
+        resumed = h1.send_now(&i);
+        assert h1.suspended == 0 && resumed == 1 && storage == -5 && i == 6;
+        storage = i = 0;
+
+        after h2 -> (p, i): two_params(p, i);
+        assert h2.suspended == 1 && i == 0;
+        resumed = h2.send_now(&i, {9});
+        assert h2.suspended == 0 && resumed == 1 && i == 9;
+        v = {2};
+        after h2 -> (p, i): two_params(p, v);
+        assert h2.suspended == 1;
+        resumed = h2.send_now(&i, {9});
+        assert h2.suspended == 0 && resumed == 1 && i == 2;
+        i = 0;
+
+        assert h3[3][4].suspended == 0;
+        after h3[3][4]: no_params();
+        for (local int idx_0; idx_0 < 6; ++idx_0) {
+            for (local int idx_1; idx_1 < 8; ++idx_1) {
+                if (idx_0 != 3 && idx_1 != 4) {
+                    assert h3[idx_0][idx_1].suspended == 0;
+                }
+            }
+        }
+        assert h3[3][4].suspended == 1 && count == 0;
+        resumed = h3[3][4].send_now();
+        assert h3[3][4].suspended == 0 && resumed == 1 && count == 1;
+        count = 0;
+
+        after h0: indexed[3][5].no_params();
+        v = {10};
+        after h0: indexed[2][3].one_serializable_param(v);
+        assert h0.suspended == 2 && last_i_indexed == -1
+            && last_j_indexed == -1 && storage_indexed == 0;
+        resumed = h0.send_now();
+        assert h0.suspended == 0 && resumed == 2 && last_i_indexed == 3
+            && last_j_indexed == 5 && storage_indexed == 10 * (2*7 + 3);
+        (last_i_indexed, last_j_indexed, storage_indexed) = (-1, -1, 0);
+
+        after h1 -> p: indexed[3][5].one_param(p);
+        assert h1.suspended == 1 && i == 0;
+        resumed = h1.send_now(&i);
+        assert h1.suspended == 0 && resumed == 1 && i == 3 * 7 + 5;
+        i = 0;
+        v = {-5};
+        after h1 -> p: indexed[3][5].one_serializable_param(v);
+        assert h1.suspended == 1 && storage_indexed == 0 && i == 0;
+        resumed = h1.send_now(&i);
+        assert h1.suspended == 0 && resumed == 1
+            && storage_indexed == -5 * (3 * 7 + 5) && i == 0;
+        storage_indexed = 0;
+
+        after h2 -> (p, i): indexed[3][5].two_params(p, i);
+        assert h2.suspended == 1;
+        v = {9};
+        resumed = h2.send_now(&i, v);
+        assert h2.suspended == 0 && resumed == 1 && i == 9 * (3*7 + 5);
+        i = 0;
+        v = {2};
+        after h2 -> (p, i): indexed[3][5].two_params(p, v);
+        assert h2.suspended == 1;
+        resumed = h2.send_now(&i, {9});
+        assert h2.suspended == 0 && resumed == 1 && i == 2*(3*7 + 5);
+
+        foreach sub in (each hookset_test in (this)) {
+            sub.testhooks();
+        }
+    }
+}
+
+in each hookset {
+    is hookset_test;
+}
+
+hook(int **) order_test_hook;
+method order_test_callback(int id, int **queue) {
+    **queue = id;
+    ++*queue;
+}
+
+method order_test() {
+    for (local int i = 0; i < 4; ++i) {
+        after order_test_hook -> p: order_test_callback(i, p);
+    }
+    local int queue[4];
+    local int *queue_ptr = queue;
+    assert order_test_hook.suspended == 4;
+    local uint64 resumed = order_test_hook.send_now(&queue_ptr);
+    assert resumed == 4;
+    for (local int i = 0; i < 4; ++i) {
+        assert queue[i] == i;
+    }
+}
+
+method init() {
+    foreach sub in (each hookset_test in (this)) {
+        sub.testhooks();
+    }
+    order_test();
+}
diff --git a/test/1.4/types/raii/T_hooks_checkpointing.cont.py b/test/1.4/types/raii/T_hooks_checkpointing.cont.py
new file mode 100644
index 000000000..ba7b3eddd
--- /dev/null
+++ b/test/1.4/types/raii/T_hooks_checkpointing.cont.py
@@ -0,0 +1,4 @@
+# © 2023 Intel Corporation
+# SPDX-License-Identifier: MPL-2.0
+
+conf.obj.test_state = None
diff --git a/test/1.4/types/raii/T_hooks_checkpointing.dml b/test/1.4/types/raii/T_hooks_checkpointing.dml
new file mode 100644
index 000000000..327109243
--- /dev/null
+++ b/test/1.4/types/raii/T_hooks_checkpointing.dml
@@ -0,0 +1,155 @@
+/*
+  © 2023 Intel Corporation
+  SPDX-License-Identifier: MPL-2.0
+*/
+dml 1.4;
+
+device test;
+
+/// DMLC-FLAG --enable-features-for-internal-testing-dont-use-this
+/// WARNING WEXPERIMENTAL hooks_common.dml
+
+import "hooks_common.dml";
+
+attribute setup_state is write_only_attr {
+    param type = "n";
+    method set(attr_value_t val) throws {
+    #foreach obj in ([dev, g[2], b1, b1.g[3], b2[1], b2[2].g[3]]) {
+            local vect(int) v = {7};
+            after obj.h0: no_params();
+            after obj.h0: one_serializable_param(v);
+            after obj.h0: indexed[3][5].no_params();
+            v = {10};
+            after obj.h0: indexed[3][5].one_serializable_param(v);
+
+            after obj.h1 -> p: one_param(p);
+            after obj.h1 -> p: one_serializable_param({9});
+
+            after obj.h2 -> (p, i): two_params(p, i);
+
+            after obj.h3[3][4]: no_params();
+
+            after obj.h4[3][4] -> p: indexed[3][5].one_param(p);
+            v = {9};
+            after obj.h4[3][4] -> p: indexed[3][5].one_serializable_param(v);
+            v = {3};
+            after obj.h5[0][1] -> (p, i): two_params(p, v);
+            after obj.h5[2][3] -> (p, i): indexed[3][5].two_params(p, i);
+            after obj.h5[4][5] -> (p, i): indexed[3][5].two_params(p, v);
+
+            after obj.h3[0][0]: obj.h3[2][3].send_now();
+
+            after obj.h4[0][0] -> p: obj.h0.send_now();
+            after obj.h4[1][1] -> p: obj.h1.send_now(p);
+            v = {4};
+            after obj.h4[2][2] -> p: obj.h2.send_now(p, v);
+            v = {7};
+            after obj.h4[3][3] -> p: obj.h6.send_now(v);
+
+            after obj.h5[0][0] -> (p, i): obj.h2.send_now(p, i);
+            v = {4};
+            after obj.h5[1][1] -> (p, i): obj.h2.send_now(p, v);
+            after obj.h5[2][2] -> (p, i): obj.h6.send_now(i);
+            after obj.h5[3][3] -> (p, i): obj.h6.send_now({6});
+        }
+    }
+}
+
+attribute test_state is write_only_attr {
+    param type = "n";
+    method set(attr_value_t val) throws {
+        #foreach obj in ([dev, g[2], b1, b1.g[3], b2[1], b2[2].g[3]]) {
+            count = storage = storage_indexed = 0;
+            last_i_indexed = last_j_indexed = -1;
+
+            local uint64 resumed = obj.h0.send_now();
+            assert resumed == 4 && count == 1 && storage == 7
+                && last_i_indexed == 3 && last_j_indexed == 5
+                && storage_indexed == 10 * (7 * 3 + 5);
+            count = storage = storage_indexed = 0;
+            last_i_indexed = last_j_indexed = -1;
+
+            local int x = 4;
+            resumed = obj.h1.send_now(&x);
+            assert resumed == 2 && x == 5 && storage == 9;
+            storage = 0;
+
+            local vect(int) v = {7};
+            resumed = obj.h2.send_now(&x, v);
+            assert resumed == 1 && x == 7;
+
+            resumed = obj.h3[3][4].send_now();
+            assert resumed == 1 && count == 1;
+            count = 0;
+
+            x = 7;
+            assert obj.h4[3][4].suspended == 2;
+            resumed = obj.h4[3][4].send_now(&x);
+            assert resumed == 2 && x == 3 * 7 + 5
+                && storage_indexed == 9 * (3 * 7 + 5);
+            x = storage_indexed = 0;
+
+            x = 0;
+            assert obj.h5[0][1].suspended == 1;
+            v = {5};
+            resumed = obj.h5[0][1].send_now(&x, v);
+            assert resumed == 1 && x == 3;
+            x = 0;
+            assert obj.h5[2][3].suspended == 1;
+            resumed = obj.h5[2][3].send_now(&x, v);
+            assert resumed == 1 && x == 5 * (7*3 + 5);
+            x = 0;
+            assert obj.h5[4][5].suspended == 1;
+            resumed = obj.h5[4][5].send_now(&x, v);
+            assert resumed == 1 && x == 3 * (7*3 + 5);
+
+            after obj.h3[2][3]: no_params();
+            resumed = obj.h3[0][0].send_now();
+            assert resumed == 1 && obj.h3[2][3].suspended == 0 && count == 1;
+            count = 0;
+
+            x = 7;
+            after obj.h0: no_params();
+            resumed = obj.h4[0][0].send_now(&x);
+            assert resumed == 1 && obj.h0.suspended == 0 && count == 1
+                && x == 7;
+            count = 0;
+
+            after obj.h1 -> p: one_param(p);
+            resumed = obj.h4[1][1].send_now(&x);
+            assert resumed == 1 && obj.h1.suspended == 0 && x == 8;
+
+            after obj.h2 -> (p, i): two_params(p, i);
+            resumed = obj.h4[2][2].send_now(&x);
+            assert resumed == 1 && obj.h2.suspended == 0 && x == 4;
+
+            after obj.h6 -> i: one_serializable_param(i);
+            resumed = obj.h4[3][3].send_now(&x);
+            assert resumed == 1 && obj.h6.suspended == 0 && x == 4
+                && storage == 7;
+            storage = 0;
+
+            x = 7;
+            after obj.h2 -> (p, i): two_params(p, i);
+            resumed = obj.h5[0][0].send_now(&x, {9});
+            assert resumed == 1 && obj.h2.suspended == 0 && x == 9;
+
+            x = 7;
+            after obj.h2 -> (p, i): two_params(p, i);
+            resumed = obj.h5[1][1].send_now(&x, {9});
+            assert resumed == 1 && obj.h2.suspended == 0 && x == 4;
+
+            x = 7;
+            after obj.h6 -> i: one_serializable_param(i);
+            resumed = obj.h5[2][2].send_now(&x, {9});
+            assert resumed == 1 && obj.h6.suspended == 0 && x == 7
+                && storage == 9;
+
+            after obj.h6 -> i: one_serializable_param(i);
+            resumed = obj.h5[3][3].send_now(&x, {9});
+            assert resumed == 1 && obj.h6.suspended == 0 && x == 7
+                && storage == 6;
+            storage = 0;
+        }
+    }
+}
diff --git a/test/1.4/types/raii/T_hooks_checkpointing.py b/test/1.4/types/raii/T_hooks_checkpointing.py
new file mode 100644
index 000000000..02d1d4c79
--- /dev/null
+++ b/test/1.4/types/raii/T_hooks_checkpointing.py
@@ -0,0 +1,18 @@
+# © 2023 Intel Corporation
+# SPDX-License-Identifier: MPL-2.0
+
+from os.path import join
+import subprocess
+from simicsutils.host import batch_suffix
+
+obj.setup_state = None
+
+SIM_write_configuration_to_file("checkpointing.chkp", Sim_Save_Nobundle)
+
+subprocess.check_call(
+    [f'{conf.sim.project}/bin/simics{batch_suffix()}'] +
+    ["--batch-mode", "--quiet", "--no-copyright", "--dump-core", "--werror",
+     '--project', conf.sim.project,
+     "-L", scratchdir,
+     "-c", "checkpointing.chkp",
+     "-p", join(basedir, "T_hooks_checkpointing.cont.py")])
diff --git a/test/1.4/types/raii/T_various.dml b/test/1.4/types/raii/T_various.dml
new file mode 100644
index 000000000..31cb7ce42
--- /dev/null
+++ b/test/1.4/types/raii/T_various.dml
@@ -0,0 +1,216 @@
+/*
+  © 2023 Intel Corporation
+  SPDX-License-Identifier: MPL-2.0
+*/
+dml 1.4;
+
+device test;
+
+typedef struct {
+    int x;
+    string s;
+} s_t;
+typedef struct {
+    vect(int) ints;
+    vect(string) strings;
+    string otherstrings[3];
+} bs_t;
+
+extern typedef struct {
+    char *elements;
+    uint32 size;
+    uint32 start;
+    uint32 len;
+} _dml_vect_t;
+
+extern typedef struct {
+    char *s;
+    uint32 size;
+    uint32 len;
+} _dml_string_t;
+
+extern void _dml_string_addfmt(string *s, const char *format, ...);
+extern string _dml_string_new(const char *msg);
+extern void _dml_string_addstr(string *s, const char *str);
+
+saved s_t sav = {0, "is, frostigt mörker"};
+saved string savic[2] = {"betvingande", "kyla"};
+
+
+typedef struct {
+    string xarx[3];
+} calka_t;
+
+typedef struct {
+    string xarx;
+    const int y;
+} racalka_t;
+
+independent method add_strs(string a, string b) -> (string) {
+    _dml_string_addstr(&a, b.c_str());
+    return a;
+}
+
+independent method splitAt(string str, uint32 at) -> (string, string) {
+    local int idx = at > str.len ? str.len : at;
+    return (mk_string_f("%.*s", idx, str.c_str()),
+            mk_string(str.c_str() + idx));
+}
+
+template t {
+    param apocalyptical : string;
+    param apocalyptical = mk_string_f(
+        "Gnistor som släcks %s %s %s %s %s %s.",
+        "i", "en", "stjärnkall", "och", "bister", "natt");
+
+    param whatdoesthisevenmean : const char *;
+    param whatdoesthisevenmean = cast({{"En ondskans tid nu domnar min hand",
+                                       1}}, racalka_t[1])[0].xarx.c_str();
+
+    param avect : vect(int);
+    param avect = cast({1, 2, 3}, vect(int));
+}
+
+is t;
+
+saved struct { string xarx[3]; } salt;
+
+session const char *witness = cast("i svårmod stillar sig", string).c_str();
+
+independent method insert_stringvect(vect(string) *v, uint32 i, string s) {
+    v->insert(i, s);
+}
+
+method init() {
+    local bs_t bs = { .otherstrings = {"bränningen", "vid", "strand"},
+                      .ints = {5,3,3}, ...};
+    assert "vid" == bs.otherstrings[1];
+    assert bs.ints.len == 3;
+    local bs_t bs2 = { .strings = {"hård", "tid", "ensamma", "timmar"}, ...};
+    bs = bs2;
+    assert bs.strings.len == 4 && bs.strings[2] == "ensamma";
+    bs.strings = {"världen", "är", "frusen"};
+    assert bs.strings[2] == "frusen";
+
+    local vect(string) v = {"och","människan","vred"};
+    assert v[1] == "människan";
+    assert v.len == 3;
+    v.len = 5;
+    assert v.len == 5 && v[4] == "";
+    v += {"all", "visdom", "är"};
+
+    v.push_back("förlorad");
+    assert v.len == 9;
+    v.pop_front();
+    assert v.len == 8;
+    local bool polarity = true;
+    insert_stringvect(&v, 4, "och glömt");
+    foreach elem in (v) {
+        elem += polarity ? cast(",", string) : cast(";", string);
+        polarity = !polarity;
+    }
+    local string to_print;
+    foreach elem in (v) {
+        to_print += elem;
+    }
+    to_print = mk_string_f("%s;all sång har", to_print.c_str());
+    assert strcmp(to_print.c_str(),
+                  "människan,vred;,;och glömt,all;visdom,är;förlorad,;"
+                  + "all sång har") == 0;
+
+    local string s1 = "Tystnat.";
+    s1.c_str()[s1.len - 1] = ',';
+    local string s_ = s1 + " och " + "kärleken " + ("med; " + s1);
+    local uint32 prev_len = s_.len;
+    local _dml_string_t *p = cast(&s_, void *);
+    s_.len = 255;
+    assert p->size == 256;
+    s_.len = prev_len;
+    assert prev_len == 36 && p->size == 128; // Shrunk to _DML_BITCEIL(36 + 1)*2
+    s_ = "";
+    assert p->size == 32; // Shrunk to _DML_STRING_INITIAL_SIZE
+    local string *ps = new string;
+    local int *pater = cast({1,2,3}, int[3]);
+    assert pater[1] == 2;
+    try {
+        local const s_t st = {1, "som"};
+        *ps = st.s + " bristande";
+        {
+            local string s2 = " båge";
+            if (*ps == "brinnande")
+                throw;
+            *ps += s2;
+        }
+        _dml_string_addstr(&s1, s1.c_str());
+        *ps = add_strs(*ps, " låga.");
+    } catch;
+
+    do
+        local string s2 = "skriande";
+    while (false);
+    sav.s = "kråka";
+    assert s1 + "SKRIANDE" == "Tystnat,Tystnat,SKRIANDE";
+    assert *ps == "som bristande båge låga.";
+    assert strcmp(cast(this, t).whatdoesthisevenmean,
+                  "En ondskans tid nu domnar min hand") == 0;
+    delete ps;
+
+    local (string sa, string sb) = splitAt("FLYGANDESPJUT", 8);
+    assert sa == "FLYGANDE" && sb == "SPJUT";
+    local char *trab = new char[10];
+    delete trab;
+
+    local (string sST, vect(int) vST) = memo();
+    assert sST == "SOM EN VÄXANDE VÅG";
+    assert vST.len == 20 && vST[10] == 'X';
+
+    local vect(string[3]) sarrv = {{"som","den","kraft"},
+                                   {"en","gång","fanns"},
+                                   {"i ett", "brustet", "svärd"}};
+    assert sarrv[1][1] == "gång";
+
+
+    try {
+        local string s = "hans blick är ";
+        s += ms("som ormens");
+        assert s == "hans blick är som ormens";
+    } catch assert false;
+    {
+        local (string s, vect(int) v) = ("I am", {1, 2});
+        s += " a string";
+        v += {3, 4};
+        assert s == "I am a string" && v.len == 4;
+        for (local uint32 i = 0; i < v.len; ++i) {
+            v[i] = i + 1;
+        }
+        v.len += 3;
+        assert v.len == 7;
+    }
+}
+
+method ms(const char *s) -> (string) throws {
+    return mk_string(s);
+}
+
+method with_string(vect(string) s) {
+    assert s.len == 2 && s[0] == "som" && s[1] == "FALLANDE BÖLJA";
+}
+
+attribute trigger is write_only_attr {
+    param type = "n";
+    method set(attr_value_t val) throws {
+        local vect(string) s = {"som", "FALLANDE BÖLJA"};
+        after 0.1 s: with_string(s);
+    }
+}
+
+
+independent startup memoized method memo() -> (string, vect(int)) {
+    local vect(int) v;
+    local string s = "SOM EN VÄXANDE VÅG";
+    v.len = s.len;
+    for (local int i = 0; i < s.len; ++i) {
+        v[i] = s[i];
+    }
+    return (s, v);
+}
diff --git a/test/1.4/types/raii/T_various.py b/test/1.4/types/raii/T_various.py
new file mode 100644
index 000000000..f713eda7a
--- /dev/null
+++ b/test/1.4/types/raii/T_various.py
@@ -0,0 +1,11 @@
+# © 2023 Intel Corporation
+# SPDX-License-Identifier: MPL-2.0
+
+import stest
+
+cpu = SIM_create_object("clock", "clock", [["freq_mhz", 1]])
+obj.queue = cpu
+
+obj.trigger = None
+SIM_continue(99999)
+SIM_continue(2)
diff --git a/test/1.4/types/raii/hooks_common.dml b/test/1.4/types/raii/hooks_common.dml
new file mode 100644
index 000000000..30877c5cf
--- /dev/null
+++ b/test/1.4/types/raii/hooks_common.dml
@@ -0,0 +1,115 @@
+/*
+  © 2023 Intel Corporation
+  SPDX-License-Identifier: MPL-2.0
+*/
+dml 1.4;
+
+session int count;
+method no_params() {
+    ++count;
+}
+method one_param(int *x) {
+    ++*x;
+}
+method two_params(int *x, vect(int) i) {
+    *x = i[0];
+}
+session int storage;
+method one_serializable_param(vect(int) i) {
+    storage = i[0];
+}
+
+session int storage_indexed;
+session (int last_i_indexed, int last_j_indexed) = (-1, -1);
+group indexed[i < 5][j < 7] {
+    method no_params() {
+        (last_i_indexed, last_j_indexed) = (i, j);
+    }
+    method one_param(int *x) {
+        *x = i*7 + j;
+    }
+    method two_params(int *x, vect(int) coeff) {
+        *x = coeff[0] * (i*7 + j);
+    }
+    method one_serializable_param(vect(int) coeff) {
+        storage_indexed = coeff[0] * (i*7 + j);
+    }
+}
+
+template hookset {
+    hook() _h0;
+    hook(int *) _h1;
+    hook(int *, vect(int)) _h2;
+
+    hook() _h3[6][8];
+    hook(int *) _h4[6][8];
+    hook(int *, vect(int)) _h5[6][8];
+
+    hook(vect(int)) _h6;
+
+    param h0 default _h0;
+    param h1 default _h1;
+    param h2 default _h2;
+    param h3 default _h3;
+    param h4 default _h4;
+    param h5 default _h5;
+    param h6 default _h6;
+}
+
+template hookset_set {
+    is hookset;
+
+    group g[i < 4] is hookset;
+
+    bank b1 is hookset {
+        group g[i < 4] is hookset;
+    }
+    bank b2[i < 3] is hookset {
+        group g[i < 4] is hookset;
+    }
+}
+
+method enforce_h0_ref(hook() h) -> (hook()) {
+    return h;
+}
+
+method enforce_h1_ref(hook(int *) h) -> (hook(int *)) {
+    return h;
+}
+
+method enforce_h2_ref(hook(int *, vect(int)) h) -> (hook(int *, vect(int))) {
+    return h;
+}
+
+method enforce_h6_ref(hook(vect(int)) h) -> (hook(vect(int))) {
+    return h;
+}
+
+group via_hookref is hookset_set {
+    in each hookset {
+        is init;
+        session hook() h3_arr[6][8];
+        session hook(int *) h4_arr[6][8];
+        session hook(int *, vect(int)) h5_arr[6][8];
+        method init() {
+            for (local int idx_0 = 0; idx_0 < 6; ++idx_0) {
+                for (local int idx_1 = 0; idx_1 < 8; ++idx_1) {
+                    h3_arr[idx_0][idx_1] = _h3[idx_0][idx_1];
+                    h4_arr[idx_0][idx_1] = _h4[idx_0][idx_1];
+                    h5_arr[idx_0][idx_1] = _h5[idx_0][idx_1];
+                }
+            }
+        }
+        param h0 = enforce_h0_ref(_h0);
+        param h1 = enforce_h1_ref(_h1);
+        param h2 = enforce_h2_ref(_h2);
+
+        param h3 = h3_arr;
+        param h4 = h4_arr;
+        param h5 = h5_arr;
+
+        param h6 = enforce_h6_ref(_h6);
+    }
+}
+
+is hookset_set;

From 3bfb6f9c5dbe2a79a5db0e40b559857858c5a87e Mon Sep 17 00:00:00 2001
From: Love Waern 
Date: Thu, 28 Sep 2023 16:26:52 +0200
Subject: [PATCH 2/8] Document `.remove` of vectors

---
 doc/1.4/language.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/doc/1.4/language.md b/doc/1.4/language.md
index 904fb641d..0ac9ca5f4 100644
--- a/doc/1.4/language.md
+++ b/doc/1.4/language.md
@@ -1774,6 +1774,13 @@ supports the following operations:
   Inserts _`item`_ at index _`index`_ within _`v`_. This operation is O(n)
   unless _`index`_ is up to one element away from either end of the vector, in
   which case it's O(1) amortized.
+* 
v.remove(index)
+ Remove and return the element at index _`index`_ within _`v`_. This requires + the vector to be non-empty. If that is not upheld, then an assertion will be + failed at run-time. + + This operation is O(n) unless _`index`_ is up to one element away from either + end of the vector, in which case it's O(1) amortized. *
v.len
Evaluates to the length of the vector. From 41261b984f360b4c2896b86a86c08b00504462df Mon Sep 17 00:00:00 2001 From: Love Waern Date: Tue, 3 Oct 2023 11:14:23 +0200 Subject: [PATCH 3/8] Bugfixes, incompatibility fixes, touch-ups --- include/simics/dmllib.h | 6 --- lib/1.2/simics-api.dml | 32 ++++++------- lib/1.4/internal.dml | 32 ++++++------- py/dml/codegen.py | 18 ++++++- py/dml/ctree.py | 79 +++++++++++++++++++++++++------ py/dml/expr.py | 19 ++++---- test/1.4/syntax/T_assign.dml | 6 +++ test/1.4/types/raii/T_various.dml | 10 ++++ 8 files changed, 136 insertions(+), 66 deletions(-) diff --git a/include/simics/dmllib.h b/include/simics/dmllib.h index 2fda5b08c..c8ae58c84 100644 --- a/include/simics/dmllib.h +++ b/include/simics/dmllib.h @@ -3210,12 +3210,6 @@ UNUSED static void _dml_vect_set_compound_init_raii( memcpy(tgt->elements, src, no_elements*elem_size); } -// TODO(RAII): Just replace usages with _dml_vect_append_array? -UNUSED static inline void _dml_vect_append_compound_init_raii( - size_t elem_size, _dml_vect_t *tgt, const void *src, uint32 no_elements) { - _dml_vect_append_array(elem_size, tgt, src, no_elements); -} - // TODO(RAII): Not currently used. #define _DML_RAII_MOVED(x) (({ \ typeof(x) *__val = &(x); \ diff --git a/lib/1.2/simics-api.dml b/lib/1.2/simics-api.dml index bc80d5baa..d8a8080c3 100644 --- a/lib/1.2/simics-api.dml +++ b/lib/1.2/simics-api.dml @@ -48,15 +48,15 @@ extern uint16 CONVERT_BE16(uint16 val); extern uint32 CONVERT_BE32(uint32 val); extern uint64 CONVERT_BE64(uint64 val); -extern uint8 LOAD_LE8 (void *src); -extern uint16 LOAD_LE16(void *src); -extern uint32 LOAD_LE32(void *src); -extern uint64 LOAD_LE64(void *src); +extern uint8 LOAD_LE8 (const void *src); +extern uint16 LOAD_LE16(const void *src); +extern uint32 LOAD_LE32(const void *src); +extern uint64 LOAD_LE64(const void *src); -extern uint8 LOAD_BE8 (void *src); -extern uint16 LOAD_BE16(void *src); -extern uint32 LOAD_BE32(void *src); -extern uint64 LOAD_BE64(void *src); +extern uint8 LOAD_BE8 (const void *src); +extern uint16 LOAD_BE16(const void *src); +extern uint32 LOAD_BE32(const void *src); +extern uint64 LOAD_BE64(const void *src); extern void STORE_LE8 (void *dst, uint8 val); extern void STORE_LE16(void *dst, uint16 val); @@ -68,15 +68,15 @@ extern void STORE_BE16(void *dst, uint16 val); extern void STORE_BE32(void *dst, uint32 val); extern void STORE_BE64(void *dst, uint64 val); -extern uint8 UNALIGNED_LOAD_LE8 (void *src); -extern uint16 UNALIGNED_LOAD_LE16(void *src); -extern uint32 UNALIGNED_LOAD_LE32(void *src); -extern uint64 UNALIGNED_LOAD_LE64(void *src); +extern uint8 UNALIGNED_LOAD_LE8 (const void *src); +extern uint16 UNALIGNED_LOAD_LE16(const void *src); +extern uint32 UNALIGNED_LOAD_LE32(const void *src); +extern uint64 UNALIGNED_LOAD_LE64(const void *src); -extern uint8 UNALIGNED_LOAD_BE8 (void *src); -extern uint16 UNALIGNED_LOAD_BE16(void *src); -extern uint32 UNALIGNED_LOAD_BE32(void *src); -extern uint64 UNALIGNED_LOAD_BE64(void *src); +extern uint8 UNALIGNED_LOAD_BE8 (const void *src); +extern uint16 UNALIGNED_LOAD_BE16(const void *src); +extern uint32 UNALIGNED_LOAD_BE32(const void *src); +extern uint64 UNALIGNED_LOAD_BE64(const void *src); extern void UNALIGNED_STORE_LE8 (void *dst, uint8 val); extern void UNALIGNED_STORE_LE16(void *dst, uint16 val); diff --git a/lib/1.4/internal.dml b/lib/1.4/internal.dml index 5a9051694..a436b507d 100644 --- a/lib/1.4/internal.dml +++ b/lib/1.4/internal.dml @@ -39,15 +39,15 @@ extern uint16 CONVERT_BE16(uint16 val); extern uint32 CONVERT_BE32(uint32 val); extern uint64 CONVERT_BE64(uint64 val); -extern uint8 LOAD_LE8 (void *src); -extern uint16 LOAD_LE16(void *src); -extern uint32 LOAD_LE32(void *src); -extern uint64 LOAD_LE64(void *src); +extern uint8 LOAD_LE8 (const void *src); +extern uint16 LOAD_LE16(const void *src); +extern uint32 LOAD_LE32(const void *src); +extern uint64 LOAD_LE64(const void *src); -extern uint8 LOAD_BE8 (void *src); -extern uint16 LOAD_BE16(void *src); -extern uint32 LOAD_BE32(void *src); -extern uint64 LOAD_BE64(void *src); +extern uint8 LOAD_BE8 (const void *src); +extern uint16 LOAD_BE16(const void *src); +extern uint32 LOAD_BE32(const void *src); +extern uint64 LOAD_BE64(const void *src); extern void STORE_LE8 (void *dst, uint8 val); extern void STORE_LE16(void *dst, uint16 val); @@ -59,15 +59,15 @@ extern void STORE_BE16(void *dst, uint16 val); extern void STORE_BE32(void *dst, uint32 val); extern void STORE_BE64(void *dst, uint64 val); -extern uint8 UNALIGNED_LOAD_LE8 (void *src); -extern uint16 UNALIGNED_LOAD_LE16(void *src); -extern uint32 UNALIGNED_LOAD_LE32(void *src); -extern uint64 UNALIGNED_LOAD_LE64(void *src); +extern uint8 UNALIGNED_LOAD_LE8 (const void *src); +extern uint16 UNALIGNED_LOAD_LE16(const void *src); +extern uint32 UNALIGNED_LOAD_LE32(const void *src); +extern uint64 UNALIGNED_LOAD_LE64(const void *src); -extern uint8 UNALIGNED_LOAD_BE8 (void *src); -extern uint16 UNALIGNED_LOAD_BE16(void *src); -extern uint32 UNALIGNED_LOAD_BE32(void *src); -extern uint64 UNALIGNED_LOAD_BE64(void *src); +extern uint8 UNALIGNED_LOAD_BE8 (const void *src); +extern uint16 UNALIGNED_LOAD_BE16(const void *src); +extern uint32 UNALIGNED_LOAD_BE32(const void *src); +extern uint64 UNALIGNED_LOAD_BE64(const void *src); extern void UNALIGNED_STORE_LE8 (void *dst, uint8 val); extern void UNALIGNED_STORE_LE16(void *dst, uint16 val); diff --git a/py/dml/codegen.py b/py/dml/codegen.py index 854ee6e5f..9e2c5629d 100644 --- a/py/dml/codegen.py +++ b/py/dml/codegen.py @@ -2895,14 +2895,28 @@ def stmt_assignop(stmt, location, scope): if ttype.is_raii: tmp_ret_sym.init = get_initializer(site, ttype, None, location, scope) + # TODO(RAII): Not ideal... 'p += m();' where m is a throwing method + # returning an integer and p is a pointer will fail. method_invocation = try_codegen_invocation( site, [src_ast], [mkLocalVariable(tgt.site, tmp_ret_sym)], location, scope) if method_invocation: src = OrphanWrap(site, mkLocalVariable(tmp_ret_sym.site, tmp_ret_sym)) else: - src = (eval_initializer(site, ttype, src_ast, location, scope, False) - .as_expr(ttype)) + # Only use eval_initializer if we have to. This is because the RHS + # of a binary operator need not be compatible with the LHS, such as + # 'p += 4', where 'p' is a pointer. + # HACK/TODO(RAII): scalar initializers may receive special treatment + # by eval_initializer. Currently, that only applies to string literals, + # which are valid initializers for TString. This one case gets handled + # by mkStringAppend. However, one could imagine additional cases, with + # which one may use a binary operator with, could be added in the + # future. + if src_ast.kind != 'initializer_scalar': + src = (eval_initializer(site, ttype, src_ast, location, scope, + False).as_expr(ttype)) + else: + src = codegen_expression(src_ast.args[0], location, scope) operation = None # TODO(RAII) this is somewhat hacky. diff --git a/py/dml/ctree.py b/py/dml/ctree.py index 9f525ecbe..dcbe247d3 100644 --- a/py/dml/ctree.py +++ b/py/dml/ctree.py @@ -2484,7 +2484,7 @@ def apply(self, inits, location, scope): class StringCStrApply(Expression): priority = dml.expr.Apply.priority - slots = ('type', 'constant', 'value') + slots = ('type',) @auto_init def __init__(self, site, expr): @@ -2493,8 +2493,6 @@ def __init__(self, site, expr): TNamed('char', const=(not expr.writable or safe_realtype_shallow(expr.ctype()).const))) - self.constant = expr.constant - self.value = expr.value if expr.constant else None def __str__(self): return str_expr_pseudomethod(self.expr, 'c_str()') @@ -2502,7 +2500,14 @@ def __str__(self): def read(self): return f'_dml_string_str({self.expr.read()})' + @property + def is_pointer_to_stack_allocation(self): + return self.expr.is_stack_allocated + def mkStringCStrApply(site, expr): + if expr.constant and isinstance(expr, FromCString): + return mkStringConstant(site, expr.value) + if expr.orphan: expr = mkAdoptedOrphan(expr.site, expr) return StringCStrApply(site, expr) @@ -2807,7 +2812,7 @@ def make_simple(cls, site, rh): @property def is_pointer_to_stack_allocation(self): - return self.rh.writable and self.rh.is_stack_allocated + return self.rh.is_stack_allocated def mkAddressOf(site, rh): if dml.globals.compat_dml12_int(site): @@ -4612,11 +4617,14 @@ class StructMember(Expression): priority = 160 explicit_type = True + slots = ('orphan',) + @auto_init def __init__(self, site, expr, sub, type, op): assert not expr.writable or expr.c_lval assert_type(site, expr, Expression) assert_type(site, sub, str) + self.orphan = expr.orphan and op == '.' @property def writable(self): @@ -4643,7 +4651,9 @@ def read(self): @property def is_stack_allocated(self): - return self.expr.writable and self.expr.is_stack_allocated + return (self.expr.is_stack_allocated + if self.op == '.' else + self.expr.is_pointer_to_stack_allocation) @property def is_pointer_to_stack_allocation(self): @@ -4697,7 +4707,12 @@ def structmember_expr(): return mkAdoptedOrphan(expr.site, expr) typ = real_basetype.get_member_qualified(sub) if not typ: raise EMEMBER(site, baseexpr, sub) - return StructMember(site, structmember_expr(), sub, typ, op) + subref = StructMember(site, structmember_expr(), sub, typ, op) + return (AdoptedOrphan(site, subref) + if (subref.orphan + and isinstance(safe_realtype_shallow(typ), TArray)) + else subref) + elif real_basetype.is_int and real_basetype.is_bitfields: member = real_basetype.members.get(sub) if member is None: @@ -4844,7 +4859,9 @@ class ArrayRef(LValue): @auto_init def __init__(self, site, expr, idx): expr_type = realtype_shallow(expr.ctype()) - self.type = conv_const(expr_type.const, expr_type.base) + self.type = conv_const(expr_type.const + and isinstance(expr_type, TArray), + expr_type.base) def __str__(self): return '%s[%s]' % (self.expr, self.idx) def read(self): @@ -4893,6 +4910,8 @@ def mkStringCharRef(site, expr, idx): # Not considered addressable, as the address of an elem is very easily # invalidated. # Users have to use .c_buf() instead to acknowledge that possibility. +# TODO(RAII): users may shoot themselves in the foot anyway, if the basetype +# is an array or a struct with array member. What do we do about that? class VectorRef(Expression): slots = ('type',) priority = dml.expr.Apply.priority @@ -4901,6 +4920,7 @@ class VectorRef(Expression): @auto_init def __init__(self, site, expr, idx): + assert not expr.orphan typ = safe_realtype_shallow(self.expr.ctype()) self.type = conv_const(typ.const, typ.base) @@ -4914,6 +4934,15 @@ def read(self): % (base_typ.declaration(''), self.expr.read(), self.idx.read())) + @property + def is_stack_allocated(self): + return self.expr.is_stack_allocated + + @property + def is_pointer_to_stack_allocation(self): + return (isinstance(safe_realtype_shallow(self.type), TArray) + and self.is_stack_allocated) + def mkVectorRef(site, expr, idx): if idx.constant and idx.value < 0: raise EOOB(expr) @@ -5029,6 +5058,8 @@ def mkCast(site, expr, new_type): # these casts are permitted by C only if old and new are # the same type, which is useless return Cast(site, expr, new_type) + if isinstance(real, (TVoid, TArray, TFunction)): + raise ECAST(site, expr, new_type) if old_type.cmp(real) == 0: if (old_type.is_int and not old_type.is_endian @@ -5041,7 +5072,7 @@ def mkCast(site, expr, new_type): raise ECAST(site, expr, new_type) if isinstance(real, TExternStruct): raise ECAST(site, expr, new_type) - if isinstance(real, (TVoid, TArray, TVector, TTraitList, TFunction)): + if isinstance(real, (TVector, TTraitList)): raise ECAST(site, expr, new_type) if isinstance(old_type, (TVoid, TStruct, TVector, TTraitList, TTrait)): raise ECAST(site, expr, new_type) @@ -5599,20 +5630,27 @@ def read(self): return f'(({self.type.declaration("")}){self.init.read()})' class ArrayCompoundLiteral(Expression): - slots = ('read_adopted',) + slots = ('read_adopted', 'is_stack_allocated') # TODO(RAII) writable? addressable = True c_lval = True @auto_init def __init__(self, site, init, type): assert isinstance(init, (CompoundInitializer, MemsetInitializer)) + self.is_stack_allocated = isinstance(TopRAIIScope.active, + MethodRAIIScope) self.read_adopted = RAIIScope.reserve_orphan_adoption( site, CompoundLiteral(site, init, type)) + def __str__(self): return 'cast(%s, %s)' % (self.init, self.type) def read(self): return self.read_adopted() + @property + def is_pointer_to_stack_allocation(self): + return self.is_stack_allocated + class VectorCompoundLiteral(Orphan): '''Initializer for a variable of vector type, using the {value1, value2, ...} syntax as in C''' @@ -5670,14 +5708,14 @@ def discard(self): class AdoptedOrphan(Expression): priority = dml.expr.Apply.priority - slots = ('read_adopted', 'constant', 'value') + slots = ('read_adopted', 'is_stack_allocated') c_lval = True @auto_init def __init__(self, site, expr): assert expr.orphan + self.is_stack_allocated = isinstance(TopRAIIScope.active, + MethodRAIIScope) self.read_adopted = RAIIScope.reserve_orphan_adoption(site, expr) - self.constant = expr.constant - self.value = expr.value if expr.constant else None def __str__(self): return str(self.expr) @@ -5947,6 +5985,13 @@ def read(): def scope_stack(self): return [self] +# TODO(RAII): Very niche, and currently unleveraged! This is for orphans +# adopted in the constant initializers of sessions/saveds. For example... +# +# session int *p = cast({0, 1, 2, 3}, int[4]); +# +# But no expression using RAIIScope.reserve_orphan_adoption is currently +# considered constant. class SessionRAIIScope(TopRAIIScope): def reserve_orphan_adoption(self, site, expr, subscope): assert subscope is self @@ -6058,7 +6103,6 @@ def toc_stmt(self): class StringCAppend(Statement): @auto_init def __init__(self, site, tgt, src): - assert not src.orphan assert tgt.c_lval def toc_stmt(self): @@ -6070,8 +6114,8 @@ def mkStringAppend(site, tgt, src): raise ERVAL(tgt, '+=') elif deep_const(tgt.ctype()): raise ENCONST(site) - if isinstance(src, FromCString): - return StringCAppend(site, tgt, src.expr) + if isinstance(src, StringConstant): + return StringCAppend(site, tgt, src) return StringAppend(site, tgt, mkAdoptedOrphan(site, src)) class VectorAppend(Statement): @@ -6177,6 +6221,11 @@ def read(self): def ctype(self): return TPtr(self.basetype) + @property + def is_pointer_to_stack_allocation(self): + return self.expr.is_stack_allocated + + mkVectorCBuf = VectorCBufRef class VectorPopRef(PseudoMethodRef): diff --git a/py/dml/expr.py b/py/dml/expr.py index 43e1bde62..2bc3145d1 100644 --- a/py/dml/expr.py +++ b/py/dml/expr.py @@ -231,17 +231,14 @@ def apply(self, inits, location, scope): @property def is_stack_allocated(self): - '''Returns true only if it's known that writing to this expression will - write to stack-allocated data. - This may only be called if the expression is writable or - addressable''' - assert self.writable or self.addressable - return False + '''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 self.orphan @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): @@ -306,12 +303,12 @@ def __str__(self): return self.str or self.cexpr def read(self): return self.cexpr - def write(self, source): - assert self.writable - return source.assign_to(self.cexpr, self.type) @property def writable(self): - return self.type is not None + return self.c_lval + @property + def addressable(self): + return self.c_lval @property def c_lval(self): return self.type is not None diff --git a/test/1.4/syntax/T_assign.dml b/test/1.4/syntax/T_assign.dml index 1eb5f7764..84db555f6 100644 --- a/test/1.4/syntax/T_assign.dml +++ b/test/1.4/syntax/T_assign.dml @@ -83,4 +83,10 @@ method init() { j = 0; (i, j) = (j, i); assert i == 0 && j == 1; + + // += with target, source of nonequal types but compatible with + + local int arr[4] = {0, 1, 2, 3}; + local int *p = arr; + p += 2; + assert *p == 2; } diff --git a/test/1.4/types/raii/T_various.dml b/test/1.4/types/raii/T_various.dml index 31cb7ce42..22829908f 100644 --- a/test/1.4/types/raii/T_various.dml +++ b/test/1.4/types/raii/T_various.dml @@ -186,6 +186,16 @@ method init() { v.len += 3; assert v.len == 7; } + local int *chunks_p = mk_chunks(1, 2).x; + assert chunks_p[0] == 1 && chunks_p[1] == 2; +} + +typedef struct { + int x[2]; +} chunks_t; + +independent method mk_chunks(int x, int y) -> (chunks_t) { + return {{x, y}}; } method ms(const char *s) -> (string) throws { From c541647dc7afa8da8e17b3d3cc3ceef185ea7db8 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Tue, 3 Oct 2023 15:18:55 +0200 Subject: [PATCH 4/8] Unconst input and output args when checking method overrides --- py/dml/c_backend.py | 6 +++--- py/dml/ctree.py | 4 ++-- py/dml/structure.py | 6 ++++-- py/dml/traits.py | 4 ++-- py/dml/types.py | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/py/dml/c_backend.py b/py/dml/c_backend.py index 4299f53a0..1ccec5fe5 100644 --- a/py/dml/c_backend.py +++ b/py/dml/c_backend.py @@ -811,13 +811,13 @@ def generate_implement_method(device, ifacestruct, meth, indices): # method in DML raise EMETH(meth.site, None, 'interface method is variadic') for ((mp, mt), it) in zip(meth.inp, iface_input_types): - if safe_realtype(mt).cmp(safe_realtype(it)) != 0: + if safe_realtype_unconst(mt).cmp(safe_realtype_unconst(it)) != 0: raise EARGT(meth.site, 'implement', meth.name, mt, mp, it, 'method') if iface_num_outputs and dml.globals.dml_version != (1, 2): [(_, mt)] = meth.outp - if safe_realtype(mt).cmp( - safe_realtype(ifacemethtype.output_type)) != 0: + if safe_realtype_unconst(mt).cmp( + safe_realtype_unconst(ifacemethtype.output_type)) != 0: raise EARGT(meth.site, 'implement', meth.name, mt, '', ifacemethtype.output_type, 'method') diff --git a/py/dml/ctree.py b/py/dml/ctree.py index dcbe247d3..86026bc2f 100644 --- a/py/dml/ctree.py +++ b/py/dml/ctree.py @@ -3160,8 +3160,8 @@ def mkInterfaceMethodRef(site, iface_node, indices, method_name): if (not isinstance(ftype, TFunction) or not ftype.input_types - or TPtr(safe_realtype(TNamed('conf_object_t'))).cmp( - safe_realtype(ftype.input_types[0])) != 0): + or TPtr(safe_realtype_unconst(TNamed('conf_object_t'))).cmp( + safe_realtype_unconst(ftype.input_types[0])) != 0): # non-method members are not accessible raise EMEMBER(site, struct_name, method_name) diff --git a/py/dml/structure.py b/py/dml/structure.py index 94bed4c39..f72f2a313 100644 --- a/py/dml/structure.py +++ b/py/dml/structure.py @@ -674,7 +674,8 @@ def typecheck_method_override(m1, m2): # TODO move to caller (_, type1) = eval_type(t1, a1.site, None, global_scope) (_, type2) = eval_type(t2, a2.site, None, global_scope) - if safe_realtype(type1).cmp(safe_realtype(type2)) != 0: + if safe_realtype_unconst(type1).cmp( + safe_realtype_unconst(type2)) != 0: raise EMETH(a1.site, a2.site, f"mismatching types in input argument {n1}") @@ -683,7 +684,8 @@ def typecheck_method_override(m1, m2): ((n1, t1), (n2, t2)) = (a1.args, a2.args) (_, type1) = eval_type(t1, a1.site, None, global_scope) (_, type2) = eval_type(t2, a2.site, None, global_scope) - if safe_realtype(type1).cmp(safe_realtype(type2)) != 0: + if safe_realtype_unconst(type1).cmp( + safe_realtype_unconst(type2)) != 0: msg = "mismatching types in return value" if len(outp1) > 1: msg += f" {i + 1}" diff --git a/py/dml/traits.py b/py/dml/traits.py index 265c386eb..60e636954 100644 --- a/py/dml/traits.py +++ b/py/dml/traits.py @@ -394,11 +394,11 @@ def typecheck_method_override(left, right): if throws0 != throws1: raise EMETH(site0, site1, "different nothrow annotations") for ((n, t0), (_, t1)) in zip(inp0, inp1): - if realtype(t0).cmp(realtype(t1)) != 0: + if safe_realtype_unconst(t0).cmp(safe_realtype_unconst(t1)) != 0: raise EMETH(site0, site1, "mismatching types in input argument %s" % (n,)) for (i, ((_, t0), (_, t1))) in enumerate(zip(outp0, outp1)): - if realtype(t0).cmp(realtype(t1)) != 0: + if safe_realtype_unconst(t0).cmp(safe_realtype_unconst(t1)) != 0: raise EMETH(site0, site1, "mismatching types in output argument %d" % (i + 1,)) diff --git a/py/dml/types.py b/py/dml/types.py index ab8e978bd..49e46ebad 100644 --- a/py/dml/types.py +++ b/py/dml/types.py @@ -886,14 +886,14 @@ def cmp_fuzzy(self, other, consider_quals=False): cconst = conv_const if consider_quals else lambda c, t: t if not dml.globals.compat_dml12: if isinstance(other, (TArray, TPtr)): - return cconst(self.const, self.base).cmp( + return cconst(self.const, self.base).cmp_fuzzy( cconst(other.const and isinstance(other, TArray), other.base), consider_quals) elif isinstance(other, (TPtr, TArray)): if self.base.void or other.base.void: return 0 - if (cconst(self.const, self.base).cmp( + if (cconst(self.const, self.base).cmp_fuzzy( cconst(other.const and isinstance(other, TArray), other.base), consider_quals) == 0): return 0 From 4a7b4f4401a9b3c014a3b8b3e5a7958f92803227 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Tue, 3 Oct 2023 18:55:02 +0200 Subject: [PATCH 5/8] Make enriched new/delete opt-in --- doc/1.4/language.md | 49 ++++++++++++++++++++++++++----- py/dml/codegen.py | 30 ++++++++++++++++--- py/dml/ctree.py | 43 ++++++++++++++++++++++++--- py/dml/dmlparse.py | 30 +++++++++++++++---- py/dml/messages.py | 28 ++++++++++++++++++ test/1.4/types/raii/T_various.dml | 6 ++-- 6 files changed, 161 insertions(+), 25 deletions(-) diff --git a/doc/1.4/language.md b/doc/1.4/language.md index 0ac9ca5f4..8efd961ae 100644 --- a/doc/1.4/language.md +++ b/doc/1.4/language.md @@ -3478,13 +3478,25 @@ method n() -> (bool, int) {
 delete expr;
+delete\<spec\> expr;
 
-Deallocates the memory pointed to by the result of evaluating -*`expr`*. The memory must have been allocated with the -`new` operator, and must not have been deallocated previously. -Equivalent to `delete` in C++; however, in DML, `delete` -can only be used as a statement, not as an expression. +Deallocates the memory pointed to by the result of evaluating *`expr`*. + +*spec* specifies an *allocation format*, and can either be `enriched` or +`extern`. If not explicitly specified, *spec* will default to `extern`. This is +for backwards compatibility reasons — in the future the default will be +changed to be `enriched`. + +The *enriched* format uses an allocation format specific to the device model, +and so can *only* be used in order to deallocate storage previously allocated +via [`new`](#new-expressions) by the same device model. + +The *extern* format compiles the `delete` statement to a use of `MM_FREE`, +meaning it may be used to deallocate storage previously allocated by any use of +Simics's memory allocation functions/macros (such as `MM_MALLOC`.) This includes +storage allocated via [`new`](#new-expressions) (which `new` without +allocation format specifier is equivalent to). ### Try Statements @@ -4145,16 +4157,39 @@ independent method callback(int i, void *aux) { new type new type[count] + +new\<spec\> type + +new\<spec\> type[count]
Allocates a chunk of memory large enough for a value of the specified -type. If the second form is used, memory for *count* values will +type. If a form specifying *count* is used, then memory for *count* values will be allocated. The result is a pointer to the allocated memory. (The pointer is never null; if allocation should fail, the Simics application will be terminated.) +*spec* specifies an *allocation format*, and can either be `enriched` or +`extern`. If not explicitly specified, *spec* will default to `extern`. This is +for backwards compatibility reasons — in the future the default will be +changed to be `enriched`. + +The *enriched* format uses an allocation format specific to the device model, +and *must* be used in order to allocate storage for values of [resource-enriched +(RAII) type](#raii-types). The fact the allocation format is model-specific +comes with the drawback that a pointer created with `new` *cannot be +freed* using `MM_FREE`/`free`: only code from the same device model can free it, +and only by using [`delete`](#delete-statements). + +The *extern* format compiles `new` to a use of `MM_ZALLOC`, meaning a pointer +allocated this way may be freed using `MM_FREE` outside of the device model. +However, this format does not support allocating storage for values of +resource-enriched type. + When the memory is no longer needed, it should be deallocated using a -`delete` statement. +[`delete` statement](#delete-statements). The allocation format specified for the +`delete` statement *must* match that of the `new` expression used to allocate +the pointer. ### Cast Expressions diff --git a/py/dml/codegen.py b/py/dml/codegen.py index 9e2c5629d..ff1789114 100644 --- a/py/dml/codegen.py +++ b/py/dml/codegen.py @@ -1600,13 +1600,22 @@ def expr_typeop(tree, location, scope): @expression_dispatcher def expr_new(tree, location, scope): - [t, count] = tree.args + [spec, t, count] = tree.args (struct_defs, t) = eval_type(t, tree.site, location, scope) + if t.is_raii and spec != 'enriched': + report(ENEWRAII(tree.site, t.describe(), + 'new' + f'<{spec}>'*(spec is not None))) + spec = 'enriched' + for (site, _) in struct_defs: report(EANONSTRUCT(site, "'new' expression")) if count: count = codegen_expression(count, location, scope) - return mkNew(tree.site, t, count) + if spec == 'enriched': + return mkNew(tree.site, t, count) + else: + assert spec is None or spec == 'extern' + return mkNewExtern(tree.site, t, count) @expression_dispatcher def expr_apply(tree, location, scope): @@ -3107,9 +3116,22 @@ def stmt_default(stmt, location, scope): @statement_dispatcher def stmt_delete(stmt, location, scope): - [expr] = stmt.args + [spec, expr] = stmt.args expr = codegen_expression(expr, location, scope) - return [mkDelete(stmt.site, expr)] + etype = safe_realtype_shallow(expr.ctype()) + if not isinstance(etype, TPtr): + raise ENOPTR(stmt.site, expr) + if etype.base.is_raii and spec != 'enriched': + report(EDELETERAII(stmt.site, + etype.base.describe(), + 'delete' + f'<{spec}>'*(spec is not None))) + spec = 'enriched' + + if spec == 'enriched': + return [mkDelete(stmt.site, expr)] + else: + assert spec is None or spec == 'extern' + return [mkDeleteExtern(stmt.site, expr)] def probable_loggroups_specification(expr): subexprs = [expr] diff --git a/py/dml/ctree.py b/py/dml/ctree.py index 86026bc2f..6ead70bf6 100644 --- a/py/dml/ctree.py +++ b/py/dml/ctree.py @@ -51,6 +51,7 @@ 'mkAssert', 'mkReturn', 'mkDelete', + 'mkDeleteExtern', 'mkExpressionStatement', 'mkAfter', 'mkAfterOnHook', @@ -126,6 +127,7 @@ 'mkHookSendRef', 'HookSendRef', 'mkHookSendApply', 'HookSendApply', 'mkNew', + 'mkNewExtern', #'Constant', 'mkIntegerConstant', 'IntegerConstant', 'mkIntegerLiteral', @@ -582,8 +584,16 @@ def toc_stmt(self): self.linemark() out(f'DML_DELETE({self.expr.read()});\n') -def mkDelete(site, expr): - return Delete(site, expr) +mkDelete = Delete + +class DeleteExtern(Statement): + @auto_init + def __init__(self, site, expr): pass + def toc_stmt(self): + self.linemark() + out(f'MM_FREE({self.expr.read()});\n') + +mkDeleteExtern = DeleteExtern class ExpressionStatement(Statement): @auto_init @@ -3329,9 +3339,9 @@ def __init__(self, site, newtype, count, raii_info): self.type = TPtr(newtype) def __str__(self): if self.count: - return 'new %s[%s]' % (self.newtype, self.count) + return 'new %s[%s]' % (self.newtype, self.count) else: - return 'new %s' % self.newtype + return 'new %s' % self.newtype def read(self): destructor = (self.raii_info.cident_destructor_array_item if self.raii_info else '_dml_raii_destructor_ref_none') @@ -3349,6 +3359,31 @@ def mkNew(site, newtype, count = None): info = None return New(site, newtype, count, info) +class NewExtern(Expression): + priority = 160 # f() + slots = ('type',) + @auto_init + def __init__(self, site, newtype, count): + self.type = TPtr(newtype) + def __str__(self): + if self.count: + return 'new %s[%s]' % (self.newtype, self.count) + else: + return 'new %s' % self.newtype + def read(self): + t = self.newtype.declaration('') + if self.count: + return 'MM_ZALLOC(%s, %s)' % (self.count.read(), t) + else: + return 'MM_ZALLOC(1, %s)' % (t) + + +def mkNewExtern(site, newtype, count = None): + assert not newtype.is_raii + if count: + count = as_int(count) + return NewExtern(site, newtype, count) + class ListItems(metaclass=abc.ABCMeta): '''A series of consecutive list elements, where each list element corresponds to one index in a multi-dimensional (but possibly diff --git a/py/dml/dmlparse.py b/py/dml/dmlparse.py index 34b5bf25b..005bea610 100644 --- a/py/dml/dmlparse.py +++ b/py/dml/dmlparse.py @@ -1824,15 +1824,33 @@ def typeop_arg_par(t): '''typeoparg : LPAREN ctypedecl RPAREN''' t[0] = t[2] +@prod_dml14 +def maybe_newdelete_spec_yes(t): + '''maybe_newdelete_spec : LT ID GT + | LT EXTERN GT''' + supported_specs = ('extern', 'enriched') + spec = t[2] + if spec not in supported_specs: + suggestions = ' or '.join(f"'{spec}'" for spec in supported_specs) + report(ESYNTAX(site(t, 1), spec, + f"expected new/delete specification ({suggestions})")) + spec = 'enriched' + t[0] = spec + +@prod +def maybe_newdelete_spec_no(t): + '''maybe_newdelete_spec : ''' + t[0] = None + @prod def expression_new(t): - '''expression : NEW ctypedecl''' - t[0] = ast.new(site(t), t[2], None) + '''expression : NEW maybe_newdelete_spec ctypedecl''' + t[0] = ast.new(site(t), t[2], t[3], None) @prod def expression_new_array(t): - '''expression : NEW ctypedecl LBRACKET expression RBRACKET''' - t[0] = ast.new(site(t), t[2], t[4]) + '''expression : NEW maybe_newdelete_spec ctypedecl LBRACKET expression RBRACKET''' + t[0] = ast.new(site(t), t[2], t[3], t[5]) @prod def expression_paren(t): @@ -2200,8 +2218,8 @@ def case_blocks_list_hashifelse(t): # Delete is an expression in C++, not a statement, but we don't care. @prod def statement_delete(t): - 'statement_except_hashif : DELETE expression SEMI' - t[0] = ast.delete(site(t), t[2]) + 'statement_except_hashif : DELETE maybe_newdelete_spec expression SEMI' + t[0] = ast.delete(site(t), t[2], t[3]) @prod def statent_try(t): diff --git a/py/dml/messages.py b/py/dml/messages.py index c8e660fdb..3a0f934fa 100644 --- a/py/dml/messages.py +++ b/py/dml/messages.py @@ -1603,6 +1603,34 @@ class EANONRAIISTRUCT(DMLError): fmt = ("method-local anonymous struct declared that has a member of " + "resource-enriched (RAII) type") +class ENEWRAII(DMLError): + """ + A `new` expression not specified as `enriched` can't be used to allocate a + storage for values of resource-enriched (RAII) type. + To address this, use `new` instead of `new` or `new`. + """ + fmt = ("'new' expression not specified as 'enriched' used to create " + + "pointer with resource-enriched (RAII) basetype '%s'. To address " + + "this, use 'new' instead of '%s', and ensure any " + + "pointer allocated this way is only deallocated using " + + "'delete'!") + +class EDELETERAII(DMLError): + """ + A `delete` statement not specified as `enriched` was used on a pointer with + resource-enriched (RAII) basetype. Except for extremely niche cases, this + is incorrect: an allocated pointer of resource-enriched basetype can only + be validly created through a `new` expression specified as `enriched`. + + To address this, use `delete` instead of `delete` or + `delete`. + """ + version = "1.4" + fmt = ("'delete' statement not specified as 'enriched' used on pointer " + + "with resource-enriched (RAII) basetype '%s'. " + + "To address this, use 'delete' instead of '%s', and " + + "ensure any pointer deallocated through this 'delete' is only " + + "allocated using 'new'!") class EIDENTSIZEOF(DMLError): """ diff --git a/test/1.4/types/raii/T_various.dml b/test/1.4/types/raii/T_various.dml index 22829908f..e6474806e 100644 --- a/test/1.4/types/raii/T_various.dml +++ b/test/1.4/types/raii/T_various.dml @@ -129,7 +129,7 @@ method init() { assert prev_len == 36 && p->size == 128; // Shrunk to _DML_BITCEIL(36 + 1)*2 s_ = ""; assert p->size == 32; // Shrunk to _DML_STRING_INITIAL_SIZE - local string *ps = new string; + local string *ps = new string; local int *pater = cast({1,2,3}, int[3]); assert pater[1] == 2; try { @@ -153,12 +153,10 @@ method init() { assert *ps == "som bristande båge låga."; assert strcmp(cast(this, t).whatdoesthisevenmean, "En ondskans tid nu domnar min hand") == 0; - delete ps; + delete ps; local (string sa, string sb) = splitAt("FLYGANDESPJUT", 8); assert sa == "FLYGANDE" && sb == "SPJUT"; - local char *trab = new char[10]; - delete trab; local (string sST, vect(int) vST) = memo(); assert sST == "SOM EN VÄXANDE VÅG"; From 0934138358f7b23480f3717563148b58eb0a24d3 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Wed, 4 Oct 2023 11:16:51 +0200 Subject: [PATCH 6/8] Fix MODULEINFO, rename header files to follow convention --- MODULEINFO | 6 ++++-- Makefile | 2 +- RELEASENOTES.docu | 6 ++++++ include/simics/{dmllib.h => dml-lib.h} | 6 +++--- include/simics/{dmlraiitypes.h => dml-raii-types.h} | 4 ++-- lib/1.2/dml-builtins.dml | 2 +- lib/1.4/dml-builtins.dml | 2 +- py/dml/ctree-test.h | 2 +- py/dml/ctree.py | 2 +- py/dml/types.py | 2 +- 10 files changed, 21 insertions(+), 13 deletions(-) rename include/simics/{dmllib.h => dml-lib.h} (99%) rename include/simics/{dmlraiitypes.h => dml-raii-types.h} (99%) diff --git a/MODULEINFO b/MODULEINFO index 90295af9c..aa9c8fdbe 100644 --- a/MODULEINFO +++ b/MODULEINFO @@ -89,7 +89,8 @@ Group: dmlc-lib/6 Require-tokens: public Make: dmlc $(HOST)/bin/dml/include/simics/LICENSE - $(HOST)/bin/dml/include/simics/dmllib.h + $(HOST)/bin/dml/include/simics/dml-lib.h + $(HOST)/bin/dml/include/simics/dml-raii-types.h $(HOST)/bin/dml-old-4.8/1.2/LICENSE $(HOST)/bin/dml-old-4.8/1.4/LICENSE $(HOST)/bin/dml/1.2/LICENSE @@ -146,7 +147,8 @@ Group: dmlc-lib/7 Require-tokens: public Make: dmlc $(HOST)/bin/dml/include/simics/LICENSE - $(HOST)/bin/dml/include/simics/dmllib.h + $(HOST)/bin/dml/include/simics/dml-lib.h + $(HOST)/bin/dml/include/simics/dml-raii-types.h $(HOST)/bin/dml/1.2/LICENSE $(HOST)/bin/dml/1.4/LICENSE # dml files available in all APIs diff --git a/Makefile b/Makefile index 945f3ba2a..d1491520c 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ SCRIPTS := $(PYTHONPATH)/port_dml.py MPL_LICENSE := $(PYTHONPATH)/LICENSE BSD0_LICENSES := $(addsuffix /LICENSE,$(DMLLIB_DESTDIRS) $(OLD_DMLLIB_DESTDIRS_4_8) $(DMLLIB_DEST)/include/simics) -HFILES := $(DMLLIB_DEST)/include/simics/dmllib.h $(DMLLIB_DEST)/include/simics/dmlraiitypes.h +HFILES := $(DMLLIB_DEST)/include/simics/dml-lib.h $(DMLLIB_DEST)/include/simics/dml-raii-types.h DMLC_BIN := $(OUT_PYFILES) $(OUT_GEN_PYFILES) $(HFILES) diff --git a/RELEASENOTES.docu b/RELEASENOTES.docu index f671560ed..9e9e49fec 100644 --- a/RELEASENOTES.docu +++ b/RELEASENOTES.docu @@ -1730,4 +1730,10 @@ extern typedef struct { } my_type_t; This warning is only enabled by default with Simics API version 7 or above. With version 6 and below it must be explicitly enabled by passing --warn=WLOGMIXUP to DMLC. + The file dmllib.h, needed + to compile DMLC-generated C files, has been renamed to dml-lib.h. + In addition, a new header file called dml-raii-types.h has been + introduced which is also needed to compile DMLC-generated C files. Both + of these are located in host/bin/dml/include/simics. + diff --git a/include/simics/dmllib.h b/include/simics/dml-lib.h similarity index 99% rename from include/simics/dmllib.h rename to include/simics/dml-lib.h index c8ae58c84..4266875ff 100644 --- a/include/simics/dmllib.h +++ b/include/simics/dml-lib.h @@ -5,8 +5,8 @@ /* DML runtime utilities needed by the C code generated by dmlc */ -#ifndef SIMICS_DMLLIB_H -#define SIMICS_DMLLIB_H +#ifndef SIMICS_DML_LIB_H +#define SIMICS_DML_LIB_H #include @@ -24,7 +24,7 @@ #include #include -#include +#include // Copy bits from y given by mask into corresponding bits in x and return the // result diff --git a/include/simics/dmlraiitypes.h b/include/simics/dml-raii-types.h similarity index 99% rename from include/simics/dmlraiitypes.h rename to include/simics/dml-raii-types.h index b09f93b3d..4e9629f1e 100644 --- a/include/simics/dmlraiitypes.h +++ b/include/simics/dml-raii-types.h @@ -5,8 +5,8 @@ /* DML runtime utilities needed by the C code generated by dmlc */ -#ifndef SIMICS_DMLRAIITYPES_H -#define SIMICS_DMLRAIITYPES_H +#ifndef SIMICS_DML_RAII_TYPES_H +#define SIMICS_DML_RAII_TYPES_H #include #include diff --git a/lib/1.2/dml-builtins.dml b/lib/1.2/dml-builtins.dml index c7766e97b..35d5aa670 100644 --- a/lib/1.2/dml-builtins.dml +++ b/lib/1.2/dml-builtins.dml @@ -11,7 +11,7 @@ loggroup Register_Read; loggroup Register_Write; header %{ - #include + #include %} import "simics-api.dml"; diff --git a/lib/1.4/dml-builtins.dml b/lib/1.4/dml-builtins.dml index e45bb8d73..00eb8fb3d 100644 --- a/lib/1.4/dml-builtins.dml +++ b/lib/1.4/dml-builtins.dml @@ -10,7 +10,7 @@ bitorder le; loggroup Register_Read; loggroup Register_Write; header %{ - #include + #include %} import "simics/C.dml"; diff --git a/py/dml/ctree-test.h b/py/dml/ctree-test.h index 1f6918c64..95598ddd2 100644 --- a/py/dml/ctree-test.h +++ b/py/dml/ctree-test.h @@ -15,7 +15,7 @@ static void capture_assert_error(int line, const char *file, static void VT_critical_error(const char *short_msg, const char *long_msg); #include -#include +#include api_function_t SIM_get_api_function(const char *name) { return NULL; } diff --git a/py/dml/ctree.py b/py/dml/ctree.py index 6ead70bf6..1e70d3642 100644 --- a/py/dml/ctree.py +++ b/py/dml/ctree.py @@ -1363,7 +1363,7 @@ class Compare(BinOp): @abc.abstractproperty def cmp_functions(self): - '''pair of dmllib.h functions for comparison between signed and + '''pair of dml-lib.h functions for comparison between signed and unsigned integer, with (int, uint) and (uint, int) args, respectively''' diff --git a/py/dml/types.py b/py/dml/types.py index 49e46ebad..2708f9195 100644 --- a/py/dml/types.py +++ b/py/dml/types.py @@ -737,7 +737,7 @@ def declaration(self, var): class TEndianInt(IntegerType): '''An integer where the byte storage order is defined. Corresponds to the (u)?intX_[be|le] family of types defined in - dmllib.h + dml-lib.h ''' __slots__ = ('byte_order',) def __init__(self, bits, signed, byte_order, members = None, const = False): From 6b78d79a051e0d8781bf9d48cdc3448ee68450bb Mon Sep 17 00:00:00 2001 From: Love Waern Date: Wed, 4 Oct 2023 13:50:51 +0200 Subject: [PATCH 7/8] Fix UB caught by valgrind and sanitizers --- include/simics/dml-raii-types.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/simics/dml-raii-types.h b/include/simics/dml-raii-types.h index 4e9629f1e..9ee869600 100644 --- a/include/simics/dml-raii-types.h +++ b/include/simics/dml-raii-types.h @@ -103,11 +103,12 @@ _dml_string_realloc_for_len(_dml_string_t *s, uint32 new_len) { UNUSED static void _dml_string_resize(_dml_string_t *s, uint32 new_len) { uint32 prev_len = s->len; + uint32 prev_size = s->size; if (prev_len == new_len) return; _dml_string_realloc_for_len(s, new_len); s->len = new_len; if (s->size) { - if (prev_len > new_len) { + if (prev_size == 0 || prev_len > new_len) { s->s[new_len] = '\0'; } else { memset(s->s + prev_len + 1, 0, new_len - prev_len); @@ -640,7 +641,7 @@ _dml_vect_realloc(size_t elem_size, _dml_vect_t *v, uint32 new_size) { if (new_size > v->size) { ASSERT(new_size >= v->start + v->len); v->elements = MM_REALLOC_SZ(v->elements, new_size*elem_size, char); - if (v->start + v->len > v->size) + if (v->size && v->start + v->len > v->size) memcpy(v->elements + v->size*elem_size, v->elements, (v->len - (v->size - v->start))*elem_size); @@ -960,6 +961,7 @@ _dml_vect_copy_to_index(size_t elem_size, _dml_vect_t *tgt, _dml_vect_t src, // Aliasing must be dealt with before this call! ASSERT(src.elements != tgt->elements); ASSERT(i + src.len <= tgt->len); + if (src.len == 0) return; uint32 tgt_start = DML_VECT_INDEX(*tgt, i); uint32 src_start_len = MIN(src.len, src.size - src.start); From 640a747c3a73d70c76dc773e1f87a5cdcb82d240 Mon Sep 17 00:00:00 2001 From: Love Waern Date: Wed, 4 Oct 2023 13:58:08 +0200 Subject: [PATCH 8/8] Make `enriched` default `new`/`delete` spec instead of `extern` --- py/dml/codegen.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/py/dml/codegen.py b/py/dml/codegen.py index ff1789114..297eb0dae 100644 --- a/py/dml/codegen.py +++ b/py/dml/codegen.py @@ -1602,20 +1602,20 @@ def expr_typeop(tree, location, scope): def expr_new(tree, location, scope): [spec, t, count] = tree.args (struct_defs, t) = eval_type(t, tree.site, location, scope) - if t.is_raii and spec != 'enriched': + if t.is_raii and spec == 'extern': report(ENEWRAII(tree.site, t.describe(), - 'new' + f'<{spec}>'*(spec is not None))) + 'new')) spec = 'enriched' for (site, _) in struct_defs: report(EANONSTRUCT(site, "'new' expression")) if count: count = codegen_expression(count, location, scope) - if spec == 'enriched': - return mkNew(tree.site, t, count) - else: - assert spec is None or spec == 'extern' + if spec == 'extern': return mkNewExtern(tree.site, t, count) + else: + assert spec is None or spec == 'enriched' + return mkNew(tree.site, t, count) @expression_dispatcher def expr_apply(tree, location, scope): @@ -3121,17 +3121,17 @@ def stmt_delete(stmt, location, scope): etype = safe_realtype_shallow(expr.ctype()) if not isinstance(etype, TPtr): raise ENOPTR(stmt.site, expr) - if etype.base.is_raii and spec != 'enriched': + if etype.base.is_raii and spec == 'extern': report(EDELETERAII(stmt.site, etype.base.describe(), - 'delete' + f'<{spec}>'*(spec is not None))) + 'delete')) spec = 'enriched' - if spec == 'enriched': - return [mkDelete(stmt.site, expr)] - else: - assert spec is None or spec == 'extern' + if spec == 'extern': return [mkDeleteExtern(stmt.site, expr)] + else: + assert spec is None or spec == 'enriched' + return [mkDelete(stmt.site, expr)] def probable_loggroups_specification(expr): subexprs = [expr]