Hrw: Supports nested If (#12562)

* HRW: Adds support for nested If operators

* HRW docs additions and cleanup for new operator

* Adds hrw4u / u4wrh support for nested If's

* HRW4U: Allow for explicit state variable slot decl
diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst
index 39002b9..11bb275 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -323,9 +323,29 @@
 A special section `VARS` is used to declare variables. There is no equivalent in
 `header_rewrite`, where you managed the variables manually.
 
-.. note::
-    The section name is always required in HRW4U, there are no implicit or default hooks. There
-    can be several if/else block per section block.
+Variables and State Slots
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Each variable type has a limited number of slots available:
+
+- ``bool`` - 16 slots (0-15)
+- ``int8`` - 4 slots (0-3)
+- ``int16`` - 1 slot (0)
+
+By default, slots are assigned automatically in declaration order. You can explicitly assign
+a slot number using the ``@`` syntax::
+
+    VARS {
+        priority: bool @7;      # Explicitly use slot 7
+        active: bool;           # Auto-assigned to slot 0
+        config: bool @12;       # Explicitly use slot 12
+        counter: int8 @2;       # Explicitly use int8 slot 2
+    }
+
+Explicit slot assignment is useful when you need predictable slot numbers across configurations
+or when integrating with existing header_rewrite rules that reference specific slot numbers. In
+addition, a remap configuration can use ``@PPARAM`` to set one of these slot variables explicitly
+as part of the configuration.
 
 Groups
 ------
diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst
index e7f8c74..5ec2e89 100644
--- a/doc/admin-guide/plugins/header_rewrite.en.rst
+++ b/doc/admin-guide/plugins/header_rewrite.en.rst
@@ -153,9 +153,19 @@
 Which converts any 4xx HTTP status code from the origin server to a 404. A
 response from the origin with a status of 200 would be unaffected by this rule.
 
+Advanced Conditionals
+---------------------
+
+The header_rewrite plugin supports advanced conditional logic that allows
+for more sophisticated rule construction, including branching logic, nested
+conditionals, and complex boolean expressions.
+
+else and elif Clauses
+~~~~~~~~~~~~~~~~~~~~~
+
 An optional ``else`` clause may be specified, which will be executed if the
-conditions are not met. The ``else`` clause is specified by starting a new line
-with the word ``else``. The following example illustrates this::
+conditions are not met. The ``else`` clause is specified by starting a new
+line with the word ``else``. The following example illustrates this::
 
     cond %{STATUS} >399 [AND]
     cond %{STATUS} <500
@@ -164,10 +174,12 @@
       set-status 503
 
 The ``else`` clause is not a condition, and does not take any flags, it is
-of course optional, but when specified must be followed by at least one operator.
+of course optional, but when specified must be followed by at least one
+operator.
 
-You can also do an ``elif`` (else if) clause, which is specified by starting a new line
-with the word ``elif``. The following example illustrates this::
+You can also do an ``elif`` (else if) clause, which is specified by
+starting a new line with the word ``elif``. The following example
+illustrates this::
 
     cond %{STATUS} >399 [AND]
     cond %{STATUS} <500
@@ -178,14 +190,105 @@
     else
       set-status 503
 
-Keep in mind that nesting the ``else`` and ``elif`` clauses is not allowed, but any
-number of ``elif`` clauses can be specified. We can consider these clauses are more
-powerful and flexible ``switch`` statement. In an ``if-elif-else`` rule, only one
-will evaluate its operators.
+Any number of ``elif`` clauses can be specified. We can consider these
+clauses are more powerful and flexible ``switch`` statement. In an
+``if-elif-else`` rule, only one will evaluate its operators.
+
+Note that while ``else`` and ``elif`` themselves cannot be directly nested,
+you can use ``if``/``endif`` blocks within ``else`` or ``elif`` operator
+sections to achieve nested conditional logic (see `Nested Conditionals with
+if/endif`_).
 
 Similarly, each ``else`` and ``elif`` have the same implied
 :ref:`Hook Condition <hook_conditions>` as the initial condition.
 
+Nested Conditionals with if/endif
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For more complex logic requiring nested conditionals, the ``if`` and
+``endif`` pseudo-operators can be used. While ``else`` and ``elif``
+themselves cannot be directly nested, you can use ``if``/``endif`` blocks
+within any operator section (including inside ``else`` or ``elif`` blocks)
+to achieve arbitrary nesting depth.
+
+The ``if`` operator starts a new conditional block, and ``endif`` closes
+it. Each ``if`` must have a matching ``endif``. Here's an example::
+
+    cond %{READ_RESPONSE_HDR_HOOK} [AND]
+    cond %{STATUS} >399
+      if
+        cond %{HEADER:X-Custom-Error} ="true"
+          set-header X-Error-Handled "yes"
+        else
+          set-header X-Error-Handled "no"
+      endif
+      set-status 500
+
+In this example, the nested ``if``/``endif`` block is only evaluated when
+the status is greater than 399. The nested block itself can contain
+``else`` or ``elif`` clauses, and you can nest multiple levels deep::
+
+    cond %{READ_RESPONSE_HDR_HOOK}
+      if
+        cond %{STATUS} =404
+          if
+            cond %{CLIENT-HEADER:User-Agent} /mobile/
+              set-header X-Error-Type "mobile-404"
+            else
+              set-header X-Error-Type "desktop-404"
+          endif
+      elif
+        cond %{STATUS} =500
+          set-header X-Error-Type "server-error"
+      endif
+
+GROUP Conditions in Advanced Conditionals
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The `GROUP`_ condition can be combined with advanced conditionals to
+create very sophisticated boolean expressions. ``GROUP`` conditions act as
+parentheses in your conditional logic, allowing you to mix AND, OR, and NOT
+operators in complex ways.
+
+Here's an example combining ``GROUP`` with ``if``/``endif``::
+
+    cond %{READ_RESPONSE_HDR_HOOK} [AND]
+    cond %{STATUS} >399
+      if
+        cond %{GROUP} [OR]
+          cond %{CLIENT-HEADER:X-Retry} ="true" [AND]
+          cond %{METHOD} =GET
+        cond %{GROUP:END}
+        cond %{CLIENT-HEADER:X-Force-Cache} ="" [NOT]
+          set-header X-Can-Retry "yes"
+      else
+        set-header X-Can-Retry "no"
+      endif
+      set-status 500
+
+This creates the logic: if error status, then set retry header when
+``((X-Retry=true AND METHOD=GET) OR X-Force-Cache header exists)``.
+The GROUP is necessary here to properly combine the two conditions with OR.
+
+You can also use ``GROUP`` with ``else`` and ``elif`` inside nested conditionals::
+
+    cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+    cond %{STATUS} >399
+      if
+        cond %{GROUP} [OR]
+          cond %{HEADER:X-Custom} ="retry" [AND]
+          cond %{METHOD} =POST
+        cond %{GROUP:END}
+        cond %{HEADER:Content-Type} /json/
+          set-header X-Error-Handler "json-retry"
+        elif
+          cond %{METHOD} =GET
+            set-header X-Error-Handler "get-error"
+        else
+          set-header X-Error-Handler "standard"
+      endif
+      set-status 500
+
 State variables
 ---------------
 
@@ -923,6 +1026,29 @@
 counter with any value other than ``0``. Additionally, the counter will reset
 whenever |TS| is restarted.
 
+if
+~~
+::
+
+  if
+    <conditions>
+    <operators>
+  endif
+
+This is a pseudo-operator that enables nested conditional blocks within
+the operator section of a rule. While ``else`` and ``elif`` themselves
+cannot be directly nested, you can use ``if``/``endif`` blocks within any
+operator section (including inside ``else`` or ``elif`` blocks) to create
+arbitrary nesting depth for complex conditional logic.
+
+The ``if`` operator must be preceded by conditions and followed by at
+least one condition or operator. Each ``if`` must have a matching
+``endif`` to close the block. Within an ``if``/``endif`` block, you can
+use regular conditions, operators, and even ``else`` and ``elif`` clauses.
+
+For detailed usage and examples, see `Nested Conditionals with if/endif`_
+in the `Advanced Conditionals`_ section.
+
 no-op
 ~~~~~
 ::
diff --git a/plugins/header_rewrite/factory.cc b/plugins/header_rewrite/factory.cc
index 9e0499f..9f5f860 100644
--- a/plugins/header_rewrite/factory.cc
+++ b/plugins/header_rewrite/factory.cc
@@ -92,6 +92,7 @@
   } else if (op == "set-next-hop-strategy") {
     o = new OperatorSetNextHopStrategy();
   } else {
+    // Note that we don't support the OperatorIf() pseudo-operator here!
     TSError("[%s] Unknown operator: %s", PLUGIN_NAME, op.c_str());
     return nullptr;
   }
diff --git a/plugins/header_rewrite/header_rewrite.cc b/plugins/header_rewrite/header_rewrite.cc
index 89fe252..6189512 100644
--- a/plugins/header_rewrite/header_rewrite.cc
+++ b/plugins/header_rewrite/header_rewrite.cc
@@ -36,6 +36,7 @@
 #include "resources.h"
 #include "conditions.h"
 #include "conditions_geo.h"
+#include "operators.h"
 
 // Debugs
 namespace header_rewrite_ns
@@ -145,15 +146,14 @@
 
   switch (rule->get_clause()) {
   case Parser::CondClause::ELIF:
-    if (!rule->cur_section()->group.has_conditions() || !rule->cur_section()->has_operator()) {
-      TSError("[%s] ELIF clause must have both conditions and operators in file: %s, lineno: %d", PLUGIN_NAME, fname.c_str(),
-              lineno);
+    if (!rule->section_has_condition() || !rule->section_has_operator()) {
+      TSError("[%s] ELIF conditions without operators are not allowed in file: %s, lineno: %d", PLUGIN_NAME, fname.c_str(), lineno);
       return false;
     }
     break;
 
   case Parser::CondClause::ELSE:
-    if (rule->cur_section()->group.has_conditions()) {
+    if (rule->section_has_condition()) {
       TSError("[%s] conditions not allowed in ELSE clause in file: %s, lineno: %d", PLUGIN_NAME, fname.c_str(), lineno);
       return false;
     }
@@ -168,6 +168,11 @@
 
   case Parser::CondClause::COND:
     break;
+
+  case Parser::CondClause::IF:
+  case Parser::CondClause::ENDIF:
+    // IF and ENDIF are handled separately in the main parsing loop
+    break;
   }
 
   return true;
@@ -185,8 +190,11 @@
   std::unique_ptr<RuleSet>     rule(nullptr);
   std::string                  filename;
   int                          lineno = 0;
+  ConditionGroup              *group  = nullptr;
   std::stack<ConditionGroup *> group_stack;
-  ConditionGroup              *group = nullptr;
+  std::stack<OperatorIf *>     if_stack;
+
+  constexpr int MAX_IF_NESTING_DEPTH = 10;
 
   if (0 == fname.size()) {
     TSError("[%s] no config filename provided", PLUGIN_NAME);
@@ -246,7 +254,11 @@
     // Deal with the elif / else special keywords, these are neither conditions nor operators.
     if (p.is_else() || p.is_elif()) {
       Dbg(pi_dbg_ctl, "Entering elif/else, CondClause=%d", static_cast<int>(p.get_clause()));
-      if (rule) {
+
+      if (!if_stack.empty()) {
+        group = if_stack.top()->new_section(p.get_clause());
+        continue;
+      } else if (rule) {
         group = rule->new_section(p.get_clause());
         continue;
       } else {
@@ -256,8 +268,7 @@
     }
 
     // If we are at the beginning of a new condition, save away the previous rule (but only if it has operators).
-    // This also has to deal with the fact that we allow implicit hooks to end / start a new rule.
-    if (p.is_cond() && rule && (is_hook || rule->cur_section()->has_operator())) {
+    if (p.is_cond() && rule && if_stack.empty() && (is_hook || rule->section_has_operator())) {
       if (!validate_rule_completion(rule.get(), fname, lineno)) {
         return false;
       } else {
@@ -299,7 +310,13 @@
     // Long term, maybe we need to percolate all this up through add_condition() / add_operator() rather than this big ugly try.
     try {
       if (p.is_cond()) {
-        Condition *cond = rule->make_condition(p, filename.c_str(), lineno);
+        Condition *cond = nullptr;
+
+        if (!if_stack.empty()) {
+          cond = if_stack.top()->make_condition(p, filename.c_str(), lineno);
+        } else {
+          cond = rule->make_condition(p, filename.c_str(), lineno);
+        }
 
         if (!cond) {
           throw std::runtime_error("add_condition() failed");
@@ -327,9 +344,54 @@
             group->add_condition(cond);
           }
         }
-      } else { // Operator
-        if (!rule->add_operator(p, filename.c_str(), lineno)) {
-          throw std::runtime_error("add_operator() failed");
+      } else {
+        if (p.is_if()) {
+          if (if_stack.size() >= MAX_IF_NESTING_DEPTH) {
+            throw std::runtime_error("maximum if nesting depth exceeded");
+          }
+
+          auto *op_if = new OperatorIf();
+
+          if_stack.push(op_if);
+          group = op_if->get_group(); // Set group to the new OperatorIf's group
+          Dbg(dbg_ctl, "Started nested OperatorIf, depth: %zu", if_stack.size());
+
+        } else if (p.is_endif()) {
+          if (if_stack.empty()) {
+            throw std::runtime_error("endif without matching if");
+          }
+
+          OperatorIf *op_if = if_stack.top();
+
+          if_stack.pop();
+          if (!if_stack.empty()) {
+            auto *parent_sec = if_stack.top()->cur_section();
+
+            if (parent_sec->ops.oper) {
+              parent_sec->ops.oper->append(op_if);
+            } else {
+              parent_sec->ops.oper.reset(op_if);
+            }
+            group = if_stack.top()->get_group();
+          } else {
+            if (!rule->add_operator(op_if)) {
+              delete op_if;
+              throw std::runtime_error("Failed to add nested OperatorIf to RuleSet");
+            }
+            group = rule->get_group();
+          }
+          Dbg(dbg_ctl, "Completed nested OperatorIf, depth now: %zu", if_stack.size());
+
+        } else {
+          if (!if_stack.empty()) {
+            if (!if_stack.top()->add_operator(p, filename.c_str(), lineno)) {
+              throw std::runtime_error("add_operator() failed in nested OperatorIf");
+            }
+          } else {
+            if (!rule->add_operator(p, filename.c_str(), lineno)) {
+              throw std::runtime_error("add_operator() failed");
+            }
+          }
         }
       }
     } catch (std::runtime_error &e) {
@@ -353,6 +415,16 @@
     return false;
   }
 
+  // Check for unmatched if statements
+  if (!if_stack.empty()) {
+    TSError("[%s] %zu unmatched 'if' statement(s) without 'endif' in file: %s", PLUGIN_NAME, if_stack.size(), fname.c_str());
+    while (!if_stack.empty()) {
+      delete if_stack.top();
+      if_stack.pop();
+    }
+    return false;
+  }
+
   // Add the last rule (possibly the only rule)
   if (rule) {
     if (!validate_rule_completion(rule.get(), fname, lineno)) {
@@ -434,10 +506,8 @@
     // Get the resources necessary to process this event
     res.gather(conf->resid(hook), hook);
 
-    // Evaluation of all rules. This code is sort of duplicate in DoRemap as well.
     while (rule) {
-      const RuleSet::OperatorAndMods &ops = rule->eval(res);
-      const OperModifiers             rt  = rule->exec(ops, res);
+      const OperModifiers rt = rule->exec(res);
 
       if (rt & OPER_NO_REENABLE) {
         reenable = false;
@@ -703,8 +773,7 @@
     res.gather(conf->resid(TS_REMAP_PSEUDO_HOOK), TS_REMAP_PSEUDO_HOOK);
 
     do {
-      const RuleSet::OperatorAndMods &ops = rule->eval(res);
-      const OperModifiers             rt  = rule->exec(ops, res);
+      const OperModifiers rt = rule->exec(res);
 
       ink_assert((rt & OPER_NO_REENABLE) == 0);
 
diff --git a/plugins/header_rewrite/operator.h b/plugins/header_rewrite/operator.h
index 2d54f6f..bc23c93 100644
--- a/plugins/header_rewrite/operator.h
+++ b/plugins/header_rewrite/operator.h
@@ -22,6 +22,7 @@
 #pragma once
 
 #include <string>
+#include <memory>
 
 #include "ts/ts.h"
 
@@ -39,6 +40,20 @@
   OPER_NO_REENABLE = 16,
 };
 
+// Forward declaration
+class Operator;
+
+// Holding the operator and mods - used by both RuleSet and OperatorIf
+struct OperatorAndMods {
+  OperatorAndMods() = default;
+
+  OperatorAndMods(const OperatorAndMods &)            = delete;
+  OperatorAndMods &operator=(const OperatorAndMods &) = delete;
+
+  std::unique_ptr<Operator> oper;
+  OperModifiers             oper_mods = OPER_NONE;
+};
+
 ///////////////////////////////////////////////////////////////////////////////
 // Base class for all Operators (this is also the interface)
 //
diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc
index ac7b750..c8c4f66 100644
--- a/plugins/header_rewrite/operators.cc
+++ b/plugins/header_rewrite/operators.cc
@@ -30,6 +30,9 @@
 
 #include "operators.h"
 #include "ts/apidefs.h"
+#include "conditions.h"
+#include "factory.h"
+#include "ruleset.h"
 
 namespace
 {
@@ -1677,3 +1680,123 @@
 
   return true;
 }
+
+///////////////////////////////////////////////////////////////////////////////
+// OperatorIf class implementations
+// Keep this at the end of the files, since this is not really an Operator.
+//
+ConditionGroup *
+OperatorIf::new_section(Parser::CondClause clause)
+{
+  TSAssert(_cur_section && !_cur_section->next);
+
+  _clause            = clause;
+  _cur_section->next = std::make_unique<CondOpSection>();
+  _cur_section       = _cur_section->next.get();
+
+  return &_cur_section->group;
+}
+
+bool
+OperatorIf::add_operator(Parser &p, const char *filename, int lineno)
+{
+  Operator *op = operator_factory(p.get_op());
+
+  if (!op) {
+    TSError("[%s] Unknown operator: %s, file: %s, line: %d", PLUGIN_NAME, p.get_op().c_str(), filename, lineno);
+    return false;
+  }
+
+  Dbg(pi_dbg_ctl, "    Adding operator: %s(%s)=\"%s\"", p.get_op().c_str(), p.get_arg().c_str(), p.get_value().c_str());
+
+  try {
+    op->initialize(p);
+  } catch (std::exception const &ex) {
+    delete op;
+    TSError("[%s] Failed to initialize operator: %s, file: %s, line: %d, error: %s", PLUGIN_NAME, p.get_op().c_str(), filename,
+            lineno, ex.what());
+    return false;
+  }
+
+  // Add to current section
+  if (_cur_section->ops.oper) {
+    _cur_section->ops.oper->append(op);
+  } else {
+    _cur_section->ops.oper.reset(op);
+    _cur_section->ops.oper_mods = op->get_oper_modifiers();
+  }
+
+  return true;
+}
+
+Condition *
+OperatorIf::make_condition(Parser &p, const char *filename, int lineno)
+{
+  Condition *cond = condition_factory(p.get_op());
+
+  if (!cond) {
+    TSError("[%s] Unknown condition: %s, file: %s, line: %d", PLUGIN_NAME, p.get_op().c_str(), filename, lineno);
+    return nullptr;
+  }
+
+  Dbg(pi_dbg_ctl, "    Creating condition: %%{%s} with arg: %s", p.get_op().c_str(), p.get_arg().c_str());
+
+  try {
+    cond->initialize(p);
+  } catch (std::exception const &ex) {
+    delete cond;
+    TSError("[%s] Failed to initialize condition: %s, file: %s, line: %d, error: %s", PLUGIN_NAME, p.get_op().c_str(), filename,
+            lineno, ex.what());
+    return nullptr;
+  }
+
+  return cond;
+}
+
+bool
+OperatorIf::has_operator() const
+{
+  const CondOpSection *section = &_sections;
+
+  while (section != nullptr) {
+    if (section->has_operator()) {
+      return true;
+    }
+    section = section->next.get();
+  }
+  return false;
+}
+
+OperModifiers
+OperatorIf::exec_and_return_mods(const Resources &res) const
+{
+  Dbg(dbg_ctl, "Executing OperatorIf");
+
+  // Go through each section (if/elif/else) until one matches
+  for (auto *section = const_cast<CondOpSection *>(&_sections); section != nullptr; section = section->next.get()) {
+    if (section->group.eval(res)) {
+      Dbg(dbg_ctl, "OperatorIf section condition matched, executing operators");
+      return exec_section(section, res);
+    }
+  }
+
+  Dbg(dbg_ctl, "OperatorIf: no section matched");
+  return OPER_NONE;
+}
+
+OperModifiers
+OperatorIf::exec_section(const CondOpSection *section, const Resources &res) const
+{
+  if (nullptr == section->ops.oper) {
+    return section->ops.oper_mods;
+  }
+
+  auto no_reenable_count = section->ops.oper->do_exec(res);
+
+  ink_assert(no_reenable_count < 2);
+  if (no_reenable_count) {
+    return static_cast<OperModifiers>(section->ops.oper_mods | OPER_NO_REENABLE);
+  }
+
+  return section->ops.oper_mods;
+}
diff --git a/plugins/header_rewrite/operators.h b/plugins/header_rewrite/operators.h
index a0b825d..179bd79 100644
--- a/plugins/header_rewrite/operators.h
+++ b/plugins/header_rewrite/operators.h
@@ -22,6 +22,7 @@
 #pragma once
 
 #include <string>
+#include <memory>
 
 #include "ts/ts.h"
 
@@ -29,6 +30,12 @@
 #include "resources.h"
 #include "value.h"
 
+// Forward declarations
+class Parser;
+
+// Full includes needed for member variables
+#include "conditions.h"
+
 ///////////////////////////////////////////////////////////////////////////////
 // Operator declarations.
 //
@@ -670,3 +677,76 @@
 private:
   Value _value;
 };
+
+///////////////////////////////////////////////////////////////////////////////
+// OperatorIf class - implements nested if/elif/else as a pseudo-operator.
+// Keep this at the end of the files, since this is not really an Operator.
+//
+class OperatorIf : public Operator
+{
+public:
+  struct CondOpSection {
+    CondOpSection() = default;
+
+    ~CondOpSection() = default;
+
+    CondOpSection(const CondOpSection &)            = delete;
+    CondOpSection &operator=(const CondOpSection &) = delete;
+
+    bool
+    has_operator() const
+    {
+      return ops.oper != nullptr;
+    }
+
+    ConditionGroup                 group;
+    OperatorAndMods                ops;
+    std::unique_ptr<CondOpSection> next; // For elif/else sections
+  };
+
+  OperatorIf() { Dbg(dbg_ctl, "Calling CTOR for OperatorIf"); }
+
+  // noncopyable
+  OperatorIf(const OperatorIf &)     = delete;
+  void operator=(const OperatorIf &) = delete;
+
+  ConditionGroup *new_section(Parser::CondClause clause);
+  bool            add_operator(Parser &p, const char *filename, int lineno);
+  Condition      *make_condition(Parser &p, const char *filename, int lineno);
+  bool            has_operator() const;
+
+  ConditionGroup *
+  get_group()
+  {
+    return &_cur_section->group;
+  }
+
+  Parser::CondClause
+  get_clause() const
+  {
+    return _clause;
+  }
+
+  CondOpSection *
+  cur_section() const
+  {
+    return _cur_section;
+  }
+
+  OperModifiers exec_and_return_mods(const Resources &res) const;
+
+protected:
+  bool
+  exec(const Resources &res) const override
+  {
+    OperModifiers mods = exec_and_return_mods(res);
+    return !(mods & OPER_NO_REENABLE);
+  }
+
+private:
+  OperModifiers exec_section(const CondOpSection *section, const Resources &res) const;
+
+  CondOpSection      _sections;
+  CondOpSection     *_cur_section = &_sections;
+  Parser::CondClause _clause      = Parser::CondClause::COND;
+};
diff --git a/plugins/header_rewrite/parser.cc b/plugins/header_rewrite/parser.cc
index a0f93af..12f0c15 100644
--- a/plugins/header_rewrite/parser.cc
+++ b/plugins/header_rewrite/parser.cc
@@ -15,6 +15,7 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 */
+
 //////////////////////////////////////////////////////////////////////////////////////////////
 // parser.cc: implementation of the config parser
 //
@@ -205,6 +206,12 @@
   } else if (tokens[0] == "elif") {
     _clause = CondClause::ELIF;
     return true;
+  } else if (tokens[0] == "if") {
+    _clause = CondClause::IF;
+    return true;
+  } else if (tokens[0] == "endif") {
+    _clause = CondClause::ENDIF;
+    return true;
   }
 
   // Is it a condition or operator?
diff --git a/plugins/header_rewrite/parser.h b/plugins/header_rewrite/parser.h
index 9844ad0..634df46 100644
--- a/plugins/header_rewrite/parser.h
+++ b/plugins/header_rewrite/parser.h
@@ -113,7 +113,7 @@
 class Parser
 {
 public:
-  enum class CondClause { OPER, COND, ELIF, ELSE };
+  enum class CondClause { OPER, COND, ELIF, ELSE, IF, ENDIF };
 
   Parser() = default; // No from/to URLs for this parser
   Parser(char *from_url, char *to_url) : _from_url(from_url), _to_url(to_url) {}
@@ -165,6 +165,18 @@
     return _clause == CondClause::ELIF;
   }
 
+  bool
+  is_if() const
+  {
+    return _clause == CondClause::IF;
+  }
+
+  bool
+  is_endif() const
+  {
+    return _clause == CondClause::ENDIF;
+  }
+
   const std::string &
   get_op() const
   {
diff --git a/plugins/header_rewrite/ruleset.cc b/plugins/header_rewrite/ruleset.cc
index 50d16ac..28fda37 100644
--- a/plugins/header_rewrite/ruleset.cc
+++ b/plugins/header_rewrite/ruleset.cc
@@ -23,10 +23,24 @@
 
 #include "ruleset.h"
 #include "factory.h"
+#include "operators.h"
 
-///////////////////////////////////////////////////////////////////////////////
-// Class implementation (no reason to have these inline)
-//
+RuleSet::RuleSet()
+{
+  Dbg(dbg_ctl, "RuleSet CTOR");
+}
+
+RuleSet::~RuleSet()
+{
+  Dbg(dbg_ctl, "RulesSet DTOR");
+}
+
+OperModifiers
+RuleSet::exec(const Resources &res) const
+{
+  return _op_if.exec_and_return_mods(res);
+}
+
 void
 RuleSet::append(std::unique_ptr<RuleSet> rule)
 {
@@ -47,7 +61,7 @@
   Condition *c = condition_factory(p.get_op());
 
   if (nullptr == c) {
-    return nullptr; // Complete failure in the factory
+    return nullptr;
   }
 
   Dbg(pi_dbg_ctl, "    Creating condition: %%{%s} with arg: %s", p.get_op().c_str(), p.get_arg().c_str());
@@ -65,7 +79,6 @@
     return nullptr;
   }
 
-  // Update some ruleset state based on this new condition;
   _last |= c->last();
   _ids   = static_cast<ResourceIDs>(_ids | c->get_resource_ids());
 
@@ -89,17 +102,16 @@
       return false;
     }
 
-    OperatorAndMods &ops = _cur_section->ops;
+    auto *cur_sec = _op_if.cur_section();
 
-    if (!ops.oper) {
-      ops.oper = op;
+    if (!cur_sec->ops.oper) {
+      cur_sec->ops.oper.reset(op);
     } else {
-      ops.oper->append(op);
+      cur_sec->ops.oper->append(op);
     }
 
-    // Update some ruleset state based on this new operator
-    ops.oper_mods = static_cast<OperModifiers>(ops.oper_mods | ops.oper->get_oper_modifiers());
-    _ids          = static_cast<ResourceIDs>(_ids | ops.oper->get_resource_ids());
+    cur_sec->ops.oper_mods = static_cast<OperModifiers>(cur_sec->ops.oper_mods | cur_sec->ops.oper->get_oper_modifiers());
+    _ids                   = static_cast<ResourceIDs>(_ids | cur_sec->ops.oper->get_resource_ids());
 
     return true;
   }
@@ -120,3 +132,21 @@
 
   return ids;
 }
+
+bool
+RuleSet::add_operator(Operator *op)
+{
+  auto *cur_sec = _op_if.cur_section();
+
+  if (!cur_sec->ops.oper) {
+    cur_sec->ops.oper.reset(op);
+  } else {
+    cur_sec->ops.oper->append(op);
+  }
+
+  // Update some ruleset state based on this new operator
+  cur_sec->ops.oper_mods = static_cast<OperModifiers>(cur_sec->ops.oper_mods | cur_sec->ops.oper->get_oper_modifiers());
+  _ids                   = static_cast<ResourceIDs>(_ids | cur_sec->ops.oper->get_resource_ids());
+
+  return true;
+}
diff --git a/plugins/header_rewrite/ruleset.h b/plugins/header_rewrite/ruleset.h
index 4a5887a..78680bc 100644
--- a/plugins/header_rewrite/ruleset.h
+++ b/plugins/header_rewrite/ruleset.h
@@ -30,74 +30,63 @@
 #include "resources.h"
 #include "parser.h"
 #include "conditions.h"
+#include "operators.h"
 
 ///////////////////////////////////////////////////////////////////////////////
-// Class holding one ruleset. A ruleset is one (or more) pre-conditions, and
-// one (or more) operators.
+// RuleSet: Represents a complete wrapping a single OperatorIf.
 //
 class RuleSet
 {
 public:
-  // Holding the IF and ELSE operators and mods, in two separate linked lists.
-  struct OperatorAndMods {
-    OperatorAndMods() = default;
-
-    OperatorAndMods(const OperatorAndMods &)            = delete;
-    OperatorAndMods &operator=(const OperatorAndMods &) = delete;
-
-    Operator     *oper      = nullptr;
-    OperModifiers oper_mods = OPER_NONE;
-  };
-
-  struct CondOpSection {
-    CondOpSection() = default;
-
-    ~CondOpSection()
-    {
-      delete ops.oper;
-      delete next;
-    }
-
-    CondOpSection(const CondOpSection &)            = delete;
-    CondOpSection &operator=(const CondOpSection &) = delete;
-
-    bool
-    has_operator() const
-    {
-      return ops.oper != nullptr;
-    }
-
-    ConditionGroup  group;
-    OperatorAndMods ops;
-    CondOpSection  *next = nullptr; // For elif / else sections.
-  };
-
-  RuleSet() { Dbg(dbg_ctl, "RuleSet CTOR"); }
-
-  ~RuleSet() { Dbg(dbg_ctl, "RulesSet DTOR"); }
+  RuleSet();
+  ~RuleSet();
 
   // noncopyable
   RuleSet(const RuleSet &)        = delete;
   void operator=(const RuleSet &) = delete;
 
-  // No reason to inline these
   void        append(std::unique_ptr<RuleSet> rule);
   Condition  *make_condition(Parser &p, const char *filename, int lineno);
-  bool        add_operator(Parser &p, const char *filename, int lineno);
   ResourceIDs get_all_resource_ids() const;
+  bool        add_operator(Parser &p, const char *filename, int lineno);
+  bool        add_operator(Operator *op);
+
+  ConditionGroup *
+  get_group()
+  {
+    return _op_if.get_group();
+  }
+
+  Parser::CondClause
+  get_clause() const
+  {
+    return _op_if.get_clause();
+  }
+
+  ConditionGroup *
+  new_section(Parser::CondClause clause)
+  {
+    return _op_if.new_section(clause);
+  }
 
   bool
   has_operator() const
   {
-    const CondOpSection *section = &_sections;
+    return _op_if.has_operator();
+  }
 
-    while (section != nullptr) {
-      if (section->has_operator()) {
-        return true;
-      }
-      section = section->next;
-    }
-    return false;
+  bool
+  section_has_condition() const
+  {
+    auto *sec = _op_if.cur_section();
+    return sec ? sec->group.has_conditions() : false;
+  }
+
+  bool
+  section_has_operator() const
+  {
+    auto *sec = _op_if.cur_section();
+    return sec ? sec->has_operator() : false;
   }
 
   void
@@ -106,41 +95,12 @@
     _hook = hook;
   }
 
-  ConditionGroup *
-  get_group()
-  {
-    return &_cur_section->group;
-  }
-
   TSHttpHookID
   get_hook() const
   {
     return _hook;
   }
 
-  Parser::CondClause
-  get_clause() const
-  {
-    return _clause;
-  }
-
-  CondOpSection *
-  cur_section()
-  {
-    return _cur_section;
-  }
-
-  ConditionGroup *
-  new_section(Parser::CondClause clause)
-  {
-    TSAssert(_cur_section && !_cur_section->next);
-    _clause            = clause;
-    _cur_section->next = new CondOpSection();
-    _cur_section       = _cur_section->next;
-
-    return &_cur_section->group;
-  }
-
   ResourceIDs
   get_resource_ids() const
   {
@@ -153,49 +113,14 @@
     return _last;
   }
 
-  OperModifiers
-  exec(const OperatorAndMods &ops, const Resources &res) const
-  {
-    if (nullptr == ops.oper) {
-      return ops.oper_mods;
-    }
-
-    auto no_reenable_count = ops.oper->do_exec(res);
-
-    ink_assert(no_reenable_count < 2);
-    if (no_reenable_count) {
-      return static_cast<OperModifiers>(ops.oper_mods | OPER_NO_REENABLE);
-    }
-
-    return ops.oper_mods;
-  }
-
-  const RuleSet::OperatorAndMods &
-  eval(const Resources &res)
-  {
-    for (CondOpSection *sec = &_sections; sec != nullptr; sec = sec->next) {
-      if (sec->group.eval(res)) {
-        return sec->ops;
-      }
-    }
-
-    // No matching condition found, return empty operator set.
-    static OperatorAndMods empty_ops;
-    return empty_ops;
-  }
+  OperModifiers exec(const Resources &res) const;
 
   // Linked list of RuleSets
   std::unique_ptr<RuleSet> next;
 
 private:
-  // This holds one condition group, and the ops and optional else_ops, there's
-  // aways at least one of these in the vector (no "elif" sections).
-  CondOpSection  _sections;
-  CondOpSection *_cur_section = &_sections;
-
-  // State values (updated when conds / operators are added)
-  TSHttpHookID       _hook   = TS_HTTP_READ_RESPONSE_HDR_HOOK; // Which hook is this rule for
-  ResourceIDs        _ids    = RSRC_NONE;
-  bool               _last   = false;
-  Parser::CondClause _clause = Parser::CondClause::OPER;
+  OperatorIf   _op_if;
+  TSHttpHookID _hook = TS_HTTP_READ_RESPONSE_HDR_HOOK; // Which hook is this rule for
+  ResourceIDs  _ids  = RSRC_NONE;
+  bool         _last = false;
 };
diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_definitely.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_definitely.gold
new file mode 100644
index 0000000..aaa750a
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_definitely.gold
@@ -0,0 +1,18 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: definitely
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Definitely
+< X-When-200-After: Yes
+``
diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else.gold
new file mode 100644
index 0000000..a04b910
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else.gold
@@ -0,0 +1,17 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Nothing
+< X-When-200-After: Yes
+``
diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else_fie.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else_fie.gold
new file mode 100644
index 0000000..09d4e47
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_else_fie.gold
@@ -0,0 +1,19 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Fie: fie
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Nothing
+< X-Fie-Anywhere: Yes
+< X-When-200-After: Yes
+``
diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_bar.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_bar.gold
new file mode 100644
index 0000000..b583ca3
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_bar.gold
@@ -0,0 +1,20 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: foo
+> X-Bar: bar
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Yes
+< X-Foo-And-Bar: Yes
+< X-When-200-After: Yes
+``
diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_fie.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_fie.gold
new file mode 100644
index 0000000..126f4bd
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_foo_fie.gold
@@ -0,0 +1,21 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: foo
+> X-Fie: fie
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Yes
+< X-Foo-And-Fie: Yes
+< X-Fie-Anywhere: Yes
+< X-When-200-After: Yes
+``
diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_maybe.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_maybe.gold
new file mode 100644
index 0000000..f975acf
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/gold/nested_ifs_maybe.gold
@@ -0,0 +1,18 @@
+``
+> GET ``
+> Host: www.example.com``
+> User-Agent: curl/``
+> Accept: */*
+> Proxy-Connection: Keep-Alive
+> X-Foo: maybe
+``
+< HTTP/1.1 200 OK
+< Date: ``
+< Age: ``
+< Transfer-Encoding: chunked
+< Proxy-Connection: keep-alive
+< Server: ATS/``
+< X-When-200-Before: Yes
+< X-Foo: Maybe
+< X-When-200-After: Yes
+``
diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py
index ed26875..e7ef3b6 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py
+++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py
@@ -94,6 +94,10 @@
         "from": f"{url_base}_11/",
         "to": f"{origin_base}_11/",
         "plugins": [("header_rewrite", [f"{mgr.run_dir}/rule_set_body_status.conf"])]
+    }, {
+        "from": f"{url_base}_12/",
+        "to": f"{origin_base}_12/",
+        "plugins": [("header_rewrite", [f"{mgr.run_dir}/nested_ifs.conf"])]
     }
 ]
 
@@ -205,6 +209,11 @@
             "timestamp": "1469733493.993",
             "body": "ATS should not serve this body"
         }),
+    ({
+        "headers": "GET /to_12/ HTTP/1.1\r\nHost: www.example.com\r\n\r\n",
+        "timestamp": "1469733493.993",
+        "body": ""
+    }, def_resp),
 ]
 mgr.add_server_responses(origin_rules)
 
@@ -328,6 +337,36 @@
         "gold": "gold/set_body_status.gold",
         "gold_stdout": "gold/set_body_status_stdout.gold",
     },
+    {
+        "desc": "Nested if/elif/else - X-Foo=foo + X-Bar=bar path",
+        "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: foo" -H "X-Bar: bar"',
+        "gold": "gold/nested_ifs_foo_bar.gold",
+    },
+    {
+        "desc": "Nested if/elif/else - X-Foo=foo + X-Fie=fie path",
+        "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: foo" -H "X-Fie: fie"',
+        "gold": "gold/nested_ifs_foo_fie.gold",
+    },
+    {
+        "desc": "Nested if/elif/else - X-Foo=maybe path",
+        "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: maybe"',
+        "gold": "gold/nested_ifs_maybe.gold",
+    },
+    {
+        "desc": "Nested if/elif/else - X-Foo=definitely path",
+        "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Foo: definitely"',
+        "gold": "gold/nested_ifs_definitely.gold",
+    },
+    {
+        "desc": "Nested if/elif/else - else path (no X-Foo)",
+        "curl": f'{curl_proxy} "http://{url_base}_12/"',
+        "gold": "gold/nested_ifs_else.gold",
+    },
+    {
+        "desc": "Nested if/elif/else - else path with X-Fie (tests second if)",
+        "curl": f'{curl_proxy} "http://{url_base}_12/" -H "X-Fie: fie"',
+        "gold": "gold/nested_ifs_else_fie.gold",
+    },
 ]
 
 mgr.execute_tests(test_runs)
diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
index 0f909f1..c957bfa 100644
--- a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
+++ b/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf
@@ -27,8 +27,6 @@
 else
   set-header X-Response-Foo "No"
 
-# ToDo: This should use the implicit hook of %{REMAP_PSEUDO_HOOK}, needs #12557
-cond %{REMAP_PSEUDO_HOOK} [AND]
 cond %{CLIENT-HEADER:X-Fie} ="fie" [NOCASE]
   add-header X-Client-Foo "Yes"
 elif
diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf
new file mode 100644
index 0000000..5453b0b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf
@@ -0,0 +1,44 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+cond %{STATUS} =200
+  set-header X-When-200-Before "Yes"
+  if
+    cond %{CLIENT-HEADER:X-Foo} ="foo"
+      set-header X-Foo "Yes"
+      if
+        cond %{CLIENT-HEADER:X-Bar} ="bar" [NOCASE]
+          set-header X-Foo-And-Bar "Yes"
+      elif
+        cond %{CLIENT-HEADER:X-Fie} ="fie" [NOCASE]
+	  set-header X-Foo-And-Fie "Yes"
+       endif
+  elif
+    cond %{CLIENT-HEADER:X-Foo} ="maybe"
+      set-header X-Foo "Maybe"
+  elif
+    cond %{CLIENT-HEADER:X-Foo} ="definitely"
+      set-header X-Foo "Definitely"
+  else
+    set-header X-Foo "Nothing"
+  endif
+  if
+    cond %{CLIENT-HEADER:X-Fie} ="fie" [NOCASE]
+      set-header X-Fie-Anywhere "Yes"
+  endif
+  set-header X-When-200-After "Yes"
diff --git a/tools/hrw4u/grammar/hrw4u.g4 b/tools/hrw4u/grammar/hrw4u.g4
index a91e314..5db49f5 100644
--- a/tools/hrw4u/grammar/hrw4u.g4
+++ b/tools/hrw4u/grammar/hrw4u.g4
@@ -80,6 +80,7 @@
 COLON         : ':';
 COMMA         : ',';
 SEMICOLON     : ';';
+AT            : '@';
 
 COMMENT       : '#' ~[\r\n]* ;
 WS            : [ \t\r\n]+ -> skip ;
@@ -121,7 +122,7 @@
     ;
 
 variableDecl
-    : name=IDENT COLON typeName=IDENT SEMICOLON
+    : name=IDENT COLON typeName=IDENT (AT slot=NUMBER)? SEMICOLON
     ;
 
 statement
@@ -156,6 +157,7 @@
 
 blockItem
     : statement
+    | conditional
     | commentLine
     ;
 
diff --git a/tools/hrw4u/grammar/u4wrh.g4 b/tools/hrw4u/grammar/u4wrh.g4
index a88b2c5..5397288 100644
--- a/tools/hrw4u/grammar/u4wrh.g4
+++ b/tools/hrw4u/grammar/u4wrh.g4
@@ -21,6 +21,8 @@
 // Lexer Rules
 // -----------------------------
 COND          : 'cond';
+IF_OP         : 'if';
+ENDIF_OP      : 'endif';
 ELIF          : 'elif';
 ELSE          : 'else';
 AND_MOD       : 'AND';
@@ -48,24 +50,24 @@
 // Percent blocks - treat entire %{...} as one token
 PERCENT_BLOCK : '%{' ~[}\r\n]* '}' '}'?;
 
-IDENT         : [@a-zA-Z_][a-zA-Z0-9_@.-]* ;
-COMPLEX_STRING : (~[ \t\r\n[\]{}(),=!><~%])+;
-NUMBER        : [0-9]+ ;
-LPAREN        : '(';
-RPAREN        : ')';
-LBRACE        : '{';
-RBRACE        : '}';
-LBRACKET      : '[';
-RBRACKET      : ']';
-EQUALS        : '=';
-NEQ           : '!=';
-GT            : '>';
-LT            : '<';
-COMMA         : ',';
+IDENT          : [@a-zA-Z_][a-zA-Z0-9_@.-]* ;
+COMPLEX_STRING : (~[ \t\r\n[\]{}(),=!><~%#])+;
+NUMBER         : [0-9]+ ;
+LPAREN         : '(';
+RPAREN         : ')';
+LBRACE         : '{';
+RBRACE         : '}';
+LBRACKET       : '[';
+RBRACKET       : ']';
+EQUALS         : '=';
+NEQ            : '!=';
+GT             : '>';
+LT             : '<';
+COMMA          : ',';
 
-EOL           : '\r'? '\n';
-COMMENT       : '#' ~[\r\n]* ;
-WS            : [ \t]+ -> skip ;
+EOL            : '\r'? '\n';
+COMMENT        : '#'~[\r\n]*;
+WS             : [ \t]+ -> skip ;
 
 // -----------------------------
 // Parser Rules
@@ -78,6 +80,8 @@
 line
     : condLine EOL
     | opLine EOL
+    | ifLine EOL
+    | endifLine EOL
     | elifLine EOL
     | elseLine EOL
     | commentLine EOL
@@ -88,6 +92,14 @@
     : COND condBody modList?
     ;
 
+ifLine
+    : IF_OP
+    ;
+
+endifLine
+    : ENDIF_OP
+    ;
+
 elifLine
     : ELIF
     ;
diff --git a/tools/hrw4u/pyproject.toml b/tools/hrw4u/pyproject.toml
index 5a85df7..f66a9ad 100644
--- a/tools/hrw4u/pyproject.toml
+++ b/tools/hrw4u/pyproject.toml
@@ -20,7 +20,7 @@
 
 [project]
 name = "hrw4u"
-version = "1.3.4"
+version = "1.4.0"
 description = "HRW4U CLI tool for Apache Traffic Server header rewrite rules"
 authors = [
     {name = "Leif Hedstrom", email = "leif@apache.org"}
diff --git a/tools/hrw4u/scripts/testcase.py b/tools/hrw4u/scripts/testcase.py
index 757af95..802e32f 100755
--- a/tools/hrw4u/scripts/testcase.py
+++ b/tools/hrw4u/scripts/testcase.py
@@ -29,6 +29,29 @@
 KNOWN_MARKS = {"hooks", "conds", "ops", "vars", "examples", "invalid"}
 
 
+def load_exceptions(test_dir: Path) -> dict[str, str]:
+    """Load exceptions from exceptions.txt in the test directory.
+    Returns a dict mapping test filename to direction (hrw4u or u4wrh)."""
+    exceptions_file = test_dir / "exceptions.txt"
+    exceptions = {}
+
+    if not exceptions_file.exists():
+        return exceptions
+
+    for line in exceptions_file.read_text().splitlines():
+        line = line.strip()
+        if not line or line.startswith('#'):
+            continue
+
+        parts = line.split(':', 1)
+        if len(parts) == 2:
+            test_name = parts[0].strip()
+            direction = parts[1].strip()
+            exceptions[test_name] = direction
+
+    return exceptions
+
+
 def parse_tree(input_text: str) -> tuple[hrw4uParser, any]:
     stream = InputStream(input_text)
     lexer = hrw4uLexer(stream)
@@ -38,12 +61,30 @@
     return parser, tree
 
 
-def process_file(input_path: Path, update_ast: bool = False, update_output: bool = False, update_error: bool = False) -> bool:
+def process_file(
+        input_path: Path,
+        update_ast: bool = False,
+        update_output: bool = False,
+        update_error: bool = False,
+        exceptions: dict[str, str] = None) -> bool:
     base = input_path.with_suffix('')
     ast_path = base.with_suffix('.ast.txt')
     output_path = base.with_suffix('.output.txt')
     error_path = base.with_suffix('.error.txt')
 
+    # Check if this test has a direction exception
+    if exceptions is None:
+        exceptions = load_exceptions(input_path.parent)
+
+    test_filename = input_path.name.replace('.input.txt', '.input')
+    if test_filename in exceptions:
+        exception_direction = exceptions[test_filename]
+        # Skip updating for hrw4u if test is u4wrh-only (and vice versa)
+        # Since this script runs hrw4u, skip if marked as u4wrh
+        if exception_direction == 'u4wrh':
+            # This test is reverse-only, skip updating
+            return True
+
     input_text = input_path.read_text()
 
     if input_path.name.endswith(".fail.input.txt"):
@@ -90,15 +131,33 @@
         print(f"No test files found for pattern: {base_dir}/{pattern}")
         sys.exit(1)
 
+    # Group files by directory to load exceptions once per directory
+    files_by_dir = {}
+    for f in input_files:
+        if f.parent not in files_by_dir:
+            files_by_dir[f.parent] = []
+        files_by_dir[f.parent].append(f)
+
     total = len(input_files)
     failed = 0
+    skipped = 0
 
-    for f in input_files:
-        ok = process_file(f, update_ast=update_ast, update_output=update_output, update_error=update_error)
-        if not ok:
-            failed += 1
+    for test_dir, files in sorted(files_by_dir.items()):
+        exceptions = load_exceptions(test_dir)
 
-    print(f"\nUpdated: {total - failed}, Failed: {failed}")
+        for f in files:
+            # Check if this test should be skipped
+            test_filename = f.name.replace('.input.txt', '.input')
+            if test_filename in exceptions and exceptions[test_filename] == 'u4wrh':
+                skipped += 1
+                continue
+
+            ok = process_file(
+                f, update_ast=update_ast, update_output=update_output, update_error=update_error, exceptions=exceptions)
+            if not ok:
+                failed += 1
+
+    print(f"\nUpdated: {total - failed - skipped}, Skipped: {skipped}, Failed: {failed}")
     if failed:
         sys.exit(1)
 
diff --git a/tools/hrw4u/src/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py
index d08c343..149b139 100644
--- a/tools/hrw4u/src/hrw_visitor.py
+++ b/tools/hrw4u/src/hrw_visitor.py
@@ -54,8 +54,9 @@
         self.symbol_resolver = InverseSymbolResolver()
 
         self._section_opened = False
-        self._in_if_block = False
+        self._if_depth = 0  # Track nesting depth of if blocks
         self._in_elif_mode = False
+        self._just_closed_nested = False
 
     @lru_cache(maxsize=128)
     def _cached_percent_parsing(self, pct_text: str) -> tuple[str, str | None]:
@@ -87,10 +88,10 @@
         with self.debug_context(f"start_section {section_type.value}"):
             if self._section_opened and self._section_label == section_type:
                 self.debug(f"continuing existing section")
-                if self._in_if_block:
+                while self._if_depth > 0:
                     self.decrease_indent()
                     self.emit("}")
-                    self._in_if_block = False
+                    self._if_depth -= 1
                 self._reset_condition_state()
                 if self.output and self.output[-1] != "":
                     self.output.append("")
@@ -171,6 +172,20 @@
             else:
                 self.output.append(comment_text)
 
+    def visitIfLine(self, ctx: u4wrhParser.IfLineContext) -> None:
+        """Handle if operator (starts nested conditional)."""
+        with self.debug_context("visitIfLine"):
+            self._flush_pending_condition()
+            self._just_closed_nested = False
+            return None
+
+    def visitEndifLine(self, ctx: u4wrhParser.EndifLineContext) -> None:
+        """Handle endif operator (closes nested conditional)."""
+        with self.debug_context("visitEndifLine"):
+            self._close_if_block()
+            self._just_closed_nested = True
+            return None
+
     def visitElifLine(self, ctx: u4wrhParser.ElifLineContext) -> None:
         """Handle elif line transitions."""
         with self.debug_context("visitElifLine"):
@@ -357,10 +372,10 @@
     # Condition block lifecycle methods - specific to inverse visitor
     def _close_if_block(self) -> None:
         """Close open if block."""
-        if self._in_if_block:
+        if self._if_depth > 0:
             self.decrease_indent()
             self.emit("}")
-            self._in_if_block = False
+            self._if_depth -= 1
 
     def _close_section(self) -> None:
         """Close open section."""
@@ -371,7 +386,8 @@
 
     def _close_if_and_section(self) -> None:
         """Close open if blocks and sections."""
-        self._close_if_block()
+        while self._if_depth > 0:
+            self._close_if_block()
         self._close_section()
         self._in_elif_mode = False
 
@@ -384,27 +400,22 @@
 
     def _start_elif_mode(self) -> None:
         """Handle elif line transitions."""
-        if self._in_if_block:
+        # After endif, we need to close the parent if-statement
+        if self._if_depth > 0:
             self.decrease_indent()
-            self._in_if_block = False
+            self._if_depth -= 1
         self._in_elif_mode = True
+        self._just_closed_nested = False
 
     def _handle_else_transition(self) -> None:
         """Handle else line transitions."""
-        if self._in_if_block:
+        if self._if_depth > 0:
             self.decrease_indent()
-
-            if self.output and self.output[-1].strip() == "}":
-                self.output[-1] = self.format_with_indent("} else {", self.current_indent)
-            else:
-                self.emit("} else {")
-
-            self._in_if_block = True
-            self.increase_indent()
-        else:
-            self.emit("else {")
-            self.increase_indent()
-            self._in_if_block = True
+            self._if_depth -= 1
+        self.emit("} else {")
+        self._if_depth += 1
+        self.increase_indent()
+        self._just_closed_nested = False
 
     def _start_if_block(self, condition_expr: str) -> None:
         """Start a new if block."""
@@ -414,5 +425,5 @@
         else:
             self.emit(f"if {condition_expr} {{")
 
-        self._in_if_block = True
+        self._if_depth += 1
         self.increase_indent()
diff --git a/tools/hrw4u/src/symbols.py b/tools/hrw4u/src/symbols.py
index a0f66de..c40010c 100644
--- a/tools/hrw4u/src/symbols.py
+++ b/tools/hrw4u/src/symbols.py
@@ -43,7 +43,7 @@
             return params.target, params.validate
         raise SymbolResolutionError(name, "Unknown operator or invalid standalone use")
 
-    def declare_variable(self, name: str, type_name: str) -> str:
+    def declare_variable(self, name: str, type_name: str, explicit_slot: int | None = None) -> str:
         try:
             var_type = types.VarType.from_str(type_name)
         except ValueError as e:
@@ -51,13 +51,24 @@
             error.add_note(f"Available types: {', '.join([vt.name for vt in types.VarType])}")
             raise error
 
-        if self._var_counter[var_type] >= var_type.limit:
-            error = SymbolResolutionError(name, f"Too many '{type_name}' variables (max {var_type.limit})")
-            error.add_note(f"Current count: {self._var_counter[var_type]}")
-            raise error
+        # Determine slot number
+        if explicit_slot is not None:
+            if explicit_slot < 0 or explicit_slot >= var_type.limit:
+                raise SymbolResolutionError(
+                    name, f"Slot @{explicit_slot} out of range for type '{type_name}' (valid: 0-{var_type.limit-1})")
+            for var_name, sym in self._symbols.items():
+                if sym.var_type == var_type and sym.slot == explicit_slot:
+                    raise SymbolResolutionError(name, f"Slot @{explicit_slot} already used by variable '{var_name}'")
 
-        symbol = types.Symbol(var_type, self._var_counter[var_type])
-        self._var_counter[var_type] += 1
+            slot = explicit_slot
+        else:
+            used_slots = {sym.slot for sym in self._symbols.values() if sym.var_type == var_type}
+            slot = next((i for i in range(var_type.limit) if i not in used_slots), None)
+
+            if slot is None:
+                raise SymbolResolutionError(name, f"No available slots for type '{type_name}' (max {var_type.limit})")
+
+        symbol = types.Symbol(var_type, slot)
         self._symbols[name] = symbol
         return symbol.as_cond()
 
diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py
index 4185f78..0cacafe 100644
--- a/tools/hrw4u/src/types.py
+++ b/tools/hrw4u/src/types.py
@@ -163,13 +163,13 @@
 @dataclass(slots=True, frozen=True)
 class Symbol:
     var_type: VarType
-    index: int
+    slot: int
 
     def as_cond(self) -> str:
-        return f"%{{STATE-{self.var_type.cond_tag}:{self.index}}}"
+        return f"%{{STATE-{self.var_type.cond_tag}:{self.slot}}}"
 
     def as_operator(self, value: str) -> str:
-        return f"{self.var_type.op_tag} {self.index} {value}"
+        return f"{self.var_type.op_tag} {self.slot} {value}"
 
 
 class MapParams:
diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py
index c038cdc..a3cbc28 100644
--- a/tools/hrw4u/src/visitor.py
+++ b/tools/hrw4u/src/visitor.py
@@ -399,17 +399,21 @@
                 if ctx.typeName is None:
                     raise SymbolResolutionError("variable", "Missing type name in declaration")
                 name = ctx.name.text
-                type = ctx.typeName.text
+                type_name = ctx.typeName.text
+                explicit_slot = int(ctx.slot.text) if ctx.slot else None
 
                 if '.' in name or ':' in name:
                     raise SymbolResolutionError("variable", f"Variable name '{name}' cannot contain '.' or ':' characters")
 
-                symbol = self.symbol_resolver.declare_variable(name, type)
-                self._dbg(f"bind `{name}' to {symbol}")
+                symbol = self.symbol_resolver.declare_variable(name, type_name, explicit_slot)
+                slot_info = f" @{explicit_slot}" if explicit_slot is not None else ""
+                self._dbg(f"bind `{name}' to {symbol}{slot_info}")
             except Exception as e:
                 name = getattr(ctx, 'name', None)
                 type_name = getattr(ctx, 'typeName', None)
-                note = f"Variable declaration: {name.text}:{type_name.text}" if name and type_name else None
+                slot = getattr(ctx, 'slot', None)
+                note = f"Variable declaration: {name.text}:{type_name.text}" + \
+                    (f" @{slot.text}" if slot else "") if name and type_name else None
                 with self.trap(ctx, note=note):
                     raise e
                 return
@@ -445,6 +449,15 @@
                 for item in ctx.blockItem():
                     if item.statement():
                         self.visit(item.statement())
+                    elif item.conditional():
+                        # Nested conditional - emit if/endif operators with saved state
+                        self.emit_statement("if")
+                        saved_indents = self.stmt_indent, self.cond_indent
+                        self.stmt_indent += 1
+                        self.cond_indent = self.stmt_indent
+                        self.visit(item.conditional())
+                        self.stmt_indent, self.cond_indent = saved_indents
+                        self.emit_statement("endif")
                     elif item.commentLine() and self.preserve_comments:
                         self.visit(item.commentLine())
 
@@ -464,7 +477,7 @@
                 else:
                     lhs = self.visitFunctionCall(comp.functionCall())
             if not lhs:
-                return  # Skip on error
+                return
             operator = ctx.getChild(1)
             negate = operator.symbol.type in (hrw4uParser.NEQ, hrw4uParser.NOT_TILDE)
 
@@ -496,7 +509,6 @@
                 case _ if ctx.set_():
                     inner = ctx.set_().getText()[1:-1]
                     # We no longer strip the quotes here for sets, fixed in #12256
-                    # parts = [s.strip().strip("'") for s in inner.split(",")]
                     cond_txt = f"{lhs} ({inner})"
 
                 case _:
diff --git a/tools/hrw4u/tests/data/conds/nested-ifs.ast.txt b/tools/hrw4u/tests/data/conds/nested-ifs.ast.txt
new file mode 100644
index 0000000..0a10091
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nested-ifs.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (varSection VARS { (variables (variablesItem (variableDecl bool_0 : bool ;)) (variablesItem (variableDecl bool_1 : bool ;)) (variablesItem (variableDecl bool_2 : bool ;))) }))) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.X-Foo) == (value "bar")))))) (block { (blockItem (statement inbound.req.X-Hello = (value "there") ;)) (blockItem (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.X-Fie) == (value "fie")))))) (block { (blockItem (statement inbound.req.X-first = (value "1") ;)) (blockItem (conditional (ifStatement if (condition (expression (expression (term (factor bool_0))) || (term (factor ( (expression (term (term (factor bool_1)) && (factor bool_2))) ))))) (block { (blockItem (statement inbound.req.X-Parsed = (value "more") ;)) })) (elseClause else (block { (blockItem (statement inbound.req.X-Parsed = (value "yes") ;)) })))) })) (elifClause elif (condition (expression (term (factor (comparison (comparable inbound.req.X-Fum) == (value "bar")))))) (block { (blockItem (statement inbound.req.X-Parsed = (value "no") ;)) })) (elseClause else (block { (blockItem (statement inbound.req.X-More = (value "yes") ;)) })))) })) (elifClause elif (condition (expression (term (factor (comparison (comparable inbound.req.X-Foo) == (value "foo") (modifier with (modifierList NOCASE , PRE))))))) (block { (blockItem (statement inbound.req.X-Nocase = (value "foo") ;)) })) (elseClause else (block { (blockItem (statement inbound.req.X-Something = (value "no-bar") ;)) })))) })) <EOF>)
diff --git a/tools/hrw4u/tests/data/conds/nested-ifs.input.txt b/tools/hrw4u/tests/data/conds/nested-ifs.input.txt
new file mode 100644
index 0000000..2716478
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nested-ifs.input.txt
@@ -0,0 +1,27 @@
+VARS {
+    bool_0: bool;
+    bool_1: bool;
+    bool_2: bool;
+}
+
+REMAP {
+    if inbound.req.X-Foo == "bar" {
+        inbound.req.X-Hello = "there";
+        if inbound.req.X-Fie == "fie" {
+            inbound.req.X-first = "1";
+            if bool_0 || (bool_1 && bool_2) {
+                inbound.req.X-Parsed = "more";
+            } else {
+                inbound.req.X-Parsed = "yes";
+            }
+        } elif inbound.req.X-Fum == "bar" {
+            inbound.req.X-Parsed = "no";
+        } else {
+            inbound.req.X-More = "yes";
+        }
+    } elif inbound.req.X-Foo == "foo" with NOCASE,PRE {
+        inbound.req.X-Nocase = "foo";
+    } else {
+        inbound.req.X-Something = "no-bar";
+    }
+}
diff --git a/tools/hrw4u/tests/data/conds/nested-ifs.output.txt b/tools/hrw4u/tests/data/conds/nested-ifs.output.txt
new file mode 100644
index 0000000..f7b802f
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nested-ifs.output.txt
@@ -0,0 +1,27 @@
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{CLIENT-HEADER:X-Foo} ="bar"
+    set-header X-Hello "there"
+    if
+        cond %{CLIENT-HEADER:X-Fie} ="fie"
+            set-header X-first "1"
+            if
+                cond %{STATE-FLAG:0} [OR]
+                cond %{GROUP}
+                    cond %{STATE-FLAG:1} [AND]
+                    cond %{STATE-FLAG:2}
+                cond %{GROUP:END}
+                    set-header X-Parsed "more"
+                else
+                    set-header X-Parsed "yes"
+            endif
+        elif
+            cond %{CLIENT-HEADER:X-Fum} ="bar"
+                set-header X-Parsed "no"
+        else
+            set-header X-More "yes"
+    endif
+elif
+    cond %{CLIENT-HEADER:X-Foo} ="foo" [NOCASE,PRE]
+        set-header X-Nocase "foo"
+else
+    set-header X-Something "no-bar"
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt b/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt
index c055507..8bc6dbd 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_invalid_bool.fail.error.txt
@@ -1 +1,3 @@
-Invalid boolean value 'invalid_value'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES
\ No newline at end of file
+tests/data/ops/http_cntl_invalid_bool.fail.input.txt:2:4: error: Invalid boolean value 'invalid_value'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES
+   2 |     http.cntl.LOGGING = invalid_value;
+     |     ^
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt b/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt
index 4a4000f..26e842f 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_quoted_bool.fail.error.txt
@@ -1 +1,3 @@
-Invalid boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES and must not be quoted
\ No newline at end of file
+tests/data/ops/http_cntl_quoted_bool.fail.input.txt:2:4: error: Invalid boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES and must not be quoted
+   2 |     http.cntl.LOGGING = "true";
+     |     ^
diff --git a/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt b/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt
index 4a4000f..8ba0760 100644
--- a/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt
+++ b/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.error.txt
@@ -1 +1,3 @@
-Invalid boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES and must not be quoted
\ No newline at end of file
+tests/data/ops/skip_remap_quoted_bool.fail.input.txt:2:4: error: Invalid boolean value '"true"'. Must be one of: 0, 1, FALSE, NO, OFF, ON, TRUE, YES and must not be quoted
+   2 |     skip-remap("true");
+     |     ^
diff --git a/tools/hrw4u/tests/data/vars/exceptions.txt b/tools/hrw4u/tests/data/vars/exceptions.txt
new file mode 100644
index 0000000..64e57bf
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/exceptions.txt
@@ -0,0 +1,5 @@
+# Operations tests direction exceptions
+# Format: test_name: direction
+#
+# Explicit slot assignment syntax cannot be reversed
+explicit_slots.input: hrw4u
diff --git a/tools/hrw4u/tests/data/vars/explicit_slots.ast.txt b/tools/hrw4u/tests/data/vars/explicit_slots.ast.txt
new file mode 100644
index 0000000..1d0b442
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/explicit_slots.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section (varSection VARS { (variables (variablesItem (variableDecl parent_config : bool @ 7 ;)) (variablesItem (variableDecl parent_child : bool @ 12 ;)) (variablesItem (variableDecl match : bool ;)) (variablesItem (variableDecl active_flag : bool @ 3 ;)) (variablesItem (variableDecl counter : int8 @ 2 ;)) (variablesItem (variableDecl priority : int8 ;)) (variablesItem (variableDecl status : int16 ;))) }))) (programItem (section SEND_RESPONSE { (sectionBody (conditional (ifStatement if (condition (expression (term (factor parent_config)))) (block { (blockItem (statement inbound.resp.X-Parent = (value true) ;)) })))) })) <EOF>)
diff --git a/tools/hrw4u/tests/data/vars/explicit_slots.input.txt b/tools/hrw4u/tests/data/vars/explicit_slots.input.txt
new file mode 100644
index 0000000..e8ff9f5
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/explicit_slots.input.txt
@@ -0,0 +1,15 @@
+VARS {
+    parent_config: bool @7;
+    parent_child: bool @12;
+    match: bool;
+    active_flag: bool @3;
+    counter: int8 @2;
+    priority: int8;
+    status: int16;
+}
+
+SEND_RESPONSE {
+    if parent_config {
+        inbound.resp.X-Parent = true;
+    }
+}
diff --git a/tools/hrw4u/tests/data/vars/explicit_slots.output.txt b/tools/hrw4u/tests/data/vars/explicit_slots.output.txt
new file mode 100644
index 0000000..adaa5f0
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/explicit_slots.output.txt
@@ -0,0 +1,3 @@
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+cond %{STATE-FLAG:7}
+    set-header X-Parent true
diff --git a/tools/hrw4u/tests/data/vars/slot_conflict.fail.error.txt b/tools/hrw4u/tests/data/vars/slot_conflict.fail.error.txt
new file mode 100644
index 0000000..721dbcd
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/slot_conflict.fail.error.txt
@@ -0,0 +1,3 @@
+tests/data/vars/slot_conflict.fail.input.txt:3:4: error: Slot @5 already used by variable 'first'
+   3 |     second: bool @5;  # Error: slot already used
+     |     ^
diff --git a/tools/hrw4u/tests/data/vars/slot_conflict.fail.input.txt b/tools/hrw4u/tests/data/vars/slot_conflict.fail.input.txt
new file mode 100644
index 0000000..f60cabb
--- /dev/null
+++ b/tools/hrw4u/tests/data/vars/slot_conflict.fail.input.txt
@@ -0,0 +1,8 @@
+VARS {
+    first: bool @5;
+    second: bool @5;  # Error: slot already used
+}
+
+SEND_RESPONSE {
+    set_header("X-Test", "value");
+}
diff --git a/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt b/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt
index 5846224..513c141 100644
--- a/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt
+++ b/tools/hrw4u/tests/data/vars/vars_count.fail.error.txt
@@ -1,3 +1,3 @@
-tests/data/vars/vars_count.fail.input.txt:7:3: error: Too many 'int8' variables (max 4)
+tests/data/vars/vars_count.fail.input.txt:7:3: error: No available slots for type 'int8' (max 4)
    7 |    Five: int8;
      |    ^