Repository: biojppm/rapidyaml (language: python)

Issue:
Use of debugbreak should be opt-in
There are two minor issues when including yml.hpp, when using sources from the 0.5.0 tag:

- I have to `#define C4_NO_DEBUG_BREAK` before inclusion otherwise in c4core (tag 0.11.0), a non-existing header c4/ext/debugbreak.h is included. 
- I have to `#pragma GCC diagnostic ignored "-Wold-style-cast"` to inhibit some g++ warnings.

While writing this ticket, I realized that the tar.gz of c4core 0.11.0 available on github does not contain files from external repositories, explaining why the header is missing. You may thus ignore this part (but why would a yaml library call `__debugbreak` ?).

For the second part, using `-Wno-old-style-cast` is of course specific to my project, but maybe others will use it and I have seen code to disable some warnings in rapidyaml, so maybe you will want to add this one.

Regards
Use of debugbreak should be opt-in
There are two minor issues when including yml.hpp, when using sources from the 0.5.0 tag:

- I have to `#define C4_NO_DEBUG_BREAK` before inclusion otherwise in c4core (tag 0.11.0), a non-existing header c4/ext/debugbreak.h is included. 
- I have to `#pragma GCC diagnostic ignored "-Wold-style-cast"` to inhibit some g++ warnings.

While writing this ticket, I realized that the tar.gz of c4core 0.11.0 available on github does not contain files from external repositories, explaining why the header is missing. You may thus ignore this part (but why would a yaml library call `__debugbreak` ?).

For the second part, using `-Wno-old-style-cast` is of course specific to my project, but maybe others will use it and I have seen code to disable some warnings in rapidyaml, so maybe you will want to add this one.

Regards


Discussion / hints:
>  the tar.gz of c4core 0.11.0 available on github does not contain files from external repositories

When a release is made, the auto-generated source tarball from github unfortunately never includes submodule repos. So you need to pick a different file to get a properly working source tarball:

![image](https://user-images.githubusercontent.com/325344/229196099-dfab0ac1-fbfc-4ad5-bf3b-ed6ca83d7e2d.png)

This also applies to the rapidyaml repo. If you pick the equivalent rapidyaml file, it will bring the c4core source, and there's no reason for you to have to get c4core separately.

> but why would a yaml library call __debugbreak ?

Fair point.

rapidyaml uses c4core, and c4core provides/uses `debugbreak` in debug builds. I chose to put it as opt-out because I use it heavily when debugging. In release builds, it goes away (and if it doesn't that's a bug), which is generally what most users will be looking for. So I never found it to be on my way.

However, on hindsight, I agree that it should be opt-in. I will revise that, and leave this issue open to track that.

For the C-cast warnings, let's track it in a different issue.
> * I have to `#pragma GCC diagnostic ignored "-Wold-style-cast"` to inhibit some g++ warnings.


Thank you for the explanation about the tar files.
>  the tar.gz of c4core 0.11.0 available on github does not contain files from external repositories

When a release is made, the auto-generated source tarball from github unfortunately never includes submodule repos. So you need to pick a different file to get a properly working source tarball:

![image](https://user-images.githubusercontent.com/325344/229196099-dfab0ac1-fbfc-4ad5-bf3b-ed6ca83d7e2d.png)

This also applies to the rapidyaml repo. If you pick the equivalent rapidyaml file, it will bring the c4core source, and there's no reason for you to have to get c4core separately.

> but why would a yaml library call __debugbreak ?

Fair point.

rapidyaml uses c4core, and c4core provides/uses `debugbreak` in debug builds. I chose to put it as opt-out because I use it heavily when debugging. In release builds, it goes away (and if it doesn't that's a bug), which is generally what most users will be looking for. So I never found it to be on my way.

However, on hindsight, I agree that it should be opt-in. I will revise that, and leave this issue open to track that.

For the C-cast warnings, let's track it in a different issue.
> * I have to `#pragma GCC diagnostic ignored "-Wold-style-cast"` to inhibit some g++ warnings.


Thank you for the explanation about the tar files.

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 11c52e064..888a45d15 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,7 +14,11 @@ c4_project(VERSION 0.5.0 STANDALONE
 
 option(RYML_WITH_TAB_TOKENS "Enable parsing of tabs after ':' and '-'. This is costly and disabled by default." OFF)
 option(RYML_DEFAULT_CALLBACKS "Enable ryml's default implementation of callbacks: allocate(), free(), error()" ON)
-option(RYML_BUILD_TOOLS "build tools" OFF)
+if(RYML_DEFAULT_CALLBACKS)
+    option(RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS "Throw exceptions instead of calling abort in the default error handler provided by ryml" OFF)
+endif()
+option(RYML_USE_ASSERT "Enable assertions regardless of build type. Default is only when NDEBUG is not defined (which is in release builds). This causes a slowdown of the code." OFF)
+option(RYML_BUILD_TOOLS "Build tools" OFF)
 option(RYML_BUILD_API "Enable API generation (python, etc)" OFF)
 option(RYML_DBG "Enable (very verbose) ryml debug prints." OFF)
 
@@ -66,12 +70,20 @@ endif()
 
 if(NOT RYML_DEFAULT_CALLBACKS)
     target_compile_definitions(ryml PRIVATE RYML_NO_DEFAULT_CALLBACKS)
+else()
+    if(RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS)
+        target_compile_definitions(ryml PRIVATE RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS)
+    endif()
 endif()
 
 if(RYML_DBG)
     target_compile_definitions(ryml PRIVATE RYML_DBG)
 endif()
 
+if(RYML_USE_ASSERT)
+    target_compile_definitions(ryml PUBLIC RYML_USE_ASSERT=1)
+endif()
+
 
 #-------------------------------------------------------
 
diff --git a/README.md b/README.md
index dfaea6740..8be614084 100644
--- a/README.md
+++ b/README.md
@@ -241,21 +241,31 @@ easily build and run this executable using any of the build samples,
 eg the [`add_subdirectory()` sample](samples/add_subdirectory/).
 
 ```c++
-// Parse YAML code in place, potentially mutating the buffer.
-// It is also possible to:
-//   - parse a read-only buffer using parse_in_arena()
-//   - reuse an existing tree (advised)
-//   - reuse an existing parser (advised)
+// Parse YAML code in place, potentially mutating the buffer:
 char yml_buf[] = "{foo: 1, bar: [2, 3], john: doe}";
 ryml::Tree tree = ryml::parse_in_place(yml_buf);
 
-// Note: it will always be significantly faster to use mutable
-// buffers and reuse tree+parser.
+// The resulting tree contains only views to the parsed string. If
+// the string was parsed in place, then the string must outlive
+// the tree! This works in this case because `yml_buf` and `tree`
+// live on the same scope, so have the same lifetime.
+
+// It is also possible to:
+//
+//   - parse a read-only buffer using parse_in_arena(). This
+//     copies the YAML buffer to the tree's arena, and spares the
+//     headache of the string's lifetime.
+//
+//   - reuse an existing tree (advised)
+//
+//   - reuse an existing parser (advised)
+//
+// Note: it will always be significantly faster to parse in place
+// and reuse tree+parser.
 //
 // Below you will find samples that show how to achieve reuse; but
 // please note that for brevity and clarity, many of the examples
-// here are parsing immutable buffers, and not reusing tree or
-// parser.
+// here are parsing in the arena, and not reusing tree or parser.
 
 
 //------------------------------------------------------------------
@@ -273,10 +283,11 @@ CHECK(tree.is_seq(bar_id));  // ... and receive the subject index
 
 // The node API is a lightweight abstraction sitting on top of the
 // index API, but offering a much more convenient interaction:
-ryml::ConstNodeRef root = tree.rootref();
+ryml::ConstNodeRef root = tree.rootref();  // a const node reference
 ryml::ConstNodeRef bar = tree["bar"];
 CHECK(root.is_map());
 CHECK(bar.is_seq());
+
 // A node ref is a lightweight handle to the tree and associated id:
 CHECK(root.tree() == &tree); // a node ref points at its tree, WITHOUT refcount
 CHECK(root.id() == root_id); // a node ref's id is the index of the node
@@ -285,10 +296,9 @@ CHECK(bar.id() == bar_id);   // a node ref's id is the index of the node
 // The node API translates very cleanly to the index API, so most
 // of the code examples below are using the node API.
 
-// One significant point of the node API is that it holds a raw
-// pointer to the tree. Care must be taken to ensure the lifetimes
-// match, so that a node will never access the tree after the tree
-// went out of scope.
+// WARNING. A node ref holds a raw pointer to the tree. Care must
+// be taken to ensure the lifetimes match, so that a node will
+// never access the tree after the goes out of scope.
 
 
 //------------------------------------------------------------------
@@ -296,16 +306,16 @@ CHECK(bar.id() == bar_id);   // a node ref's id is the index of the node
 
 // ConstNodeRef::operator[] does a lookup, is O(num_children[node]).
 CHECK(tree["foo"].is_keyval());
-CHECK(tree["foo"].key() == "foo");
-CHECK(tree["foo"].val() == "1");
+CHECK(tree["foo"].val() == "1");   // get the val of a node (must be leaf node, otherwise it is a container and has no val)
+CHECK(tree["foo"].key() == "foo"); // get the key of a node (must be child of a map, otherwise it has no key)
 CHECK(tree["bar"].is_seq());
 CHECK(tree["bar"].has_key());
 CHECK(tree["bar"].key() == "bar");
-// maps use string keys, seqs use integral keys:
+// maps use string keys, seqs use index keys:
 CHECK(tree["bar"][0].val() == "2");
 CHECK(tree["bar"][1].val() == "3");
 CHECK(tree["john"].val() == "doe");
-// An integral key is the position of the child within its parent,
+// An index key is the position of the child within its parent,
 // so even maps can also use int keys, if the key position is
 // known.
 CHECK(tree[0].id() == tree["foo"].id());
@@ -313,8 +323,8 @@ CHECK(tree[1].id() == tree["bar"].id());
 CHECK(tree[2].id() == tree["john"].id());
 // Tree::operator[](int) searches a ***root*** child by its position.
 CHECK(tree[0].id() == tree["foo"].id());  // 0: first child of root
-CHECK(tree[1].id() == tree["bar"].id());  // 1: first child of root
-CHECK(tree[2].id() == tree["john"].id()); // 2: first child of root
+CHECK(tree[1].id() == tree["bar"].id());  // 1: second child of root
+CHECK(tree[2].id() == tree["john"].id()); // 2: third child of root
 // NodeRef::operator[](int) searches a ***node*** child by its position:
 CHECK(bar[0].val() == "2"); // 0 means first child of bar
 CHECK(bar[1].val() == "3"); // 1 means second child of bar
@@ -333,11 +343,12 @@ CHECK(root["foo"].id() == root[0].id());
 CHECK(root["bar"].id() == root[1].id());
 CHECK(root["john"].id() == root[2].id());
 
-// IMPORTANT. The ryml tree uses indexed linked lists for storing
-// children, so the complexity of `Tree::operator[csubstr]` and
-// `Tree::operator[size_t]` is linear on the number of root
-// children. If you use `Tree::operator[]` with a large tree where
-// the root has many children, you will see a performance hit.
+// IMPORTANT. The ryml tree uses an index-based linked list for
+// storing children, so the complexity of
+// `Tree::operator[csubstr]` and `Tree::operator[size_t]` is O(n),
+// linear on the number of root children. If you use
+// `Tree::operator[]` with a large tree where the root has many
+// children, you will see a performance hit.
 //
 // To avoid this hit, you can create your own accelerator
 // structure. For example, before doing a lookup, do a single
@@ -357,6 +368,7 @@ CHECK(root["john"].id() == root[2].id());
 // depending on the data, that number may be very different from
 // one to another.
 
+
 //------------------------------------------------------------------
 // Hierarchy:
 
@@ -416,32 +428,71 @@ CHECK(root["john"].id() == root[2].id());
 
 //------------------------------------------------------------------
 // Gotchas:
-CHECK(!tree["bar"].has_val());          // seq is a container, so no val
-CHECK(!tree["bar"][0].has_key());       // belongs to a seq, so no key
-CHECK(!tree["bar"][1].has_key());       // belongs to a seq, so no key
-//CHECK(tree["bar"].val() == BOOM!);    // ... so attempting to get a val is undefined behavior
-//CHECK(tree["bar"][0].key() == BOOM!); // ... so attempting to get a key is undefined behavior
-//CHECK(tree["bar"][1].key() == BOOM!); // ... so attempting to get a key is undefined behavior
+
+// ryml uses assertions to prevent you from trying to obtain
+// things that do not exist. For example:
+
+{
+    ryml::ConstNodeRef seq_node = tree["bar"];
+    ryml::ConstNodeRef val_node = seq_node[0];
+
+    CHECK(seq_node.is_seq());          // seq is a container
+    CHECK(!seq_node.has_val());        // ... so it has no val
+    //CHECK(seq_node.val() == BOOM!);  // ... so attempting to get a val is undefined behavior
+
+    CHECK(val_node.parent() == seq_node); // belongs to a seq
+    CHECK(!val_node.has_key());           // ... so it has no key
+    //CHECK(val_node.key() == BOOM!);     // ... so attempting to get a key is undefined behavior
+
+    CHECK(val_node.is_val());         // this node is a val
+    //CHECK(val_node.first_child() == BOOM!); // ... so attempting to get a child is undefined behavior
+
+    // assertions are also present in methods that /may/ read the val:
+    CHECK(seq_node.is_seq());              // seq is a container
+    //CHECK(seq_node.val_is_null() BOOM!); // so cannot get the val to check
+}
+
+
+// By default, assertions are enabled unless the NDEBUG macro is
+// defined (which happens in release builds).
+//
+// This adheres to the pay-only-for-what-you-use philosophy: if
+// you are sure that your intent is correct, why would you need to
+// pay the runtime cost for the assertions?
+//
+// The downside, of course, is that when you are not sure, release
+// builds may be doing something crazy.
+//
+// So you can override this behavior and enable/disable
+// assertions, by defining the macro RYML_USE_ASSERT to a proper
+// value (see c4/yml/common.hpp).
+//
+// Also, to be clear, this does not apply to parse errors
+// happening when the YAML is parsed. Checking for these errors is
+// always enabled and cannot be switched off.
 
 
 //------------------------------------------------------------------
 // Deserializing: use operator>>
 {
     int foo = 0, bar0 = 0, bar1 = 0;
-    std::string john;
+    std::string john_str;
+    std::string bar_str;
     root["foo"] >> foo;
     root["bar"][0] >> bar0;
     root["bar"][1] >> bar1;
-    root["john"] >> john; // requires from_chars(std::string). see serialization samples below.
+    root["john"] >> john_str; // requires from_chars(std::string). see serialization samples below.
+    root["bar"] >> ryml::key(bar_str); // to deserialize the key, use the tag function ryml::key()
     CHECK(foo == 1);
     CHECK(bar0 == 2);
     CHECK(bar1 == 3);
-    CHECK(john == "doe");
+    CHECK(john_str == "doe");
+    CHECK(bar_str == "bar");
 }
 
 
 //------------------------------------------------------------------
-// Modifying existing nodes: operator<< vs operator=
+// Modifying existing nodes: operator= vs operator<<
 
 // As implied by its name, ConstNodeRef is a reference to a const
 // node. It can be used to read from the node, but not write to it
@@ -450,20 +501,21 @@ CHECK(!tree["bar"][1].has_key());       // belongs to a seq, so no key
 ryml::NodeRef wroot = tree.rootref();
 
 // operator= assigns an existing string to the receiving node.
-// This pointer will be in effect until the tree goes out of scope
-// so beware to only assign from strings outliving the tree.
+// The contents are NOT copied, and this pointer will be in effect
+// until the tree goes out of scope! So BEWARE to only assign from
+// strings outliving the tree.
 wroot["foo"] = "says you";
 wroot["bar"][0] = "-2";
 wroot["bar"][1] = "-3";
 wroot["john"] = "ron";
 // Now the tree is _pointing_ at the memory of the strings above.
-// That is OK because those are static strings and will outlive
-// the tree.
+// In this case it is OK because those are static strings and will
+// outlive the tree.
 CHECK(root["foo"].val() == "says you");
 CHECK(root["bar"][0].val() == "-2");
 CHECK(root["bar"][1].val() == "-3");
 CHECK(root["john"].val() == "ron");
-// WATCHOUT: do not assign from temporary objects:
+// But WATCHOUT: do not assign from temporary objects:
 // {
 //     std::string crash("will dangle");
 //     root["john"] = ryml::to_csubstr(crash);
@@ -489,18 +541,23 @@ CHECK(tree.arena() == "says who2030deere"); // the result of serializations to t
     // root["john"] = ryml::to_csubstr(ok); // don't, will dangle
     wroot["john"] << ryml::to_csubstr(ok); // OK, copy to the tree's arena
 }
-CHECK(root["john"] == "in_scope"); // OK!
-CHECK(tree.arena() == "says who2030deerein_scope"); // the result of serializations to the tree arena
+CHECK(root["john"].val() == "in_scope"); // OK!
+// serializing floating points:
+wroot["float"] << 2.4;
+// to force a particular precision or float format:
+// (see sample_float_precision() and sample_formatting())
+wroot["digits"] << ryml::fmt::real(2.4, /*num_digits*/6, ryml::FTOA_FLOAT);
+CHECK(tree.arena() == "says who2030deerein_scope2.42.400000"); // the result of serializations to the tree arena
 
 
 //------------------------------------------------------------------
 // Adding new nodes:
 
 // adding a keyval node to a map:
-CHECK(root.num_children() == 3);
+CHECK(root.num_children() == 5);
 wroot["newkeyval"] = "shiny and new"; // using these strings
 wroot.append_child() << ryml::key("newkeyval (serialized)") << "shiny and new (serialized)"; // serializes and assigns the serialization
-CHECK(root.num_children() == 5);
+CHECK(root.num_children() == 7);
 CHECK(root["newkeyval"].key() == "newkeyval");
 CHECK(root["newkeyval"].val() == "shiny and new");
 CHECK(root["newkeyval (serialized)"].key() == "newkeyval (serialized)");
@@ -517,22 +574,27 @@ CHECK(root["bar"].num_children() == 4);
 CHECK(root["bar"][2].val() == "oh so nice");
 CHECK(root["bar"][3].val() == "oh so nice (serialized)");
 // adding a seq node:
-CHECK(root.num_children() == 5);
+CHECK(root.num_children() == 7);
 wroot["newseq"] |= ryml::SEQ;
 wroot.append_child() << ryml::key("newseq (serialized)") |= ryml::SEQ;
-CHECK(root.num_children() == 7);
+CHECK(root.num_children() == 9);
 CHECK(root["newseq"].num_children() == 0);
+CHECK(root["newseq"].is_seq());
 CHECK(root["newseq (serialized)"].num_children() == 0);
+CHECK(root["newseq (serialized)"].is_seq());
 // adding a map node:
-CHECK(root.num_children() == 7);
-wroot["newmap"] |= ryml::MAP;
-wroot.append_child() << ryml::key("newmap (serialized)") |= ryml::SEQ;
 CHECK(root.num_children() == 9);
+wroot["newmap"] |= ryml::MAP;
+wroot.append_child() << ryml::key("newmap (serialized)") |= ryml::MAP;
+CHECK(root.num_children() == 11);
 CHECK(root["newmap"].num_children() == 0);
+CHECK(root["newmap"].is_map());
 CHECK(root["newmap (serialized)"].num_children() == 0);
+CHECK(root["newmap (serialized)"].is_map());
 //
-// When the tree is mutable, operator[] does not mutate the tree
-// until the returned node is written to.
+// When the tree is mutable, operator[] first searches the tree
+// for the does not mutate the tree until the returned node is
+// written to.
 //
 // Until such time, the NodeRef object keeps in itself the required
 // information to write to the proper place in the tree. This is
@@ -541,7 +603,8 @@ CHECK(root["newmap (serialized)"].num_children() == 0);
 // This means that passing a key/index which does not exist will
 // not mutate the tree, but will instead store (in the node) the
 // proper place of the tree to be able to do so, if and when it is
-// required.
+// required. This is why the node is said to be in "seed" state -
+// it allows creating the entry in the tree in the future.
 //
 // This is a significant difference from eg, the behavior of
 // std::map, which mutates the map immediately within the call to
@@ -551,50 +614,70 @@ CHECK(root["newmap (serialized)"].num_children() == 0);
 // the tree is const, then a NodeRef cannot be obtained from it;
 // only a ConstNodeRef, which can never be used to mutate the
 // tree.
+//
 CHECK(!root.has_child("I am not nothing"));
-ryml::NodeRef nothing = wroot["I am nothing"];
-CHECK(nothing.valid());   // points at the tree, and a specific place in the tree
+ryml::NodeRef nothing;
+CHECK(nothing.invalid());    // invalid because it points at nothing
+nothing = wroot["I am nothing"];
+CHECK(!nothing.invalid());   // points at the tree, and a specific place in the tree
 CHECK(nothing.is_seed()); // ... but nothing is there yet.
 CHECK(!root.has_child("I am nothing")); // same as above
+CHECK(!nothing.readable()); // ... and this node cannot be used to
+                            // read anything from the tree
 ryml::NodeRef something = wroot["I am something"];
 ryml::ConstNodeRef constsomething = wroot["I am something"];
 CHECK(!root.has_child("I am something")); // same as above
-CHECK(something.valid());
+CHECK(!something.invalid());
 CHECK(something.is_seed()); // same as above
-CHECK(!constsomething.valid()); // NOTE: because a ConstNodeRef
-                                // cannot be used to mutate a
-                                // tree, it is only valid() if it
-                                // is pointing at an existing
-                                // node.
-something = "indeed";  // this will commit to the tree, mutating at the proper place
+CHECK(!something.readable()); // same as above
+CHECK(constsomething.invalid()); // NOTE: because a ConstNodeRef cannot be
+                                 // used to mutate a tree, it is only valid()
+                                 // if it is pointing at an existing node.
+something = "indeed";  // this will commit the seed to the tree, mutating at the proper place
 CHECK(root.has_child("I am something"));
 CHECK(root["I am something"].val() == "indeed");
-CHECK(something.valid());
+CHECK(!something.invalid()); // it was already valid
 CHECK(!something.is_seed()); // now the tree has this node, so the
                              // ref is no longer a seed
+CHECK(something.readable()); // and it is now readable
+//
 // now the constref is also valid (but it needs to be reassigned):
 ryml::ConstNodeRef constsomethingnew = wroot["I am something"];
-CHECK(constsomethingnew.valid());
+CHECK(!constsomethingnew.invalid());
+CHECK(constsomethingnew.readable());
 // note that the old constref is now stale, because it only keeps
 // the state at creation:
-CHECK(!constsomething.valid());
+CHECK(constsomething.invalid());
+CHECK(!constsomething.readable());
+//
+// -----------------------------------------------------------
+// Remember: a seed node cannot be used to read from the tree!
+// -----------------------------------------------------------
+//
+// The seed node needs to be created and become readable first.
+//
+// Trying to invoke any tree-reading method on a node that is not
+// readable will cause an assertion (see RYML_USE_ASSERT).
+//
+// It is your responsibility to verify that the preconditions are
+// met. If you are not sure about the structure of your data,
+// write your code defensively to signify your full intent:
+//
+ryml::NodeRef wbar = wroot["bar"];
+if(wbar.readable() && wbar.is_seq()) // .is_seq() requires .readable()
+{
+    CHECK(wbar[0].readable() && wbar[0].val() == "20");
+    CHECK( ! wbar[100].readable());
+    CHECK( ! wbar[100].readable() || wbar[100].val() == "100"); // <- no crash because it is not .readable(), so never tries to call .val()
+    // this would work as well:
+    CHECK( ! wbar[0].is_seed() && wbar[0].val() == "20");
+    CHECK(wbar[100].is_seed() || wbar[100].val() == "100");
+}
 
 
 //------------------------------------------------------------------
 // Emitting:
 
-// emit to a FILE*
-ryml::emit_yaml(tree, stdout); // there is also emit_json()
-// emit to a stream
-std::stringstream ss;
-ss << tree;
-std::string stream_result = ss.str();
-// emit to a buffer:
-std::string str_result = ryml::emitrs_yaml<std::string>(tree); // there is also emitrs_json()
-// can emit to any given buffer:
-char buf[1024];
-ryml::csubstr buf_result = ryml::emit_yaml(tree, buf);
-// now check
 ryml::csubstr expected_result = R"(foo: says who
 bar:
 - 20
@@ -602,29 +685,46 @@ bar:
 - oh so nice
 - oh so nice (serialized)
 john: in_scope
+float: 2.4
+digits: 2.400000
 newkeyval: shiny and new
 newkeyval (serialized): shiny and new (serialized)
 newseq: []
 newseq (serialized): []
 newmap: {}
-newmap (serialized): []
+newmap (serialized): {}
 I am something: indeed
 )";
+
+// emit to a FILE*
+ryml::emit_yaml(tree, stdout);
+// emit to a stream
+std::stringstream ss;
+ss << tree;
+std::string stream_result = ss.str();
+// emit to a buffer:
+std::string str_result = ryml::emitrs_yaml<std::string>(tree);
+// can emit to any given buffer:
+char buf[1024];
+ryml::csubstr buf_result = ryml::emit_yaml(tree, buf);
+// now check
 CHECK(buf_result == expected_result);
 CHECK(str_result == expected_result);
 CHECK(stream_result == expected_result);
 // There are many possibilities to emit to buffer;
 // please look at the emit sample functions below.
 
+
 //------------------------------------------------------------------
 // ConstNodeRef vs NodeRef
 
 ryml::NodeRef noderef = tree["bar"][0];
 ryml::ConstNodeRef constnoderef = tree["bar"][0];
 
-// ConstNodeRef cannot be used to mutate the tree, but a NodeRef can:
+// ConstNodeRef cannot be used to mutate the tree:
 //constnoderef = "21";  // compile error
 //constnoderef << "22"; // compile error
+// ... but a NodeRef can:
 noderef = "21";         // ok, can assign because it's not const
 CHECK(tree["bar"][0].val() == "21");
 noderef << "22";        // ok, can serialize and assign because it's not const
@@ -656,10 +756,12 @@ fr: Planète (Gazeuse)
 ru: Планета (Газ)
 ja: 惑星（ガス）
 zh: 行星（气体）
-# UTF8 decoding only happens in double-quoted strings,\
+# UTF8 decoding only happens in double-quoted strings,
 # as per the YAML standard
 decode this: "\u263A \xE2\x98\xBA"
 and this as well: "\u2705 \U0001D11E"
+not decoded: '\u263A \xE2\x98\xBA'
+neither this: '\u2705 \U0001D11E'
 )");
 // in-place UTF8 just works:
 CHECK(langs["en"].val() == "Planet (Gas)");
@@ -667,60 +769,28 @@ CHECK(langs["fr"].val() == "Planète (Gazeuse)");
 CHECK(langs["ru"].val() == "Планета (Газ)");
 CHECK(langs["ja"].val() == "惑星（ガス）");
 CHECK(langs["zh"].val() == "行星（气体）");
-// and \x \u \U codepoints are decoded (but only when they appear
+// and \x \u \U codepoints are decoded, but only when they appear
 // inside double-quoted strings, as dictated by the YAML
-// standard):
+// standard:
 CHECK(langs["decode this"].val() == "☺ ☺");
 CHECK(langs["and this as well"].val() == "✅ 𝄞");
+CHECK(langs["not decoded"].val() == "\\u263A \\xE2\\x98\\xBA");
+CHECK(langs["neither this"].val() == "\\u2705 \\U0001D11E");
 
 //------------------------------------------------------------------
 // Getting the location of nodes in the source:
-ryml::Parser parser;
+//
+// Location tracking is opt-in:
+ryml::Parser parser(ryml::ParserOptions().locations(true));
+// Now the parser will start by building the accelerator structure:
 ryml::Tree tree2 = parser.parse_in_arena("expected.yml", expected_result);
+// ... and use it when querying
 ryml::Location loc = parser.location(tree2["bar"][1]);
 CHECK(parser.location_contents(loc).begins_with("30"));
 CHECK(loc.line == 3u);
 CHECK(loc.col == 4u);
 ```
 
-The [quickstart.cpp sample](./samples/quickstart.cpp) (from which the
-above overview was taken) has many more detailed examples, and should
-be your first port of call to find out any particular point about
-ryml's API. It is tested in the CI, and thus has the correct behavior.
-There you can find the following subjects being addressed:
-
-```c++
-sample_substr();               ///< about ryml's string views (from c4core)
-sample_parse_file();           ///< ready-to-go example of parsing a file from disk
-sample_parse_in_place();       ///< parse a mutable YAML source buffer
-sample_parse_in_arena();       ///< parse a read-only YAML source buffer
-sample_parse_reuse_tree();     ///< parse into an existing tree, maybe into a node
-sample_parse_reuse_parser();   ///< reuse an existing parser
-sample_parse_reuse_tree_and_parser(); ///< how to reuse existing trees and parsers
-sample_iterate_trees();        ///< visit individual nodes and iterate through trees
-sample_create_trees();         ///< programatically create trees
-sample_tree_arena();           ///< interact with the tree's serialization arena
-sample_fundamental_types();    ///< serialize/deserialize fundamental types
-sample_formatting();           ///< control formatting when serializing/deserializing
-sample_base64();               ///< encode/decode base64
-sample_user_scalar_types();    ///< serialize/deserialize scalar (leaf/string) types
-sample_user_container_types(); ///< serialize/deserialize container (map or seq) types
-sample_std_types();            ///< serialize/deserialize STL containers
-sample_emit_to_container();    ///< emit to memory, eg a string or vector-like container
-sample_emit_to_stream();       ///< emit to a stream, eg std::ostream
-sample_emit_to_file();         ///< emit to a FILE*
-sample_emit_nested_node();     ///< pick a nested node as the root when emitting
-sample_json();                 ///< JSON parsing and emitting
-sample_anchors_and_aliases();  ///< deal with YAML anchors and aliases
-sample_tags();                 ///< deal with YAML type tags
-sample_docs();                 ///< deal with YAML docs
-sample_error_handler();        ///< set a custom error handler
-sample_global_allocator();     ///< set a global allocator for ryml
-sample_per_tree_allocator();   ///< set per-tree allocators
-sample_static_trees();         ///< how to use static trees in ryml
-sample_location_tracking();    ///< track node locations in the parsed source tree
-```
-
 
 ------
 
@@ -844,6 +914,12 @@ ryml:
     because this may cost up to 10% in processing time.
   * `RYML_DEFAULT_CALLBACKS=ON/OFF`. Enable/disable ryml's default
     implementation of error and allocation callbacks. Defaults to `ON`.
+  * `RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS=ON/OFF` - Enable/disable
+    the same-named macro, which will make the default error handler
+    provided by ryml throw a `std::runtime_error` exception.
+  * `RYML_USE_ASSERT` - enable assertions in the code regardless of
+    build type. This is disabled by default. Failed assertions will
+    trigger a call to the error callback.
   * `RYML_STANDALONE=ON/OFF`. ryml uses
     [c4core](https://github.com/biojppm/c4core), a C++ library with low-level
     multi-platform utilities for C++. When `RYML_STANDALONE=ON`, c4core is
diff --git a/changelog/current.md b/changelog/current.md
index d464d10d9..afad6d057 100644
--- a/changelog/current.md
+++ b/changelog/current.md
@@ -1,7 +1,64 @@
+### Error handling
 
-### Fixes
+Fix major error handling problem reported in [#389](https://github.com/biojppm/rapidyaml/issues/389) ([PR#411](https://github.com/biojppm/rapidyaml/pull/411)):
 
-- Fix [#390](https://github.com/biojppm/rapidyaml/pull/390) - `csubstr::first_real_span()` failed on number with one digit in the exponent.
+  - The `NodeRef` and `ConstNodeRef` classes are now conditional noexcept using `RYML_NOEXCEPT`, which evaluates either to nothing when assertions are enabled, and to `noexcept` otherwise. The problem was that these classes had many methods explicitly marked `noexcept`, but were doing assertions which could throw exceptions, causing an abort instead of a throw whenever the assertion called an exception-throwing error callback.
+  - Also, this problem was compounded by exceptions being enabled in every build type -- despite the intention to have them only in debug builds. There was a problem in the preprocessor code to enable assertions which led to assertions being enabled in release builds even when `RYML_USE_ASSERT` was defined to 0. Thanks to @jdrouhard for reporting this.
+  - Although the code is and was extensively tested, the testing was addressing mostly the happy path. Tests were added to ensure that the error behavior is as intended.
+  - Together with this changeset, a major revision was carried out of the asserting/checking status of each function in the node classes. In most cases, assertions were added to functions cases that were missing them. So **beware** - user code that was invalid will now assert or error out. Also, assertions and checks are now directed as much as possible to the callbacks of the closest scope, ie if a tree has custom callbacks, errors within the tree class should go through those callbacks.
+  - Also, the intended assertion behavior is now in place: *no assertions in release builds*. **Beware** as well - user code which was relying on this will now silently succeed and return garbage in release builds. See the next points, which may help:
+  - Added new methods to the `NodeRef`/`ConstNodeRef` classes:
+    ```c++
+    /** Distinguish between a valid seed vs a valid non-seed ref. */
+    bool readable() const { return valid() && !is_seed(); }
+
+    /** Get a child by name, with error checking; complexity is
+     * O(num_children).
+     *
+     * Behaves as operator[](csubstr) const, but always raises an
+     * error (even when RYML_USE_ASSERT is set to false) when the
+     * returned node does not exist, or when this node is not
+     * readable, or when it is not a map. This behaviour is similar to
+     * std::vector::at(), but the error consists in calling the error
+     * callback instead of directly raising an exception. */
+    ConstNodeRef at(csubstr key) const;
+    /** Likewise, but return a seed node when the key is not found */
+    NodeRef at(csubstr key);
+
+    /** Get a child by position, with error checking; complexity is
+     * O(pos).
+     *
+     * Behaves as operator[](size_t) const, but always raises an error
+     * (even when RYML_USE_ASSERT is set to false) when the returned
+     * node does not exist, or when this node is not readable, or when
+     * it is not a container. This behaviour is similar to
+     * std::vector::at(), but the error consists in calling the error
+     * callback instead of directly raising an exception. */
+    ConstNodeRef at(size_t pos) const;
+    /** Likewise, but return a seed node when pos is not found */
+    NodeRef at(csubstr key);
+    ```
+  - The state for `NodeRef` was refined, and now there are three mutually exclusive states (and class predicates) for an object of this class:
+    - `.invalid()` when the object was not initialized to any node
+    - `.readable()` when the object points at an existing tree+node
+    - `.is_seed()` when the object points at an hypotethic tree+node
+    - The previous state `.valid()` was deprecated: its semantics were confusing as it actually could be any of `.readable()` or `.is_seed()`
+  - Deprecated also the following methods for `NodeRef`/`ConstNodeRef`:
+    ```c++
+    RYML_DEPRECATED() bool operator== (std::nullptr_t) const;
+    RYML_DEPRECATED() bool operator!= (std::nullptr_t) const;
+    RYML_DEPRECATED() bool operator== (csubstr val) const;
+    RYML_DEPRECATED() bool operator!= (csubstr val) const;
+    ```
+  - Added macros and respective cmake options to control error handling:
+    - `RYML_USE_ASSERT` - enable assertions regardless of build type. This is disabled by default. This macro was already defined; the current PR adds the cmake option.
+    - `RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS` - make the default error handler provided by ryml throw exceptions instead of calling `std::abort()`. This is disabled by default.
+  - Also, `RYML_DEBUG_BREAK()` is now enabled only if `RYML_DBG` is defined, as reported in [#362](https://github.com/biojppm/rapidyaml/issues/362).
+
+
+### More fixes
+
+- Fix [#390](https://github.com/biojppm/rapidyaml/pull/390) - `csubstr::first_real_span()` failed on scientific numbers with one digit in the exponent.
 - Fix [#361](https://github.com/biojppm/rapidyaml/pull/361) - parse error on map scalars containing `:` and starting on the next line:
   ```yaml
   ---
@@ -14,5 +71,11 @@
   ```
 - [PR#368](https://github.com/biojppm/rapidyaml/pull/368) - fix pedantic compiler warnings.
 - Fix [#373](https://github.com/biojppm/rapidyaml/issues/373) - false parse error with empty quoted keys in block-style map ([PR#374](https://github.com/biojppm/rapidyaml/pull/374)).
-- Fix [#356](https://github.com/biojppm/rapidyaml/issues/356) - fix overzealous check in `emit_as()`. An id may be larger than the tree's size, eg when nodes were removed.
-([PR#357](https://github.com/biojppm/rapidyaml/pull/357)).
+- Fix [#356](https://github.com/biojppm/rapidyaml/issues/356) - fix overzealous check in `emit_as()`. An id may be larger than the tree's size, eg when nodes were removed. ([PR#357](https://github.com/biojppm/rapidyaml/pull/357)).
+- Fix [#417](https://github.com/biojppm/rapidyaml/issues/417)) - add quickstart example explaining how to avoid precision loss while serializing floats ([PR#420](https://github.com/biojppm/rapidyaml/pull/420)).
+
+
+### Thanks
+
+- @Neko-Box-Coder
+- @jdrouhard
diff --git a/samples/quickstart.cpp b/samples/quickstart.cpp
index ce036bb8b..889cff178 100644
--- a/samples/quickstart.cpp
+++ b/samples/quickstart.cpp
@@ -167,21 +167,31 @@ struct CheckPredicate {
 /** a brief tour over most features */
 void sample_quick_overview()
 {
-    // Parse YAML code in place, potentially mutating the buffer.
-    // It is also possible to:
-    //   - parse a read-only buffer using parse_in_arena()
-    //   - reuse an existing tree (advised)
-    //   - reuse an existing parser (advised)
+    // Parse YAML code in place, potentially mutating the buffer:
     char yml_buf[] = "{foo: 1, bar: [2, 3], john: doe}";
     ryml::Tree tree = ryml::parse_in_place(yml_buf);
 
-    // Note: it will always be significantly faster to use mutable
-    // buffers and reuse tree+parser.
+    // The resulting tree contains only views to the parsed string. If
+    // the string was parsed in place, then the string must outlive
+    // the tree! This works in this case because `yml_buf` and `tree`
+    // live on the same scope, so have the same lifetime.
+
+    // It is also possible to:
+    //
+    //   - parse a read-only buffer using parse_in_arena(). This
+    //     copies the YAML buffer to the tree's arena, and spares the
+    //     headache of the string's lifetime.
+    //
+    //   - reuse an existing tree (advised)
+    //
+    //   - reuse an existing parser (advised)
+    //
+    // Note: it will always be significantly faster to parse in place
+    // and reuse tree+parser.
     //
     // Below you will find samples that show how to achieve reuse; but
     // please note that for brevity and clarity, many of the examples
-    // here are parsing immutable buffers, and not reusing tree or
-    // parser.
+    // here are parsing in the arena, and not reusing tree or parser.
 
 
     //------------------------------------------------------------------
@@ -199,10 +209,11 @@ void sample_quick_overview()
 
     // The node API is a lightweight abstraction sitting on top of the
     // index API, but offering a much more convenient interaction:
-    ryml::ConstNodeRef root = tree.rootref();
+    ryml::ConstNodeRef root = tree.rootref();  // a const node reference
     ryml::ConstNodeRef bar = tree["bar"];
     CHECK(root.is_map());
     CHECK(bar.is_seq());
+
     // A node ref is a lightweight handle to the tree and associated id:
     CHECK(root.tree() == &tree); // a node ref points at its tree, WITHOUT refcount
     CHECK(root.id() == root_id); // a node ref's id is the index of the node
@@ -211,10 +222,9 @@ void sample_quick_overview()
     // The node API translates very cleanly to the index API, so most
     // of the code examples below are using the node API.
 
-    // One significant point of the node API is that it holds a raw
-    // pointer to the tree. Care must be taken to ensure the lifetimes
-    // match, so that a node will never access the tree after the tree
-    // went out of scope.
+    // WARNING. A node ref holds a raw pointer to the tree. Care must
+    // be taken to ensure the lifetimes match, so that a node will
+    // never access the tree after the goes out of scope.
 
 
     //------------------------------------------------------------------
@@ -222,16 +232,16 @@ void sample_quick_overview()
 
     // ConstNodeRef::operator[] does a lookup, is O(num_children[node]).
     CHECK(tree["foo"].is_keyval());
-    CHECK(tree["foo"].key() == "foo");
-    CHECK(tree["foo"].val() == "1");
+    CHECK(tree["foo"].val() == "1");   // get the val of a node (must be leaf node, otherwise it is a container and has no val)
+    CHECK(tree["foo"].key() == "foo"); // get the key of a node (must be child of a map, otherwise it has no key)
     CHECK(tree["bar"].is_seq());
     CHECK(tree["bar"].has_key());
     CHECK(tree["bar"].key() == "bar");
-    // maps use string keys, seqs use integral keys:
+    // maps use string keys, seqs use index keys:
     CHECK(tree["bar"][0].val() == "2");
     CHECK(tree["bar"][1].val() == "3");
     CHECK(tree["john"].val() == "doe");
-    // An integral key is the position of the child within its parent,
+    // An index key is the position of the child within its parent,
     // so even maps can also use int keys, if the key position is
     // known.
     CHECK(tree[0].id() == tree["foo"].id());
@@ -239,8 +249,8 @@ void sample_quick_overview()
     CHECK(tree[2].id() == tree["john"].id());
     // Tree::operator[](int) searches a ***root*** child by its position.
     CHECK(tree[0].id() == tree["foo"].id());  // 0: first child of root
-    CHECK(tree[1].id() == tree["bar"].id());  // 1: first child of root
-    CHECK(tree[2].id() == tree["john"].id()); // 2: first child of root
+    CHECK(tree[1].id() == tree["bar"].id());  // 1: second child of root
+    CHECK(tree[2].id() == tree["john"].id()); // 2: third child of root
     // NodeRef::operator[](int) searches a ***node*** child by its position:
     CHECK(bar[0].val() == "2"); // 0 means first child of bar
     CHECK(bar[1].val() == "3"); // 1 means second child of bar
@@ -259,11 +269,12 @@ void sample_quick_overview()
     CHECK(root["bar"].id() == root[1].id());
     CHECK(root["john"].id() == root[2].id());
 
-    // IMPORTANT. The ryml tree uses indexed linked lists for storing
-    // children, so the complexity of `Tree::operator[csubstr]` and
-    // `Tree::operator[size_t]` is linear on the number of root
-    // children. If you use `Tree::operator[]` with a large tree where
-    // the root has many children, you will see a performance hit.
+    // IMPORTANT. The ryml tree uses an index-based linked list for
+    // storing children, so the complexity of
+    // `Tree::operator[csubstr]` and `Tree::operator[size_t]` is O(n),
+    // linear on the number of root children. If you use
+    // `Tree::operator[]` with a large tree where the root has many
+    // children, you will see a performance hit.
     //
     // To avoid this hit, you can create your own accelerator
     // structure. For example, before doing a lookup, do a single
@@ -283,6 +294,7 @@ void sample_quick_overview()
     // depending on the data, that number may be very different from
     // one to another.
 
+
     //------------------------------------------------------------------
     // Hierarchy:
 
@@ -342,12 +354,48 @@ void sample_quick_overview()
 
     //------------------------------------------------------------------
     // Gotchas:
-    CHECK(!tree["bar"].has_val());          // seq is a container, so no val
-    CHECK(!tree["bar"][0].has_key());       // belongs to a seq, so no key
-    CHECK(!tree["bar"][1].has_key());       // belongs to a seq, so no key
-    //CHECK(tree["bar"].val() == BOOM!);    // ... so attempting to get a val is undefined behavior
-    //CHECK(tree["bar"][0].key() == BOOM!); // ... so attempting to get a key is undefined behavior
-    //CHECK(tree["bar"][1].key() == BOOM!); // ... so attempting to get a key is undefined behavior
+
+    // ryml uses assertions to prevent you from trying to obtain
+    // things that do not exist. For example:
+
+    {
+        ryml::ConstNodeRef seq_node = tree["bar"];
+        ryml::ConstNodeRef val_node = seq_node[0];
+
+        CHECK(seq_node.is_seq());          // seq is a container
+        CHECK(!seq_node.has_val());        // ... so it has no val
+        //CHECK(seq_node.val() == BOOM!);  // ... so attempting to get a val is undefined behavior
+
+        CHECK(val_node.parent() == seq_node); // belongs to a seq
+        CHECK(!val_node.has_key());           // ... so it has no key
+        //CHECK(val_node.key() == BOOM!);     // ... so attempting to get a key is undefined behavior
+
+        CHECK(val_node.is_val());         // this node is a val
+        //CHECK(val_node.first_child() == BOOM!); // ... so attempting to get a child is undefined behavior
+
+        // assertions are also present in methods that /may/ read the val:
+        CHECK(seq_node.is_seq());              // seq is a container
+        //CHECK(seq_node.val_is_null() BOOM!); // so cannot get the val to check
+    }
+
+
+    // By default, assertions are enabled unless the NDEBUG macro is
+    // defined (which happens in release builds).
+    //
+    // This adheres to the pay-only-for-what-you-use philosophy: if
+    // you are sure that your intent is correct, why would you need to
+    // pay the runtime cost for the assertions?
+    //
+    // The downside, of course, is that when you are not sure, release
+    // builds may be doing something crazy.
+    //
+    // So you can override this behavior and enable/disable
+    // assertions, by defining the macro RYML_USE_ASSERT to a proper
+    // value (see c4/yml/common.hpp).
+    //
+    // Also, to be clear, this does not apply to parse errors
+    // happening when the YAML is parsed. Checking for these errors is
+    // always enabled and cannot be switched off.
 
 
     //------------------------------------------------------------------
@@ -370,7 +418,7 @@ void sample_quick_overview()
 
 
     //------------------------------------------------------------------
-    // Modifying existing nodes: operator<< vs operator=
+    // Modifying existing nodes: operator= vs operator<<
 
     // As implied by its name, ConstNodeRef is a reference to a const
     // node. It can be used to read from the node, but not write to it
@@ -379,20 +427,21 @@ void sample_quick_overview()
     ryml::NodeRef wroot = tree.rootref();
 
     // operator= assigns an existing string to the receiving node.
-    // This pointer will be in effect until the tree goes out of scope
-    // so BEWARE to only assign from strings outliving the tree.
+    // The contents are NOT copied, and this pointer will be in effect
+    // until the tree goes out of scope! So BEWARE to only assign from
+    // strings outliving the tree.
     wroot["foo"] = "says you";
     wroot["bar"][0] = "-2";
     wroot["bar"][1] = "-3";
     wroot["john"] = "ron";
     // Now the tree is _pointing_ at the memory of the strings above.
-    // That is OK because those are static strings and will outlive
-    // the tree.
+    // In this case it is OK because those are static strings and will
+    // outlive the tree.
     CHECK(root["foo"].val() == "says you");
     CHECK(root["bar"][0].val() == "-2");
     CHECK(root["bar"][1].val() == "-3");
     CHECK(root["john"].val() == "ron");
-    // WATCHOUT: do not assign from temporary objects:
+    // But WATCHOUT: do not assign from temporary objects:
     // {
     //     std::string crash("will dangle");
     //     root["john"] = ryml::to_csubstr(crash);
@@ -418,7 +467,7 @@ void sample_quick_overview()
         // root["john"] = ryml::to_csubstr(ok); // don't, will dangle
         wroot["john"] << ryml::to_csubstr(ok); // OK, copy to the tree's arena
     }
-    CHECK(root["john"] == "in_scope"); // OK!
+    CHECK(root["john"].val() == "in_scope"); // OK!
     // serializing floating points:
     wroot["float"] << 2.4;
     // to force a particular precision or float format:
@@ -469,8 +518,9 @@ void sample_quick_overview()
     CHECK(root["newmap (serialized)"].num_children() == 0);
     CHECK(root["newmap (serialized)"].is_map());
     //
-    // When the tree is mutable, operator[] does not mutate the tree
-    // until the returned node is written to.
+    // When the tree is mutable, operator[] first searches the tree
+    // for the does not mutate the tree until the returned node is
+    // written to.
     //
     // Until such time, the NodeRef object keeps in itself the required
     // information to write to the proper place in the tree. This is
@@ -479,7 +529,8 @@ void sample_quick_overview()
     // This means that passing a key/index which does not exist will
     // not mutate the tree, but will instead store (in the node) the
     // proper place of the tree to be able to do so, if and when it is
-    // required.
+    // required. This is why the node is said to be in "seed" state -
+    // it allows creating the entry in the tree in the future.
     //
     // This is a significant difference from eg, the behavior of
     // std::map, which mutates the map immediately within the call to
@@ -489,50 +540,70 @@ void sample_quick_overview()
     // the tree is const, then a NodeRef cannot be obtained from it;
     // only a ConstNodeRef, which can never be used to mutate the
     // tree.
+    //
     CHECK(!root.has_child("I am not nothing"));
-    ryml::NodeRef nothing = wroot["I am nothing"];
-    CHECK(nothing.valid());   // points at the tree, and a specific place in the tree
+    ryml::NodeRef nothing;
+    CHECK(nothing.invalid());    // invalid because it points at nothing
+    nothing = wroot["I am nothing"];
+    CHECK(!nothing.invalid());   // points at the tree, and a specific place in the tree
     CHECK(nothing.is_seed()); // ... but nothing is there yet.
     CHECK(!root.has_child("I am nothing")); // same as above
+    CHECK(!nothing.readable()); // ... and this node cannot be used to
+                                // read anything from the tree
     ryml::NodeRef something = wroot["I am something"];
     ryml::ConstNodeRef constsomething = wroot["I am something"];
     CHECK(!root.has_child("I am something")); // same as above
-    CHECK(something.valid());
+    CHECK(!something.invalid());
     CHECK(something.is_seed()); // same as above
-    CHECK(!constsomething.valid()); // NOTE: because a ConstNodeRef
-                                    // cannot be used to mutate a
-                                    // tree, it is only valid() if it
-                                    // is pointing at an existing
-                                    // node.
-    something = "indeed";  // this will commit to the tree, mutating at the proper place
+    CHECK(!something.readable()); // same as above
+    CHECK(constsomething.invalid()); // NOTE: because a ConstNodeRef cannot be
+                                     // used to mutate a tree, it is only valid()
+                                     // if it is pointing at an existing node.
+    something = "indeed";  // this will commit the seed to the tree, mutating at the proper place
     CHECK(root.has_child("I am something"));
     CHECK(root["I am something"].val() == "indeed");
-    CHECK(something.valid());
+    CHECK(!something.invalid()); // it was already valid
     CHECK(!something.is_seed()); // now the tree has this node, so the
                                  // ref is no longer a seed
+    CHECK(something.readable()); // and it is now readable
+    //
     // now the constref is also valid (but it needs to be reassigned):
     ryml::ConstNodeRef constsomethingnew = wroot["I am something"];
-    CHECK(constsomethingnew.valid());
+    CHECK(!constsomethingnew.invalid());
+    CHECK(constsomethingnew.readable());
     // note that the old constref is now stale, because it only keeps
     // the state at creation:
-    CHECK(!constsomething.valid());
+    CHECK(constsomething.invalid());
+    CHECK(!constsomething.readable());
+    //
+    // -----------------------------------------------------------
+    // Remember: a seed node cannot be used to read from the tree!
+    // -----------------------------------------------------------
+    //
+    // The seed node needs to be created and become readable first.
+    //
+    // Trying to invoke any tree-reading method on a node that is not
+    // readable will cause an assertion (see RYML_USE_ASSERT).
+    //
+    // It is your responsibility to verify that the preconditions are
+    // met. If you are not sure about the structure of your data,
+    // write your code defensively to signify your full intent:
+    //
+    ryml::NodeRef wbar = wroot["bar"];
+    if(wbar.readable() && wbar.is_seq()) // .is_seq() requires .readable()
+    {
+        CHECK(wbar[0].readable() && wbar[0].val() == "20");
+        CHECK( ! wbar[100].readable());
+        CHECK( ! wbar[100].readable() || wbar[100].val() == "100"); // <- no crash because it is not .readable(), so never tries to call .val()
+        // this would work as well:
+        CHECK( ! wbar[0].is_seed() && wbar[0].val() == "20");
+        CHECK(wbar[100].is_seed() || wbar[100].val() == "100");
+    }
 
 
     //------------------------------------------------------------------
     // Emitting:
 
-    // emit to a FILE*
-    ryml::emit_yaml(tree, stdout);
-    // emit to a stream
-    std::stringstream ss;
-    ss << tree;
-    std::string stream_result = ss.str();
-    // emit to a buffer:
-    std::string str_result = ryml::emitrs_yaml<std::string>(tree);
-    // can emit to any given buffer:
-    char buf[1024];
-    ryml::csubstr buf_result = ryml::emit_yaml(tree, buf);
-    // now check
     ryml::csubstr expected_result = R"(foo: says who
 bar:
   - 20
@@ -550,12 +621,26 @@ newmap: {}
 newmap (serialized): {}
 I am something: indeed
 )";
+
+    // emit to a FILE*
+    ryml::emit_yaml(tree, stdout);
+    // emit to a stream
+    std::stringstream ss;
+    ss << tree;
+    std::string stream_result = ss.str();
+    // emit to a buffer:
+    std::string str_result = ryml::emitrs_yaml<std::string>(tree);
+    // can emit to any given buffer:
+    char buf[1024];
+    ryml::csubstr buf_result = ryml::emit_yaml(tree, buf);
+    // now check
     CHECK(buf_result == expected_result);
     CHECK(str_result == expected_result);
     CHECK(stream_result == expected_result);
     // There are many possibilities to emit to buffer;
     // please look at the emit sample functions below.
 
+
     //------------------------------------------------------------------
     // ConstNodeRef vs NodeRef
 
@@ -601,6 +686,8 @@ zh: 行星（气体）
 # as per the YAML standard
 decode this: "\u263A \xE2\x98\xBA"
 and this as well: "\u2705 \U0001D11E"
+not decoded: '\u263A \xE2\x98\xBA'
+neither this: '\u2705 \U0001D11E'
 )");
     // in-place UTF8 just works:
     CHECK(langs["en"].val() == "Planet (Gas)");
@@ -608,11 +695,13 @@ and this as well: "\u2705 \U0001D11E"
     CHECK(langs["ru"].val() == "Планета (Газ)");
     CHECK(langs["ja"].val() == "惑星（ガス）");
     CHECK(langs["zh"].val() == "行星（气体）");
-    // and \x \u \U codepoints are decoded (but only when they appear
+    // and \x \u \U codepoints are decoded, but only when they appear
     // inside double-quoted strings, as dictated by the YAML
-    // standard):
+    // standard:
     CHECK(langs["decode this"].val() == "☺ ☺");
     CHECK(langs["and this as well"].val() == "✅ 𝄞");
+    CHECK(langs["not decoded"].val() == "\\u263A \\xE2\\x98\\xBA");
+    CHECK(langs["neither this"].val() == "\\u2705 \\U0001D11E");
 
     //------------------------------------------------------------------
     // Getting the location of nodes in the source:
@@ -1909,13 +1998,13 @@ cars: GTO
 void sample_create_trees()
 {
     ryml::NodeRef doe;
-    CHECK(!doe.valid()); // it's pointing at nowhere
+    CHECK(doe.invalid()); // it's pointing at nowhere
 
     ryml::Tree tree;
     ryml::NodeRef root = tree.rootref();
     root |= ryml::MAP; // mark root as a map
     doe = root["doe"];
-    CHECK(doe.valid()); // it's now pointing at the tree
+    CHECK(!doe.invalid()); // it's now pointing at the tree
     CHECK(doe.is_seed()); // but the tree has nothing there, so this is only a seed
 
     // set the value of the node
@@ -3872,8 +3961,8 @@ ship_to: *id001
     CHECK( ! tree["val"].is_key_ref()); // notice *valref is now turned to val
     CHECK( ! tree["val"].is_val_ref()); // notice *valref is now turned to val
 
-    CHECK(tree["ship_to"]["city"] == "East Centerville");
-    CHECK(tree["ship_to"]["state"] == "KS");
+    CHECK(tree["ship_to"]["city"].val() == "East Centerville");
+    CHECK(tree["ship_to"]["state"].val() == "KS");
 }
 
 
@@ -4077,6 +4166,10 @@ d: 3
 // However, it is important to note that the error callback must never
 // return to the caller! Otherwise, an infinite loop or program crash
 // may occur.
+//
+// The default error handler calls std::abort(). You can use the cmake
+// option and the macro RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS to have
+// the default error handler throw a std::runtime_error instead.
 
 
 struct ErrorHandlerExample
@@ -4137,11 +4230,9 @@ void sample_error_handler()
     // crash.
     ryml::set_callbacks(errh.callbacks());
     errh.check_effect(/*committed*/true);
-    errh.check_error_occurs([]{
+    errh.check_error_occurs([&]{
         ryml::Tree tree = ryml::parse_in_arena("errorhandler.yml", "[a: b\n}");
-        std::cout << tree;
     });
-
     ryml::set_callbacks(errh.defaults); // restore defaults.
     errh.check_effect(/*committed*/false);
 }
diff --git a/src/c4/yml/common.cpp b/src/c4/yml/common.cpp
index 1a86e86ae..00f14a03f 100644
--- a/src/c4/yml/common.cpp
+++ b/src/c4/yml/common.cpp
@@ -3,6 +3,9 @@
 #ifndef RYML_NO_DEFAULT_CALLBACKS
 #   include <stdlib.h>
 #   include <stdio.h>
+#   ifdef RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS
+#       include <stdexcept>
+#   endif
 #endif // RYML_NO_DEFAULT_CALLBACKS
 
 namespace c4 {
@@ -39,7 +42,11 @@ void report_error_impl(const char* msg, size_t length, Location loc, FILE *f)
 void error_impl(const char* msg, size_t length, Location loc, void * /*user_data*/)
 {
     report_error_impl(msg, length, loc, nullptr);
+#ifdef RYML_DEFAULT_CALLBACK_USES_EXCEPTIONS
+    throw std::runtime_error(std::string(msg, length));
+#else
     ::abort();
+#endif
 }
 
 void* allocate_impl(size_t length, void * /*hint*/, void * /*user_data*/)
diff --git a/src/c4/yml/common.hpp b/src/c4/yml/common.hpp
index f74de9dd8..11284c7a3 100644
--- a/src/c4/yml/common.hpp
+++ b/src/c4/yml/common.hpp
@@ -7,6 +7,8 @@
 
 
 #ifndef RYML_USE_ASSERT
+/** define this macro with a boolean value to force enable/disable
+    assertions. This causes a slowdown of the code.*/
 #   define RYML_USE_ASSERT C4_USE_ASSERT
 #endif
 
@@ -14,15 +16,17 @@
 #if RYML_USE_ASSERT
 #   define RYML_ASSERT(cond) RYML_CHECK(cond)
 #   define RYML_ASSERT_MSG(cond, msg) RYML_CHECK_MSG(cond, msg)
+#   define _RYML_CB_ASSERT(cb, cond) _RYML_CB_CHECK((cb), (cond))
+#   define RYML_NOEXCEPT
 #else
 #   define RYML_ASSERT(cond)
 #   define RYML_ASSERT_MSG(cond, msg)
+#   define _RYML_CB_ASSERT(cb, cond)
+#   define RYML_NOEXCEPT noexcept
 #endif
 
 
-#if defined(NDEBUG) || defined(C4_NO_DEBUG_BREAK)
-#   define RYML_DEBUG_BREAK()
-#else
+#if defined(RYML_DBG) && !defined(NDEBUG) && !defined(C4_NO_DEBUG_BREAK)
 #   define RYML_DEBUG_BREAK()                               \
     {                                                       \
         if(c4::get_error_flags() & c4::ON_ERROR_DEBUGBREAK) \
@@ -30,6 +34,8 @@
             C4_DEBUG_BREAK();                               \
         }                                                   \
     }
+#else
+#   define RYML_DEBUG_BREAK()
 #endif
 
 
@@ -210,11 +216,6 @@ do                                                                      \
             (cb).m_error(msg, sizeof(msg), c4::yml::Location(__FILE__, 0, __LINE__, 0), (cb).m_user_data); \
         }                                                               \
     } while(0)
-#ifdef RYML_USE_ASSERT
-#define _RYML_CB_ASSERT(cb, cond) _RYML_CB_CHECK((cb), (cond))
-#else
-#define _RYML_CB_ASSERT(cb, cond) do {} while(0)
-#endif
 #define _RYML_CB_ALLOC_HINT(cb, T, num, hint) (T*) (cb).m_allocate((num) * sizeof(T), (hint), (cb).m_user_data)
 #define _RYML_CB_ALLOC(cb, T, num) _RYML_CB_ALLOC_HINT((cb), (T), (num), nullptr)
 #define _RYML_CB_FREE(cb, buf, T, num)                              \
diff --git a/src/c4/yml/detail/stack.hpp b/src/c4/yml/detail/stack.hpp
index a3f060d70..c12b03b99 100644
--- a/src/c4/yml/detail/stack.hpp
+++ b/src/c4/yml/detail/stack.hpp
@@ -51,7 +51,7 @@ class stack
         _free();
     }
 
-    stack(stack const& that) noexcept : stack(that.m_callbacks)
+    stack(stack const& that) RYML_NOEXCEPT : stack(that.m_callbacks)
     {
         resize(that.m_size);
         _cp(&that);
@@ -62,7 +62,7 @@ class stack
         _mv(&that);
     }
 
-    stack& operator= (stack const& that) noexcept
+    stack& operator= (stack const& that) RYML_NOEXCEPT
     {
         _cb(that.m_callbacks);
         resize(that.m_size);
diff --git a/src/c4/yml/emit.def.hpp b/src/c4/yml/emit.def.hpp
index cf184ad77..8f904bcc2 100644
--- a/src/c4/yml/emit.def.hpp
+++ b/src/c4/yml/emit.def.hpp
@@ -38,7 +38,7 @@ substr Emitter<Writer>::emit_as(EmitType_e type, Tree const& t, bool error_on_ex
 template<class Writer>
 substr Emitter<Writer>::emit_as(EmitType_e type, ConstNodeRef const& n, bool error_on_excess)
 {
-    _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+    _RYML_CB_CHECK(n.tree()->callbacks(), n.readable());
     return this->emit_as(type, *n.tree(), n.id(), error_on_excess);
 }
 
diff --git a/src/c4/yml/emit.hpp b/src/c4/yml/emit.hpp
index c7cdd2a1a..09ae9fc3c 100644
--- a/src/c4/yml/emit.hpp
+++ b/src/c4/yml/emit.hpp
@@ -433,7 +433,7 @@ CharOwningContainer emitrs_json(Tree const& t)
 template<class CharOwningContainer>
 substr emitrs_yaml(ConstNodeRef const& n, CharOwningContainer * cont)
 {
-    _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+    _RYML_CB_CHECK(n.tree()->callbacks(), n.readable());
     return emitrs_yaml(*n.tree(), n.id(), cont);
 }
 template<class CharOwningContainer>
@@ -447,7 +447,7 @@ RYML_DEPRECATE_EMITRS substr emitrs(ConstNodeRef const& n, CharOwningContainer *
 template<class CharOwningContainer>
 substr emitrs_json(ConstNodeRef const& n, CharOwningContainer * cont)
 {
-    _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+    _RYML_CB_CHECK(n.tree()->callbacks(), n.readable());
     return emitrs_json(*n.tree(), n.id(), cont);
 }
 
@@ -457,7 +457,7 @@ substr emitrs_json(ConstNodeRef const& n, CharOwningContainer * cont)
 template<class CharOwningContainer>
 CharOwningContainer emitrs_yaml(ConstNodeRef const& n)
 {
-    _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+    _RYML_CB_CHECK(n.tree()->callbacks(), n.readable());
     CharOwningContainer c;
     emitrs_yaml(*n.tree(), n.id(), &c);
     return c;
@@ -473,7 +473,7 @@ RYML_DEPRECATE_EMITRS CharOwningContainer emitrs(ConstNodeRef const& n)
 template<class CharOwningContainer>
 CharOwningContainer emitrs_json(ConstNodeRef const& n)
 {
-    _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+    _RYML_CB_CHECK(n.tree()->callbacks(), n.readable());
     CharOwningContainer c;
     emitrs_json(*n.tree(), n.id(), &c);
     return c;
diff --git a/src/c4/yml/node.hpp b/src/c4/yml/node.hpp
index d97aa7f42..f4672d69b 100644
--- a/src/c4/yml/node.hpp
+++ b/src/c4/yml/node.hpp
@@ -158,10 +158,12 @@ struct RoNodeMethods
     #define id_ ((ConstImpl const* C4_RESTRICT)this)->m_id
     #define tree__ ((Impl const* C4_RESTRICT)this)->m_tree
     #define id__ ((Impl const* C4_RESTRICT)this)->m_id
-    // require valid
-    #define _C4RV()                                       \
+    // require readable: this is a precondition for reading from the
+    // tree using this object.
+    #define _C4RR()                                       \
         RYML_ASSERT(tree_ != nullptr);                    \
-        _RYML_CB_ASSERT(tree_->m_callbacks, id_ != NONE)
+        _RYML_CB_ASSERT(tree_->m_callbacks, id_ != NONE); \
+        _RYML_CB_ASSERT(tree_->m_callbacks, (((Impl const* C4_RESTRICT)this)->readable()));
     #define _C4_IF_MUTABLE(ty) typename std::enable_if<!std::is_same<U, ConstImpl>::value, ty>::type
 
 public:
@@ -170,29 +172,29 @@ struct RoNodeMethods
     /** @{ */
 
     /** returns the data or null when the id is NONE */
-    C4_ALWAYS_INLINE C4_PURE NodeData const* get() const noexcept { RYML_ASSERT(tree_ != nullptr); return tree_->get(id_); }
+    C4_ALWAYS_INLINE NodeData const* get() const RYML_NOEXCEPT { return ((Impl const*)this)->readable() ? tree_->get(id_) : nullptr; }
     /** returns the data or null when the id is NONE */
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto get() noexcept -> _C4_IF_MUTABLE(NodeData*) { RYML_ASSERT(tree_ != nullptr); return tree__->get(id__); }
+    C4_ALWAYS_INLINE auto get() RYML_NOEXCEPT -> _C4_IF_MUTABLE(NodeData*) { return ((Impl const*)this)->readable() ? tree__->get(id__) : nullptr; }
 
-    C4_ALWAYS_INLINE C4_PURE NodeType    type() const noexcept { _C4RV(); return tree_->type(id_); }
-    C4_ALWAYS_INLINE C4_PURE const char* type_str() const noexcept { return tree_->type_str(id_); }
+    C4_ALWAYS_INLINE NodeType    type() const RYML_NOEXCEPT { _C4RR(); return tree_->type(id_); }
+    C4_ALWAYS_INLINE const char* type_str() const RYML_NOEXCEPT { _C4RR(); return tree_->type_str(id_); }
 
-    C4_ALWAYS_INLINE C4_PURE csubstr key()        const noexcept { _C4RV(); return tree_->key(id_); }
-    C4_ALWAYS_INLINE C4_PURE csubstr key_tag()    const noexcept { _C4RV(); return tree_->key_tag(id_); }
-    C4_ALWAYS_INLINE C4_PURE csubstr key_ref()    const noexcept { _C4RV(); return tree_->key_ref(id_); }
-    C4_ALWAYS_INLINE C4_PURE csubstr key_anchor() const noexcept { _C4RV(); return tree_->key_anchor(id_); }
+    C4_ALWAYS_INLINE csubstr key()        const RYML_NOEXCEPT { _C4RR(); return tree_->key(id_); }
+    C4_ALWAYS_INLINE csubstr key_tag()    const RYML_NOEXCEPT { _C4RR(); return tree_->key_tag(id_); }
+    C4_ALWAYS_INLINE csubstr key_ref()    const RYML_NOEXCEPT { _C4RR(); return tree_->key_ref(id_); }
+    C4_ALWAYS_INLINE csubstr key_anchor() const RYML_NOEXCEPT { _C4RR(); return tree_->key_anchor(id_); }
 
-    C4_ALWAYS_INLINE C4_PURE csubstr val()        const noexcept { _C4RV(); return tree_->val(id_); }
-    C4_ALWAYS_INLINE C4_PURE csubstr val_tag()    const noexcept { _C4RV(); return tree_->val_tag(id_); }
-    C4_ALWAYS_INLINE C4_PURE csubstr val_ref()    const noexcept { _C4RV(); return tree_->val_ref(id_); }
-    C4_ALWAYS_INLINE C4_PURE csubstr val_anchor() const noexcept { _C4RV(); return tree_->val_anchor(id_); }
+    C4_ALWAYS_INLINE csubstr val()        const RYML_NOEXCEPT { _C4RR(); return tree_->val(id_); }
+    C4_ALWAYS_INLINE csubstr val_tag()    const RYML_NOEXCEPT { _C4RR(); return tree_->val_tag(id_); }
+    C4_ALWAYS_INLINE csubstr val_ref()    const RYML_NOEXCEPT { _C4RR(); return tree_->val_ref(id_); }
+    C4_ALWAYS_INLINE csubstr val_anchor() const RYML_NOEXCEPT { _C4RR(); return tree_->val_anchor(id_); }
 
-    C4_ALWAYS_INLINE C4_PURE NodeScalar const& keysc() const noexcept { _C4RV(); return tree_->keysc(id_); }
-    C4_ALWAYS_INLINE C4_PURE NodeScalar const& valsc() const noexcept { _C4RV(); return tree_->valsc(id_); }
+    C4_ALWAYS_INLINE NodeScalar const& keysc() const RYML_NOEXCEPT { _C4RR(); return tree_->keysc(id_); }
+    C4_ALWAYS_INLINE NodeScalar const& valsc() const RYML_NOEXCEPT { _C4RR(); return tree_->valsc(id_); }
 
-    C4_ALWAYS_INLINE C4_PURE bool key_is_null() const noexcept { _C4RV(); return tree_->key_is_null(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool val_is_null() const noexcept { _C4RV(); return tree_->val_is_null(id_); }
+    C4_ALWAYS_INLINE bool key_is_null() const RYML_NOEXCEPT { _C4RR(); return tree_->key_is_null(id_); }
+    C4_ALWAYS_INLINE bool val_is_null() const RYML_NOEXCEPT { _C4RR(); return tree_->val_is_null(id_); }
 
     /** @} */
 
@@ -201,33 +203,33 @@ struct RoNodeMethods
     /** @name node property predicates */
     /** @{ */
 
-    C4_ALWAYS_INLINE C4_PURE bool empty()            const noexcept { _C4RV(); return tree_->empty(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_stream()        const noexcept { _C4RV(); return tree_->is_stream(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_doc()           const noexcept { _C4RV(); return tree_->is_doc(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_container()     const noexcept { _C4RV(); return tree_->is_container(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_map()           const noexcept { _C4RV(); return tree_->is_map(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_seq()           const noexcept { _C4RV(); return tree_->is_seq(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_val()          const noexcept { _C4RV(); return tree_->has_val(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_key()          const noexcept { _C4RV(); return tree_->has_key(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_val()           const noexcept { _C4RV(); return tree_->is_val(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_keyval()        const noexcept { _C4RV(); return tree_->is_keyval(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_key_tag()      const noexcept { _C4RV(); return tree_->has_key_tag(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_val_tag()      const noexcept { _C4RV(); return tree_->has_val_tag(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_key_anchor()   const noexcept { _C4RV(); return tree_->has_key_anchor(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_key_anchor()    const noexcept { _C4RV(); return tree_->is_key_anchor(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_val_anchor()   const noexcept { _C4RV(); return tree_->has_val_anchor(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_val_anchor()    const noexcept { _C4RV(); return tree_->is_val_anchor(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_anchor()       const noexcept { _C4RV(); return tree_->has_anchor(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_anchor()        const noexcept { _C4RV(); return tree_->is_anchor(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_key_ref()       const noexcept { _C4RV(); return tree_->is_key_ref(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_val_ref()       const noexcept { _C4RV(); return tree_->is_val_ref(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_ref()           const noexcept { _C4RV(); return tree_->is_ref(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_anchor_or_ref() const noexcept { _C4RV(); return tree_->is_anchor_or_ref(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_key_quoted()    const noexcept { _C4RV(); return tree_->is_key_quoted(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_val_quoted()    const noexcept { _C4RV(); return tree_->is_val_quoted(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool is_quoted()        const noexcept { _C4RV(); return tree_->is_quoted(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool parent_is_seq()    const noexcept { _C4RV(); return tree_->parent_is_seq(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool parent_is_map()    const noexcept { _C4RV(); return tree_->parent_is_map(id_); }
+    C4_ALWAYS_INLINE bool empty()            const RYML_NOEXCEPT { _C4RR(); return tree_->empty(id_); }
+    C4_ALWAYS_INLINE bool is_stream()        const RYML_NOEXCEPT { _C4RR(); return tree_->is_stream(id_); }
+    C4_ALWAYS_INLINE bool is_doc()           const RYML_NOEXCEPT { _C4RR(); return tree_->is_doc(id_); }
+    C4_ALWAYS_INLINE bool is_container()     const RYML_NOEXCEPT { _C4RR(); return tree_->is_container(id_); }
+    C4_ALWAYS_INLINE bool is_map()           const RYML_NOEXCEPT { _C4RR(); return tree_->is_map(id_); }
+    C4_ALWAYS_INLINE bool is_seq()           const RYML_NOEXCEPT { _C4RR(); return tree_->is_seq(id_); }
+    C4_ALWAYS_INLINE bool has_val()          const RYML_NOEXCEPT { _C4RR(); return tree_->has_val(id_); }
+    C4_ALWAYS_INLINE bool has_key()          const RYML_NOEXCEPT { _C4RR(); return tree_->has_key(id_); }
+    C4_ALWAYS_INLINE bool is_val()           const RYML_NOEXCEPT { _C4RR(); return tree_->is_val(id_); }
+    C4_ALWAYS_INLINE bool is_keyval()        const RYML_NOEXCEPT { _C4RR(); return tree_->is_keyval(id_); }
+    C4_ALWAYS_INLINE bool has_key_tag()      const RYML_NOEXCEPT { _C4RR(); return tree_->has_key_tag(id_); }
+    C4_ALWAYS_INLINE bool has_val_tag()      const RYML_NOEXCEPT { _C4RR(); return tree_->has_val_tag(id_); }
+    C4_ALWAYS_INLINE bool has_key_anchor()   const RYML_NOEXCEPT { _C4RR(); return tree_->has_key_anchor(id_); }
+    C4_ALWAYS_INLINE bool is_key_anchor()    const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_anchor(id_); }
+    C4_ALWAYS_INLINE bool has_val_anchor()   const RYML_NOEXCEPT { _C4RR(); return tree_->has_val_anchor(id_); }
+    C4_ALWAYS_INLINE bool is_val_anchor()    const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_anchor(id_); }
+    C4_ALWAYS_INLINE bool has_anchor()       const RYML_NOEXCEPT { _C4RR(); return tree_->has_anchor(id_); }
+    C4_ALWAYS_INLINE bool is_anchor()        const RYML_NOEXCEPT { _C4RR(); return tree_->is_anchor(id_); }
+    C4_ALWAYS_INLINE bool is_key_ref()       const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_ref(id_); }
+    C4_ALWAYS_INLINE bool is_val_ref()       const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_ref(id_); }
+    C4_ALWAYS_INLINE bool is_ref()           const RYML_NOEXCEPT { _C4RR(); return tree_->is_ref(id_); }
+    C4_ALWAYS_INLINE bool is_anchor_or_ref() const RYML_NOEXCEPT { _C4RR(); return tree_->is_anchor_or_ref(id_); }
+    C4_ALWAYS_INLINE bool is_key_quoted()    const RYML_NOEXCEPT { _C4RR(); return tree_->is_key_quoted(id_); }
+    C4_ALWAYS_INLINE bool is_val_quoted()    const RYML_NOEXCEPT { _C4RR(); return tree_->is_val_quoted(id_); }
+    C4_ALWAYS_INLINE bool is_quoted()        const RYML_NOEXCEPT { _C4RR(); return tree_->is_quoted(id_); }
+    C4_ALWAYS_INLINE bool parent_is_seq()    const RYML_NOEXCEPT { _C4RR(); return tree_->parent_is_seq(id_); }
+    C4_ALWAYS_INLINE bool parent_is_map()    const RYML_NOEXCEPT { _C4RR(); return tree_->parent_is_map(id_); }
 
     /** @} */
 
@@ -236,19 +238,22 @@ struct RoNodeMethods
     /** @name hierarchy predicates */
     /** @{ */
 
-    C4_ALWAYS_INLINE C4_PURE bool is_root()    const noexcept { _C4RV(); return tree_->is_root(id_); }
-    C4_ALWAYS_INLINE C4_PURE bool has_parent() const noexcept { _C4RV(); return tree_->has_parent(id_); }
+    C4_ALWAYS_INLINE bool is_root()    const RYML_NOEXCEPT { _C4RR(); return tree_->is_root(id_); }
+    C4_ALWAYS_INLINE bool has_parent() const RYML_NOEXCEPT { _C4RR(); return tree_->has_parent(id_); }
 
-    C4_ALWAYS_INLINE C4_PURE bool has_child(ConstImpl const& ch) const noexcept { _C4RV(); return tree_->has_child(id_, ch.m_id); }
-    C4_ALWAYS_INLINE C4_PURE bool has_child(csubstr name) const noexcept { _C4RV(); return tree_->has_child(id_, name); }
-    C4_ALWAYS_INLINE C4_PURE bool has_children() const noexcept { _C4RV(); return tree_->has_children(id_); }
+    C4_ALWAYS_INLINE bool has_child(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); return n.readable() ? tree_->has_child(id_, n.m_id) : false; }
+    C4_ALWAYS_INLINE bool has_child(size_t node) const RYML_NOEXCEPT { _C4RR(); return tree_->has_child(id_, node); }
+    C4_ALWAYS_INLINE bool has_child(csubstr name) const RYML_NOEXCEPT { _C4RR(); return tree_->has_child(id_, name); }
+    C4_ALWAYS_INLINE bool has_children() const RYML_NOEXCEPT { _C4RR(); return tree_->has_children(id_); }
 
-    C4_ALWAYS_INLINE C4_PURE bool has_sibling(ConstImpl const& n) const noexcept { _C4RV(); return tree_->has_sibling(id_, n.m_id); }
-    C4_ALWAYS_INLINE C4_PURE bool has_sibling(csubstr name) const noexcept { _C4RV(); return tree_->has_sibling(id_, name); }
-    /** counts with this */
-    C4_ALWAYS_INLINE C4_PURE bool has_siblings() const noexcept { _C4RV(); return tree_->has_siblings(id_); }
+    C4_ALWAYS_INLINE bool has_sibling(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); return n.readable() ? tree_->has_sibling(id_, n.m_id) : false; }
+    C4_ALWAYS_INLINE bool has_sibling(size_t node) const RYML_NOEXCEPT { _C4RR(); return tree_->has_sibling(id_, node); }
+    C4_ALWAYS_INLINE bool has_sibling(csubstr name) const RYML_NOEXCEPT { _C4RR(); return tree_->has_sibling(id_, name); }
     /** does not count with this */
-    C4_ALWAYS_INLINE C4_PURE bool has_other_siblings() const noexcept { _C4RV(); return tree_->has_other_siblings(id_); }
+    C4_ALWAYS_INLINE bool has_other_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->has_other_siblings(id_); }
+
+    /** counts with this */
+    RYML_DEPRECATED("use has_other_siblings()") bool has_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->has_siblings(id_); }
 
     /** @} */
 
@@ -257,101 +262,289 @@ struct RoNodeMethods
     /** @name hierarchy getters */
     /** @{ */
 
-
+    /** succeeds even when the node may have invalid or seed id */
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto doc(size_t num) noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->doc(num)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl doc(size_t num) const noexcept { _C4RV(); return {tree_, tree_->doc(num)}; }
-
+    C4_ALWAYS_INLINE auto doc(size_t num) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { RYML_ASSERT(tree_); return {tree__, tree__->doc(num)}; }
+    /** succeeds even when the node may have invalid or seed id */
+    C4_ALWAYS_INLINE ConstImpl doc(size_t num) const RYML_NOEXCEPT { RYML_ASSERT(tree_); return {tree_, tree_->doc(num)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto parent() noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->parent(id__)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl parent() const noexcept { _C4RV(); return {tree_, tree_->parent(id_)}; }
-
-
-    /** O(#num_children) */
-    C4_ALWAYS_INLINE C4_PURE size_t child_pos(ConstImpl const& n) const noexcept { _C4RV(); return tree_->child_pos(id_, n.m_id); }
-    C4_ALWAYS_INLINE C4_PURE size_t num_children() const noexcept { _C4RV(); return tree_->num_children(id_); }
+    C4_ALWAYS_INLINE auto parent() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->parent(id__)}; }
+    C4_ALWAYS_INLINE ConstImpl parent() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->parent(id_)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto first_child() noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->first_child(id__)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl first_child() const noexcept { _C4RV(); return {tree_, tree_->first_child(id_)}; }
+    C4_ALWAYS_INLINE auto first_child() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->first_child(id__)}; }
+    C4_ALWAYS_INLINE ConstImpl first_child() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->first_child(id_)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto last_child() noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->last_child(id__)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl last_child () const noexcept { _C4RV(); return {tree_, tree_->last_child (id_)}; }
+    C4_ALWAYS_INLINE auto last_child() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->last_child(id__)}; }
+    C4_ALWAYS_INLINE ConstImpl last_child () const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->last_child (id_)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto child(size_t pos) noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->child(id__, pos)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl child(size_t pos) const noexcept { _C4RV(); return {tree_, tree_->child(id_, pos)}; }
+    C4_ALWAYS_INLINE auto child(size_t pos) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->child(id__, pos)}; }
+    C4_ALWAYS_INLINE ConstImpl child(size_t pos) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->child(id_, pos)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto find_child(csubstr name)  noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->find_child(id__, name)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl find_child(csubstr name) const noexcept { _C4RV(); return {tree_, tree_->find_child(id_, name)}; }
-
-
-    /** O(#num_siblings) */
-    C4_ALWAYS_INLINE C4_PURE size_t num_siblings() const noexcept { _C4RV(); return tree_->num_siblings(id_); }
-    C4_ALWAYS_INLINE C4_PURE size_t num_other_siblings() const noexcept { _C4RV(); return tree_->num_other_siblings(id_); }
-    C4_ALWAYS_INLINE C4_PURE size_t sibling_pos(ConstImpl const& n) const noexcept { _C4RV(); return tree_->child_pos(tree_->parent(id_), n.m_id); }
+    C4_ALWAYS_INLINE auto find_child(csubstr name)  RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->find_child(id__, name)}; }
+    C4_ALWAYS_INLINE ConstImpl find_child(csubstr name) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->find_child(id_, name)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto prev_sibling() noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->prev_sibling(id__)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl prev_sibling() const noexcept { _C4RV(); return {tree_, tree_->prev_sibling(id_)}; }
+    C4_ALWAYS_INLINE auto prev_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->prev_sibling(id__)}; }
+    C4_ALWAYS_INLINE ConstImpl prev_sibling() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->prev_sibling(id_)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto next_sibling() noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->next_sibling(id__)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl next_sibling() const noexcept { _C4RV(); return {tree_, tree_->next_sibling(id_)}; }
+    C4_ALWAYS_INLINE auto next_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->next_sibling(id__)}; }
+    C4_ALWAYS_INLINE ConstImpl next_sibling() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->next_sibling(id_)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto first_sibling() noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->first_sibling(id__)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl first_sibling() const noexcept { _C4RV(); return {tree_, tree_->first_sibling(id_)}; }
+    C4_ALWAYS_INLINE auto first_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->first_sibling(id__)}; }
+    C4_ALWAYS_INLINE ConstImpl first_sibling() const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->first_sibling(id_)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto last_sibling() noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->last_sibling(id__)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl last_sibling () const noexcept { _C4RV(); return {tree_, tree_->last_sibling(id_)}; }
+    C4_ALWAYS_INLINE auto last_sibling() RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->last_sibling(id__)}; }
+    C4_ALWAYS_INLINE ConstImpl last_sibling () const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->last_sibling(id_)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto sibling(size_t pos) noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->sibling(id__, pos)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl sibling(size_t pos) const noexcept { _C4RV(); return {tree_, tree_->sibling(id_, pos)}; }
+    C4_ALWAYS_INLINE auto sibling(size_t pos) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->sibling(id__, pos)}; }
+    C4_ALWAYS_INLINE ConstImpl sibling(size_t pos) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->sibling(id_, pos)}; }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto find_sibling(csubstr name) noexcept -> _C4_IF_MUTABLE(Impl) { _C4RV(); return {tree__, tree__->find_sibling(id__, name)}; }
-    C4_ALWAYS_INLINE C4_PURE ConstImpl find_sibling(csubstr name) const noexcept { _C4RV(); return {tree_, tree_->find_sibling(id_, name)}; }
+    C4_ALWAYS_INLINE auto find_sibling(csubstr name) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl) { _C4RR(); return {tree__, tree__->find_sibling(id__, name)}; }
+    C4_ALWAYS_INLINE ConstImpl find_sibling(csubstr name) const RYML_NOEXCEPT { _C4RR(); return {tree_, tree_->find_sibling(id_, name)}; }
 
+    /** O(#num_children) */
+    C4_ALWAYS_INLINE size_t child_pos(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); _RYML_CB_ASSERT(tree_->m_callbacks, n.readable()); return tree_->child_pos(id_, n.m_id); }
 
-    /** O(num_children) */
-    C4_ALWAYS_INLINE C4_PURE ConstImpl operator[] (csubstr k) const noexcept
+    /** O(#num_children) */
+    C4_ALWAYS_INLINE size_t num_children() const RYML_NOEXCEPT { _C4RR(); return tree_->num_children(id_); }
+
+    /** O(#num_siblings) */
+    C4_ALWAYS_INLINE size_t num_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->num_siblings(id_); }
+    C4_ALWAYS_INLINE size_t num_other_siblings() const RYML_NOEXCEPT { _C4RR(); return tree_->num_other_siblings(id_); }
+    C4_ALWAYS_INLINE size_t sibling_pos(ConstImpl const& n) const RYML_NOEXCEPT { _C4RR(); _RYML_CB_ASSERT(tree_->callbacks(), n.readable()); return tree_->child_pos(tree_->parent(id_), n.m_id); }
+
+    /** @} */
+
+public:
+
+    /** @name square_brackets
+     * operator[] */
+    /** @{ */
+
+    /** Find child by key; complexity is O(num_children).
+     *
+     * Returns the requested node, or an object in seed state if no
+     * such child is found (see @ref NodeRef for an explanation of
+     * what is seed state). When the object is in seed state, using it
+     * to read from the tree is UB. The seed node can be used to write
+     * to the tree provided that its create() method is called prior
+     * to writing, which happens in most modifying methods in
+     * NodeRef. It is the caller's responsibility to verify that the
+     * returned node is readable before subsequently using it to read
+     * from the tree.
+     *
+     * @warning the calling object must be readable. This precondition
+     * is asserted. The assertion is performed only if @ref
+     * RYML_USE_ASSERT is set to true. As with the non-const overload,
+     * it is UB to call this method if the node is not readable.
+     *
+     * @see https://github.com/biojppm/rapidyaml/issues/389 */
+    template<class U=Impl>
+    C4_ALWAYS_INLINE auto operator[] (csubstr key) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl)
+    {
+        _C4RR();
+        size_t ch = tree__->find_child(id__, key);
+        return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, key);
+    }
+
+    /** Find child by position; complexity is O(pos).
+     *
+     * Returns the requested node, or an object in seed state if no
+     * such child is found (see @ref NodeRef for an explanation of
+     * what is seed state). When the object is in seed state, using it
+     * to read from the tree is UB. The seed node can be used to write
+     * to the tree provided that its create() method is called prior
+     * to writing, which happens in most modifying methods in
+     * NodeRef. It is the caller's responsibility to verify that the
+     * returned node is readable before subsequently using it to read
+     * from the tree.
+     *
+     * @warning the calling object must be readable. This precondition
+     * is asserted. The assertion is performed only if @ref
+     * RYML_USE_ASSERT is set to true. As with the non-const overload,
+     * it is UB to call this method if the node is not readable.
+     *
+     * @see https://github.com/biojppm/rapidyaml/issues/389 */
+    template<class U=Impl>
+    C4_ALWAYS_INLINE auto operator[] (size_t pos) RYML_NOEXCEPT -> _C4_IF_MUTABLE(Impl)
     {
-        _C4RV();
-        size_t ch = tree_->find_child(id_, k);
+        _C4RR();
+        size_t ch = tree__->child(id__, pos);
+        return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, pos);
+    }
+
+    /** Find a child by key; complexity is O(num_children).
+     *
+     * Behaves similar to the non-const overload, but further asserts
+     * that the returned node is readable (because it can never be in
+     * a seed state). The assertion is performed only if @ref
+     * RYML_USE_ASSERT is set to true. As with the non-const overload,
+     * it is UB to use the return value if it is not valid.
+     *
+     * @see https://github.com/biojppm/rapidyaml/issues/389  */
+    C4_ALWAYS_INLINE ConstImpl operator[] (csubstr key) const RYML_NOEXCEPT
+    {
+        _C4RR();
+        size_t ch = tree_->find_child(id_, key);
         _RYML_CB_ASSERT(tree_->m_callbacks, ch != NONE);
         return {tree_, ch};
     }
-    /** Find child by key. O(num_children). returns a seed node if no such child is found.  */
-    template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto operator[] (csubstr k) noexcept -> _C4_IF_MUTABLE(Impl)
-    {
-        _C4RV();
-        size_t ch = tree__->find_child(id__, k);
-        return ch != NONE ? Impl(tree__, ch) : NodeRef(tree__, id__, k);
-    }
 
-    /** O(num_children) */
-    C4_ALWAYS_INLINE C4_PURE ConstImpl operator[] (size_t pos) const noexcept
+    /** Find a child by position; complexity is O(pos).
+     *
+     * Behaves similar to the non-const overload, but further asserts
+     * that the returned node is readable (because it can never be in
+     * a seed state). This assertion is performed only if @ref
+     * RYML_USE_ASSERT is set to true. As with the non-const overload,
+     * it is UB to use the return value if it is not valid.
+     *
+     * @see https://github.com/biojppm/rapidyaml/issues/389  */
+    C4_ALWAYS_INLINE ConstImpl operator[] (size_t pos) const RYML_NOEXCEPT
     {
-        _C4RV();
+        _C4RR();
         size_t ch = tree_->child(id_, pos);
         _RYML_CB_ASSERT(tree_->m_callbacks, ch != NONE);
         return {tree_, ch};
     }
 
-    /** Find child by position. O(pos). returns a seed node if no such child is found.  */
+    /** @} */
+
+public:
+
+    /** @name at
+     *
+     * These functions are the analogue to operator[], with the
+     * difference that they emit an error instead of an
+     * assertion. That is, if any of the pre or post conditions is
+     * violated, an error is always emitted (resulting in a call to
+     * the error callback).
+     *
+     * @{ */
+
+    /** Find child by key; complexity is O(num_children).
+     *
+     * Returns the requested node, or an object in seed state if no
+     * such child is found (see @ref NodeRef for an explanation of
+     * what is seed state). When the object is in seed state, using it
+     * to read from the tree is UB. The seed node can be subsequently
+     * used to write to the tree provided that its create() method is
+     * called prior to writing, which happens inside most mutating
+     * methods in NodeRef. It is the caller's responsibility to verify
+     * that the returned node is readable before subsequently using it
+     * to read from the tree.
+     *
+     * @warning This method will call the error callback (regardless
+     * of build type or of the value of RYML_USE_ASSERT) whenever any
+     * of the following preconditions is violated: a) the object is
+     * valid (points at a tree and a node), b) the calling object must
+     * be readable (must not be in seed state), c) the calling object
+     * must be pointing at a MAP node. The preconditions are similar
+     * to the non-const operator[](csubstr), but instead of using
+     * assertions, this function directly checks those conditions and
+     * calls the error callback if any of the checks fail.
+     *
+     * @note since it is valid behavior for the returned node to be in
+     * seed state, the error callback is not invoked when this
+     * happens. */
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto operator[] (size_t pos) noexcept -> _C4_IF_MUTABLE(Impl)
-    {
-        _C4RV();
+    C4_ALWAYS_INLINE auto at(csubstr key) -> _C4_IF_MUTABLE(Impl)
+    {
+        RYML_CHECK(tree_ != nullptr);
+        _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < tree_->capacity()));
+        _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable());
+        _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_map(id_));
+        size_t ch = tree__->find_child(id__, key);
+        return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, key);
+    }
+
+    /** Find child by position; complexity is O(pos).
+     *
+     * Returns the requested node, or an object in seed state if no
+     * such child is found (see @ref NodeRef for an explanation of
+     * what is seed state). When the object is in seed state, using it
+     * to read from the tree is UB. The seed node can be used to write
+     * to the tree provided that its create() method is called prior
+     * to writing, which happens in most modifying methods in
+     * NodeRef. It is the caller's responsibility to verify that the
+     * returned node is readable before subsequently using it to read
+     * from the tree.
+     *
+     * @warning This method will call the error callback (regardless
+     * of build type or of the value of RYML_USE_ASSERT) whenever any
+     * of the following preconditions is violated: a) the object is
+     * valid (points at a tree and a node), b) the calling object must
+     * be readable (must not be in seed state), c) the calling object
+     * must be pointing at a MAP node. The preconditions are similar
+     * to the non-const operator[](size_t), but instead of using
+     * assertions, this function directly checks those conditions and
+     * calls the error callback if any of the checks fail.
+     *
+     * @note since it is valid behavior for the returned node to be in
+     * seed state, the error callback is not invoked when this
+     * happens. */
+    template<class U=Impl>
+    C4_ALWAYS_INLINE auto at(size_t pos) -> _C4_IF_MUTABLE(Impl)
+    {
+        RYML_CHECK(tree_ != nullptr);
+        const size_t cap = tree_->capacity();
+        _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < cap));
+        _RYML_CB_CHECK(tree_->m_callbacks, (pos >= 0 && pos < cap));
+        _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable());
+        _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_container(id_));
         size_t ch = tree__->child(id__, pos);
-        return ch != NONE ? Impl(tree__, ch) : NodeRef(tree__, id__, pos);
+        return ch != NONE ? Impl(tree__, ch) : Impl(tree__, id__, pos);
+    }
+
+    /** Get a child by name, with error checking; complexity is
+     * O(num_children).
+     *
+     * Behaves as operator[](csubstr) const, but always raises an
+     * error (even when RYML_USE_ASSERT is set to false) when the
+     * returned node does not exist, or when this node is not
+     * readable, or when it is not a map. This behaviour is similar to
+     * std::vector::at(), but the error consists in calling the error
+     * callback instead of directly raising an exception. */
+    ConstImpl at(csubstr key) const
+    {
+        RYML_CHECK(tree_ != nullptr);
+        _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < tree_->capacity()));
+        _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable());
+        _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_map(id_));
+        size_t ch = tree_->find_child(id_, key);
+        _RYML_CB_CHECK(tree_->m_callbacks, ch != NONE);
+        return {tree_, ch};
+    }
+
+    /** Get a child by position, with error checking; complexity is
+     * O(pos).
+     *
+     * Behaves as operator[](size_t) const, but always raises an error
+     * (even when RYML_USE_ASSERT is set to false) when the returned
+     * node does not exist, or when this node is not readable, or when
+     * it is not a container. This behaviour is similar to
+     * std::vector::at(), but the error consists in calling the error
+     * callback instead of directly raising an exception. */
+    ConstImpl at(size_t pos) const
+    {
+        RYML_CHECK(tree_ != nullptr);
+        const size_t cap = tree_->capacity();
+        _RYML_CB_CHECK(tree_->m_callbacks, (id_ >= 0 && id_ < cap));
+        _RYML_CB_CHECK(tree_->m_callbacks, (pos >= 0 && pos < cap));
+        _RYML_CB_CHECK(tree_->m_callbacks, ((Impl const*)this)->readable());
+        _RYML_CB_CHECK(tree_->m_callbacks, tree_->is_container(id_));
+        size_t ch = tree_->child(id_, pos);
+        _RYML_CB_CHECK(tree_->m_callbacks, ch != NONE);
+        return {tree_, ch};
     }
 
     /** @} */
@@ -364,7 +557,7 @@ struct RoNodeMethods
     template<class T>
     ConstImpl const& operator>> (T &v) const
     {
-        _C4RV();
+        _C4RR();
         if( ! read((ConstImpl const&)*this, &v))
             _RYML_CB_ERR(tree_->m_callbacks, "could not deserialize value");
         return *((ConstImpl const*)this);
@@ -374,7 +567,7 @@ struct RoNodeMethods
     template<class T>
     ConstImpl const& operator>> (Key<T> v) const
     {
-        _C4RV();
+        _C4RR();
         if( ! from_chars(key(), &v.k))
             _RYML_CB_ERR(tree_->m_callbacks, "could not deserialize key");
         return *((ConstImpl const*)this);
@@ -399,7 +592,7 @@ struct RoNodeMethods
      * @return the size of base64-decoded blob */
     size_t deserialize_key(fmt::base64_wrapper v) const
     {
-        _C4RV();
+        _C4RR();
         return from_chars(key(), &v);
     }
     /** decode the base64-encoded key and assign the
@@ -407,15 +600,16 @@ struct RoNodeMethods
      * @return the size of base64-decoded blob */
     size_t deserialize_val(fmt::base64_wrapper v) const
     {
-        _C4RV();
+        _C4RR();
         return from_chars(val(), &v);
     };
 
     template<class T>
     bool get_if(csubstr name, T *var) const
     {
-        auto ch = find_child(name);
-        if(!ch.valid())
+        _C4RR();
+        ConstImpl ch = find_child(name);
+        if(!ch.readable())
             return false;
         ch >> *var;
         return true;
@@ -424,8 +618,9 @@ struct RoNodeMethods
     template<class T>
     bool get_if(csubstr name, T *var, T const& fallback) const
     {
-        auto ch = find_child(name);
-        if(ch.valid())
+        _C4RR();
+        ConstImpl ch = find_child(name);
+        if(ch.readable())
         {
             ch >> *var;
             return true;
@@ -460,28 +655,28 @@ struct RoNodeMethods
     using const_children_view = detail::children_view_<ConstImpl>;
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto begin() noexcept -> _C4_IF_MUTABLE(iterator) { _C4RV(); return iterator(tree__, tree__->first_child(id__)); }
-    C4_ALWAYS_INLINE C4_PURE const_iterator begin() const noexcept { _C4RV(); return const_iterator(tree_, tree_->first_child(id_)); }
-    C4_ALWAYS_INLINE C4_PURE const_iterator cbegin() const noexcept { _C4RV(); return const_iterator(tree_, tree_->first_child(id_)); }
+    C4_ALWAYS_INLINE auto begin() RYML_NOEXCEPT -> _C4_IF_MUTABLE(iterator) { _C4RR(); return iterator(tree__, tree__->first_child(id__)); }
+    C4_ALWAYS_INLINE const_iterator begin() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, tree_->first_child(id_)); }
+    C4_ALWAYS_INLINE const_iterator cbegin() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, tree_->first_child(id_)); }
 
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto end() noexcept -> _C4_IF_MUTABLE(iterator) { _C4RV(); return iterator(tree__, NONE); }
-    C4_ALWAYS_INLINE C4_PURE const_iterator end() const noexcept { _C4RV(); return const_iterator(tree_, NONE); }
-    C4_ALWAYS_INLINE C4_PURE const_iterator cend() const noexcept { _C4RV(); return const_iterator(tree_, tree_->first_child(id_)); }
+    C4_ALWAYS_INLINE auto end() RYML_NOEXCEPT -> _C4_IF_MUTABLE(iterator) { _C4RR(); return iterator(tree__, NONE); }
+    C4_ALWAYS_INLINE const_iterator end() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, NONE); }
+    C4_ALWAYS_INLINE const_iterator cend() const RYML_NOEXCEPT { _C4RR(); return const_iterator(tree_, tree_->first_child(id_)); }
 
     /** get an iterable view over children */
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto children() noexcept -> _C4_IF_MUTABLE(children_view) { _C4RV(); return children_view(begin(), end()); }
+    C4_ALWAYS_INLINE auto children() RYML_NOEXCEPT -> _C4_IF_MUTABLE(children_view) { _C4RR(); return children_view(begin(), end()); }
     /** get an iterable view over children */
-    C4_ALWAYS_INLINE C4_PURE const_children_view children() const noexcept { _C4RV(); return const_children_view(begin(), end()); }
+    C4_ALWAYS_INLINE const_children_view children() const RYML_NOEXCEPT { _C4RR(); return const_children_view(begin(), end()); }
     /** get an iterable view over children */
-    C4_ALWAYS_INLINE C4_PURE const_children_view cchildren() const noexcept { _C4RV(); return const_children_view(begin(), end()); }
+    C4_ALWAYS_INLINE const_children_view cchildren() const RYML_NOEXCEPT { _C4RR(); return const_children_view(begin(), end()); }
 
     /** get an iterable view over all siblings (including the calling node) */
     template<class U=Impl>
-    C4_ALWAYS_INLINE C4_PURE auto siblings() noexcept -> _C4_IF_MUTABLE(children_view)
+    C4_ALWAYS_INLINE auto siblings() RYML_NOEXCEPT -> _C4_IF_MUTABLE(children_view)
     {
-        _C4RV();
+        _C4RR();
         NodeData const *nd = tree__->get(id__);
         return (nd->m_parent != NONE) ? // does it have a parent?
             children_view(iterator(tree__, tree_->get(nd->m_parent)->m_first_child), iterator(tree__, NONE))
@@ -489,9 +684,9 @@ struct RoNodeMethods
             children_view(end(), end());
     }
     /** get an iterable view over all siblings (including the calling node) */
-    C4_ALWAYS_INLINE C4_PURE const_children_view siblings() const noexcept
+    C4_ALWAYS_INLINE const_children_view siblings() const RYML_NOEXCEPT
     {
-        _C4RV();
+        _C4RR();
         NodeData const *nd = tree_->get(id_);
         return (nd->m_parent != NONE) ? // does it have a parent?
             const_children_view(const_iterator(tree_, tree_->get(nd->m_parent)->m_first_child), const_iterator(tree_, NONE))
@@ -499,33 +694,37 @@ struct RoNodeMethods
             const_children_view(end(), end());
     }
     /** get an iterable view over all siblings (including the calling node) */
-    C4_ALWAYS_INLINE C4_PURE const_children_view csiblings() const noexcept { return siblings(); }
+    C4_ALWAYS_INLINE const_children_view csiblings() const RYML_NOEXCEPT { return siblings(); }
 
     /** visit every child node calling fn(node) */
     template<class Visitor>
-    C4_ALWAYS_INLINE bool visit(Visitor fn, size_t indentation_level=0, bool skip_root=true) const noexcept
+    bool visit(Visitor fn, size_t indentation_level=0, bool skip_root=true) const RYML_NOEXCEPT
     {
+        _C4RR();
         return detail::_visit(*(ConstImpl const*)this, fn, indentation_level, skip_root);
     }
     /** visit every child node calling fn(node) */
     template<class Visitor, class U=Impl>
-    auto visit(Visitor fn, size_t indentation_level=0, bool skip_root=true) noexcept
+    auto visit(Visitor fn, size_t indentation_level=0, bool skip_root=true) RYML_NOEXCEPT
         -> _C4_IF_MUTABLE(bool)
     {
+        _C4RR();
         return detail::_visit(*(Impl*)this, fn, indentation_level, skip_root);
     }
 
     /** visit every child node calling fn(node, level) */
     template<class Visitor>
-    C4_ALWAYS_INLINE bool visit_stacked(Visitor fn, size_t indentation_level=0, bool skip_root=true) const noexcept
+    bool visit_stacked(Visitor fn, size_t indentation_level=0, bool skip_root=true) const RYML_NOEXCEPT
     {
+        _C4RR();
         return detail::_visit_stacked(*(ConstImpl const*)this, fn, indentation_level, skip_root);
     }
     /** visit every child node calling fn(node, level) */
     template<class Visitor, class U=Impl>
-    auto visit_stacked(Visitor fn, size_t indentation_level=0, bool skip_root=true) noexcept
+    auto visit_stacked(Visitor fn, size_t indentation_level=0, bool skip_root=true) RYML_NOEXCEPT
         -> _C4_IF_MUTABLE(bool)
     {
+        _C4RR();
         return detail::_visit_stacked(*(Impl*)this, fn, indentation_level, skip_root);
     }
 
@@ -538,7 +737,7 @@ struct RoNodeMethods
     #endif
 
     #undef _C4_IF_MUTABLE
-    #undef _C4RV
+    #undef _C4RR
     #undef tree_
     #undef tree__
     #undef id_
@@ -553,6 +752,11 @@ struct RoNodeMethods
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
+/** This object holds a pointer to an existing tree, and a node id. It
+ * can be used only to read from the tree.
+ *
+ * @warning The lifetime of the tree must be larger than that of this
+ * object. It is up to the user to ensure that this happens. */
 class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, ConstNodeRef>
 {
 public:
@@ -604,10 +808,20 @@ class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, Cons
 
 public:
 
-    /** @name state queries */
+    /** @name state queries
+     *
+     * see @ref NodeRef for an explanation on what these states mean */
     /** @{ */
 
-    C4_ALWAYS_INLINE C4_PURE bool valid() const noexcept { return m_tree != nullptr && m_id != NONE; }
+    C4_ALWAYS_INLINE bool invalid() const noexcept { return (!m_tree) || (m_id == NONE); }
+    /** because a ConstNodeRef cannot be used to write to the tree,
+     * readable() has the same meaning as !invalid() */
+    C4_ALWAYS_INLINE bool readable() const noexcept { return m_tree != nullptr && m_id != NONE; }
+    /** because a ConstNodeRef cannot be used to write to the tree, it can never be a seed.
+     * This method is provided for API equivalence between ConstNodeRef and NodeRef. */
+    constexpr static C4_ALWAYS_INLINE bool is_seed() noexcept { return false; }
+
+    RYML_DEPRECATED("use one of readable(), is_seed() or !invalid()") bool valid() const noexcept { return m_tree != nullptr && m_id != NONE; }
 
     /** @} */
 
@@ -616,8 +830,8 @@ class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, Cons
     /** @name member getters */
     /** @{ */
 
-    C4_ALWAYS_INLINE C4_PURE Tree const* tree() const noexcept { return m_tree; }
-    C4_ALWAYS_INLINE C4_PURE size_t id() const noexcept { return m_id; }
+    C4_ALWAYS_INLINE Tree const* tree() const noexcept { return m_tree; }
+    C4_ALWAYS_INLINE size_t id() const noexcept { return m_id; }
 
     /** @} */
 
@@ -626,14 +840,14 @@ class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, Cons
     /** @name comparisons */
     /** @{ */
 
-    C4_ALWAYS_INLINE C4_PURE bool operator== (ConstNodeRef const& that) const noexcept { RYML_ASSERT(that.m_tree == m_tree); return m_id == that.m_id; }
-    C4_ALWAYS_INLINE C4_PURE bool operator!= (ConstNodeRef const& that) const noexcept { RYML_ASSERT(that.m_tree == m_tree); return ! this->operator==(that); }
+    C4_ALWAYS_INLINE bool operator== (ConstNodeRef const& that) const RYML_NOEXCEPT { return that.m_tree == m_tree && m_id == that.m_id; }
+    C4_ALWAYS_INLINE bool operator!= (ConstNodeRef const& that) const RYML_NOEXCEPT { return ! this->operator== (that); }
 
-    C4_ALWAYS_INLINE C4_PURE bool operator== (std::nullptr_t) const noexcept { return m_tree == nullptr || m_id == NONE; }
-    C4_ALWAYS_INLINE C4_PURE bool operator!= (std::nullptr_t) const noexcept { return ! this->operator== (nullptr); }
+    RYML_DEPRECATED("use invalid()")  bool operator== (std::nullptr_t) const noexcept { return m_tree == nullptr || m_id == NONE; }
+    RYML_DEPRECATED("use !invalid()") bool operator!= (std::nullptr_t) const noexcept { return !(m_tree == nullptr || m_id == NONE); }
 
-    C4_ALWAYS_INLINE C4_PURE bool operator== (csubstr val) const noexcept { RYML_ASSERT(has_val()); return m_tree->val(m_id) == val; }
-    C4_ALWAYS_INLINE C4_PURE bool operator!= (csubstr val) const noexcept { RYML_ASSERT(has_val()); return m_tree->val(m_id) != val; }
+    RYML_DEPRECATED("use (this->val() == s)") bool operator== (csubstr s) const RYML_NOEXCEPT { RYML_ASSERT(m_tree); _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE); return m_tree->val(m_id) == s; }
+    RYML_DEPRECATED("use (this->val() != s)") bool operator!= (csubstr s) const RYML_NOEXCEPT { RYML_ASSERT(m_tree); _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE); return m_tree->val(m_id) != s; }
 
     /** @} */
 
@@ -644,8 +858,42 @@ class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, Cons
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 
-/** a reference to a node in an existing yaml tree, offering a more
- * convenient API than the index-based API used in the tree. */
+/** A reference to a node in an existing yaml tree, offering a more
+ * convenient API than the index-based API used in the tree.
+ *
+ * Unlike its imutable ConstNodeRef peer, a NodeRef can be used to
+ * mutate the tree, both by writing to existing nodes and by creating
+ * new nodes to subsequently write to. So, semantically, a NodeRef
+ * object can be in one of three states:
+ *
+ * ```
+ * invalid  := not pointing at anything
+ * readable := points at an existing tree/node
+ * seed     := points at an existing tree, and the node
+ *             may come to exist, if we write to it.
+ * ```
+ *
+ * So both `readable` and `seed` are states where the node is also `valid`.
+ *
+ * ```c++
+ * Tree tree = parse("{a: b}");
+ * NodeRef invalid; // not pointing at anything.
+ * NodeRef readable = tree["a"]; // also valid, because "a" exists
+ * NodeRef seed = tree["none"]; // also valid, but is seed because "none" is not in the map
+ * ```
+ *
+ * When the object is in seed state, using it to read from the tree is
+ * UB. The seed node can be used to write to the tree, provided that
+ * its create() method is called prior to writing, which happens in
+ * most modifying methods in NodeRef.
+ *
+ * It is the owners's responsibility to verify that an existing
+ * node is readable before subsequently using it to read from the
+ * tree.
+ *
+ * @warning The lifetime of the tree must be larger than that of this
+ * object. It is up to the user to ensure that this happens.
+ */
 class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
 {
 public:
@@ -677,6 +925,10 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     #define _C4RV()                                                         \
         RYML_ASSERT(m_tree != nullptr);                                     \
         _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE && !is_seed())
+    // require id: a helper macro, undefined at the end
+    #define _C4RID()                                                        \
+        RYML_ASSERT(m_tree != nullptr);                                     \
+        _RYML_CB_ASSERT(m_tree->m_callbacks, m_id != NONE)
 
 public:
 
@@ -708,13 +960,19 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
 
 public:
 
-    /** @name state queries */
-    /** @{ */
+    /** @name state_queries
+     * @{ */
 
-    inline bool valid() const { return m_tree != nullptr && m_id != NONE; }
-    inline bool is_seed() const { return m_seed.str != nullptr || m_seed.len != NONE; }
+    /** true if the object is not referring to any existing or seed node @see the doc for the NodeRef */
+    inline bool invalid() const { return m_tree == nullptr || m_id == NONE; }
+    /** true if the object is not invalid and in seed state. @see the doc for the NodeRef */
+    inline bool is_seed() const { return (m_tree != NULL && m_id != NONE) && (m_seed.str != nullptr || m_seed.len != (size_t)NONE); }
+    /** true if the object is not invalid and not in seed state. @see the doc for the NodeRef */
+    inline bool readable() const { return (m_tree != NULL && m_id != NONE) && (m_seed.str == nullptr && m_seed.len == (size_t)NONE); }
 
-    inline void _clear_seed() { /*do this manually or an assert is triggered*/ m_seed.str = nullptr; m_seed.len = NONE; }
+    RYML_DEPRECATED("use one of readable(), is_seed() or !invalid()") inline bool valid() const { return m_tree != nullptr && m_id != NONE; }
+
+    inline void _clear_seed() { /*do the following manually or an assert is triggered: */ m_seed.str = nullptr; m_seed.len = NONE; }
 
     /** @} */
 
@@ -723,19 +981,34 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     /** @name comparisons */
     /** @{ */
 
-    inline bool operator== (NodeRef const& that) const { _C4RV(); RYML_ASSERT(that.valid() && !that.is_seed()); RYML_ASSERT(that.m_tree == m_tree); return m_id == that.m_id; }
+    bool operator== (NodeRef const& that) const
+    {
+        if(m_tree == that.m_tree && m_id == that.m_id)
+        {
+            bool seed = is_seed();
+            if(seed == that.is_seed())
+            {
+                if(seed)
+                {
+                    return (m_seed.len == that.m_seed.len)
+                        && (m_seed.str == that.m_seed.str
+                            || m_seed == that.m_seed); // do strcmp only in the last resort
+                }
+                return true;
+            }
+        }
+        return false;
+    }
     inline bool operator!= (NodeRef const& that) const { return ! this->operator==(that); }
 
-    inline bool operator== (ConstNodeRef const& that) const { _C4RV(); RYML_ASSERT(that.valid()); RYML_ASSERT(that.m_tree == m_tree); return m_id == that.m_id; }
+    inline bool operator== (ConstNodeRef const& that) const { return m_tree == that.m_tree && m_id == that.m_id && !is_seed(); }
     inline bool operator!= (ConstNodeRef const& that) const { return ! this->operator==(that); }
 
-    inline bool operator== (std::nullptr_t) const { return m_tree == nullptr || m_id == NONE || is_seed(); }
-    inline bool operator!= (std::nullptr_t) const { return m_tree != nullptr && m_id != NONE && !is_seed(); }
-
-    inline bool operator== (csubstr val) const { _C4RV(); RYML_ASSERT(has_val()); return m_tree->val(m_id) == val; }
-    inline bool operator!= (csubstr val) const { _C4RV(); RYML_ASSERT(has_val()); return m_tree->val(m_id) != val; }
+    RYML_DEPRECATED("use !readable()") bool operator== (std::nullptr_t) const { return m_tree == nullptr || m_id == NONE || is_seed(); }
+    RYML_DEPRECATED("use readable()")  bool operator!= (std::nullptr_t) const { return !(m_tree == nullptr || m_id == NONE || is_seed()); }
 
-    //inline operator bool () const { return m_tree == nullptr || m_id == NONE || is_seed(); }
+    RYML_DEPRECATED("use (this->val() == s)") bool operator== (csubstr s) const { _C4RV(); _RYML_CB_ASSERT(m_tree->m_callbacks, has_val()); return m_tree->val(m_id) == s; }
+    RYML_DEPRECATED("use (this->val() != s)") bool operator!= (csubstr s) const { _C4RV(); _RYML_CB_ASSERT(m_tree->m_callbacks, has_val()); return m_tree->val(m_id) != s; }
 
     /** @} */
 
@@ -756,22 +1029,24 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     /** @name node modifiers */
     /** @{ */
 
+    void create() { _apply_seed(); }
+
     void change_type(NodeType t) { _C4RV(); m_tree->change_type(m_id, t); }
 
-    void set_type(NodeType t) { _C4RV(); m_tree->_set_flags(m_id, t); }
-    void set_key(csubstr key) { _C4RV(); m_tree->_set_key(m_id, key); }
-    void set_val(csubstr val) { _C4RV(); m_tree->_set_val(m_id, val); }
-    void set_key_tag(csubstr key_tag) { _C4RV(); m_tree->set_key_tag(m_id, key_tag); }
-    void set_val_tag(csubstr val_tag) { _C4RV(); m_tree->set_val_tag(m_id, val_tag); }
-    void set_key_anchor(csubstr key_anchor) { _C4RV(); m_tree->set_key_anchor(m_id, key_anchor); }
-    void set_val_anchor(csubstr val_anchor) { _C4RV(); m_tree->set_val_anchor(m_id, val_anchor); }
-    void set_key_ref(csubstr key_ref) { _C4RV(); m_tree->set_key_ref(m_id, key_ref); }
-    void set_val_ref(csubstr val_ref) { _C4RV(); m_tree->set_val_ref(m_id, val_ref); }
+    void set_type(NodeType t) { _apply_seed(); m_tree->_set_flags(m_id, t); }
+    void set_key(csubstr key) { _apply_seed(); m_tree->_set_key(m_id, key); }
+    void set_val(csubstr val) { _apply_seed(); m_tree->_set_val(m_id, val); }
+    void set_key_tag(csubstr key_tag) { _apply_seed(); m_tree->set_key_tag(m_id, key_tag); }
+    void set_val_tag(csubstr val_tag) { _apply_seed(); m_tree->set_val_tag(m_id, val_tag); }
+    void set_key_anchor(csubstr key_anchor) { _apply_seed(); m_tree->set_key_anchor(m_id, key_anchor); }
+    void set_val_anchor(csubstr val_anchor) { _apply_seed(); m_tree->set_val_anchor(m_id, val_anchor); }
+    void set_key_ref(csubstr key_ref) { _apply_seed(); m_tree->set_key_ref(m_id, key_ref); }
+    void set_val_ref(csubstr val_ref) { _apply_seed(); m_tree->set_val_ref(m_id, val_ref); }
 
     template<class T>
     size_t set_key_serialized(T const& C4_RESTRICT k)
     {
-        _C4RV();
+        _apply_seed();
         csubstr s = m_tree->to_arena(k);
         m_tree->_set_key(m_id, s);
         return s.len;
@@ -779,23 +1054,25 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     template<class T>
     size_t set_val_serialized(T const& C4_RESTRICT v)
     {
-        _C4RV();
+        _apply_seed();
         csubstr s = m_tree->to_arena(v);
         m_tree->_set_val(m_id, s);
         return s.len;
     }
     size_t set_val_serialized(std::nullptr_t)
     {
-        _C4RV();
+        _apply_seed();
         m_tree->_set_val(m_id, csubstr{});
         return 0;
     }
 
-    /** encode a blob as base64, then assign the result to the node's key
-     * @return the size of base64-encoded blob */
+    /** encode a blob as base64 into the tree's arena, then assign the
+     * result to the node's key @return the size of base64-encoded
+     * blob */
     size_t set_key_serialized(fmt::const_base64_wrapper w);
-    /** encode a blob as base64, then assign the result to the node's val
-     * @return the size of base64-encoded blob */
+    /** encode a blob as base64 into the tree's arena, then assign the
+     * result to the node's val @return the size of base64-encoded
+     * blob */
     size_t set_val_serialized(fmt::const_base64_wrapper w);
 
 public:
@@ -829,8 +1106,6 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
         m_tree->remove_children(m_id);
     }
 
-    void create() { _apply_seed(); }
-
     inline void operator= (NodeType_e t)
     {
         _apply_seed();
@@ -887,7 +1162,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     template<class T>
     inline csubstr to_arena(T const& C4_RESTRICT s)
     {
-        _C4RV();
+        RYML_ASSERT(m_tree); // no need for valid or readable
         return m_tree->to_arena(s);
     }
 
@@ -898,7 +1173,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
         // operator<< for writing a substr to a stream)
         _apply_seed();
         write(this, s);
-        RYML_ASSERT(val() == s);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, val() == s);
         return *this;
     }
 
@@ -946,9 +1221,9 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
 
     void _apply_seed()
     {
+        _C4RID();
         if(m_seed.str) // we have a seed key: use it to create the new child
         {
-            //RYML_ASSERT(i.key.scalar.empty() || m_key == i.key.scalar || m_key.empty());
             m_id = m_tree->append_child(m_id);
             m_tree->_set_key(m_id, m_seed);
             m_seed.str = nullptr;
@@ -956,14 +1231,14 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
         }
         else if(m_seed.len != NONE) // we have a seed index: create a child at that position
         {
-            RYML_ASSERT(m_tree->num_children(m_id) == m_seed.len);
+            _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree->num_children(m_id) == m_seed.len);
             m_id = m_tree->append_child(m_id);
             m_seed.str = nullptr;
             m_seed.len = NONE;
         }
         else
         {
-            RYML_ASSERT(valid());
+            _RYML_CB_ASSERT(m_tree->m_callbacks, readable());
         }
     }
 
@@ -990,7 +1265,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline NodeRef insert_child(NodeRef after)
     {
         _C4RV();
-        RYML_ASSERT(after.m_tree == m_tree);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree);
         NodeRef r(m_tree, m_tree->insert_child(m_id, after.m_id));
         return r;
     }
@@ -998,7 +1273,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline NodeRef insert_child(NodeInit const& i, NodeRef after)
     {
         _C4RV();
-        RYML_ASSERT(after.m_tree == m_tree);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree);
         NodeRef r(m_tree, m_tree->insert_child(m_id, after.m_id));
         r._apply(i);
         return r;
@@ -1039,7 +1314,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline NodeRef insert_sibling(ConstNodeRef const& after)
     {
         _C4RV();
-        RYML_ASSERT(after.m_tree == m_tree);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree);
         NodeRef r(m_tree, m_tree->insert_sibling(m_id, after.m_id));
         return r;
     }
@@ -1047,7 +1322,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline NodeRef insert_sibling(NodeInit const& i, ConstNodeRef const& after)
     {
         _C4RV();
-        RYML_ASSERT(after.m_tree == m_tree);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, after.m_tree == m_tree);
         NodeRef r(m_tree, m_tree->insert_sibling(m_id, after.m_id));
         r._apply(i);
         return r;
@@ -1088,8 +1363,8 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline void remove_child(NodeRef & child)
     {
         _C4RV();
-        RYML_ASSERT(has_child(child));
-        RYML_ASSERT(child.parent().id() == id());
+        _RYML_CB_ASSERT(m_tree->m_callbacks, has_child(child));
+        _RYML_CB_ASSERT(m_tree->m_callbacks, child.parent().id() == id());
         m_tree->remove(child.id());
         child.clear();
     }
@@ -1098,9 +1373,9 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline void remove_child(size_t pos)
     {
         _C4RV();
-        RYML_ASSERT(pos >= 0 && pos < num_children());
+        _RYML_CB_ASSERT(m_tree->m_callbacks, pos >= 0 && pos < num_children());
         size_t child = m_tree->child(m_id, pos);
-        RYML_ASSERT(child != NONE);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, child != NONE);
         m_tree->remove(child);
     }
 
@@ -1109,7 +1384,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     {
         _C4RV();
         size_t child = m_tree->find_child(m_id, key);
-        RYML_ASSERT(child != NONE);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, child != NONE);
         m_tree->remove(child);
     }
 
@@ -1150,7 +1425,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline NodeRef duplicate(ConstNodeRef const& after) const
     {
         _C4RV();
-        RYML_ASSERT(m_tree == after.m_tree || after.m_id == NONE);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, m_tree == after.m_tree || after.m_id == NONE);
         size_t dup = m_tree->duplicate(m_id, m_tree->parent(m_id), after.m_id);
         NodeRef r(m_tree, dup);
         return r;
@@ -1164,7 +1439,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline NodeRef duplicate(NodeRef const& parent, ConstNodeRef const& after) const
     {
         _C4RV();
-        RYML_ASSERT(parent.m_tree == after.m_tree || after.m_id == NONE);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, parent.m_tree == after.m_tree || after.m_id == NONE);
         if(parent.m_tree == m_tree)
         {
             size_t dup = m_tree->duplicate(m_id, parent.m_id, after.m_id);
@@ -1182,7 +1457,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     inline void duplicate_children(NodeRef const& parent, ConstNodeRef const& after) const
     {
         _C4RV();
-        RYML_ASSERT(parent.m_tree == after.m_tree);
+        _RYML_CB_ASSERT(m_tree->m_callbacks, parent.m_tree == after.m_tree);
         if(parent.m_tree == m_tree)
         {
             m_tree->duplicate_children(m_id, parent.m_id, after.m_id);
@@ -1196,6 +1471,7 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
     /** @} */
 
 #undef _C4RV
+#undef _C4RID
 };
 
 
diff --git a/src/c4/yml/parse.cpp b/src/c4/yml/parse.cpp
index 84f6abcad..0007e4d50 100644
--- a/src/c4/yml/parse.cpp
+++ b/src/c4/yml/parse.cpp
@@ -4527,7 +4527,7 @@ bool Parser::_filter_nl(substr r, size_t *C4_RESTRICT i, size_t *C4_RESTRICT pos
     #define _c4dbgfnl(...)
     #endif
 
-    const char curr = r[*i];
+    const char curr = r[*i];(void)curr;
     bool replaced = false;
 
     _RYML_CB_ASSERT(m_stack.m_callbacks, indentation != npos);
@@ -5528,7 +5528,7 @@ csubstr Parser::location_contents(Location const& loc) const
 
 Location Parser::location(ConstNodeRef node) const
 {
-    _RYML_CB_ASSERT(m_stack.m_callbacks, node.valid());
+    _RYML_CB_ASSERT(m_stack.m_callbacks, node.readable());
     return location(*node.tree(), node.id());
 }
 
diff --git a/src/c4/yml/parse.hpp b/src/c4/yml/parse.hpp
index 659edf7e0..93a652d27 100644
--- a/src/c4/yml/parse.hpp
+++ b/src/c4/yml/parse.hpp
@@ -388,9 +388,9 @@ class RYML_EXPORT Parser
     csubstr _consume_scalar();
     void  _move_scalar_from_top();
 
-    inline NodeData* _append_val_null(const char *str) { _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); return _append_val({nullptr, size_t(0)}); }
-    inline NodeData* _append_key_val_null(const char *str) { _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); return _append_key_val({nullptr, size_t(0)}); }
-    inline void      _store_scalar_null(const char *str) {  _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); _store_scalar({nullptr, size_t(0)}, false); }
+    inline NodeData* _append_val_null(const char *str) { _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); (void)str; return _append_val({nullptr, size_t(0)}); }
+    inline NodeData* _append_key_val_null(const char *str) { _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); (void)str; return _append_key_val({nullptr, size_t(0)}); }
+    inline void      _store_scalar_null(const char *str) {  _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); (void)str; _store_scalar({nullptr, size_t(0)}, false); }
 
     void  _set_indentation(size_t behind);
     void  _save_indentation(size_t behind=0);
diff --git a/src/c4/yml/std/vector.hpp b/src/c4/yml/std/vector.hpp
index 1b6a4610a..49963ee93 100644
--- a/src/c4/yml/std/vector.hpp
+++ b/src/c4/yml/std/vector.hpp
@@ -38,7 +38,7 @@ bool read(c4::yml::ConstNodeRef const& n, std::vector<bool, Alloc> *vec)
 {
     vec->resize(n.num_children());
     size_t pos = 0;
-    bool tmp;
+    bool tmp = false;
     for(auto const ch : n)
     {
         ch >> tmp;
diff --git a/src/c4/yml/tree.cpp b/src/c4/yml/tree.cpp
index b3819868e..2930dffdc 100644
--- a/src/c4/yml/tree.cpp
+++ b/src/c4/yml/tree.cpp
@@ -246,10 +246,6 @@ ConstNodeRef Tree::rootref() const
     return ConstNodeRef(this, root_id());
 }
 
-ConstNodeRef Tree::crootref()
-{
-    return ConstNodeRef(this, root_id());
-}
 ConstNodeRef Tree::crootref() const
 {
     return ConstNodeRef(this, root_id());
@@ -257,23 +253,17 @@ ConstNodeRef Tree::crootref() const
 
 NodeRef Tree::ref(size_t id)
 {
-    _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_size);
+    _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_cap);
     return NodeRef(this, id);
 }
 ConstNodeRef Tree::ref(size_t id) const
 {
-    _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_size);
-    return ConstNodeRef(this, id);
-}
-
-ConstNodeRef Tree::cref(size_t id)
-{
-    _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_size);
+    _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_cap);
     return ConstNodeRef(this, id);
 }
 ConstNodeRef Tree::cref(size_t id) const
 {
-    _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_size);
+    _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_cap);
     return ConstNodeRef(this, id);
 }
 
@@ -331,12 +321,12 @@ Tree::~Tree()
 }
 
 
-Tree::Tree(Tree const& that) noexcept : Tree(that.m_callbacks)
+Tree::Tree(Tree const& that) : Tree(that.m_callbacks)
 {
     _copy(that);
 }
 
-Tree& Tree::operator= (Tree const& that) noexcept
+Tree& Tree::operator= (Tree const& that)
 {
     _free();
     m_callbacks = that.m_callbacks;
@@ -344,12 +334,12 @@ Tree& Tree::operator= (Tree const& that) noexcept
     return *this;
 }
 
-Tree::Tree(Tree && that) noexcept : Tree(that.m_callbacks)
+Tree::Tree(Tree && that) : Tree(that.m_callbacks)
 {
     _move(that);
 }
 
-Tree& Tree::operator= (Tree && that) noexcept
+Tree& Tree::operator= (Tree && that)
 {
     _free();
     m_callbacks = that.m_callbacks;
@@ -1584,6 +1574,7 @@ size_t Tree::child(size_t node, size_t pos) const
 
 size_t Tree::child_pos(size_t node, size_t ch) const
 {
+    _RYML_CB_ASSERT(m_callbacks, node != NONE);
     size_t count = 0;
     for(size_t i = first_child(node); i != NONE; i = next_sibling(i))
     {
@@ -1821,7 +1812,7 @@ csubstr _transform_tag(Tree *t, csubstr tag, size_t node)
     size_t required_size = t->resolve_tag(substr{}, tag, node);
     if(!required_size)
         return tag;
-    const char *prev_arena = t->arena().str;
+    const char *prev_arena = t->arena().str; (void)prev_arena;
     substr buf = t->alloc_arena(required_size);
     _RYML_CB_ASSERT(t->m_callbacks, t->arena().str == prev_arena);
     size_t actual_size = t->resolve_tag(buf, tag, node);
diff --git a/src/c4/yml/tree.hpp b/src/c4/yml/tree.hpp
index c99a20726..28ec2a10f 100644
--- a/src/c4/yml/tree.hpp
+++ b/src/c4/yml/tree.hpp
@@ -339,7 +339,7 @@ struct NodeScalar
 
     void clear() noexcept { tag.clear(); scalar.clear(); anchor.clear(); }
 
-    void set_ref_maybe_replacing_scalar(csubstr ref, bool has_scalar) noexcept
+    void set_ref_maybe_replacing_scalar(csubstr ref, bool has_scalar) RYML_NOEXCEPT
     {
         csubstr trimmed = ref.begins_with('*') ? ref.sub(1) : ref;
         anchor = trimmed;
@@ -453,11 +453,11 @@ class RYML_EXPORT Tree
 
     ~Tree();
 
-    Tree(Tree const& that) noexcept;
-    Tree(Tree     && that) noexcept;
+    Tree(Tree const& that);
+    Tree(Tree     && that);
 
-    Tree& operator= (Tree const& that) noexcept;
-    Tree& operator= (Tree     && that) noexcept;
+    Tree& operator= (Tree const& that);
+    Tree& operator= (Tree     && that);
 
     /** @} */
 
@@ -491,14 +491,14 @@ class RYML_EXPORT Tree
     /** @{ */
 
     //! get the index of a node belonging to this tree.
-    //! @p n can be nullptr, in which case a
+    //! @p n can be nullptr, in which case NONE is returned
     size_t id(NodeData const* n) const
     {
         if( ! n)
         {
             return NONE;
         }
-        RYML_ASSERT(n >= m_buf && n < m_buf + m_cap);
+        _RYML_CB_ASSERT(m_callbacks, n >= m_buf && n < m_buf + m_cap);
         return static_cast<size_t>(n - m_buf);
     }
 
@@ -508,7 +508,7 @@ class RYML_EXPORT Tree
     {
         if(i == NONE)
             return nullptr;
-        RYML_ASSERT(i >= 0 && i < m_cap);
+        _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap);
         return m_buf + i;
     }
     //! get a pointer to a node's NodeData.
@@ -517,40 +517,43 @@ class RYML_EXPORT Tree
     {
         if(i == NONE)
             return nullptr;
-        RYML_ASSERT(i >= 0 && i < m_cap);
+        _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap);
         return m_buf + i;
     }
 
     //! An if-less form of get() that demands a valid node index.
     //! This function is implementation only; use at your own risk.
-    inline NodeData       * _p(size_t i)       { RYML_ASSERT(i != NONE && i >= 0 && i < m_cap); return m_buf + i; }
+    inline NodeData       * _p(size_t i)       { _RYML_CB_ASSERT(m_callbacks, i != NONE && i >= 0 && i < m_cap); return m_buf + i; }
     //! An if-less form of get() that demands a valid node index.
     //! This function is implementation only; use at your own risk.
-    inline NodeData const * _p(size_t i) const { RYML_ASSERT(i != NONE && i >= 0 && i < m_cap); return m_buf + i; }
+    inline NodeData const * _p(size_t i) const { _RYML_CB_ASSERT(m_callbacks, i != NONE && i >= 0 && i < m_cap); return m_buf + i; }
 
     //! Get the id of the root node
-    size_t root_id()       { if(m_cap == 0) { reserve(16); } RYML_ASSERT(m_cap > 0 && m_size > 0); return 0; }
+    size_t root_id()       { if(m_cap == 0) { reserve(16); } _RYML_CB_ASSERT(m_callbacks, m_cap > 0 && m_size > 0); return 0; }
     //! Get the id of the root node
-    size_t root_id() const {                                 RYML_ASSERT(m_cap > 0 && m_size > 0); return 0; }
+    size_t root_id() const {                                 _RYML_CB_ASSERT(m_callbacks, m_cap > 0 && m_size > 0); return 0; }
 
     //! Get a NodeRef of a node by id
     NodeRef      ref(size_t id);
     //! Get a NodeRef of a node by id
     ConstNodeRef ref(size_t id) const;
     //! Get a NodeRef of a node by id
-    ConstNodeRef cref(size_t id);
-    //! Get a NodeRef of a node by id
     ConstNodeRef cref(size_t id) const;
 
     //! Get the root as a NodeRef
     NodeRef      rootref();
-    //! Get the root as a NodeRef
+    //! Get the root as a ConstNodeRef
     ConstNodeRef rootref() const;
-    //! Get the root as a NodeRef
-    ConstNodeRef crootref();
-    //! Get the root as a NodeRef
+    //! Get the root as a ConstNodeRef
     ConstNodeRef crootref() const;
 
+    //! get the i-th document of the stream
+    //! @note @i is NOT the node id, but the doc position within the stream
+    NodeRef      docref(size_t i);
+    //! get the i-th document of the stream
+    //! @note @i is NOT the node id, but the doc position within the stream
+    ConstNodeRef docref(size_t i) const;
+
     //! find a root child by name, return it as a NodeRef
     //! @note requires the root to be a map.
     NodeRef      operator[] (csubstr key);
@@ -565,13 +568,6 @@ class RYML_EXPORT Tree
     //! @note @i is NOT the node id, but the child's position
     ConstNodeRef operator[] (size_t i) const;
 
-    //! get the i-th document of the stream
-    //! @note @i is NOT the node id, but the doc position within the stream
-    NodeRef      docref(size_t i);
-    //! get the i-th document of the stream
-    //! @note @i is NOT the node id, but the doc position within the stream
-    ConstNodeRef docref(size_t i) const;
-
     /** @} */
 
 public:
@@ -582,17 +578,17 @@ class RYML_EXPORT Tree
     NodeType type(size_t node) const { return _p(node)->m_type; }
     const char* type_str(size_t node) const { return NodeType::type_str(_p(node)->m_type); }
 
-    csubstr    const& key       (size_t node) const { RYML_ASSERT(has_key(node)); return _p(node)->m_key.scalar; }
-    csubstr    const& key_tag   (size_t node) const { RYML_ASSERT(has_key_tag(node)); return _p(node)->m_key.tag; }
-    csubstr    const& key_ref   (size_t node) const { RYML_ASSERT(is_key_ref(node) && ! has_key_anchor(node)); return _p(node)->m_key.anchor; }
-    csubstr    const& key_anchor(size_t node) const { RYML_ASSERT( ! is_key_ref(node) && has_key_anchor(node)); return _p(node)->m_key.anchor; }
-    NodeScalar const& keysc     (size_t node) const { RYML_ASSERT(has_key(node)); return _p(node)->m_key; }
+    csubstr    const& key       (size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_key(node)); return _p(node)->m_key.scalar; }
+    csubstr    const& key_tag   (size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_key_tag(node)); return _p(node)->m_key.tag; }
+    csubstr    const& key_ref   (size_t node) const { _RYML_CB_ASSERT(m_callbacks, is_key_ref(node) && ! has_key_anchor(node)); return _p(node)->m_key.anchor; }
+    csubstr    const& key_anchor(size_t node) const { _RYML_CB_ASSERT(m_callbacks,  ! is_key_ref(node) && has_key_anchor(node)); return _p(node)->m_key.anchor; }
+    NodeScalar const& keysc     (size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_key(node)); return _p(node)->m_key; }
 
-    csubstr    const& val       (size_t node) const { RYML_ASSERT(has_val(node)); return _p(node)->m_val.scalar; }
-    csubstr    const& val_tag   (size_t node) const { RYML_ASSERT(has_val_tag(node)); return _p(node)->m_val.tag; }
-    csubstr    const& val_ref   (size_t node) const { RYML_ASSERT(is_val_ref(node) && ! has_val_anchor(node)); return _p(node)->m_val.anchor; }
-    csubstr    const& val_anchor(size_t node) const { RYML_ASSERT( ! is_val_ref(node) && has_val_anchor(node)); return _p(node)->m_val.anchor; }
-    NodeScalar const& valsc     (size_t node) const { RYML_ASSERT(has_val(node)); return _p(node)->m_val; }
+    csubstr    const& val       (size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_val(node)); return _p(node)->m_val.scalar; }
+    csubstr    const& val_tag   (size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_val_tag(node)); return _p(node)->m_val.tag; }
+    csubstr    const& val_ref   (size_t node) const { _RYML_CB_ASSERT(m_callbacks, is_val_ref(node) && ! has_val_anchor(node)); return _p(node)->m_val.anchor; }
+    csubstr    const& val_anchor(size_t node) const { _RYML_CB_ASSERT(m_callbacks,  ! is_val_ref(node) && has_val_anchor(node)); return _p(node)->m_val.anchor; }
+    NodeScalar const& valsc     (size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_val(node)); return _p(node)->m_val; }
 
     /** @} */
 
@@ -626,16 +622,16 @@ class RYML_EXPORT Tree
     C4_ALWAYS_INLINE bool is_val_quoted(size_t node) const { return _p(node)->m_type.is_val_quoted(); }
     C4_ALWAYS_INLINE bool is_quoted(size_t node) const { return _p(node)->m_type.is_quoted(); }
 
-    C4_ALWAYS_INLINE bool parent_is_seq(size_t node) const { RYML_ASSERT(has_parent(node)); return is_seq(_p(node)->m_parent); }
-    C4_ALWAYS_INLINE bool parent_is_map(size_t node) const { RYML_ASSERT(has_parent(node)); return is_map(_p(node)->m_parent); }
+    C4_ALWAYS_INLINE bool parent_is_seq(size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_parent(node)); return is_seq(_p(node)->m_parent); }
+    C4_ALWAYS_INLINE bool parent_is_map(size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_parent(node)); return is_map(_p(node)->m_parent); }
 
     /** true when key and val are empty, and has no children */
     C4_ALWAYS_INLINE bool empty(size_t node) const { return ! has_children(node) && _p(node)->m_key.empty() && (( ! (_p(node)->m_type & VAL)) || _p(node)->m_val.empty()); }
     /** true when the node has an anchor named a */
     C4_ALWAYS_INLINE bool has_anchor(size_t node, csubstr a) const { return _p(node)->m_key.anchor == a || _p(node)->m_val.anchor == a; }
 
-    C4_ALWAYS_INLINE bool key_is_null(size_t node) const { RYML_ASSERT(has_key(node)); NodeData const* C4_RESTRICT n = _p(node); return !n->m_type.is_key_quoted() && scalar_is_null(n->m_key.scalar); }
-    C4_ALWAYS_INLINE bool val_is_null(size_t node) const { RYML_ASSERT(has_val(node)); NodeData const* C4_RESTRICT n = _p(node); return !n->m_type.is_val_quoted() && scalar_is_null(n->m_val.scalar); }
+    C4_ALWAYS_INLINE bool key_is_null(size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_key(node)); NodeData const* C4_RESTRICT n = _p(node); return !n->m_type.is_key_quoted() && scalar_is_null(n->m_key.scalar); }
+    C4_ALWAYS_INLINE bool val_is_null(size_t node) const { _RYML_CB_ASSERT(m_callbacks, has_val(node)); NodeData const* C4_RESTRICT n = _p(node); return !n->m_type.is_val_quoted() && scalar_is_null(n->m_val.scalar); }
 
     /** @todo move this function to node_type.hpp */
     static bool scalar_is_null(csubstr s) noexcept
@@ -654,7 +650,7 @@ class RYML_EXPORT Tree
     /** @name hierarchy predicates */
     /** @{ */
 
-    bool is_root(size_t node) const { RYML_ASSERT(_p(node)->m_parent != NONE || node == 0); return _p(node)->m_parent == NONE; }
+    bool is_root(size_t node) const { _RYML_CB_ASSERT(m_callbacks, _p(node)->m_parent != NONE || node == 0); return _p(node)->m_parent == NONE; }
 
     bool has_parent(size_t node) const { return _p(node)->m_parent != NONE; }
 
@@ -707,14 +703,14 @@ class RYML_EXPORT Tree
     /** counts with this */
     size_t num_siblings(size_t node) const { return is_root(node) ? 1 : num_children(_p(node)->m_parent); }
     /** does not count with this */
-    size_t num_other_siblings(size_t node) const { size_t ns = num_siblings(node); RYML_ASSERT(ns > 0); return ns-1; }
-    size_t sibling_pos(size_t node, size_t sib) const { RYML_ASSERT( ! is_root(node) || node == root_id()); return child_pos(_p(node)->m_parent, sib); }
+    size_t num_other_siblings(size_t node) const { size_t ns = num_siblings(node); _RYML_CB_ASSERT(m_callbacks, ns > 0); return ns-1; }
+    size_t sibling_pos(size_t node, size_t sib) const { _RYML_CB_ASSERT(m_callbacks,  ! is_root(node) || node == root_id()); return child_pos(_p(node)->m_parent, sib); }
     size_t first_sibling(size_t node) const { return is_root(node) ? node : _p(_p(node)->m_parent)->m_first_child; }
     size_t last_sibling(size_t node) const { return is_root(node) ? node : _p(_p(node)->m_parent)->m_last_child; }
     size_t sibling(size_t node, size_t pos) const { return child(_p(node)->m_parent, pos); }
     size_t find_sibling(size_t node, csubstr const& key) const { return find_child(_p(node)->m_parent, key); }
 
-    size_t doc(size_t i) const { size_t rid = root_id(); RYML_ASSERT(is_stream(rid)); return child(rid, i); } //!< gets the @p i document node index. requires that the root node is a stream.
+    size_t doc(size_t i) const { size_t rid = root_id(); _RYML_CB_ASSERT(m_callbacks, is_stream(rid)); return child(rid, i); } //!< gets the @p i document node index. requires that the root node is a stream.
 
     /** @} */
 
@@ -732,16 +728,16 @@ class RYML_EXPORT Tree
     void to_doc(size_t node, type_bits more_flags=0);
     void to_stream(size_t node, type_bits more_flags=0);
 
-    void set_key(size_t node, csubstr key) { RYML_ASSERT(has_key(node)); _p(node)->m_key.scalar = key; }
-    void set_val(size_t node, csubstr val) { RYML_ASSERT(has_val(node)); _p(node)->m_val.scalar = val; }
+    void set_key(size_t node, csubstr key) { _RYML_CB_ASSERT(m_callbacks, has_key(node)); _p(node)->m_key.scalar = key; }
+    void set_val(size_t node, csubstr val) { _RYML_CB_ASSERT(m_callbacks, has_val(node)); _p(node)->m_val.scalar = val; }
 
-    void set_key_tag(size_t node, csubstr tag) { RYML_ASSERT(has_key(node)); _p(node)->m_key.tag = tag; _add_flags(node, KEYTAG); }
-    void set_val_tag(size_t node, csubstr tag) { RYML_ASSERT(has_val(node) || is_container(node)); _p(node)->m_val.tag = tag; _add_flags(node, VALTAG); }
+    void set_key_tag(size_t node, csubstr tag) { _RYML_CB_ASSERT(m_callbacks, has_key(node)); _p(node)->m_key.tag = tag; _add_flags(node, KEYTAG); }
+    void set_val_tag(size_t node, csubstr tag) { _RYML_CB_ASSERT(m_callbacks, has_val(node) || is_container(node)); _p(node)->m_val.tag = tag; _add_flags(node, VALTAG); }
 
-    void set_key_anchor(size_t node, csubstr anchor) { RYML_ASSERT( ! is_key_ref(node)); _p(node)->m_key.anchor = anchor.triml('&'); _add_flags(node, KEYANCH); }
-    void set_val_anchor(size_t node, csubstr anchor) { RYML_ASSERT( ! is_val_ref(node)); _p(node)->m_val.anchor = anchor.triml('&'); _add_flags(node, VALANCH); }
-    void set_key_ref   (size_t node, csubstr ref   ) { RYML_ASSERT( ! has_key_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_key.set_ref_maybe_replacing_scalar(ref, n->m_type.has_key()); _add_flags(node, KEY|KEYREF); }
-    void set_val_ref   (size_t node, csubstr ref   ) { RYML_ASSERT( ! has_val_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_val.set_ref_maybe_replacing_scalar(ref, n->m_type.has_val()); _add_flags(node, VAL|VALREF); }
+    void set_key_anchor(size_t node, csubstr anchor) { _RYML_CB_ASSERT(m_callbacks,  ! is_key_ref(node)); _p(node)->m_key.anchor = anchor.triml('&'); _add_flags(node, KEYANCH); }
+    void set_val_anchor(size_t node, csubstr anchor) { _RYML_CB_ASSERT(m_callbacks,  ! is_val_ref(node)); _p(node)->m_val.anchor = anchor.triml('&'); _add_flags(node, VALANCH); }
+    void set_key_ref   (size_t node, csubstr ref   ) { _RYML_CB_ASSERT(m_callbacks,  ! has_key_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_key.set_ref_maybe_replacing_scalar(ref, n->m_type.has_key()); _add_flags(node, KEY|KEYREF); }
+    void set_val_ref   (size_t node, csubstr ref   ) { _RYML_CB_ASSERT(m_callbacks,  ! has_val_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_val.set_ref_maybe_replacing_scalar(ref, n->m_type.has_val()); _add_flags(node, VAL|VALREF); }
 
     void rem_key_anchor(size_t node) { _p(node)->m_key.anchor.clear(); _rem_flags(node, KEYANCH); }
     void rem_val_anchor(size_t node) { _p(node)->m_val.anchor.clear(); _rem_flags(node, VALANCH); }
@@ -827,9 +823,9 @@ class RYML_EXPORT Tree
      * first child, set after to NONE */
     C4_ALWAYS_INLINE size_t insert_child(size_t parent, size_t after)
     {
-        RYML_ASSERT(parent != NONE);
-        RYML_ASSERT(is_container(parent) || is_root(parent));
-        RYML_ASSERT(after == NONE || (_p(after)->m_parent == parent));
+        _RYML_CB_ASSERT(m_callbacks, parent != NONE);
+        _RYML_CB_ASSERT(m_callbacks, is_container(parent) || is_root(parent));
+        _RYML_CB_ASSERT(m_callbacks, after == NONE || (_p(after)->m_parent == parent));
         size_t child = _claim();
         _set_hierarchy(child, parent, after);
         return child;
@@ -977,7 +973,7 @@ class RYML_EXPORT Tree
     /** get the current capacity of the tree's internal arena */
     inline size_t arena_capacity() const { return m_arena.len; }
     /** get the current slack of the tree's internal arena */
-    inline size_t arena_slack() const { RYML_ASSERT(m_arena.len >= m_arena_pos); return m_arena.len - m_arena_pos; }
+    inline size_t arena_slack() const { _RYML_CB_ASSERT(m_callbacks, m_arena.len >= m_arena_pos); return m_arena.len - m_arena_pos; }
 
     /** get the current arena */
     substr arena() const { return m_arena.first(m_arena_pos); }
@@ -1008,7 +1004,7 @@ class RYML_EXPORT Tree
         {
             rem = _grow_arena(num);
             num = to_chars_float(rem, a);
-            RYML_ASSERT(num <= rem.len);
+            _RYML_CB_ASSERT(m_callbacks, num <= rem.len);
         }
         rem = _request_span(num);
         return rem;
@@ -1034,7 +1030,7 @@ class RYML_EXPORT Tree
         {
             rem = _grow_arena(num);
             num = to_chars(rem, a);
-            RYML_ASSERT(num <= rem.len);
+            _RYML_CB_ASSERT(m_callbacks, num <= rem.len);
         }
         rem = _request_span(num);
         return rem;
@@ -1060,7 +1056,7 @@ class RYML_EXPORT Tree
             {
                 rem = _grow_arena(num);
                 num = to_chars(rem, a);
-                RYML_ASSERT(num <= rem.len);
+                _RYML_CB_ASSERT(m_callbacks, num <= rem.len);
             }
             return _request_span(num);
         }
@@ -1103,8 +1099,8 @@ class RYML_EXPORT Tree
     substr copy_to_arena(csubstr s)
     {
         substr cp = alloc_arena(s.len);
-        RYML_ASSERT(cp.len == s.len);
-        RYML_ASSERT(!s.overlaps(cp));
+        _RYML_CB_ASSERT(m_callbacks, cp.len == s.len);
+        _RYML_CB_ASSERT(m_callbacks, !s.overlaps(cp));
         #if (!defined(__clang__)) && (defined(__GNUC__) && __GNUC__ >= 10)
         C4_SUPPRESS_WARNING_GCC_PUSH
         C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow=") // no need for terminating \0
@@ -1149,7 +1145,7 @@ class RYML_EXPORT Tree
             buf.len = arena_cap;
             if(m_arena.str)
             {
-                RYML_ASSERT(m_arena.len >= 0);
+                _RYML_CB_ASSERT(m_callbacks, m_arena.len >= 0);
                 _relocate(buf); // does a memcpy and changes nodes using the arena
                 m_callbacks.m_free(m_arena.str, m_arena.len, m_callbacks.m_user_data);
             }
@@ -1180,12 +1176,12 @@ class RYML_EXPORT Tree
 
     substr _relocated(csubstr s, substr next_arena) const
     {
-        RYML_ASSERT(m_arena.is_super(s));
-        RYML_ASSERT(m_arena.sub(0, m_arena_pos).is_super(s));
+        _RYML_CB_ASSERT(m_callbacks, m_arena.is_super(s));
+        _RYML_CB_ASSERT(m_callbacks, m_arena.sub(0, m_arena_pos).is_super(s));
         auto pos = (s.str - m_arena.str);
         substr r(next_arena.str + pos, s.len);
-        RYML_ASSERT(r.str - next_arena.str == pos);
-        RYML_ASSERT(next_arena.sub(0, m_arena_pos).is_super(r));
+        _RYML_CB_ASSERT(m_callbacks, r.str - next_arena.str == pos);
+        _RYML_CB_ASSERT(m_callbacks, next_arena.sub(0, m_arena_pos).is_super(r));
         return r;
     }
 
@@ -1288,14 +1284,14 @@ class RYML_EXPORT Tree
         }
         if(f & KEY)
         {
-            RYML_ASSERT(!is_root(node));
+            _RYML_CB_ASSERT(m_callbacks, !is_root(node));
             auto pid = parent(node); C4_UNUSED(pid);
-            RYML_ASSERT(is_map(pid));
+            _RYML_CB_ASSERT(m_callbacks, is_map(pid));
         }
         if((f & VAL) && !is_root(node))
         {
             auto pid = parent(node); C4_UNUSED(pid);
-            RYML_ASSERT(is_map(pid) || is_seq(pid));
+            _RYML_CB_ASSERT(m_callbacks, is_map(pid) || is_seq(pid));
         }
     }
     #endif
@@ -1322,24 +1318,24 @@ class RYML_EXPORT Tree
 
     void _set_val(size_t node, csubstr val, type_bits more_flags=0)
     {
-        RYML_ASSERT(num_children(node) == 0);
-        RYML_ASSERT(!is_seq(node) && !is_map(node));
+        _RYML_CB_ASSERT(m_callbacks, num_children(node) == 0);
+        _RYML_CB_ASSERT(m_callbacks, !is_seq(node) && !is_map(node));
         _p(node)->m_val.scalar = val;
         _add_flags(node, VAL|more_flags);
     }
     void _set_val(size_t node, NodeScalar const& val, type_bits more_flags=0)
     {
-        RYML_ASSERT(num_children(node) == 0);
-        RYML_ASSERT( ! is_container(node));
+        _RYML_CB_ASSERT(m_callbacks, num_children(node) == 0);
+        _RYML_CB_ASSERT(m_callbacks,  ! is_container(node));
         _p(node)->m_val = val;
         _add_flags(node, VAL|more_flags);
     }
 
     void _set(size_t node, NodeInit const& i)
     {
-        RYML_ASSERT(i._check());
+        _RYML_CB_ASSERT(m_callbacks, i._check());
         NodeData *n = _p(node);
-        RYML_ASSERT(n->m_key.scalar.empty() || i.key.scalar.empty() || i.key.scalar == n->m_key.scalar);
+        _RYML_CB_ASSERT(m_callbacks, n->m_key.scalar.empty() || i.key.scalar.empty() || i.key.scalar == n->m_key.scalar);
         _add_flags(node, i.type);
         if(n->m_key.scalar.empty())
         {
@@ -1377,7 +1373,7 @@ class RYML_EXPORT Tree
 
     void _seq2map(size_t node)
     {
-        RYML_ASSERT(is_seq(node));
+        _RYML_CB_ASSERT(m_callbacks, is_seq(node));
         for(size_t i = first_child(node); i != NONE; i = next_sibling(i))
         {
             NodeData *C4_RESTRICT ch = _p(i);


Test patch:
diff --git a/test/callbacks_tester.hpp b/test/callbacks_tester.hpp
index 13be9e40b..89f183728 100644
--- a/test/callbacks_tester.hpp
+++ b/test/callbacks_tester.hpp
@@ -21,7 +21,7 @@ struct CallbacksTester
     size_t num_allocs, alloc_size;
     size_t num_deallocs, dealloc_size;
 
-    CallbacksTester(const char *id_="notset", size_t sz=10u * 1024u) // 10KB
+    CallbacksTester(const char *id_="notset", size_t sz=30u * 1024u) // 30KB
         : memory_pool(sz)
         , id(id_)
         , num_allocs()
diff --git a/test/test_case.cpp b/test/test_case.cpp
index 538bfd334..d317a51b0 100644
--- a/test/test_case.cpp
+++ b/test/test_case.cpp
@@ -215,6 +215,20 @@ ExpectError::~ExpectError()
     set_callbacks(m_tree_prev);
 }
 
+void ExpectError::check_success(Tree *tree, std::function<void()> fn)
+{
+    auto context = ExpectError(tree, {});
+    try
+    {
+        fn();
+    }
+    catch(ExpectedError const&)
+    {
+        ;
+    }
+    EXPECT_FALSE(context.m_got_an_error);
+}
+
 void ExpectError::do_check(Tree *tree, std::function<void()> fn, Location expected_location)
 {
     auto context = ExpectError(tree, expected_location);
@@ -348,8 +362,8 @@ void CaseNode::compare_child(yml::ConstNodeRef const& n, size_t pos) const
 
     if(type & MAP)
     {
-        auto actualch = n.find_child(expectedch.key);
-        if(actualch != nullptr)
+        ConstNodeRef actualch = n.find_child(expectedch.key);
+        if(!actualch.invalid())
         {
             // there may be duplicate keys.
             if(actualch.id() != n[pos].id())
@@ -461,7 +475,7 @@ void CaseNode::compare(yml::ConstNodeRef const& actual, bool ignore_quote) const
             compare_child(actual, ic++);
         }
 
-        if(actual.first_child() != nullptr)
+        if(!actual.first_child().invalid())
         {
             ic = 0;
             for(auto const ch : actual.first_child().siblings())
@@ -508,7 +522,7 @@ void print_path(ConstNodeRef const& n)
     size_t len = 0;
     char buf[1024];
     ConstNodeRef p = n;
-    while(p != nullptr)
+    while(p.readable())
     {
         if(p.has_key())
         {
@@ -525,7 +539,7 @@ void print_path(ConstNodeRef const& n)
     C4_ASSERT(len < sizeof(buf));
     size_t pos = len;
     p = n;
-    while(p.valid() && p != nullptr)
+    while(p.readable())
     {
         if(p.has_key())
         {
@@ -669,7 +683,7 @@ void test_invariants(ConstNodeRef const& n)
         EXPECT_TRUE(s.has_sibling(n)) _MORE_INFO;
         EXPECT_EQ(s.parent().get(), n.parent().get()) _MORE_INFO;
     }
-    if(n.parent() != nullptr)
+    if(n.parent().readable())
     {
         EXPECT_EQ(n.parent().num_children() > 1, n.has_other_siblings()) _MORE_INFO;
         EXPECT_TRUE(n.parent().has_child(n)) _MORE_INFO;
@@ -741,7 +755,7 @@ size_t test_tree_invariants(ConstNodeRef const& n)
 
     if(n.get()->m_prev_sibling == NONE)
     {
-        if(parent != nullptr)
+        if(parent.readable())
         {
             EXPECT_EQ(parent.first_child().get(), n.get());
             EXPECT_EQ(parent.first_child().id(), n.id());
@@ -750,14 +764,14 @@ size_t test_tree_invariants(ConstNodeRef const& n)
 
     if(n.get()->m_next_sibling == NONE)
     {
-        if(parent != nullptr)
+        if(parent.readable())
         {
             EXPECT_EQ(parent.last_child().get(), n.get());
             EXPECT_EQ(parent.last_child().id(), n.id());
         }
     }
 
-    if(parent == nullptr)
+    if(!parent.readable())
     {
         EXPECT_TRUE(n.is_root());
         EXPECT_EQ(n.prev_sibling().get(), nullptr);
diff --git a/test/test_case.hpp b/test/test_case.hpp
index a5c08917b..99f29e590 100644
--- a/test/test_case.hpp
+++ b/test/test_case.hpp
@@ -171,6 +171,8 @@ struct ExpectError
 
     static void do_check(            std::function<void()> fn, Location expected={}) { do_check(nullptr, fn, expected); }
     static void do_check(Tree *tree, std::function<void()> fn, Location expected={});
+    static void check_success(            std::function<void()> fn) { check_success(nullptr, fn); }
+    static void check_success(Tree *tree, std::function<void()> fn);
     static void check_assertion(            std::function<void()> fn, Location expected={}) { check_assertion(nullptr, fn, expected); }
     static void check_assertion(Tree *tree, std::function<void()> fn, Location expected={});
 };
diff --git a/test/test_github_issues.cpp b/test/test_github_issues.cpp
index 3c12307fc..df68fd578 100644
--- a/test/test_github_issues.cpp
+++ b/test/test_github_issues.cpp
@@ -93,10 +93,11 @@ TEST(github, 60)
     ASSERT_TRUE(root.is_map());
     ASSERT_TRUE(root.has_child("traits"));
     auto rb = root["traits"]["roleBonuses"][0];
-    ASSERT_TRUE(rb.valid());
+    ASSERT_FALSE(rb.invalid());
+    ASSERT_TRUE(rb.readable());
     EXPECT_EQ(rb["bonus"].val(), "5");
     auto txt = rb["bonusText"];
-    ASSERT_TRUE(txt.valid());
+    ASSERT_TRUE(txt.readable());
     ASSERT_TRUE(txt.is_map());
     EXPECT_TRUE(txt.has_child("de"));
     EXPECT_TRUE(txt.has_child("en"));
diff --git a/test/test_group.hpp b/test/test_group.hpp
index f661ec9b5..fe0b4550a 100644
--- a/test/test_group.hpp
+++ b/test/test_group.hpp
@@ -15,6 +15,9 @@
 #elif defined(__GNUC__)
 #   pragma GCC diagnostic push
 #   pragma GCC diagnostic ignored "-Wunknown-pragmas"
+#   if __GNUC__ > 5
+#       pragma GCC diagnostic ignored "-Wunused-const-variable"
+#   endif
 //#   pragma GCC diagnostic ignored "-Wpragma-system-header-outside-header"
 #endif
 
diff --git a/test/test_noderef.cpp b/test/test_noderef.cpp
index 2745c71f2..017b0f518 100644
--- a/test/test_noderef.cpp
+++ b/test/test_noderef.cpp
@@ -149,6 +149,311 @@ TEST(NodeRef, general)
     EXPECT_EQ(root["b"]["aaa"].val(), "0");
 }
 
+TEST(NodeRef, operator_equal_equal)
+{
+    Tree tree1 = parse_in_arena("{a: a1, b: b1}");
+    NodeRef a1 = tree1["a"];
+    NodeRef b1 = tree1["b"];
+    NodeRef a1_ = a1;
+    NodeRef b1_ = b1;
+    NodeRef seedc1 = tree1["c"];
+    NodeRef seedc1_ = seedc1;
+    NodeRef seedd1 = tree1["d"];
+    NodeRef seedd1_ = seedd1;
+    EXPECT_EQ(a1, a1_);
+    EXPECT_EQ(b1, b1_);
+    EXPECT_NE(a1, b1);
+    EXPECT_NE(b1, a1);
+    EXPECT_EQ(seedc1, seedc1_);
+    EXPECT_EQ(seedd1, seedd1_);
+    EXPECT_NE(seedc1, seedd1);
+    Tree tree2 = parse_in_arena("{a: a2, b: b2}");
+    NodeRef a2 = tree2["a"];
+    NodeRef b2 = tree2["b"];
+    NodeRef a2_ = a2;
+    NodeRef b2_ = b2;
+    NodeRef seedc2 = tree2["c"];
+    NodeRef seedc2_ = seedc2;
+    NodeRef seedd2 = tree2["d"];
+    NodeRef seedd2_ = seedd2;
+    EXPECT_EQ(a2, a2_);
+    EXPECT_EQ(b2, b2_);
+    EXPECT_NE(a2, b2);
+    EXPECT_NE(b2, a2);
+    EXPECT_EQ(seedc2, seedc2_);
+    EXPECT_EQ(seedd2, seedd2_);
+    EXPECT_NE(seedc2, seedd2);
+    //
+    EXPECT_NE(a1, a2);
+    EXPECT_NE(b1, b2);
+    EXPECT_NE(seedc1, seedc2);
+    EXPECT_NE(seedd1, seedd2);
+}
+
+TEST(NodeRef, valid_vs_seed_vs_readable)
+{
+    static_assert(!ConstNodeRef::is_seed(), "ConstNodeRef must never be a seed");
+    Tree tree = parse_in_arena("foo: bar");
+    NodeRef foo = tree["foo"];
+    ConstNodeRef const_foo = tree["foo"];
+    EXPECT_FALSE(foo.invalid());
+    EXPECT_FALSE(foo.is_seed());
+    EXPECT_TRUE(foo.readable());
+    EXPECT_FALSE(const_foo.invalid());
+    EXPECT_FALSE(const_foo.is_seed());
+    EXPECT_TRUE(const_foo.readable());
+    NodeRef none;
+    EXPECT_TRUE(none.invalid());
+    EXPECT_FALSE(none.is_seed());
+    EXPECT_FALSE(none.readable());
+    ConstNodeRef const_none;
+    EXPECT_TRUE(const_none.invalid());
+    EXPECT_FALSE(const_none.is_seed());
+    EXPECT_FALSE(const_none.readable());
+    none = tree["none"];
+    EXPECT_FALSE(none.invalid());
+    EXPECT_TRUE(none.is_seed());
+    EXPECT_FALSE(none.readable());
+    ASSERT_FALSE(tree.rootref().has_child(none));
+    const_none = tree["none"];
+    EXPECT_TRUE(const_none.invalid());
+    EXPECT_FALSE(const_none.is_seed());
+    EXPECT_TRUE(none.is_seed());
+    EXPECT_FALSE(none.readable());
+}
+
+#define _TEST_FAIL(method_expr)                                    \
+    {                                                                   \
+        SCOPED_TRACE(#method_expr);                                     \
+        if(tree)                                                        \
+            ExpectError::check_assertion(tree, [&]{ return method_expr; }); \
+        else                                                            \
+            ExpectError::check_assertion([&]{ return method_expr; });   \
+    }
+#define _TEST_SUCCEED(method_expr)                                 \
+    {                                                                   \
+        SCOPED_TRACE(#method_expr);                                     \
+        if(tree)                                                        \
+            ExpectError::check_success(tree, [&]{ return method_expr; }); \
+        else                                                            \
+            ExpectError::check_success([&]{ return method_expr; });     \
+    }
+
+template<class NodeT>
+void test_fail_read(Tree *tree, NodeT node)
+{
+    _TEST_SUCCEED(node.get())
+    _TEST_FAIL(node.type())
+    _TEST_FAIL(node.type_str())
+    _TEST_FAIL(node.key())
+    _TEST_FAIL(node.key_tag())
+    _TEST_FAIL(node.key_anchor())
+    _TEST_FAIL(node.key_ref())
+    _TEST_FAIL(node.key_is_null())
+    _TEST_FAIL(node.keysc())
+    _TEST_FAIL(node.val())
+    _TEST_FAIL(node.val_tag())
+    _TEST_FAIL(node.val_anchor())
+    _TEST_FAIL(node.val_ref())
+    _TEST_FAIL(node.val_is_null())
+    _TEST_FAIL(node.valsc())
+    _TEST_FAIL(node.is_map())
+    _TEST_FAIL(node.empty())
+    _TEST_FAIL(node.is_stream())
+    _TEST_FAIL(node.is_doc())
+    _TEST_FAIL(node.is_container())
+    _TEST_FAIL(node.is_map())
+    _TEST_FAIL(node.is_seq())
+    _TEST_FAIL(node.has_val())
+    _TEST_FAIL(node.has_key())
+    _TEST_FAIL(node.is_keyval())
+    _TEST_FAIL(node.has_key_tag())
+    _TEST_FAIL(node.has_val_tag())
+    _TEST_FAIL(node.has_key_anchor())
+    _TEST_FAIL(node.has_val_anchor())
+    _TEST_FAIL(node.is_val_anchor())
+    _TEST_FAIL(node.has_anchor())
+    _TEST_FAIL(node.is_anchor())
+    _TEST_FAIL(node.is_key_ref())
+    _TEST_FAIL(node.is_val_ref())
+    _TEST_FAIL(node.is_ref())
+    _TEST_FAIL(node.is_anchor_or_ref())
+    _TEST_FAIL(node.is_key_quoted())
+    _TEST_FAIL(node.is_val_quoted())
+    _TEST_FAIL(node.parent_is_seq())
+    _TEST_FAIL(node.parent_is_map())
+    _TEST_FAIL(node.is_root())
+    _TEST_FAIL(node.has_parent())
+    _TEST_FAIL(node.has_child(0))
+    _TEST_FAIL(node.has_child("key"))
+    _TEST_FAIL(node.has_children())
+    _TEST_FAIL(node.has_sibling("key"))
+    _TEST_FAIL(node.has_other_siblings())
+    _TEST_FAIL(node.doc(0))
+    _TEST_FAIL(node.parent())
+    _TEST_FAIL(node.num_children())
+    _TEST_FAIL(node.first_child())
+    _TEST_FAIL(node.last_child())
+    _TEST_FAIL(node.child(0))
+    _TEST_FAIL(node.find_child("key"))
+    _TEST_FAIL(node.prev_sibling())
+    _TEST_FAIL(node.next_sibling())
+    _TEST_FAIL(node.first_sibling())
+    _TEST_FAIL(node.last_sibling())
+    _TEST_FAIL(node.sibling(0))
+    _TEST_FAIL(node.find_sibling("key"))
+    _TEST_FAIL(node.num_children())
+    _TEST_FAIL(node.num_siblings())
+    _TEST_FAIL(node.num_other_siblings())
+    _TEST_FAIL(node["key"])
+    _TEST_FAIL(node[0])
+    _TEST_FAIL(node.at("key"))
+    _TEST_FAIL(node.at(0))
+    int val;
+    _TEST_FAIL(node >> val)
+    _TEST_FAIL(node >> key(val))
+    _TEST_FAIL(node >> fmt::base64(val))
+    _TEST_FAIL(node >> key(fmt::base64(val)))
+    _TEST_FAIL(node.deserialize_key(fmt::base64(val)))
+    _TEST_FAIL(node.deserialize_val(fmt::base64(val)))
+    _TEST_FAIL(node.get_if("key", &val));
+    _TEST_FAIL(node.get_if("key", &val, 0));
+    const NodeT const_node = node;
+    _TEST_FAIL(node.begin());
+    _TEST_FAIL(node.cbegin());
+    _TEST_FAIL(const_node.begin());
+    _TEST_FAIL(const_node.cbegin());
+    _TEST_FAIL(node.end());
+    _TEST_FAIL(node.end());
+    _TEST_FAIL(const_node.end());
+    _TEST_FAIL(const_node.end());
+    _TEST_FAIL(node.children());
+    _TEST_FAIL(node.children());
+    _TEST_FAIL(const_node.children());
+    _TEST_FAIL(const_node.children());
+    _TEST_FAIL(node.siblings());
+    _TEST_FAIL(node.siblings());
+    _TEST_FAIL(const_node.siblings());
+    _TEST_FAIL(const_node.siblings());
+    //_TEST_FAIL(node.visit([](NodeT &n, size_t level){ (void)n; (void)level; return false; }));
+    //_TEST_FAIL(const_node.visit([](const NodeT &n, size_t level){ (void)n; (void)level; return false; }));
+    _TEST_SUCCEED(const_node == node);
+    _TEST_SUCCEED(const_node != node);
+    if(std::is_same<NodeT, NodeRef>::value)
+    {
+        ConstNodeRef other;
+        _TEST_SUCCEED(node == other);
+        _TEST_SUCCEED(node != node);
+    }
+}
+template<class NodeT>
+void test_fail_read_subject(Tree *tree, NodeT node, NodeT subject)
+{
+    if(node.readable())
+    {
+        _TEST_SUCCEED(node.has_child(subject))
+        _TEST_SUCCEED(node.has_sibling(subject))
+    }
+    else
+    {
+        _TEST_FAIL(node.has_child(subject))
+        _TEST_FAIL(node.has_sibling(subject))
+    }
+    _TEST_FAIL(node.child_pos(subject))
+    _TEST_FAIL(node.sibling_pos(subject))
+}
+#undef _TEST_FAIL_READ
+#undef _TEST_SUCCEED_READ
+
+TEST(NodeRef, cannot_read_from_invalid)
+{
+    SCOPED_TRACE("here");
+    NodeRef none;
+    ASSERT_EQ(none.tree(), nullptr);
+    ASSERT_EQ(none.id(), NONE);
+    EXPECT_TRUE(none.invalid());
+    EXPECT_FALSE(none.is_seed());
+    EXPECT_FALSE(none.readable());
+    test_fail_read(nullptr, none);
+    test_fail_read_subject(nullptr, none, none);
+}
+
+TEST(NodeRef, cannot_read_from_invalid_subject)
+{
+    SCOPED_TRACE("here");
+    NodeRef none;
+    Tree tree = parse_in_arena("foo: bar");
+    test_fail_read_subject(&tree, tree["foo"], none);
+}
+
+TEST(ConstNodeRef, cannot_read_from_invalid)
+{
+    SCOPED_TRACE("here");
+    ConstNodeRef const_none;
+    ASSERT_EQ(const_none.tree(), nullptr);
+    ASSERT_EQ(const_none.id(), NONE);
+    EXPECT_TRUE(const_none.invalid());
+    EXPECT_FALSE(const_none.is_seed());
+    EXPECT_FALSE(const_none.readable());
+    test_fail_read(nullptr, const_none);
+    test_fail_read_subject(nullptr, const_none, const_none);
+    Tree tree = parse_in_arena("foo: bar");
+    ConstNodeRef foo = tree["foo"];
+    test_fail_read_subject(&tree, foo, const_none);
+}
+
+TEST(ConstNodeRef, cannot_read_from_invalid_subject)
+{
+    SCOPED_TRACE("here");
+    Tree tree = parse_in_arena("foo: bar");
+    ConstNodeRef foo = tree["foo"];
+    ConstNodeRef const_none;
+    test_fail_read_subject(&tree, foo, const_none);
+}
+
+TEST(NodeRef, cannot_read_from_seed)
+{
+    SCOPED_TRACE("here");
+    Tree tree = parse_in_arena("foo: bar");
+    NodeRef none = tree["none"];
+    ASSERT_EQ(none.tree(), &tree);
+    ASSERT_EQ(none.id(), 0);
+    EXPECT_FALSE(none.invalid());
+    EXPECT_TRUE(none.is_seed());
+    EXPECT_FALSE(none.readable());
+    test_fail_read(&tree, none);
+    test_fail_read_subject(&tree, none, none);
+}
+
+TEST(NodeRef, cannot_read_from_seed_subject)
+{
+    SCOPED_TRACE("here");
+    Tree tree = parse_in_arena("foo: bar");
+    NodeRef none = tree["none"];
+    ASSERT_EQ(none.tree(), &tree);
+    ASSERT_EQ(none.id(), 0);
+    EXPECT_FALSE(none.invalid());
+    EXPECT_TRUE(none.is_seed());
+    EXPECT_FALSE(none.readable());
+    test_fail_read(&tree, none);
+    test_fail_read_subject(&tree, none, none);
+}
+
+TEST(ConstNodeRef, cannot_read_from_seed_subject)
+{
+    SCOPED_TRACE("here");
+    Tree tree = parse_in_arena("foo: bar");
+    ConstNodeRef const_none = tree["none"];
+    ASSERT_EQ(const_none.tree(), &tree);
+    ASSERT_EQ(const_none.id(), NONE);
+    EXPECT_TRUE(const_none.invalid());
+    EXPECT_FALSE(const_none.is_seed());
+    EXPECT_FALSE(const_none.readable());
+    test_fail_read(&tree, const_none);
+    ConstNodeRef foo = tree["foo"];
+    test_fail_read_subject(&tree, foo, const_none);
+}
+
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -177,12 +482,10 @@ void noderef_check_tree(ConstNodeRef const& root)
     EXPECT_EQ(  root[5].val(), "5");
 }
 
-TEST(NodeRef, append_child)
+TEST(NodeRef, append_child_1)
 {
     Tree t;
-
     NodeRef root(&t);
-
     root |= SEQ;
     root.append_child({"0"});
     root.append_child({"1"});
@@ -190,16 +493,55 @@ TEST(NodeRef, append_child)
     root.append_child({"3"});
     root.append_child({"4"});
     root.append_child({"5"});
+    noderef_check_tree(root);
+}
 
+TEST(NodeRef, append_child_2)
+{
+    Tree t;
+    NodeRef root(&t);
+    root |= SEQ;
+    root.append_child() = "0";
+    root.append_child() = "1";
+    root.append_child() = "2";
+    root.append_child() = "3";
+    root.append_child() = "4";
+    root.append_child() = "5";
     noderef_check_tree(root);
 }
 
-TEST(NodeRef, prepend_child)
+TEST(NodeRef, append_sibling_1)
 {
     Tree t;
+    NodeRef root(&t);
+    root |= SEQ;
+    NodeRef first = root.append_child({"0"});
+    first.append_sibling({"1"});
+    first.append_sibling({"2"});
+    first.append_sibling({"3"});
+    first.append_sibling({"4"});
+    first.append_sibling({"5"});
+    noderef_check_tree(root);
+}
 
+TEST(NodeRef, append_sibling_2)
+{
+    Tree t;
     NodeRef root(&t);
+    root |= SEQ;
+    NodeRef first = root.append_child() << "0";
+    first.append_sibling() << "1";
+    first.append_sibling() << "2";
+    first.append_sibling() << "3";
+    first.append_sibling() << "4";
+    first.append_sibling() << "5";
+    noderef_check_tree(root);
+}
 
+TEST(NodeRef, prepend_child_1)
+{
+    Tree t;
+    NodeRef root(&t);
     root |= SEQ;
     root.prepend_child({"5"});
     root.prepend_child({"4"});
@@ -207,17 +549,57 @@ TEST(NodeRef, prepend_child)
     root.prepend_child({"2"});
     root.prepend_child({"1"});
     root.prepend_child({"0"});
+    noderef_check_tree(root);
+}
 
+TEST(NodeRef, prepend_child_2)
+{
+    Tree t;
+    NodeRef root(&t);
+    root |= SEQ;
+    root.prepend_child() << "5";
+    root.prepend_child() << "4";
+    root.prepend_child() << "3";
+    root.prepend_child() << "2";
+    root.prepend_child() << "1";
+    root.prepend_child() << "0";
     noderef_check_tree(root);
 }
 
-TEST(NodeRef, insert_child)
+TEST(NodeRef, prepend_sibling_1)
 {
     Tree t;
+    NodeRef root(&t);
+    root |= SEQ;
+    NodeRef last = root.prepend_child({"5"});
+    last.prepend_sibling({"4"});
+    last.prepend_sibling({"3"});
+    last.prepend_sibling({"2"});
+    last.prepend_sibling({"1"});
+    last.prepend_sibling({"0"});
+    noderef_check_tree(root);
+}
 
+TEST(NodeRef, prepend_sibling_2)
+{
+    Tree t;
     NodeRef root(&t);
-    NodeRef none(&t, NONE);
+    root |= SEQ;
+    NodeRef last = root.prepend_child();
+    last = "5";
+    last.prepend_sibling() = "4";
+    last.prepend_sibling() = "3";
+    last.prepend_sibling() = "2";
+    last.prepend_sibling() = "1";
+    last.prepend_sibling() = "0";
+    noderef_check_tree(root);
+}
 
+TEST(NodeRef, insert_child_1)
+{
+    Tree t;
+    NodeRef root(&t);
+    NodeRef none(&t, NONE);
     root |= SEQ;
     root.insert_child({"3"}, none);
     root.insert_child({"4"}, root[0]);
@@ -225,7 +607,51 @@ TEST(NodeRef, insert_child)
     root.insert_child({"5"}, root[2]);
     root.insert_child({"1"}, root[0]);
     root.insert_child({"2"}, root[1]);
+    noderef_check_tree(root);
+}
+
+TEST(NodeRef, insert_child_2)
+{
+    Tree t;
+    NodeRef root(&t);
+    NodeRef none(&t, NONE);
+    root |= SEQ;
+    root.insert_child(none) << "3";
+    root.insert_child(root[0]) << "4";
+    root.insert_child(none) << "0";
+    root.insert_child(root[2]) << "5";
+    root.insert_child(root[0]) << "1";
+    root.insert_child(root[1]) << "2";
+    noderef_check_tree(root);
+}
+
+TEST(NodeRef, insert_sibling_1)
+{
+    Tree t;
+    NodeRef root(&t);
+    NodeRef none(&t, NONE);
+    root |= SEQ;
+    NodeRef first = root.insert_child({"3"}, none);
+    first.insert_sibling({"4"}, root[0]);
+    first.insert_sibling({"0"}, none);
+    first.insert_sibling({"5"}, root[2]);
+    first.insert_sibling({"1"}, root[0]);
+    first.insert_sibling({"2"}, root[1]);
+    noderef_check_tree(root);
+}
 
+TEST(NodeRef, insert_sibling_2)
+{
+    Tree t;
+    NodeRef root(&t);
+    NodeRef none(&t, NONE);
+    root |= SEQ;
+    NodeRef first = root.insert_child(none) << "3";
+    first.insert_sibling(root[0]) << "4";
+    first.insert_sibling(none) << "0";
+    first.insert_sibling(root[2]) << "5";
+    first.insert_sibling(root[0]) << "1";
+    first.insert_sibling(root[1]) << "2";
     noderef_check_tree(root);
 }
 
@@ -302,7 +728,7 @@ TEST(NodeRef, remove_child__issue_356)
         SCOPED_TRACE(id);
         NodeRef formats = root["formats"];
         std::cout << id << " id=" << formats.id() << "\n";
-        EXPECT_TRUE(formats.valid());
+        EXPECT_TRUE(formats.readable());
         print_tree(tree);
         check_invariants(tree);
         EXPECT_EQ(emitrs_yaml<std::string>(formats), expected);
@@ -830,6 +1256,26 @@ TEST(NodeRef, overload_sets)
     }
 }
 
+TEST(NodeRef, get_if)
+{
+    const Tree tree = parse_in_arena("{a: 1, b: 2}");
+    ConstNodeRef root = tree.rootref();
+    int val = 0;
+    EXPECT_TRUE(root.get_if("a", &val));
+    EXPECT_EQ(val, 1);
+    EXPECT_TRUE(root.get_if("b", &val));
+    EXPECT_EQ(val, 2);
+    EXPECT_FALSE(root.get_if("c", &val));
+    EXPECT_EQ(val, 2);
+    int fallback = 3;
+    EXPECT_TRUE(root.get_if("a", &val, fallback));
+    EXPECT_EQ(val, 1);
+    EXPECT_TRUE(root.get_if("b", &val, fallback));
+    EXPECT_EQ(val, 2);
+    EXPECT_FALSE(root.get_if("c", &val, fallback));
+    EXPECT_EQ(val, 3);
+}
+
 
 //-------------------------------------------
 // this is needed to use the test case library
diff --git a/test/test_tree.cpp b/test/test_tree.cpp
index 18c6a1b5e..def97b834 100644
--- a/test/test_tree.cpp
+++ b/test/test_tree.cpp
@@ -832,6 +832,73 @@ TEST(Tree, clear)
 
 //-------------------------------------------
 
+template<class Function>
+void verify_success_(Tree &tree, Function &&fn)
+{
+    ExpectError::check_success(&tree, [&]{
+        (void)fn(tree);
+    });
+}
+template<class Function>
+void verify_success_(csubstr src, Function &&fn)
+{
+    Tree tree = parse_in_arena(src);
+    ExpectError::check_success(&tree, [&]{
+        (void)fn(tree);
+    });
+}
+
+template<class Function>
+void verify_assertion_(Tree &tree, Function &&fn)
+{
+    ExpectError::check_assertion(&tree, [&]{
+        (void)fn(tree);
+    });
+}
+template<class Function>
+void verify_assertion_(csubstr src, Function &&fn)
+{
+    Tree tree = parse_in_arena(src);
+    ExpectError::check_assertion(&tree, [&]{
+        (void)fn(tree);
+    });
+}
+
+template<class Function>
+void verify_error_(Tree &tree, Function &&fn)
+{
+    ExpectError::do_check(&tree, [&]{
+        (void)fn(tree);
+    });
+}
+template<class Function>
+void verify_error_(csubstr src, Function &&fn)
+{
+    Tree tree = parse_in_arena(src);
+    ExpectError::do_check(&tree, [&]{
+        (void)fn(tree);
+    });
+}
+
+#define verify_success(...)                     \
+    {                                           \
+        SCOPED_TRACE("caller");                 \
+        verify_success_(__VA_ARGS__);           \
+    }
+
+#define verify_assertion(...)                   \
+    {                                           \
+        SCOPED_TRACE("caller");                 \
+        verify_assertion_(__VA_ARGS__);         \
+    }
+
+#define verify_error(...)                       \
+    {                                           \
+        SCOPED_TRACE("caller");                 \
+        verify_error_(__VA_ARGS__);             \
+    }
+
+
 TEST(Tree, ref)
 {
     Tree t = parse_in_arena("[0, 1, 2, 3]");
@@ -845,6 +912,25 @@ TEST(Tree, ref)
     EXPECT_TRUE(t.ref(2).is_val());
     EXPECT_TRUE(t.ref(3).is_val());
     EXPECT_TRUE(t.ref(4).is_val());
+    verify_assertion(t, [](Tree & tree){ return tree.ref(tree.capacity()); });
+    verify_assertion(t, [](Tree & tree){ return tree.ref(NONE); });
+}
+
+TEST(Tree, cref)
+{
+    Tree t = parse_in_arena("[0, 1, 2, 3]");
+    EXPECT_EQ(t.cref(0).id(), 0);
+    EXPECT_EQ(t.cref(1).id(), 1);
+    EXPECT_EQ(t.cref(2).id(), 2);
+    EXPECT_EQ(t.cref(3).id(), 3);
+    EXPECT_EQ(t.cref(4).id(), 4);
+    EXPECT_TRUE(t.cref(0).is_seq());
+    EXPECT_TRUE(t.cref(1).is_val());
+    EXPECT_TRUE(t.cref(2).is_val());
+    EXPECT_TRUE(t.cref(3).is_val());
+    EXPECT_TRUE(t.cref(4).is_val());
+    verify_assertion(t, [](Tree & tree){ return tree.cref(tree.capacity()); });
+    verify_assertion(t, [](Tree & tree){ return tree.cref(NONE); });
 }
 
 TEST(Tree, ref_const)
@@ -860,85 +946,138 @@ TEST(Tree, ref_const)
     EXPECT_TRUE(t.ref(2).is_val());
     EXPECT_TRUE(t.ref(3).is_val());
     EXPECT_TRUE(t.ref(4).is_val());
+    verify_assertion("[0, 1, 2, 3]", [](Tree const& tree){ return tree.cref(tree.capacity()); });
+    verify_assertion("[0, 1, 2, 3]", [](Tree const& tree){ return tree.cref(NONE); });
 }
 
 
-TEST(Tree, operator_square_brackets)
+TEST(Tree, operator_square_brackets_seq)
 {
-    {
-        Tree t = parse_in_arena("[0, 1, 2, 3, 4]");
-        Tree &m = t;
-        Tree const& cm = t;
-        EXPECT_EQ(m[0].val(), "0");
-        EXPECT_EQ(m[1].val(), "1");
-        EXPECT_EQ(m[2].val(), "2");
-        EXPECT_EQ(m[3].val(), "3");
-        EXPECT_EQ(m[4].val(), "4");
-        EXPECT_EQ(cm[0].val(), "0");
-        EXPECT_EQ(cm[1].val(), "1");
-        EXPECT_EQ(cm[2].val(), "2");
-        EXPECT_EQ(cm[3].val(), "3");
-        EXPECT_EQ(cm[4].val(), "4");
-        //
-        EXPECT_TRUE(m[0]  == "0");
-        EXPECT_TRUE(m[1]  == "1");
-        EXPECT_TRUE(m[2]  == "2");
-        EXPECT_TRUE(m[3]  == "3");
-        EXPECT_TRUE(m[4]  == "4");
-        EXPECT_TRUE(cm[0] == "0");
-        EXPECT_TRUE(cm[1] == "1");
-        EXPECT_TRUE(cm[2] == "2");
-        EXPECT_TRUE(cm[3] == "3");
-        EXPECT_TRUE(cm[4] == "4");
-        //
-        EXPECT_FALSE(m[0]  != "0");
-        EXPECT_FALSE(m[1]  != "1");
-        EXPECT_FALSE(m[2]  != "2");
-        EXPECT_FALSE(m[3]  != "3");
-        EXPECT_FALSE(m[4]  != "4");
-        EXPECT_FALSE(cm[0] != "0");
-        EXPECT_FALSE(cm[1] != "1");
-        EXPECT_FALSE(cm[2] != "2");
-        EXPECT_FALSE(cm[3] != "3");
-        EXPECT_FALSE(cm[4] != "4");
-    }
-    {
-        Tree t = parse_in_arena("{a: 0, b: 1, c: 2, d: 3, e: 4}");
-        Tree &m = t;
-        Tree const& cm = t;
-        EXPECT_EQ(m["a"].val(), "0");
-        EXPECT_EQ(m["b"].val(), "1");
-        EXPECT_EQ(m["c"].val(), "2");
-        EXPECT_EQ(m["d"].val(), "3");
-        EXPECT_EQ(m["e"].val(), "4");
-        EXPECT_EQ(cm["a"].val(), "0");
-        EXPECT_EQ(cm["b"].val(), "1");
-        EXPECT_EQ(cm["c"].val(), "2");
-        EXPECT_EQ(cm["d"].val(), "3");
-        EXPECT_EQ(cm["e"].val(), "4");
-        //
-        EXPECT_TRUE(m["a"] == "0");
-        EXPECT_TRUE(m["b"] == "1");
-        EXPECT_TRUE(m["c"] == "2");
-        EXPECT_TRUE(m["d"] == "3");
-        EXPECT_TRUE(m["e"] == "4");
-        EXPECT_TRUE(cm["a"] == "0");
-        EXPECT_TRUE(cm["b"] == "1");
-        EXPECT_TRUE(cm["c"] == "2");
-        EXPECT_TRUE(cm["d"] == "3");
-        EXPECT_TRUE(cm["e"] == "4");
-        //
-        EXPECT_FALSE(m["a"] != "0");
-        EXPECT_FALSE(m["b"] != "1");
-        EXPECT_FALSE(m["c"] != "2");
-        EXPECT_FALSE(m["d"] != "3");
-        EXPECT_FALSE(m["e"] != "4");
-        EXPECT_FALSE(cm["a"] != "0");
-        EXPECT_FALSE(cm["b"] != "1");
-        EXPECT_FALSE(cm["c"] != "2");
-        EXPECT_FALSE(cm["d"] != "3");
-        EXPECT_FALSE(cm["e"] != "4");
-    }
+    Tree t = parse_in_arena("[0, 1, 2, 3, 4]");
+    Tree &m = t;
+    Tree const& cm = t;
+    EXPECT_EQ(m[0].val(), "0");
+    EXPECT_EQ(m[1].val(), "1");
+    EXPECT_EQ(m[2].val(), "2");
+    EXPECT_EQ(m[3].val(), "3");
+    EXPECT_EQ(m[4].val(), "4");
+    EXPECT_EQ(cm[0].val(), "0");
+    EXPECT_EQ(cm[1].val(), "1");
+    EXPECT_EQ(cm[2].val(), "2");
+    EXPECT_EQ(cm[3].val(), "3");
+    EXPECT_EQ(cm[4].val(), "4");
+    //
+    verify_assertion(t, [&](Tree const&){ return cm[m.capacity()]; });
+    verify_assertion(t, [&](Tree const&){ return cm[NONE]; });
+    verify_assertion(t, [&](Tree const&){ return cm[0][0]; });
+    verify_assertion(t, [&](Tree const&){ return cm["a"]; });
+}
+
+TEST(Tree, operator_square_brackets_map)
+{
+    Tree t = parse_in_arena("{a: 0, b: 1, c: 2, d: 3, e: 4}");
+    Tree &m = t;
+    Tree const& cm = t;
+    EXPECT_EQ(m["a"].val(), "0");
+    EXPECT_EQ(m["b"].val(), "1");
+    EXPECT_EQ(m["c"].val(), "2");
+    EXPECT_EQ(m["d"].val(), "3");
+    EXPECT_EQ(m["e"].val(), "4");
+    EXPECT_EQ(cm["a"].val(), "0");
+    EXPECT_EQ(cm["b"].val(), "1");
+    EXPECT_EQ(cm["c"].val(), "2");
+    EXPECT_EQ(cm["d"].val(), "3");
+    EXPECT_EQ(cm["e"].val(), "4");
+    //
+    verify_assertion(t, [&](Tree const&){ return cm["f"]; });
+    verify_assertion(t, [&](Tree const&){ return cm["g"]["h"]; });
+}
+
+TEST(Tree, noderef_at_seq)
+{
+    Tree t = parse_in_arena("[0, 1, 2, 3, 4]");
+    NodeRef m = t.rootref();
+    ConstNodeRef const cm = t.rootref();
+    EXPECT_EQ(m.at(0).val(), "0");
+    EXPECT_EQ(m.at(1).val(), "1");
+    EXPECT_EQ(m.at(2).val(), "2");
+    EXPECT_EQ(m.at(3).val(), "3");
+    EXPECT_EQ(m.at(4).val(), "4");
+    EXPECT_EQ(cm.at(0).val(), "0");
+    EXPECT_EQ(cm.at(1).val(), "1");
+    EXPECT_EQ(cm.at(2).val(), "2");
+    EXPECT_EQ(cm.at(3).val(), "3");
+    EXPECT_EQ(cm.at(4).val(), "4");
+    //
+    EXPECT_EQ(cm.num_children(), 5);
+    EXPECT_EQ(cm.num_children(), m.num_children());
+    //
+    verify_error(t, [&](Tree const&){ return cm.at(NONE); });
+    verify_error(t, [&](Tree const&){ return cm.at(t.capacity()); });
+    verify_error(t, [&](Tree const&){ return cm.at(5); });
+    verify_error(t, [&](Tree const&){ return cm.at(6); });
+    verify_error(t, [&](Tree const&){ return cm.at(7); });
+    verify_error(t, [&](Tree const&){ return cm.at(10); });
+    verify_error(t, [&](Tree const&){ return cm.at(0).at(0); });
+    verify_error(t, [&](Tree const&){ return cm.at("a"); });
+    //
+    verify_error(t, [&](Tree const&){ return m.at(NONE); });
+    verify_error(t, [&](Tree const&){ return m.at(t.capacity()); });
+    verify_success(t, [&](Tree const&){ return m.at(5); });
+    verify_success(t, [&](Tree const&){ return m.at(6); });
+    verify_error(t, [&](Tree const&){ return m.at(0).at(0); });
+    verify_error(t, [&](Tree const&){ return m.at("a"); });
+    EXPECT_TRUE(m.at(5).is_seed());
+    EXPECT_TRUE(m.at(6).is_seed());
+    //
+    NodeRef to_be_removed_orig = m.at(4);
+    NodeRef to_be_removed = to_be_removed_orig;
+    EXPECT_EQ(to_be_removed.id(), 5);
+    EXPECT_EQ(to_be_removed_orig.id(), 5);
+    m.remove_child(to_be_removed);
+    EXPECT_EQ(to_be_removed.id(), 5); // it is stale now
+    EXPECT_EQ(to_be_removed_orig.id(), 5); // it is stale now
+    EXPECT_EQ(m.num_children(), 4);
+    EXPECT_TRUE(m.at(4).is_seed());
+    verify_success(t, [&](Tree const&){ return m.at(4); });
+    verify_error(t, [&](Tree const&){ return cm.at(4); });
+}
+
+TEST(Tree, noderef_at_map)
+{
+    Tree t = parse_in_arena("{a: 0, b: 1, c: 2, d: 3, e: 4}");
+    NodeRef m = t.rootref();
+    ConstNodeRef const cm = t.rootref();
+    EXPECT_EQ(m.at("a").val(), "0");
+    EXPECT_EQ(m.at("b").val(), "1");
+    EXPECT_EQ(m.at("c").val(), "2");
+    EXPECT_EQ(m.at("d").val(), "3");
+    EXPECT_EQ(m.at("e").val(), "4");
+    EXPECT_EQ(cm.at("a").val(), "0");
+    EXPECT_EQ(cm.at("b").val(), "1");
+    EXPECT_EQ(cm.at("c").val(), "2");
+    EXPECT_EQ(cm.at("d").val(), "3");
+    EXPECT_EQ(cm.at("e").val(), "4");
+    //
+    verify_error(t, [&](Tree const&){ return cm.at(t.capacity()); });
+    verify_error(t, [&](Tree const&){ return cm.at(NONE); });
+    verify_error(t, [&](Tree const&){ return cm.at(cm.num_children()); });
+    verify_error(t, [&](Tree const&){ return cm.at(5); });
+    verify_error(t, [&](Tree const&){ return cm.at(6); });
+    verify_error(t, [&](Tree const&){ return cm.at("f"); });
+    verify_error(t, [&](Tree const&){ return cm.at("g").at("h"); });
+    //
+    verify_error(t, [&](Tree const&){ return m.at(t.capacity()); });
+    verify_error(t, [&](Tree const&){ return m.at(NONE); });
+    verify_success(t, [&](Tree const&){ return m.at(cm.num_children()); });
+    verify_success(t, [&](Tree const&){ return m.at(5); });
+    verify_success(t, [&](Tree const&){ return m.at(6); });
+    verify_success(t, [&](Tree const&){ return m.at("f"); });
+    verify_error(t, [&](Tree const&){ return m.at("g").at("h"); });
+    EXPECT_TRUE(m.at(cm.num_children()).is_seed());
+    EXPECT_TRUE(m.at(5).is_seed());
+    EXPECT_TRUE(m.at(6).is_seed());
+    EXPECT_TRUE(m.at("f").is_seed());
 }
 
 TEST(Tree, relocate)
@@ -1045,6 +1184,8 @@ TEST(NodeType, type_str)
     EXPECT_EQ(to_csubstr(NodeType(VALANCH).type_str()), "(unk)");
 }
 
+
+
 TEST(NodeType, is_stream)
 {
     EXPECT_FALSE(NodeType(NOTYPE).is_stream());
@@ -1074,6 +1215,13 @@ foo: bar
     EXPECT_EQ(stream.is_stream(), stream.get()->m_type.is_stream());
     EXPECT_EQ(doc.is_stream(), doc.get()->m_type.is_stream());
     EXPECT_EQ(keyval.is_stream(), keyval.get()->m_type.is_stream());
+    //
+    ASSERT_TRUE(!t.docref(0)["none"].invalid());
+    ASSERT_TRUE(t.docref(0)["none"].is_seed());
+    ASSERT_FALSE(t.docref(0)["none"].readable());
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_stream(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_stream(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_stream(NONE); });
 }
 
 TEST(NodeType, is_doc)
@@ -1125,6 +1273,11 @@ a scalar
     EXPECT_EQ(mdoc.is_doc(), mdoc.get()->m_type.is_doc());
     EXPECT_EQ(mkeyval.is_doc(), mkeyval.get()->m_type.is_doc());
     EXPECT_EQ(mdocval.is_doc(), mdocval.get()->m_type.is_doc());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_doc(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_doc(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_doc(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_doc(NONE); });
 }
 
 TEST(NodeType, is_container)
@@ -1212,6 +1365,11 @@ a scalar
     EXPECT_EQ(mseq.is_container(), mseq.get()->m_type.is_container());
     EXPECT_EQ(mval.is_container(), mval.get()->m_type.is_container());
     EXPECT_EQ(mdocval.is_container(), mdocval.get()->m_type.is_container());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_container(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_container(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_container(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_container(NONE); });
 }
 
 TEST(NodeType, is_map)
@@ -1296,6 +1454,11 @@ a scalar
     EXPECT_EQ(mseq.is_map(), mseq.get()->m_type.is_map());
     EXPECT_EQ(mval.is_map(), mval.get()->m_type.is_map());
     EXPECT_EQ(mdocval.is_map(), mdocval.get()->m_type.is_map());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_map(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_map(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_map(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_map(NONE); });
 }
 
 TEST(NodeType, is_seq)
@@ -1380,6 +1543,11 @@ a scalar
     EXPECT_EQ(mseq.is_seq(), mseq.get()->m_type.is_seq());
     EXPECT_EQ(mval.is_seq(), mval.get()->m_type.is_seq());
     EXPECT_EQ(mdocval.is_seq(), mdocval.get()->m_type.is_seq());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_seq(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_seq(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_seq(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_seq(NONE); });
 }
 
 TEST(NodeType, has_val)
@@ -1464,6 +1632,11 @@ a scalar
     EXPECT_EQ(mseq.has_val(), mseq.get()->m_type.has_val());
     EXPECT_EQ(mval.has_val(), mval.get()->m_type.has_val());
     EXPECT_EQ(mdocval.has_val(), mdocval.get()->m_type.has_val());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].has_val(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).has_val(); });
+    verify_assertion(t, [&](Tree const&){ return t.has_val(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_val(NONE); });
 }
 
 TEST(NodeType, is_val)
@@ -1548,6 +1721,11 @@ a scalar
     EXPECT_EQ(mseq.is_val(), mseq.get()->m_type.is_val());
     EXPECT_EQ(mval.is_val(), mval.get()->m_type.is_val());
     EXPECT_EQ(mdocval.is_val(), mdocval.get()->m_type.is_val());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_val(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(1)[1].is_val(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val(NONE); });
 }
 
 TEST(NodeType, has_key)
@@ -1631,6 +1809,11 @@ a scalar
     EXPECT_EQ(mseq.has_key(), mseq.get()->m_type.has_key());
     EXPECT_EQ(mval.has_key(), mval.get()->m_type.has_key());
     EXPECT_EQ(mdocval.has_key(), mdocval.get()->m_type.has_key());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].has_key(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).has_key(); });
+    verify_assertion(t, [&](Tree const&){ return t.has_key(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_key(NONE); });
 }
 
 TEST(NodeType, is_keyval)
@@ -1715,6 +1898,11 @@ a scalar
     EXPECT_EQ(mseq.is_keyval(), mseq.get()->m_type.is_keyval());
     EXPECT_EQ(mval.is_keyval(), mval.get()->m_type.is_keyval());
     EXPECT_EQ(mdocval.is_keyval(), mdocval.get()->m_type.is_keyval());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_keyval(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_keyval(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_keyval(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_keyval(NONE); });
 }
 
 TEST(NodeType, has_key_tag)
@@ -1813,6 +2001,11 @@ a scalar
     EXPECT_EQ(mval.has_key_tag(), mval.get()->m_type.has_key_tag());
     EXPECT_EQ(mvalnotag.has_key_tag(), mvalnotag.get()->m_type.has_key_tag());
     EXPECT_EQ(mdocval.has_key_tag(), mdocval.get()->m_type.has_key_tag());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].has_key_tag(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).has_key_tag(); });
+    verify_assertion(t, [&](Tree const&){ return t.has_key_tag(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_key_tag(NONE); });
 }
 
 TEST(NodeType, has_val_tag)
@@ -1911,6 +2104,11 @@ a scalar
     EXPECT_EQ(mval.has_val_tag(), mval.get()->m_type.has_val_tag());
     EXPECT_EQ(mvalnotag.has_val_tag(), mvalnotag.get()->m_type.has_val_tag());
     EXPECT_EQ(mdocval.has_val_tag(), mdocval.get()->m_type.has_val_tag());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].has_val_tag(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).has_val_tag(); });
+    verify_assertion(t, [&](Tree const&){ return t.has_val_tag(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_val_tag(NONE); });
 }
 
 TEST(NodeType, has_key_anchor)
@@ -1998,6 +2196,11 @@ TEST(Tree, has_key_anchor)
     EXPECT_EQ(mseq.has_key_anchor(), mseq.get()->m_type.has_key_anchor());
     EXPECT_EQ(mval.has_key_anchor(), mval.get()->m_type.has_key_anchor());
     EXPECT_EQ(mvalnoanchor.has_key_anchor(), mvalnoanchor.get()->m_type.has_key_anchor());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].has_key(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(1).has_key_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.has_key_anchor(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_key_anchor(NONE); });
 }
 
 TEST(NodeType, is_key_anchor)
@@ -2085,6 +2288,11 @@ TEST(Tree, is_key_anchor)
     EXPECT_EQ(mseq.is_key_anchor(), mseq.get()->m_type.is_key_anchor());
     EXPECT_EQ(mval.is_key_anchor(), mval.get()->m_type.is_key_anchor());
     EXPECT_EQ(mvalnoanchor.is_key_anchor(), mvalnoanchor.get()->m_type.is_key_anchor());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_key_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(1).is_key_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_key_anchor(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_key_anchor(NONE); });
 }
 
 TEST(NodeType, has_val_anchor)
@@ -2172,6 +2380,11 @@ seq: &seqanchor [&valanchor foo, bar]
     EXPECT_EQ(mseq.has_val_anchor(), mseq.get()->m_type.has_val_anchor());
     EXPECT_EQ(mval.has_val_anchor(), mval.get()->m_type.has_val_anchor());
     EXPECT_EQ(mvalnoanchor.has_val_anchor(), mvalnoanchor.get()->m_type.has_val_anchor());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].has_val_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(1).has_val_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.has_val_anchor(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_val_anchor(NONE); });
 }
 
 TEST(NodeType, is_val_anchor)
@@ -2259,6 +2472,11 @@ seq: &seqanchor [&valanchor foo, bar]
     EXPECT_EQ(mseq.is_val_anchor(), mseq.get()->m_type.is_val_anchor());
     EXPECT_EQ(mval.is_val_anchor(), mval.get()->m_type.is_val_anchor());
     EXPECT_EQ(mvalnoanchor.is_val_anchor(), mvalnoanchor.get()->m_type.is_val_anchor());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_val_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(1).is_val_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val_anchor(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val_anchor(NONE); });
 }
 
 TEST(NodeType, has_anchor)
@@ -2351,6 +2569,8 @@ map: &mapanchor {foo: &keyvalanchor bar, anchor: none}
     EXPECT_EQ(mseq.has_anchor(), mseq.get()->m_type.has_anchor());
     EXPECT_EQ(mval.has_anchor(), mval.get()->m_type.has_anchor());
     EXPECT_EQ(mvalnoanchor.has_anchor(), mvalnoanchor.get()->m_type.has_anchor());
+    verify_assertion(t, [&](Tree const&){ return t.has_anchor(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_anchor(NONE); });
 }
 
 TEST(NodeType, is_anchor)
@@ -2443,6 +2663,11 @@ map: &mapanchor {foo: &keyvalanchor bar, anchor: none}
     EXPECT_EQ(mseq.is_anchor(), mseq.get()->m_type.is_anchor());
     EXPECT_EQ(mval.is_anchor(), mval.get()->m_type.is_anchor());
     EXPECT_EQ(mvalnoanchor.is_anchor(), mvalnoanchor.get()->m_type.is_anchor());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_anchor(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_anchor(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_anchor(NONE); });
 }
 
 TEST(NodeType, is_key_ref)
@@ -2512,6 +2737,11 @@ TEST(Tree, is_key_ref)
     EXPECT_EQ(mkeyval.is_key_ref(), mkeyval.get()->m_type.is_key_ref());
     EXPECT_EQ(mseq.is_key_ref(), mseq.get()->m_type.is_key_ref());
     EXPECT_EQ(mval.is_key_ref(), mval.get()->m_type.is_key_ref());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_key_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_key_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_key_ref(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_key_ref(NONE); });
 }
 
 TEST(NodeType, is_val_ref)
@@ -2581,6 +2811,11 @@ seq: [*valref, bar]
     EXPECT_EQ(mkeyval.is_val_ref(), mkeyval.get()->m_type.is_val_ref());
     EXPECT_EQ(mseq.is_val_ref(), mseq.get()->m_type.is_val_ref());
     EXPECT_EQ(mval.is_val_ref(), mval.get()->m_type.is_val_ref());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_val_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_val_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val_ref(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val_ref(NONE); });
 }
 
 TEST(NodeType, is_ref)
@@ -2654,6 +2889,11 @@ seq: [*valref, bar]
     EXPECT_EQ(mkeyval.is_ref(), mkeyval.get()->m_type.is_ref());
     EXPECT_EQ(mseq.is_ref(), mseq.get()->m_type.is_ref());
     EXPECT_EQ(mval.is_ref(), mval.get()->m_type.is_ref());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_ref(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_ref(NONE); });
 }
 
 TEST(NodeType, is_anchor_or_ref)
@@ -2733,6 +2973,11 @@ seq: &seq [*valref, bar]
     EXPECT_EQ(mkeyval.is_anchor_or_ref(), mkeyval.get()->m_type.is_anchor_or_ref());
     EXPECT_EQ(mseq.is_anchor_or_ref(), mseq.get()->m_type.is_anchor_or_ref());
     EXPECT_EQ(mval.is_anchor_or_ref(), mval.get()->m_type.is_anchor_or_ref());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_anchor_or_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_anchor_or_ref(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_anchor_or_ref(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_anchor_or_ref(NONE); });
 }
 
 TEST(NodeType, is_key_quoted)
@@ -2775,6 +3020,11 @@ notquoted: bar
     EXPECT_EQ(mmap.is_key_quoted(), mmap.get()->m_type.is_key_quoted());
     EXPECT_EQ(mquoted.is_key_quoted(), mquoted.get()->m_type.is_key_quoted());
     EXPECT_EQ(mnotquoted.is_key_quoted(), mnotquoted.get()->m_type.is_key_quoted());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_key_quoted(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_key_quoted(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_key_quoted(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_key_quoted(NONE); });
 }
 
 TEST(NodeType, is_val_quoted)
@@ -2817,6 +3067,11 @@ notquoted: bar
     EXPECT_EQ(mmap.is_val_quoted(), mmap.get()->m_type.is_val_quoted());
     EXPECT_EQ(mquoted.is_val_quoted(), mquoted.get()->m_type.is_val_quoted());
     EXPECT_EQ(mnotquoted.is_val_quoted(), mnotquoted.get()->m_type.is_val_quoted());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_val_quoted(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_val_quoted(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val_quoted(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_val_quoted(NONE); });
 }
 
 TEST(NodeType, is_quoted)
@@ -2914,6 +3169,11 @@ notquoted: bar
     EXPECT_EQ(mquoted5.is_quoted(), mquoted5.get()->m_type.is_quoted());
     EXPECT_EQ(mquoted6.is_quoted(), mquoted6.get()->m_type.is_quoted());
     EXPECT_EQ(mnotquoted.is_quoted(), mnotquoted.get()->m_type.is_quoted());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].is_quoted(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).is_quoted(); });
+    verify_assertion(t, [&](Tree const&){ return t.is_quoted(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.is_quoted(NONE); });
 }
 
 
@@ -2968,6 +3228,11 @@ seq: &seq [*valref, bar]
     EXPECT_EQ(t.parent_is_seq(keyval_id), mkeyval.parent_is_seq());
     EXPECT_EQ(t.parent_is_seq(seq_id), mseq.parent_is_seq());
     EXPECT_EQ(t.parent_is_seq(val_id), mval.parent_is_seq());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].parent_is_seq(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).parent_is_seq(); });
+    verify_assertion(t, [&](Tree const&){ return t.parent_is_seq(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.parent_is_seq(NONE); });
 }
 
 TEST(Tree, parent_is_map)
@@ -3022,6 +3287,11 @@ seq: &seq [*valref, bar]
     EXPECT_EQ(t.parent_is_map(keyval_id), mkeyval.parent_is_map());
     EXPECT_EQ(t.parent_is_map(seq_id), mseq.parent_is_map());
     EXPECT_EQ(t.parent_is_map(val_id), mval.parent_is_map());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].parent_is_map(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).parent_is_map(); });
+    verify_assertion(t, [&](Tree const&){ return t.parent_is_map(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.parent_is_map(NONE); });
 }
 
 TEST(Tree, has_parent)
@@ -3078,6 +3348,11 @@ seq: &seq [*valref, bar]
     EXPECT_EQ(t.has_parent(keyval_id), mkeyval.has_parent());
     EXPECT_EQ(t.has_parent(seq_id), mseq.has_parent());
     EXPECT_EQ(t.has_parent(val_id), mval.has_parent());
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].has_parent(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).has_parent(); });
+    verify_assertion(t, [&](Tree const&){ return t.has_parent(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.has_parent(NONE); });
 }
 
 
@@ -3126,6 +3401,11 @@ seq: &seq [*valref, bar]
     EXPECT_EQ(mkeyval.num_children(), t.num_children(keyval_id));
     EXPECT_EQ(mseq.num_children(), t.num_children(seq_id));
     EXPECT_EQ(mval.num_children(), t.num_children(val_id));
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].num_children(); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).num_children(); });
+    verify_assertion(t, [&](Tree const&){ return t.num_children(t.capacity()); });
+    verify_assertion(t, [&](Tree const&){ return t.num_children(NONE); });
 }
 
 TEST(Tree, child)
@@ -3170,6 +3450,11 @@ seq: &seq [*valref, bar]
     EXPECT_EQ(mkeyval.child(0).id(), t.child(keyval_id, 0));
     EXPECT_EQ(mseq.child(0).id(), t.child(seq_id, 0));
     EXPECT_EQ(mval.child(0).id(), t.child(val_id, 0));
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].child(0); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).child(0); });
+    verify_assertion(t, [&](Tree const&){ return t.child(t.capacity(), 0); });
+    verify_assertion(t, [&](Tree const&){ return t.child(NONE, 0); });
 }
 
 TEST(Tree, find_child_by_name)
@@ -3202,6 +3487,11 @@ seq: &seq [*valref, bar]
     EXPECT_EQ(mdoc.find_child("...").id(), t.find_child(doc_id, "..."));
     EXPECT_EQ(mmap.find_child("foo").id(), t.find_child(map_id, "foo"));
     EXPECT_EQ(mmap.find_child("bar").id(), t.find_child(map_id, "bar"));
+    //
+    verify_assertion(t, [&](Tree const&){ return t.docref(0)["none"].find_child("foo"); });
+    verify_assertion(t, [&](Tree const&){ return t.docref(2).find_child("foo"); });
+    verify_assertion(t, [&](Tree const&){ return t.find_child(t.capacity(), "foo"); });
+    verify_assertion(t, [&](Tree const&){ return t.find_child(NONE, "foo"); });
 }
 
 
@@ -3214,6 +3504,7 @@ TEST(change_type, from_val)
     t[0].change_type(VAL);
     t[1].change_type(MAP);
     t[2].change_type(SEQ);
+    verify_assertion(t, [&](Tree const&){ return t[3].change_type(VAL); });
     Tree expected = parse_in_arena("[val0, {}, []]");
     EXPECT_EQ(emitrs_yaml<std::string>(t), emitrs_yaml<std::string>(expected));
 }
@@ -3223,6 +3514,7 @@ TEST(change_type, from_keyval)
     t[0].change_type(VAL);
     t[1].change_type(MAP);
     t[2].change_type(SEQ);
+    verify_assertion(t, [&](Tree const&){ return t[3].change_type(VAL); });
     Tree expected = parse_in_arena("{keyval0: val0, keyval1: {}, keyval2: []}");
     EXPECT_EQ(emitrs_yaml<std::string>(t), emitrs_yaml<std::string>(expected));
 }
@@ -3233,6 +3525,7 @@ TEST(change_type, from_map)
     t[0].change_type(VAL);
     t[1].change_type(MAP);
     t[2].change_type(SEQ);
+    verify_assertion(t, [&](Tree const&){ return t[3].change_type(VAL); });
     EXPECT_FALSE(t[0].val_is_null());
     EXPECT_NE(t[0].val(), nullptr);
     Tree expected = parse_in_arena("['', {map1: {map1key0: a, map1key1: b}}, []]");
@@ -3244,6 +3537,7 @@ TEST(change_type, from_keymap)
     t[0].change_type(VAL);
     t[1].change_type(MAP);
     t[2].change_type(SEQ);
+    verify_assertion(t, [&](Tree const&){ return t[3].change_type(VAL); });
     EXPECT_FALSE(t[0].val_is_null());
     EXPECT_NE(t[0].val(), nullptr);
     Tree expected = parse_in_arena("{map0: '', map1: {map1: {map1key0: a, map1key1: b}}, map2: []}");
@@ -3256,6 +3550,7 @@ TEST(change_type, from_seq)
     t[0].change_type(VAL);
     t[1].change_type(MAP);
     t[2].change_type(SEQ);
+    verify_assertion(t, [&](Tree const&){ return t[3].change_type(VAL); });
     EXPECT_FALSE(t[0].val_is_null());
     EXPECT_NE(t[0].val(), nullptr);
     Tree expected = parse_in_arena("['', {}, [seq20, seq21]]");
@@ -3267,6 +3562,7 @@ TEST(change_type, from_keyseq)
     t[0].change_type(VAL);
     t[1].change_type(MAP);
     t[2].change_type(SEQ);
+    verify_assertion(t, [&](Tree const&){ return t[3].change_type(VAL); });
     EXPECT_FALSE(t[0].val_is_null());
     EXPECT_NE(t[0].val(), nullptr);
     Tree expected = parse_in_arena("{map0: '', map1: {}, map2: [seq20, seq21]}");