]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
move keyword compile_foo() to foo.c
authorAlan T. DeKok <aland@freeradius.org>
Thu, 3 Jul 2025 17:24:30 +0000 (13:24 -0400)
committerAlan T. DeKok <aland@freeradius.org>
Thu, 3 Jul 2025 18:25:51 +0000 (14:25 -0400)
mostly. compile_edit(), compile_tmpl(), compile_module(), and
compile_function() are still in compile.c

Many helper functions are exported in interpret_priv.h.  Some of
those should be cleaned up / renamed / un-exported.

The function names could likely be cleaned up.  Much of the code
uses unlang_foo() for private functions.  It should arguably use
private function names.

The unlang_register() function takes a type as both a function
argument, and as a field in unlang_op_t.  Fixing that will require
changing instruction->type to instruction->op in the interpreter,
which should be a next step.

The hash function is just fr_hash().  We should arguably come up
with a perfect hash for the keywords, which will help simplify
the lookups.

27 files changed:
src/lib/unlang/base.c
src/lib/unlang/call.c
src/lib/unlang/caller.c
src/lib/unlang/catch.c
src/lib/unlang/child_request.c
src/lib/unlang/compile.c
src/lib/unlang/condition.c
src/lib/unlang/detach.c
src/lib/unlang/edit.c
src/lib/unlang/finally.c
src/lib/unlang/foreach.c
src/lib/unlang/function.c
src/lib/unlang/group.c
src/lib/unlang/limit.c
src/lib/unlang/load_balance.c
src/lib/unlang/map.c
src/lib/unlang/module.c
src/lib/unlang/parallel.c
src/lib/unlang/return.c
src/lib/unlang/subrequest.c
src/lib/unlang/switch.c
src/lib/unlang/timeout.c
src/lib/unlang/tmpl.c
src/lib/unlang/transaction.c
src/lib/unlang/try.c
src/lib/unlang/unlang_priv.h
src/lib/unlang/xlat.c

index 91d9ef2669f374e60aaf1b8ded3f0bf58912d38a..2f5716041e3eeb4d09fc1b858e955a3e78628b64 100644 (file)
@@ -42,6 +42,8 @@ bool unlang_section(CONF_SECTION *cs)
        return (cf_data_find(cs, unlang_group_t, NULL) != NULL);
 }
 
+fr_hash_table_t *unlang_op_table = NULL;
+
 /** Register an operation with the interpreter
  *
  * The main purpose of this registration API is to avoid intermixing the xlat,
@@ -63,15 +65,42 @@ bool unlang_section(CONF_SECTION *cs)
 void unlang_register(int type, unlang_op_t *op)
 {
        fr_assert(type < UNLANG_TYPE_MAX);      /* Unlang max isn't a valid type */
+       fr_assert(unlang_op_table != NULL);
+       fr_assert(op->type == (unlang_type_t) type);
 
        memcpy(&unlang_ops[type], op, sizeof(unlang_ops[type]));
+
+       /*
+        *      Some instruction types are internal, and are not real keywords.
+        */
+       if ((op->flag & UNLANG_OP_FLAG_INTERNAL) != 0) return;
+
+       MEM(fr_hash_table_insert(unlang_op_table, &unlang_ops[type]));
 }
 
 static TALLOC_CTX *unlang_ctx = NULL;
 
+static uint32_t op_hash(void const *data)
+{
+       unlang_op_t const *a = data;
+
+       return fr_hash_string(a->name);
+}
+
+static int8_t op_cmp(void const *one, void const *two)
+{
+       unlang_op_t const *a = one;
+       unlang_op_t const *b = two;
+
+       ERROR("CMP ::%s:: ::%s::", a->name, b->name);
+
+       return CMP(strcmp(a->name, b->name), 0);
+}
+
 static int _unlang_global_free(UNUSED void *uctx)
 {
        TALLOC_FREE(unlang_ctx);
+       unlang_op_table = NULL;
 
        return 0;
 }
@@ -81,6 +110,9 @@ static int _unlang_global_init(UNUSED void *uctx)
        unlang_ctx = talloc_init("unlang");
        if (!unlang_ctx) return -1;
 
+       unlang_op_table = fr_hash_table_alloc(unlang_ctx, op_hash, op_cmp, NULL);
+       if (!unlang_op_table) goto fail;
+
        /*
         *      Explicitly initialise the xlat tree, and perform dictionary lookups.
         */
index 53f35896c396b04fdcdd06581faf93a537d2cbb9..aef7a3a5c747b746eea19676d438fa1ebfbad9a4 100644 (file)
@@ -246,6 +246,81 @@ CONF_SECTION *unlang_call_current(request_t *request)
        return NULL;
 }
 
+static unlang_t *unlang_compile_call(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION                    *cs = cf_item_to_section(ci);
+       virtual_server_t const          *vs;
+       unlang_t                        *c;
+
+       unlang_group_t                  *g;
+       unlang_call_t                   *gext;
+
+       fr_token_t                      type;
+       char const                      *server;
+       CONF_SECTION                    *server_cs;
+       fr_dict_t const                 *dict;
+       fr_dict_attr_t const            *attr_packet_type;
+
+       server = cf_section_name2(cs);
+       if (!server) {
+               cf_log_err(cs, "You MUST specify a server name for 'call <server> { ... }'");
+       print_url:
+               cf_log_err(ci, DOC_KEYWORD_REF(call));
+               return NULL;
+       }
+
+       type = cf_section_name2_quote(cs);
+       if (type != T_BARE_WORD) {
+               cf_log_err(cs, "The arguments to 'call' cannot be a quoted string or a dynamic value");
+               goto print_url;
+       }
+
+       vs = virtual_server_find(server);
+       if (!vs) {
+               cf_log_err(cs, "Unknown virtual server '%s'", server);
+               return NULL;
+       }
+
+       server_cs = virtual_server_cs(vs);
+
+       /*
+        *      The dictionaries are not compatible, forbid it.
+        */
+       dict = virtual_server_dict_by_name(server);
+       if (!dict) {
+               cf_log_err(cs, "Cannot call virtual server '%s', failed retrieving its namespace",
+                          server);
+               return NULL;
+       }
+       if ((dict != fr_dict_internal()) && fr_dict_internal() &&
+           unlang_ctx->rules->attr.dict_def && !fr_dict_compatible(unlang_ctx->rules->attr.dict_def, dict)) {
+               cf_log_err(cs, "Cannot call server %s with namespace '%s' from namespaces '%s' - they have incompatible protocols",
+                          server, fr_dict_root(dict)->name, fr_dict_root(unlang_ctx->rules->attr.dict_def)->name);
+               return NULL;
+       }
+
+       attr_packet_type = fr_dict_attr_by_name(NULL, fr_dict_root(dict), "Packet-Type");
+       if (!attr_packet_type) {
+               cf_log_err(cs, "Cannot call server %s with namespace '%s' - it has no Packet-Type attribute",
+                          server, fr_dict_root(dict)->name);
+               return NULL;
+       }
+
+       c = unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_CALL);
+       if (!c) return NULL;
+
+       /*
+        *      Set the virtual server name, which tells unlang_call()
+        *      which virtual server to call.
+        */
+       g = unlang_generic_to_group(c);
+       gext = unlang_group_to_call(g);
+       gext->server_cs = server_cs;
+       gext->attr_packet_type = attr_packet_type;
+
+       return c;
+}
+
 void unlang_call_init(void)
 {
        unlang_register(UNLANG_TYPE_CALL,
@@ -254,6 +329,7 @@ void unlang_call_init(void)
                                .flag           = UNLANG_OP_FLAG_RCODE_SET | UNLANG_OP_FLAG_DEBUG_BRACES,
                                .type           = UNLANG_TYPE_CALL,
 
+                               .compile        = unlang_compile_call,
                                .interpret      = unlang_call_frame_init,
 
 
index def5c58345e635918f0906f2ab238eab94c0065f..ad21042c8817dede93c1f01319236ab8d29f21b4 100644 (file)
@@ -52,6 +52,93 @@ static unlang_action_t unlang_caller(unlang_result_t *p_result, request_t *reque
 }
 
 
+static unlang_t *unlang_compile_caller(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION                    *cs = cf_item_to_section(ci);
+       unlang_t                        *c;
+
+       unlang_group_t                  *g;
+       unlang_caller_t                 *gext;
+
+       fr_token_t                      type;
+       char const                      *name;
+       fr_dict_t const                 *dict;
+       unlang_compile_ctx_t            unlang_ctx2;
+       tmpl_rules_t                    parent_rules, t_rules;
+
+       fr_dict_autoload_talloc_t       *dict_ref = NULL;
+
+       name = cf_section_name2(cs);
+       if (!name) {
+               cf_log_err(cs, "You MUST specify a protocol name for 'caller <protocol> { ... }'");
+       print_url:
+               cf_log_err(ci, DOC_KEYWORD_REF(caller));
+               return NULL;
+       }
+
+       type = cf_section_name2_quote(cs);
+       if (type != T_BARE_WORD) {
+               cf_log_err(cs, "The argument to 'caller' cannot be a quoted string or a dynamic value");
+               goto print_url;
+       }
+
+       dict = fr_dict_by_protocol_name(name);
+       if (!dict) {
+               dict_ref = fr_dict_autoload_talloc(NULL, &dict, name);
+               if (!dict_ref) {
+                       cf_log_perr(cs, "Unknown protocol '%s'", name);
+                       goto print_url;
+               }
+       }
+
+       /*
+        *      Create a new parent context with the new dictionary.
+        */
+       memcpy(&parent_rules, unlang_ctx->rules, sizeof(parent_rules));
+       memcpy(&t_rules, unlang_ctx->rules, sizeof(t_rules));
+       parent_rules.attr.dict_def = dict;
+       t_rules.parent = &parent_rules;
+
+       /*
+        *      We don't want to modify the context we were passed, so
+        *      we just clone it
+        */
+       memcpy(&unlang_ctx2, unlang_ctx, sizeof(unlang_ctx2));
+       unlang_ctx2.rules = &t_rules;
+       unlang_ctx2.section_name1 = "caller";
+       unlang_ctx2.section_name2 = name;
+
+       c = unlang_compile_section(parent, &unlang_ctx2, cs, UNLANG_TYPE_CALLER);
+       if (!c) {
+               talloc_free(dict_ref);
+               return NULL;
+       }
+
+       /*
+        *      Set the virtual server name, which tells unlang_call()
+        *      which virtual server to call.
+        */
+       g = unlang_generic_to_group(c);
+       gext = unlang_group_to_caller(g);
+       gext->dict = dict;
+
+       if (dict_ref) {
+               /*
+                *      Parent the dictionary reference correctly now that we
+                *      have the section with the dependency.  This should
+                *      be fast as dict_ref has no siblings.
+                */
+               talloc_steal(gext, dict_ref);
+       }
+
+       if (!g->num_children) {
+               talloc_free(c);
+               return UNLANG_IGNORE;
+       }
+
+       return c;
+}
+
 void unlang_caller_init(void)
 {
        unlang_register(UNLANG_TYPE_CALLER,
@@ -60,6 +147,7 @@ void unlang_caller_init(void)
                                .type = UNLANG_TYPE_CALLER,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_caller,
                                .interpret = unlang_caller,
 
                                .unlang_size = sizeof(unlang_caller_t),
index c607be9f274ed12509dd0590b0a71eb6fc2e0775..5be1865910758b07de91b27c0314586066fce466 100644 (file)
@@ -99,6 +99,100 @@ unlang_action_t unlang_interpret_skip_to_catch(UNUSED unlang_result_t *p_result,
        return frame_set_next(frame, unlang);
 }
 
+static int catch_argv(CONF_SECTION *cs, unlang_catch_t *ca, char const *name)
+{
+       int rcode;
+
+       rcode = fr_table_value_by_str(mod_rcode_table, name, -1);
+       if (rcode < 0) {
+               cf_log_err(cs, "Unknown rcode '%s'.", name);
+               return -1;
+       }
+
+       if (ca->catching[rcode]) {
+               cf_log_err(cs, "Duplicate rcode '%s'.", name);
+               return -1;
+       }
+
+       ca->catching[rcode] = true;
+
+       return 0;
+}
+
+static unlang_t *unlang_compile_catch(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION *cs = cf_item_to_section(ci);
+       unlang_group_t *g;
+       unlang_t *c;
+       unlang_catch_t *ca;
+       CONF_ITEM *prev;
+       char const *name;
+
+       prev = cf_item_prev(cf_parent(ci), ci);
+       while (prev && cf_item_is_data(prev)) prev = cf_item_prev(cf_parent(ci), prev);
+
+       if (!prev || !cf_item_is_section(prev)) {
+       fail:
+               cf_log_err(cs, "Found 'catch' section with no previous 'try'");
+               cf_log_err(ci, DOC_KEYWORD_REF(catch));
+               return NULL;
+       }
+
+       name = cf_section_name1(cf_item_to_section(prev));
+       fr_assert(name != NULL);
+
+       if ((strcmp(name, "try") != 0) && (strcmp(name, "catch") != 0)) {
+               /*
+                *      The previous thing has to be a section.  And it has to
+                *      be either a "try" or a "catch".
+                */
+               goto fail;
+       }
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_CATCH);
+       if (!g) return NULL;
+
+       c = unlang_group_to_generic(g);
+
+       /*
+        *      Want to log what we caught
+        */
+       c->debug_name = c->name = talloc_typed_asprintf(c, "%s %s", cf_section_name1(cs), cf_section_name2(cs));
+
+       ca = unlang_group_to_catch(g);
+       if (!cf_section_name2(cs)) {
+               /*
+                *      No arg2: catch errors
+                */
+               ca->catching[RLM_MODULE_REJECT] = true;
+               ca->catching[RLM_MODULE_FAIL] = true;
+               ca->catching[RLM_MODULE_INVALID] = true;
+               ca->catching[RLM_MODULE_DISALLOW] = true;
+
+       } else {
+               int i;
+
+               name = cf_section_name2(cs);
+
+               if (catch_argv(cs, ca, name) < 0) {
+                       talloc_free(c);
+                       return NULL;
+               }
+
+               for (i = 0; (name = cf_section_argv(cs, i)) != NULL; i++) {
+                       if (catch_argv(cs, ca, name) < 0) {
+                               talloc_free(c);
+                               return NULL;
+                       }
+               }
+       }
+
+       /*
+        *      @todo - Else parse and limit the things we catch
+        */
+       return unlang_compile_children(g, unlang_ctx, true);
+}
+
 void unlang_catch_init(void)
 {
        unlang_register(UNLANG_TYPE_CATCH,
@@ -107,6 +201,7 @@ void unlang_catch_init(void)
                                .type = UNLANG_TYPE_CATCH,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_catch,
                                .interpret = unlang_catch,
 
                                .unlang_size = sizeof(unlang_catch_t),
index 323bbd2d62618f50793a7bef14d3122492c145b8..1677ad4f272a8a9f4ee424d1d7263a592f1de25f 100644 (file)
@@ -281,8 +281,8 @@ int unlang_child_request_op_init(void)
        unlang_register(UNLANG_TYPE_CHILD_REQUEST,
                        &(unlang_op_t){
                                .name = "child-request",
-                               .interpret = unlang_child_request_done,
-                               .signal = unlang_child_request_signal,
+                               .type = UNLANG_TYPE_CHILD_REQUEST,
+
                                /*
                                 *      Frame can't be cancelled, because children need to
                                 *      write out status to the parent.  If we don't do this,
@@ -294,7 +294,11 @@ int unlang_child_request_op_init(void)
                                 *      to end normally so that non-detachable requests are
                                 *      guaranteed the parent still exists.
                                 */
-                               .flag = UNLANG_OP_FLAG_NO_FORCE_UNWIND,
+                               .flag = UNLANG_OP_FLAG_NO_FORCE_UNWIND | UNLANG_OP_FLAG_INTERNAL,
+
+                               .interpret = unlang_child_request_done,
+                               .signal = unlang_child_request_signal,
+
                                .frame_state_size = sizeof(unlang_frame_state_child_request_t),
                                .frame_state_type = "unlang_frame_state_child_request_t"
                        });
index 537880159aa8e5ad47f60def7352796f6e0127f6..8a4c94b028c0b86a0a6cef7141ccb87114d45638 100644 (file)
@@ -55,8 +55,6 @@ RCSID("$Id$")
 #include "try_priv.h"
 #include "mod_action.h"
 
-#define UNLANG_IGNORE ((unlang_t *) -1)
-
 static unsigned int unlang_number = 1;
 
 /*
@@ -98,64 +96,12 @@ fr_table_num_sorted_t const mod_action_table[] = {
 };
 size_t mod_action_table_len = NUM_ELEMENTS(mod_action_table);
 
-typedef struct {
-       virtual_server_t const          *vs;                    //!< Virtual server we're compiling in the context of.
-                                                               ///< This shouldn't change during the compilation of
-                                                               ///< a single unlang section.
-       char const                      *section_name1;
-       char const                      *section_name2;
-       unlang_mod_actions_t            actions;
-       tmpl_rules_t const              *rules;
-} unlang_compile_t;
-
-/*
- *     When we switch to a new unlang ctx, we use the new component
- *     name and number, but we use the CURRENT actions.
- */
-static inline CC_HINT(always_inline)
-void compile_copy_context(unlang_compile_t *dst, unlang_compile_t const *src)
-{
-       int i;
-
-       *dst = *src;
-
-       /*
-        *      Ensure that none of the actions are RETRY.
-        */
-       for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
-               if (dst->actions.actions[i] == MOD_ACTION_RETRY) dst->actions.actions[i] = MOD_PRIORITY_MIN;
-       }
-       memset(&dst->actions.retry, 0, sizeof(dst->actions.retry)); \
-}
-
-#define UPDATE_CTX2 compile_copy_context(&unlang_ctx2, unlang_ctx)
+#define UPDATE_CTX2 unlang_compile_ctx_copy(&unlang_ctx2, unlang_ctx)
 
 
-static unlang_t *compile_empty(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type);
-
 static char const unlang_spaces[] = "                                                                                                                                                                                                                                                                ";
 
-static inline CC_HINT(always_inline) int unlang_attr_rules_verify(tmpl_attr_rules_t const *rules)
-{
-       if (!fr_cond_assert_msg(rules->dict_def, "No protocol dictionary set")) return -1;
-       if (!fr_cond_assert_msg(rules->dict_def != fr_dict_internal(), "rules->attr.dict_def must not be the internal dictionary")) return -1;
-       if (!fr_cond_assert_msg(!rules->allow_foreign, "rules->attr.allow_foreign must be false")) return -1;
-
-       return 0;
-}
-
-static inline CC_HINT(always_inline) int unlang_rules_verify(tmpl_rules_t const *rules)
-{
-       if (!fr_cond_assert_msg(!rules->at_runtime, "rules->at_runtime must be false")) return -1;
-       return unlang_attr_rules_verify(&rules->attr);
-}
-
-#if 0
-#define ATTR_RULES_VERIFY(_rules) if (unlang_attr_rules_verify(_rules) < 0) return NULL;
-#endif
-#define RULES_VERIFY(_rules) do { if (unlang_rules_verify(_rules) < 0) return NULL; } while (0)
-
-static bool pass2_fixup_tmpl(UNUSED TALLOC_CTX *ctx, tmpl_t **vpt_p, CONF_ITEM const *ci, fr_dict_t const *dict)
+bool pass2_fixup_tmpl(UNUSED TALLOC_CTX *ctx, tmpl_t **vpt_p, CONF_ITEM const *ci, fr_dict_t const *dict)
 {
        tmpl_t *vpt = *vpt_p;
 
@@ -183,7 +129,7 @@ static bool pass2_fixup_tmpl(UNUSED TALLOC_CTX *ctx, tmpl_t **vpt_p, CONF_ITEM c
  *  This function resolves most things.  Most notable it CAN leave the
  *  RHS unresolved, for use in `map` sections.
  */
-static bool pass2_fixup_map(map_t *map, tmpl_rules_t const *rules, fr_dict_attr_t const *parent)
+bool pass2_fixup_map(map_t *map, tmpl_rules_t const *rules, fr_dict_attr_t const *parent)
 {
        RULES_VERIFY(rules);
 
@@ -247,7 +193,7 @@ static bool pass2_fixup_map(map_t *map, tmpl_rules_t const *rules, fr_dict_attr_
 /*
  *     Do all kinds of fixups and checks for update sections.
  */
-static bool pass2_fixup_update(unlang_group_t *g, tmpl_rules_t const *rules)
+bool pass2_fixup_update(unlang_group_t *g, tmpl_rules_t const *rules)
 {
        unlang_map_t    *gext = unlang_group_to_map(g);
        map_t           *map = NULL;
@@ -274,7 +220,7 @@ static bool pass2_fixup_update(unlang_group_t *g, tmpl_rules_t const *rules)
 /*
  *     Compile the RHS of map sections to xlat_exp_t
  */
-static bool pass2_fixup_map_rhs(unlang_group_t *g, tmpl_rules_t const *rules)
+bool pass2_fixup_map_rhs(unlang_group_t *g, tmpl_rules_t const *rules)
 {
        unlang_map_t    *gext = unlang_group_to_map(g);
        map_t           *map = NULL;
@@ -402,52 +348,6 @@ static void unlang_dump(unlang_t *instruction, int depth)
        }
 }
 
-/** Validate and fixup a map that's part of an map section.
- *
- * @param map to validate.
- * @param ctx data to pass to fixup function (currently unused).
- * @return 0 if valid else -1.
- */
-static int unlang_fixup_map(map_t *map, UNUSED void *ctx)
-{
-       switch (map->lhs->type) {
-       case TMPL_TYPE_ATTR:
-       case TMPL_TYPE_XLAT_UNRESOLVED:
-       case TMPL_TYPE_XLAT:
-               break;
-
-       default:
-               cf_log_err(map->ci, "Left side of map must be an attribute "
-                          "or an xlat (that expands to an attribute), not a %s",
-                          tmpl_type_to_str(map->lhs->type));
-               return -1;
-       }
-
-       switch (map->rhs->type) {
-       case TMPL_TYPE_XLAT_UNRESOLVED:
-       case TMPL_TYPE_DATA_UNRESOLVED:
-       case TMPL_TYPE_DATA:
-       case TMPL_TYPE_XLAT:
-       case TMPL_TYPE_ATTR:
-       case TMPL_TYPE_EXEC:
-               break;
-
-       default:
-               cf_log_err(map->ci, "Right side of map must be an attribute, literal, xlat or exec, got type %s",
-                          tmpl_type_to_str(map->rhs->type));
-               return -1;
-       }
-
-       if (!fr_assignment_op[map->op] && !fr_comparison_op[map->op]) {
-               cf_log_err(map->ci, "Invalid operator \"%s\" in map section.  "
-                          "Only assignment or filter operators are allowed",
-                          fr_table_str_by_value(fr_tokens_table, map->op, "<INVALID>"));
-               return -1;
-       }
-
-       return 0;
-}
-
 
 /** Validate and fixup a map that's part of an update section.
  *
@@ -553,7 +453,7 @@ int unlang_fixup_update(map_t *map, void *ctx)
 }
 
 
-static unlang_group_t *group_allocate(unlang_t *parent, CONF_SECTION *cs, unlang_type_t type)
+unlang_group_t *unlang_group_allocate(unlang_t *parent, CONF_SECTION *cs, unlang_type_t type)
 {
        unlang_group_t  *g;
        unlang_t        *c;
@@ -584,7 +484,7 @@ static unlang_group_t *group_allocate(unlang_t *parent, CONF_SECTION *cs, unlang
        return g;
 }
 
-static void compile_action_defaults(unlang_t *c, unlang_compile_t *unlang_ctx)
+void unlang_compile_action_defaults(unlang_t *c, unlang_compile_ctx_t *unlang_ctx)
 {
        int i;
 
@@ -630,3741 +530,1175 @@ static void compile_action_defaults(unlang_t *c, unlang_compile_t *unlang_ctx)
        }
 }
 
-static int compile_map_name(unlang_group_t *g)
-{
-       unlang_map_t    *gext = unlang_group_to_map(g);
+#define T(_x) [T_OP_ ## _x] = true
 
-       /*
-        *      map <module-name> <arg>
-        */
-       if (gext->vpt) {
-               char    quote;
-               size_t  quoted_len;
-               char    *quoted_str;
-
-               switch (cf_section_argv_quote(g->cs, 0)) {
-               case T_DOUBLE_QUOTED_STRING:
-                       quote = '"';
-                       break;
+static const bool edit_list_sub_op[T_TOKEN_LAST] = {
+       T(NE),
+       T(GE),
+       T(GT),
+       T(LE),
+       T(LT),
+       T(CMP_EQ),
+};
 
-               case T_SINGLE_QUOTED_STRING:
-                       quote = '\'';
-                       break;
+/** Validate and fixup a map that's part of an edit section.
+ *
+ * @param map to validate.
+ * @param ctx data to pass to fixup function (currently unused).
+ * @return 0 if valid else -1.
+ *
+ *  @todo - this is only called for CONF_PAIR maps, not for
+ *  CONF_SECTION.  So when we parse nested maps, there's no validation
+ *  done of the CONF_SECTION.  In order to fix this, we need to have
+ *  map_afrom_cs() call the validation function for the CONF_SECTION
+ *  *before* recursing.
+ */
+static int unlang_fixup_edit(map_t *map, void *ctx)
+{
+       CONF_PAIR *cp = cf_item_to_pair(map->ci);
+       fr_dict_attr_t const *da;
+       fr_dict_attr_t const *parent = NULL;
+       map_t           *parent_map = ctx;
 
-               case T_BACK_QUOTED_STRING:
-                       quote = '`';
-                       break;
+       fr_assert(parent_map);
+#ifdef STATIC_ANALYZER
+       if (!parent_map) return -1;
+#endif
 
-               default:
-                       quote = '\0';
-                       break;
+       fr_assert(tmpl_is_attr(parent_map->lhs));
+
+       if (parent_map && (parent_map->op == T_OP_SUB_EQ)) {
+               if (!edit_list_sub_op[map->op]) {
+                       cf_log_err(cp, "Invalid operator '%s' for right-hand side list.  It must be a comparison operator", fr_tokens[map->op]);
+                       return -1;
                }
 
-               quoted_len = fr_snprint_len(gext->vpt->name, gext->vpt->len, quote);
-               quoted_str = talloc_array(g, char, quoted_len);
-               fr_snprint(quoted_str, quoted_len, gext->vpt->name, gext->vpt->len, quote);
+       } else if (map->op != T_OP_EQ) {
+               cf_log_err(cp, "Invalid operator '%s' for right-hand side list.  It must be '='", fr_tokens[map->op]);
+               return -1;
+       }
 
-               g->self.name = talloc_typed_asprintf(g, "map %s %s", cf_section_name2(g->cs), quoted_str);
-               g->self.debug_name = g->self.name;
-               talloc_free(quoted_str);
+       /*
+        *      map_afrom_cs() will build its tree recursively, and call us for each child map.
+        */
+       if (map->parent && (map->parent != parent_map)) parent_map = map->parent;
 
-               return 0;
-       }
+       parent = tmpl_attr_tail_da(parent_map->lhs);
 
-       g->self.name = talloc_typed_asprintf(g, "map %s", cf_section_name2(g->cs));
-       g->self.debug_name = g->self.name;
+       switch (map->lhs->type) {
+       case TMPL_TYPE_ATTR:
+               da = tmpl_attr_tail_da(map->lhs);
+               if (!da->flags.internal && parent && (parent->type != FR_TYPE_GROUP) &&
+                   (da->parent != parent)) {
+                       /* FIXME - Broken check, doesn't work for key attributes */
+                       cf_log_err(cp, "Invalid location for %s - it is not a child of %s",
+                                  da->name, parent->name);
+                       return 0;
+               }
+               break;
 
-       return 0;
-}
+       case TMPL_TYPE_XLAT_UNRESOLVED:
+       case TMPL_TYPE_XLAT:
+               break;
 
-static unlang_t *compile_map(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION            *cs = cf_item_to_section(ci);
-       int                     rcode;
+       default:
+               cf_log_err(map->ci, "Left side of map must be an attribute "
+                          "or an xlat (that expands to an attribute), not a %s",
+                          tmpl_type_to_str(map->lhs->type));
+               return -1;
+       }
 
-       unlang_group_t          *g;
-       unlang_map_t    *gext;
+       fr_assert(map->rhs);
 
-       unlang_t                *c;
-       CONF_SECTION            *modules;
-       char const              *tmpl_str;
+       switch (map->rhs->type) {
+       case TMPL_TYPE_DATA_UNRESOLVED:
+       case TMPL_TYPE_XLAT_UNRESOLVED:
+       case TMPL_TYPE_XLAT:
+       case TMPL_TYPE_DATA:
+       case TMPL_TYPE_ATTR:
+       case TMPL_TYPE_EXEC:
+               break;
 
-       tmpl_t                  *vpt = NULL;
+       default:
+               cf_log_err(map->ci, "Right side of map must be an attribute, literal, xlat or exec, got type %s",
+                          tmpl_type_to_str(map->rhs->type));
+               return -1;
+       }
 
-       map_proc_t              *proc;
-       map_proc_inst_t         *proc_inst;
+       return 0;
+}
 
-       char const              *name2 = cf_section_name2(cs);
+/** Compile one edit section.
+ */
+static unlang_t *compile_edit_section(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs)
+{
+       unlang_edit_t           *edit;
+       unlang_t                *c, *out = UNLANG_IGNORE;
+       map_t                   *map;
+       char const              *name;
+       fr_token_t              op;
+       ssize_t                 slen;
+       fr_dict_attr_t const    *parent_da;
+       int                     num;
 
        tmpl_rules_t            t_rules;
 
-       /*
-        *      The RHS is NOT resolved in the context of the LHS.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.disallow_rhs_resolve = true;
-       RULES_VERIFY(&t_rules);
-
-       modules = cf_section_find(cf_root(cs), "modules", NULL);
-       if (!modules) {
-               cf_log_err(cs, "'map' sections require a 'modules' section");
+       name = cf_section_name2(cs);
+       if (name) {
+               cf_log_err(cs, "Unexpected name2 '%s' for editing list %s ", name, cf_section_name1(cs));
                return NULL;
        }
 
-       proc = map_proc_find(name2);
-       if (!proc) {
-               cf_log_err(cs, "Failed to find map processor '%s'", name2);
+       op = cf_section_name2_quote(cs);
+       if ((op == T_INVALID) || !fr_list_assignment_op[op]) {
+               cf_log_err(cs, "Invalid operator '%s' for editing list %s.", fr_tokens[op], cf_section_name1(cs));
                return NULL;
        }
-       t_rules.literals_safe_for = map_proc_literals_safe_for(proc);
-
-       g = group_allocate(parent, cs, UNLANG_TYPE_MAP);
-       if (!g) return NULL;
 
-       gext = unlang_group_to_map(g);
+       if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) {
+               cf_log_err(cs, "Invalid operator \"%s\".",
+                          fr_table_str_by_value(fr_tokens_table, op, "<INVALID>"));
+               return NULL;
+       }
 
        /*
-        *      If there's a third string, it's the map src.
-        *
-        *      Convert it into a template.
+        *      We allow unknown attributes here.
         */
-       tmpl_str = cf_section_argv(cs, 0); /* AFTER name1, name2 */
-       if (tmpl_str) {
-               fr_token_t type;
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.allow_unknown = true;
+       RULES_VERIFY(&t_rules);
 
-               type = cf_section_argv_quote(cs, 0);
+       edit = talloc_zero(parent, unlang_edit_t);
+       if (!edit) return NULL;
 
-               /*
-                *      Try to parse the template.
-                */
-               (void) tmpl_afrom_substr(gext, &vpt,
-                                        &FR_SBUFF_IN(tmpl_str, talloc_array_length(tmpl_str) - 1),
-                                        type,
-                                        NULL,
-                                        &t_rules);
-               if (!vpt) {
-                       cf_log_perr(cs, "Failed parsing map");
-               error:
-                       talloc_free(g);
-                       return NULL;
-               }
+       c = out = unlang_edit_to_generic(edit);
+       c->parent = parent;
+       c->next = NULL;
+       c->name = cf_section_name1(cs);
+       c->debug_name = c->name;
+       c->type = UNLANG_TYPE_EDIT;
+       c->ci = CF_TO_ITEM(cs);
 
-               /*
-                *      Limit the allowed template types.
-                */
-               switch (vpt->type) {
-               case TMPL_TYPE_DATA_UNRESOLVED:
-               case TMPL_TYPE_ATTR:
-               case TMPL_TYPE_ATTR_UNRESOLVED:
-               case TMPL_TYPE_XLAT:
-               case TMPL_TYPE_XLAT_UNRESOLVED:
-               case TMPL_TYPE_EXEC:
-               case TMPL_TYPE_EXEC_UNRESOLVED:
-               case TMPL_TYPE_DATA:
-                       break;
+       map_list_init(&edit->maps);
 
-               default:
-                       talloc_free(vpt);
-                       cf_log_err(cs, "Invalid third argument for map");
-                       return NULL;
-               }
-       }
+       unlang_compile_action_defaults(c, unlang_ctx);
 
        /*
-        *      This looks at cs->name2 to determine which list to update
+        *      Allocate the map and initialize it.
         */
-       map_list_init(&gext->map);
-       rcode = map_afrom_cs(gext, &gext->map, cs, unlang_ctx->rules, &t_rules, unlang_fixup_map, NULL, 256);
-       if (rcode < 0) return NULL; /* message already printed */
-       if (map_list_empty(&gext->map)) {
-               cf_log_err(cs, "'map' sections cannot be empty");
-               goto error;
-       }
+       MEM(map = talloc_zero(parent, map_t));
+       map->op = op;
+       map->ci = cf_section_to_item(cs);
+       map_list_init(&map->child);
+
+       name = cf_section_name1(cs);
 
+       slen = tmpl_afrom_attr_str(map, NULL, &map->lhs, name, &t_rules);
+       if (slen <= 0) {
+               cf_log_err(cs, "Failed parsing list reference %s - %s", name, fr_strerror());
+       fail:
+               talloc_free(edit);
+               return NULL;
+       }
 
        /*
-        *      Call the map's instantiation function to validate
-        *      the map and perform any caching required.
+        *      Can't assign to [*] or [#]
         */
-       proc_inst = map_proc_instantiate(gext, proc, cs, vpt, &gext->map);
-       if (!proc_inst) {
-               cf_log_err(cs, "Failed instantiating map function '%s'", name2);
-               goto error;
+       num = tmpl_attr_tail_num(map->lhs);
+       if ((num == NUM_ALL) || (num == NUM_COUNT)) {
+               cf_log_err(cs, "Invalid array reference in %s", name);
+               goto fail;
        }
-       c = unlang_group_to_generic(g);
-
-       gext->vpt = vpt;
-       gext->proc_inst = proc_inst;
-
-       compile_map_name(g);
 
        /*
-        *      Cache the module in the unlang_group_t struct.
-        *
-        *      Ensure that the module has a "map" entry in its module
-        *      header?  Or ensure that the map is registered in the
-        *      "bootstrap" phase, so that it's always available here.
+        *      If the DA isn't structural, then it can't have children.
         */
-       if (!pass2_fixup_map_rhs(g, unlang_ctx->rules)) goto error;
-
-       compile_action_defaults(c, unlang_ctx);
-
-       return c;
-}
-
-static int edit_section_alloc(CONF_SECTION *parent, CONF_SECTION **child, char const *name1, fr_token_t op)
-{
-       CONF_SECTION *cs;
-
-       cs = cf_section_alloc(parent, parent, name1, NULL);
-       if (!cs) return -1;
+       parent_da = tmpl_attr_tail_da(map->lhs);
+       if (fr_type_is_structural(parent_da->type)) {
+               map_t *child;
 
-       cf_section_add_name2_quote(cs, op);
-
-       if (child) *child = cs;
-
-       return 0;
-}
-
-static int edit_pair_alloc(CONF_SECTION *cs, CONF_PAIR *original, char const *attr, fr_token_t op, char const *value, fr_token_t list_op)
-{
-       CONF_PAIR *cp;
-       fr_token_t rhs_quote;
-
-       if (original) {
-               rhs_quote = cf_pair_value_quote(original);
-       } else {
-               rhs_quote = T_BARE_WORD;
-       }
-
-       cp = cf_pair_alloc(cs, attr, value, op, T_BARE_WORD, rhs_quote);
-       if (!cp) return -1;
-
-       if (!original) return 0;
-
-       cf_filename_set(cp, cf_filename(original));
-       cf_lineno_set(cp, cf_lineno(original));
-
-       if (fr_debug_lvl >= 3) {
-               if (list_op == T_INVALID) {
-                       cf_log_err(original, "%s %s %s --> %s %s %s",
-                                  cf_pair_attr(original), fr_tokens[cf_pair_operator(original)], cf_pair_value(original),
-                                  attr, fr_tokens[op], value);
-               } else {
-                       if (*attr == '&') attr++;
-                       cf_log_err(original, "%s %s %s --> %s %s { %s %s %s }",
-                                  cf_pair_attr(original), fr_tokens[cf_pair_operator(original)], cf_pair_value(original),
-                                  cf_section_name1(cs), fr_tokens[list_op], attr, fr_tokens[op], value);
-               }
-       } else if (fr_debug_lvl >= 2) {
-               if (list_op == T_INVALID) {
-                       cf_log_err(original, "--> %s %s %s",
-                                  attr, fr_tokens[op], value);
-               } else {
-                       cf_log_err(original, "--> %s %s { %s %s %s }",
-                                  cf_section_name1(cs), fr_tokens[list_op], attr, fr_tokens[op], value);
+               /*
+                *      Don't update namespace for &reply += { ... }
+                *
+                *      Do update namespace for &reply.foo += { ... }
+                *
+                *      Don't update if the LHS is an internal group.
+                */
+               if ((tmpl_attr_num_elements(map->lhs) > 1) && (t_rules.attr.list_def != parent_da) &&
+                   !((parent_da->type == FR_TYPE_GROUP) && parent_da->flags.internal)) {
+                       t_rules.attr.namespace = parent_da;
                }
-       }
-
-       return 0;
-}
-
-/*
- *     Convert "update" to "edit" using evil spells and sorcery.
- */
-static unlang_t *compile_update_to_edit(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs)
-{
-       char const              *name2 = cf_section_name2(cs);
-       CONF_ITEM               *ci;
-       CONF_SECTION            *group;
-       unlang_group_t          *g;
-       char                    list_buffer[32];
-       char                    value_buffer[256];
-       char                    attr_buffer[256];
-       char const              *list;
-
-       g = unlang_generic_to_group(parent);
-
-       /*
-        *      Wrap it all in a group, no matter what.  Because of
-        *      limitations in the cf_pair_alloc() API.
-        */
-       group = cf_section_alloc(g->cs, g->cs, "group", NULL);
-       if (!group) return NULL;
-
-       (void) cf_item_remove(g->cs, group); /* was added at the end */
-       cf_item_insert_after(g->cs, cs, group);
-
-       /*
-        *      Hoist this out of the loop, and make sure it never has a '&' prefix.
-        */
-       if (name2) {
-               if (*name2 == '&') name2++;
-               snprintf(list_buffer, sizeof(list_buffer), "%s", name2);
-       } else {
-               snprintf(list_buffer, sizeof(list_buffer), "%s", tmpl_list_name(unlang_ctx->rules->attr.list_def, "<INVALID>"));
-
-       }
-
-       /*
-        *      Loop over the entries, rewriting them.
-        */
-       for (ci = cf_item_next(cs, NULL);
-            ci != NULL;
-            ci = cf_item_next(cs, ci)) {
-               CONF_PAIR       *cp;
-               CONF_SECTION    *child;
-               int             rcode;
-               fr_token_t      op;
-               char const      *attr, *value, *end;
 
-               if (cf_item_is_section(ci)) {
-                       cf_log_err(ci, "Cannot specify subsections for 'update'");
-                       return NULL;
+               if (map_afrom_cs_edit(map, &map->child, cs, &t_rules, &t_rules, unlang_fixup_edit, map, 256) < 0) {
+                       goto fail;
                }
 
-               if (!cf_item_is_pair(ci)) continue;
-
-               cp = cf_item_to_pair(ci);
-
-               attr = cf_pair_attr(cp);
-               value = cf_pair_value(cp);
-               op = cf_pair_operator(cp);
-
-               fr_assert(attr);
-               fr_assert(value);
-
-               list = list_buffer;
-
-               if (*attr == '&') attr++;
-
-               end = strchr(attr, '.');
-               if (!end) end = attr + strlen(attr);
-
                /*
-                *      Separate out the various possibilities for the "name", which could be a list, an
-                *      attribute name, or a list followed by an attribute name.
-                *
-                *      Note that even if we have "update request { ....}", the v3 parser allowed the contents
-                *      of the "update" section to still specify parent / lists.  Which makes parsing it all
-                *      annoying.
-                *
-                *      The good news is that all we care about is whether or not there's a parent / list ref.
-                *      We don't care what that ref is.
+                *      As a set of fixups... we can't do array references in -=
                 */
-               {
-                       fr_dict_attr_t const *tmpl_list;
+               if (map->op == T_OP_SUB_EQ) {
+                       for (child = map_list_head(&map->child); child != NULL; child = map_list_next(&map->child, child)) {
+                               if (!tmpl_is_attr(child->lhs)) continue;
 
-                       /*
-                        *      Allow for a "parent" or "outer" reference.  There may be multiple
-                        *      "parent.parent", so we keep processing them until we get a list reference.
-                        */
-                       if (fr_table_value_by_substr(tmpl_request_ref_table, attr, end - attr, REQUEST_UNKNOWN) != REQUEST_UNKNOWN) {
+                               if (tmpl_attr_tail_num(child->lhs) != NUM_UNSPEC) {
+                                       cf_log_err(child->ci, "Cannot use array references and values when deleting from a list");
+                                       goto fail;
+                               }
 
                                /*
-                                *      Catch one more case where the behavior is different.
-                                *
-                                *      &request += &config[*]
+                                *      The edit code doesn't do this correctly, so we just forbid it.
                                 */
-                               if ((cf_pair_value_quote(cp) == T_BARE_WORD) && (*value == '&') &&
-                                   (strchr(value, '.') == NULL) && (strchr(value, '[') != NULL)) {
-                                       char const *p = strchr(value, '[');
-
-                                       cf_log_err(cp, "Cannot do array assignments for lists.  Just use '%s %s %.*s'",
-                                                  list, fr_tokens[op], (int) (p - value), value);
-                                       return NULL;
-                               }
-
-                               goto attr_is_list;
-
-                       /*
-                        *      Doesn't have a parent ref, maybe it's a list ref?
-                        */
-                       } else if (tmpl_attr_list_from_substr(&tmpl_list, &FR_SBUFF_IN(attr, (end - attr))) > 0) {
-                               char *p;
-
-                       attr_is_list:
-                               snprintf(attr_buffer, sizeof(attr_buffer), "%s", attr);
-                               list = attr_buffer;
-                               attr = NULL;
-
-                               p = strchr(attr_buffer, '.');
-                               if (p) {
-                                       *(p++) = '\0';
-                                       attr = p;
+                               if ((tmpl_attr_num_elements(child->lhs) - tmpl_attr_num_elements(map->lhs)) > 1) {
+                                       cf_log_err(child->ci, "List deletion must operate directly on the final child");
+                                       goto fail;
                                }
-                       }
-               }
 
-               switch (op) {
-                       /*
-                        *      FOO !* ANY
-                        *
-                        *      The RHS doesn't matter, so we ignore it.
-                        */
-               case T_OP_CMP_FALSE:
-                       if (!attr) {
                                /*
-                                *      Set list to empty value.
+                                *      We don't do list comparisons either.
                                 */
-                               rcode = edit_section_alloc(group, NULL, list, T_OP_SET);
-
-                       } else {
-                               if (strchr(attr, '[') == NULL) {
-                                       snprintf(value_buffer, sizeof(value_buffer), "%s[*]", attr);
-                               } else {
-                                       snprintf(value_buffer, sizeof(value_buffer), "%s", attr);
-                               }
-
-                               rcode = edit_pair_alloc(group, cp, list, T_OP_SUB_EQ, value_buffer, T_INVALID);
-                       }
-                       break;
-
-               case T_OP_SET:
-                       /*
-                        *      Must be a list-to-list operation
-                        */
-                       if (!attr) {
-                       list_op:
-                               rcode = edit_pair_alloc(group, cp, list, op, value, T_INVALID);
-                               break;
-                       }
-                       goto pair_op;
-
-               case T_OP_EQ:
-                       /*
-                        *      Allow &list = "foo"
-                        */
-                       if (!attr) {
-                               if (!value) {
-                                       cf_log_err(cp, "Missing value");
-                                       return NULL;
+                               if (fr_type_is_structural(tmpl_attr_tail_da(child->lhs)->type)) {
+                                       cf_log_err(child->ci, "List deletion cannot operate on lists");
+                                       goto fail;
                                }
-
-                               rcode = edit_pair_alloc(group, cp, list, op, value, T_INVALID);
-                               break;
-                       }
-
-               pair_op:
-                       fr_assert(*attr != '&');
-                       if (snprintf(value_buffer, sizeof(value_buffer), "%s.%s", list, attr) < 0) {
-                               cf_log_err(cp, "RHS of update too long to convert to edit automatically");
-                               return NULL;
-                       }
-
-                       rcode = edit_pair_alloc(group, cp, value_buffer, op, value, T_INVALID);
-                       break;
-
-               case T_OP_ADD_EQ:
-               case T_OP_PREPEND:
-                       if (!attr) goto list_op;
-
-                       rcode = edit_section_alloc(group, &child, list, op);
-                       if (rcode < 0) break;
-
-                       rcode = edit_pair_alloc(child, cp, attr, T_OP_EQ, value, op);
-                       break;
-
-                       /*
-                        *      Remove matching attributes
-                        */
-               case T_OP_SUB_EQ:
-                       op = T_OP_CMP_EQ;
-
-               filter:
-                       if (!attr) {
-                               cf_log_err(cp, "Invalid operator for list assignment");
-                               return NULL;
-                       }
-
-                       rcode = edit_section_alloc(group, &child, list, T_OP_SUB_EQ);
-                       if (rcode < 0) break;
-
-                       if (strchr(attr, '[') != 0) {
-                               cf_log_err(cp, "Cannot do filtering with array indexes");
-                               return NULL;
                        }
-
-                       rcode = edit_pair_alloc(child, cp, attr, op, value, T_OP_SUB_EQ);
-                       break;
-
-                       /*
-                        *      Keep matching attributes, i.e. remove non-matching ones.
-                        */
-               case T_OP_CMP_EQ:
-                       op = T_OP_NE;
-                       goto filter;
-
-               case T_OP_NE:
-                       op = T_OP_CMP_EQ;
-                       goto filter;
-
-               case T_OP_LT:
-                       op = T_OP_GE;
-                       goto filter;
-
-               case T_OP_LE:
-                       op = T_OP_GT;
-                       goto filter;
-
-               case T_OP_GT:
-                       op = T_OP_LE;
-                       goto filter;
-
-               case T_OP_GE:
-                       op = T_OP_LT;
-                       goto filter;
-
-               default:
-                       cf_log_err(cp, "Unsupported operator - cannot auto-convert to edit section");
-                       return NULL;
+               }
+       } else {
+               /*
+                *      &foo := { a, b, c }
+                */
+               if (map_list_afrom_cs(map, &map->child, cs, &t_rules, NULL, NULL, 256) < 0) {
+                       goto fail;
                }
 
-               if (rcode < 0) {
-                       cf_log_err(cp, "Failed converting entry");
-                       return NULL;
+               if ((map->op != T_OP_SET) && !map_list_num_elements(&map->child)) {
+                       cf_log_err(cs, "Cannot use operator '%s' for assigning empty list to '%s' data type.",
+                                  fr_tokens[map->op], fr_type_to_str(parent_da->type));
+                       goto fail;
                }
        }
+       /*
+        *      Do basic sanity checks and resolving.
+        */
+       if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail;
 
-       return UNLANG_IGNORE;
-}
+       /*
+        *      Check operators, and ensure that the RHS has been
+        *      resolved.
+        */
+//     if (unlang_fixup_update(map, NULL) < 0) goto fail;
 
-static unlang_t *compile_update(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION            *cs = cf_item_to_section(ci);
-       int                     rcode;
+       map_list_insert_tail(&edit->maps, map);
 
-       unlang_group_t          *g;
-       unlang_map_t    *gext;
+       return out;
+}
 
-       unlang_t                *c;
-       char const              *name2 = cf_section_name2(cs);
+/** Compile one edit pair
+ *
+ */
+static unlang_t *compile_edit_pair(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_PAIR *cp)
+{
+       unlang_edit_t           *edit;
+       unlang_t                *c = NULL, *out = UNLANG_IGNORE;
+       map_t                   *map;
+       int                     num;
 
        tmpl_rules_t            t_rules;
-
-       if (main_config_migrate_option_get("forbid_update")) {
-               cf_log_err(cs, "The use of 'update' sections is forbidden by the server configuration");
-               return NULL;
-       }
-
-       /*
-        *      If we're migrating "update" sections to edit, then go
-        *      do that now.
-        */
-       if (main_config_migrate_option_get("rewrite_update")) {
-               return compile_update_to_edit(parent, unlang_ctx, cs);
-       }
+       fr_token_t              op;
 
        /*
         *      We allow unknown attributes here.
         */
        t_rules = *(unlang_ctx->rules);
        t_rules.attr.allow_unknown = true;
-       t_rules.attr.allow_wildcard = true;
        RULES_VERIFY(&t_rules);
 
-       g = group_allocate(parent, cs, UNLANG_TYPE_UPDATE);
-       if (!g) return NULL;
+       edit = talloc_zero(parent, unlang_edit_t);
+       if (!edit) return NULL;
+
+       c = out = unlang_edit_to_generic(edit);
+       c->parent = parent;
+       c->next = NULL;
+       c->name = cf_pair_attr(cp);
+       c->debug_name = c->name;
+       c->type = UNLANG_TYPE_EDIT;
+       c->ci = CF_TO_ITEM(cp);
+
+       map_list_init(&edit->maps);
+
+       unlang_compile_action_defaults(c, unlang_ctx);
 
-       gext = unlang_group_to_map(g);
+       op = cf_pair_operator(cp);
+       if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) {
+               cf_log_err(cp, "Invalid operator \"%s\".",
+                          fr_table_str_by_value(fr_tokens_table, op, "<INVALID>"));
+               return NULL;
+       }
 
        /*
-        *      This looks at cs->name2 to determine which list to update
+        *      Convert this particular map.
         */
-       map_list_init(&gext->map);
-       rcode = map_afrom_cs(gext, &gext->map, cs, &t_rules, &t_rules, unlang_fixup_update, NULL, 128);
-       if (rcode < 0) return NULL; /* message already printed */
-       if (map_list_empty(&gext->map)) {
-               cf_log_err(cs, "'update' sections cannot be empty");
-       error:
-               talloc_free(g);
+       if (map_afrom_cp(edit, &map, map_list_tail(&edit->maps), cp, &t_rules, NULL, true) < 0) {
+       fail:
+               talloc_free(edit);
                return NULL;
        }
 
-       c = unlang_group_to_generic(g);
-       if (name2) {
-               c->name = name2;
-               c->debug_name = talloc_typed_asprintf(c, "update %s", name2);
-       } else {
-               c->name = "update";
-               c->debug_name = c->name;
+       /*
+        *      @todo - we still want to do fixups on the RHS?
+        */
+       if (tmpl_is_attr(map->lhs)) {
+               /*
+                *      Can't assign to [*] or [#]
+                */
+               num = tmpl_attr_tail_num(map->lhs);
+               if ((num == NUM_ALL) || (num == NUM_COUNT)) {
+                       cf_log_err(cp, "Invalid array reference in %s", map->lhs->name);
+                       goto fail;
+               }
+
+               if ((map->op == T_OP_SUB_EQ) && fr_type_is_structural(tmpl_attr_tail_da(map->lhs)->type) &&
+                   tmpl_is_attr(map->rhs) && tmpl_attr_tail_da(map->rhs)->flags.local) {
+                       cf_log_err(cp, "Cannot delete local variable %s", map->rhs->name);
+                       goto fail;
+               }
        }
 
-       if (!pass2_fixup_update(g, unlang_ctx->rules)) goto error;
+       /*
+        *      Do basic sanity checks and resolving.
+        */
+       if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail;
+
+       /*
+        *      Check operators, and ensure that the RHS has been
+        *      resolved.
+        */
+       if (unlang_fixup_update(map, c) < 0) goto fail;
 
-       compile_action_defaults(c, unlang_ctx);
+       map_list_insert_tail(&edit->maps, map);
 
-       return c;
+       return out;
 }
 
-#define T(_x) [T_OP_ ## _x] = true
+#define debug_braces(_type)    (unlang_ops[_type].flag & UNLANG_OP_FLAG_DEBUG_BRACES)
 
-static const bool edit_list_sub_op[T_TOKEN_LAST] = {
-       T(NE),
-       T(GE),
-       T(GT),
-       T(LE),
-       T(LT),
-       T(CMP_EQ),
-};
-
-/** Validate and fixup a map that's part of an edit section.
- *
- * @param map to validate.
- * @param ctx data to pass to fixup function (currently unused).
- * @return 0 if valid else -1.
- *
- *  @todo - this is only called for CONF_PAIR maps, not for
- *  CONF_SECTION.  So when we parse nested maps, there's no validation
- *  done of the CONF_SECTION.  In order to fix this, we need to have
- *  map_afrom_cs() call the validation function for the CONF_SECTION
- *  *before* recursing.
- */
-static int unlang_fixup_edit(map_t *map, void *ctx)
-{
-       CONF_PAIR *cp = cf_item_to_pair(map->ci);
-       fr_dict_attr_t const *da;
-       fr_dict_attr_t const *parent = NULL;
-       map_t           *parent_map = ctx;
-
-       fr_assert(parent_map);
-#ifdef STATIC_ANALYZER
-       if (!parent_map) return -1;
-#endif
-
-       fr_assert(tmpl_is_attr(parent_map->lhs));
-
-       if (parent_map && (parent_map->op == T_OP_SUB_EQ)) {
-               if (!edit_list_sub_op[map->op]) {
-                       cf_log_err(cp, "Invalid operator '%s' for right-hand side list.  It must be a comparison operator", fr_tokens[map->op]);
-                       return -1;
-               }
-
-       } else if (map->op != T_OP_EQ) {
-               cf_log_err(cp, "Invalid operator '%s' for right-hand side list.  It must be '='", fr_tokens[map->op]);
-               return -1;
-       }
-
-       /*
-        *      map_afrom_cs() will build its tree recursively, and call us for each child map.
-        */
-       if (map->parent && (map->parent != parent_map)) parent_map = map->parent;
-
-       parent = tmpl_attr_tail_da(parent_map->lhs);
-
-       switch (map->lhs->type) {
-       case TMPL_TYPE_ATTR:
-               da = tmpl_attr_tail_da(map->lhs);
-               if (!da->flags.internal && parent && (parent->type != FR_TYPE_GROUP) &&
-                   (da->parent != parent)) {
-                       /* FIXME - Broken check, doesn't work for key attributes */
-                       cf_log_err(cp, "Invalid location for %s - it is not a child of %s",
-                                  da->name, parent->name);
-                       return 0;
-               }
-               break;
-
-       case TMPL_TYPE_XLAT_UNRESOLVED:
-       case TMPL_TYPE_XLAT:
-               break;
-
-       default:
-               cf_log_err(map->ci, "Left side of map must be an attribute "
-                          "or an xlat (that expands to an attribute), not a %s",
-                          tmpl_type_to_str(map->lhs->type));
-               return -1;
-       }
-
-       fr_assert(map->rhs);
-
-       switch (map->rhs->type) {
-       case TMPL_TYPE_DATA_UNRESOLVED:
-       case TMPL_TYPE_XLAT_UNRESOLVED:
-       case TMPL_TYPE_XLAT:
-       case TMPL_TYPE_DATA:
-       case TMPL_TYPE_ATTR:
-       case TMPL_TYPE_EXEC:
-               break;
-
-       default:
-               cf_log_err(map->ci, "Right side of map must be an attribute, literal, xlat or exec, got type %s",
-                          tmpl_type_to_str(map->rhs->type));
-               return -1;
-       }
-
-       return 0;
-}
-
-/** Compile one edit section.
- */
-static unlang_t *compile_edit_section(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs)
-{
-       unlang_edit_t           *edit;
-       unlang_t                *c, *out = UNLANG_IGNORE;
-       map_t                   *map;
-       char const              *name;
-       fr_token_t              op;
-       ssize_t                 slen;
-       fr_dict_attr_t const    *parent_da;
-       int                     num;
-
-       tmpl_rules_t            t_rules;
-
-       name = cf_section_name2(cs);
-       if (name) {
-               cf_log_err(cs, "Unexpected name2 '%s' for editing list %s ", name, cf_section_name1(cs));
-               return NULL;
-       }
-
-       op = cf_section_name2_quote(cs);
-       if ((op == T_INVALID) || !fr_list_assignment_op[op]) {
-               cf_log_err(cs, "Invalid operator '%s' for editing list %s.", fr_tokens[op], cf_section_name1(cs));
-               return NULL;
-       }
-
-       if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) {
-               cf_log_err(cs, "Invalid operator \"%s\".",
-                          fr_table_str_by_value(fr_tokens_table, op, "<INVALID>"));
-               return NULL;
-       }
-
-       /*
-        *      We allow unknown attributes here.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.allow_unknown = true;
-       RULES_VERIFY(&t_rules);
-
-       edit = talloc_zero(parent, unlang_edit_t);
-       if (!edit) return NULL;
-
-       c = out = unlang_edit_to_generic(edit);
-       c->parent = parent;
-       c->next = NULL;
-       c->name = cf_section_name1(cs);
-       c->debug_name = c->name;
-       c->type = UNLANG_TYPE_EDIT;
-       c->ci = CF_TO_ITEM(cs);
-
-       map_list_init(&edit->maps);
-
-       compile_action_defaults(c, unlang_ctx);
-
-       /*
-        *      Allocate the map and initialize it.
-        */
-       MEM(map = talloc_zero(parent, map_t));
-       map->op = op;
-       map->ci = cf_section_to_item(cs);
-       map_list_init(&map->child);
-
-       name = cf_section_name1(cs);
-
-       slen = tmpl_afrom_attr_str(map, NULL, &map->lhs, name, &t_rules);
-       if (slen <= 0) {
-               cf_log_err(cs, "Failed parsing list reference %s - %s", name, fr_strerror());
-       fail:
-               talloc_free(edit);
-               return NULL;
-       }
-
-       /*
-        *      Can't assign to [*] or [#]
-        */
-       num = tmpl_attr_tail_num(map->lhs);
-       if ((num == NUM_ALL) || (num == NUM_COUNT)) {
-               cf_log_err(cs, "Invalid array reference in %s", name);
-               goto fail;
-       }
-
-       /*
-        *      If the DA isn't structural, then it can't have children.
-        */
-       parent_da = tmpl_attr_tail_da(map->lhs);
-       if (fr_type_is_structural(parent_da->type)) {
-               map_t *child;
-
-               /*
-                *      Don't update namespace for &reply += { ... }
-                *
-                *      Do update namespace for &reply.foo += { ... }
-                *
-                *      Don't update if the LHS is an internal group.
-                */
-               if ((tmpl_attr_num_elements(map->lhs) > 1) && (t_rules.attr.list_def != parent_da) &&
-                   !((parent_da->type == FR_TYPE_GROUP) && parent_da->flags.internal)) {
-                       t_rules.attr.namespace = parent_da;
-               }
-
-               if (map_afrom_cs_edit(map, &map->child, cs, &t_rules, &t_rules, unlang_fixup_edit, map, 256) < 0) {
-                       goto fail;
-               }
-
-               /*
-                *      As a set of fixups... we can't do array references in -=
-                */
-               if (map->op == T_OP_SUB_EQ) {
-                       for (child = map_list_head(&map->child); child != NULL; child = map_list_next(&map->child, child)) {
-                               if (!tmpl_is_attr(child->lhs)) continue;
-
-                               if (tmpl_attr_tail_num(child->lhs) != NUM_UNSPEC) {
-                                       cf_log_err(child->ci, "Cannot use array references and values when deleting from a list");
-                                       goto fail;
-                               }
-
-                               /*
-                                *      The edit code doesn't do this correctly, so we just forbid it.
-                                */
-                               if ((tmpl_attr_num_elements(child->lhs) - tmpl_attr_num_elements(map->lhs)) > 1) {
-                                       cf_log_err(child->ci, "List deletion must operate directly on the final child");
-                                       goto fail;
-                               }
-
-                               /*
-                                *      We don't do list comparisons either.
-                                */
-                               if (fr_type_is_structural(tmpl_attr_tail_da(child->lhs)->type)) {
-                                       cf_log_err(child->ci, "List deletion cannot operate on lists");
-                                       goto fail;
-                               }
-                       }
-               }
-       } else {
-               /*
-                *      &foo := { a, b, c }
-                */
-               if (map_list_afrom_cs(map, &map->child, cs, &t_rules, NULL, NULL, 256) < 0) {
-                       goto fail;
-               }
-
-               if ((map->op != T_OP_SET) && !map_list_num_elements(&map->child)) {
-                       cf_log_err(cs, "Cannot use operator '%s' for assigning empty list to '%s' data type.",
-                                  fr_tokens[map->op], fr_type_to_str(parent_da->type));
-                       goto fail;
-               }
-       }
-       /*
-        *      Do basic sanity checks and resolving.
-        */
-       if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail;
-
-       /*
-        *      Check operators, and ensure that the RHS has been
-        *      resolved.
-        */
-//     if (unlang_fixup_update(map, NULL) < 0) goto fail;
-
-       map_list_insert_tail(&edit->maps, map);
-
-       return out;
-}
-
-/** Compile one edit pair
- *
- */
-static unlang_t *compile_edit_pair(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_PAIR *cp)
-{
-       unlang_edit_t           *edit;
-       unlang_t                *c = NULL, *out = UNLANG_IGNORE;
-       map_t                   *map;
-       int                     num;
-
-       tmpl_rules_t            t_rules;
-       fr_token_t              op;
-
-       /*
-        *      We allow unknown attributes here.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.allow_unknown = true;
-       RULES_VERIFY(&t_rules);
-
-       edit = talloc_zero(parent, unlang_edit_t);
-       if (!edit) return NULL;
-
-       c = out = unlang_edit_to_generic(edit);
-       c->parent = parent;
-       c->next = NULL;
-       c->name = cf_pair_attr(cp);
-       c->debug_name = c->name;
-       c->type = UNLANG_TYPE_EDIT;
-       c->ci = CF_TO_ITEM(cp);
-
-       map_list_init(&edit->maps);
-
-       compile_action_defaults(c, unlang_ctx);
-
-       op = cf_pair_operator(cp);
-       if ((op == T_OP_CMP_TRUE) || (op == T_OP_CMP_FALSE)) {
-               cf_log_err(cp, "Invalid operator \"%s\".",
-                          fr_table_str_by_value(fr_tokens_table, op, "<INVALID>"));
-               return NULL;
-       }
-
-       /*
-        *      Convert this particular map.
-        */
-       if (map_afrom_cp(edit, &map, map_list_tail(&edit->maps), cp, &t_rules, NULL, true) < 0) {
-       fail:
-               talloc_free(edit);
-               return NULL;
-       }
-
-       /*
-        *      @todo - we still want to do fixups on the RHS?
-        */
-       if (tmpl_is_attr(map->lhs)) {
-               /*
-                *      Can't assign to [*] or [#]
-                */
-               num = tmpl_attr_tail_num(map->lhs);
-               if ((num == NUM_ALL) || (num == NUM_COUNT)) {
-                       cf_log_err(cp, "Invalid array reference in %s", map->lhs->name);
-                       goto fail;
-               }
-
-               if ((map->op == T_OP_SUB_EQ) && fr_type_is_structural(tmpl_attr_tail_da(map->lhs)->type) &&
-                   tmpl_is_attr(map->rhs) && tmpl_attr_tail_da(map->rhs)->flags.local) {
-                       cf_log_err(cp, "Cannot delete local variable %s", map->rhs->name);
-                       goto fail;
-               }
-       }
-
-       /*
-        *      Do basic sanity checks and resolving.
-        */
-       if (!pass2_fixup_map(map, unlang_ctx->rules, NULL)) goto fail;
-
-       /*
-        *      Check operators, and ensure that the RHS has been
-        *      resolved.
-        */
-       if (unlang_fixup_update(map, c) < 0) goto fail;
-
-       map_list_insert_tail(&edit->maps, map);
-
-       return out;
-}
-
-static int define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rules_t *t_rules, fr_type_t type, char const *name,
-                                fr_dict_attr_t const *ref);
-
-#define debug_braces(_type)    (unlang_ops[_type].flag & UNLANG_OP_FLAG_DEBUG_BRACES)
-
-/** Compile a variable definition.
- *
- *  Definitions which are adjacent to one another are automatically merged
- *  into one larger variable definition.
- */
-static int compile_variable(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_PAIR *cp, tmpl_rules_t *t_rules)
-{
-       unlang_variable_t *var;
-       fr_type_t       type;
-       char const      *attr, *value;
-       unlang_group_t  *group;
-
-       fr_assert(debug_braces(parent->type));
-
-       /*
-        *      Enforce locations for local variables.
-        */
-       switch (parent->type) {
-       case UNLANG_TYPE_CASE:
-       case UNLANG_TYPE_ELSE:
-       case UNLANG_TYPE_ELSIF:
-       case UNLANG_TYPE_FOREACH:
-       case UNLANG_TYPE_GROUP:
-       case UNLANG_TYPE_IF:
-       case UNLANG_TYPE_TIMEOUT:
-       case UNLANG_TYPE_LIMIT:
-       case UNLANG_TYPE_POLICY:
-       case UNLANG_TYPE_REDUNDANT:
-       case UNLANG_TYPE_SUBREQUEST:
-       case UNLANG_TYPE_LOAD_BALANCE:
-       case UNLANG_TYPE_REDUNDANT_LOAD_BALANCE:
-               break;
-
-       default:
-               cf_log_err(cp, "Local variables cannot be used here");
-               return -1;
-       }
-
-       /*
-        *      The variables exist in the parent block.
-        */
-       group = unlang_generic_to_group(parent);
-       if (group->variables) {
-               var = group->variables;
-
-       } else {
-               group->variables = var = talloc_zero(parent, unlang_variable_t);
-               if (!var) return -1;
-
-               var->dict = fr_dict_protocol_alloc(unlang_ctx->rules->attr.dict_def);
-               if (!var->dict) {
-                       talloc_free(var);
-                       return -1;
-               }
-               var->root = fr_dict_root(var->dict);
-
-               var->max_attr = 1;
-
-               /*
-                *      Initialize the new rules, and point them to the parent rules.
-                *
-                *      Then replace the parse rules with our rules, and our dictionary.
-                */
-               *t_rules = *unlang_ctx->rules;
-               t_rules->parent = unlang_ctx->rules;
-
-               t_rules->attr.dict_def = var->dict;
-               t_rules->attr.namespace = NULL;
-
-               unlang_ctx->rules = t_rules;
-       }
-
-       attr = cf_pair_attr(cp);        /* data type */
-       value = cf_pair_value(cp);      /* variable name */
-
-       type = fr_table_value_by_str(fr_type_table, attr, FR_TYPE_NULL);
-       if (type == FR_TYPE_NULL) {
-invalid_type:
-               cf_log_err(cp, "Invalid data type '%s'", attr);
-               return -1;
-       }
-
-       /*
-        *      Leaf and group are OK.  TLV, Vendor, Struct, VSA, etc. are not.
-        */
-       if (!(fr_type_is_leaf(type) || (type == FR_TYPE_GROUP))) goto invalid_type;
-
-       return define_local_variable(cf_pair_to_item(cp), var, t_rules, type, value, NULL);
-}
-
-/*
- *     Compile action && rcode for later use.
- */
-static int compile_action_pair(unlang_mod_actions_t *actions, CONF_PAIR *cp)
-{
-       int action;
-       char const *attr, *value;
-
-       attr = cf_pair_attr(cp);
-       value = cf_pair_value(cp);
-       if (!value) return 0;
-
-       if (!strcasecmp(value, "return"))
-               action = MOD_ACTION_RETURN;
-
-       else if (!strcasecmp(value, "break"))
-               action = MOD_ACTION_RETURN;
-
-       else if (!strcasecmp(value, "reject"))
-               action = MOD_ACTION_REJECT;
-
-       else if (!strcasecmp(value, "retry"))
-               action = MOD_ACTION_RETRY;
-
-       else if (strspn(value, "0123456789")==strlen(value)) {
-               action = atoi(value);
-
-               if (!action || (action > MOD_PRIORITY_MAX)) {
-                       cf_log_err(cp, "Priorities MUST be between 1 and 64.");
-                       return 0;
-               }
-
-       } else {
-               cf_log_err(cp, "Unknown action '%s'.\n",
-                          value);
-               return 0;
-       }
-
-       if (strcasecmp(attr, "default") != 0) {
-               int rcode;
-
-               rcode = fr_table_value_by_str(mod_rcode_table, attr, -1);
-               if (rcode < 0) {
-                       cf_log_err(cp,
-                                  "Unknown module rcode '%s'.",
-                                  attr);
-                       return 0;
-               }
-               actions->actions[rcode] = action;
-
-       } else {                /* set all unset values to the default */
-               int i;
-
-               for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
-                       if (!actions->actions[i]) actions->actions[i] = action;
-               }
-       }
-
-       return 1;
-}
-
-static bool compile_retry_section(unlang_mod_actions_t *actions, CONF_ITEM *ci)
-{
-       CONF_ITEM *csi;
-       CONF_SECTION *cs;
-
-       cs = cf_item_to_section(ci);
-       for (csi=cf_item_next(cs, NULL);
-            csi != NULL;
-            csi=cf_item_next(cs, csi)) {
-               CONF_PAIR *cp;
-               char const *name, *value;
-
-               if (cf_item_is_section(csi)) {
-                       cf_log_err(csi, "Invalid subsection in 'retry' configuration.");
-                       return false;
-               }
-
-               if (!cf_item_is_pair(csi)) continue;
-
-               cp = cf_item_to_pair(csi);
-               name = cf_pair_attr(cp);
-               value = cf_pair_value(cp);
-
-               if (!value) {
-                       cf_log_err(csi, "Retry configuration must specify a value");
-                       return false;
-               }
-
-#define CLAMP(_name, _field, _limit) do { \
-                       if (!fr_time_delta_ispos(actions->retry._field)) { \
-                               cf_log_err(csi, "Invalid value for '" STRINGIFY(_name) " = %s' - value must be positive", \
-                                          value); \
-                               return false; \
-                       } \
-                       if (fr_time_delta_cmp(actions->retry._field, fr_time_delta_from_sec(_limit)) > 0) { \
-                               cf_log_err(csi, "Invalid value for '" STRINGIFY(_name) " = %s' - value must be less than " STRINGIFY(_limit) "s", \
-                                          value); \
-                               return false; \
-                       } \
-       } while (0)
-
-               /*
-                *      We don't use conf_parser_t here for various
-                *      magical reasons.
-                */
-               if (strcmp(name, "initial_rtx_time") == 0) {
-                       if (fr_time_delta_from_str(&actions->retry.irt, value, strlen(value), FR_TIME_RES_SEC) < 0) {
-                       error:
-                               cf_log_err(csi, "Failed parsing '%s = %s' - %s",
-                                          name, value, fr_strerror());
-                               return false;
-                       }
-                       CLAMP(initial_rtx_time, irt, 2);
-
-               } else if (strcmp(name, "max_rtx_time") == 0) {
-                       if (fr_time_delta_from_str(&actions->retry.mrt, value, strlen(value), FR_TIME_RES_SEC) < 0) goto error;
-
-                       CLAMP(max_rtx_time, mrt, 10);
-
-               } else if (strcmp(name, "max_rtx_count") == 0) {
-                       unsigned long v = strtoul(value, 0, 0);
-
-                       if (v > 10) {
-                               cf_log_err(csi, "Invalid value for 'max_rtx_count = %s' - value must be between 0 and 10",
-                                          value);
-                               return false;
-                       }
-
-                       actions->retry.mrc = v;
-
-               } else if (strcmp(name, "max_rtx_duration") == 0) {
-                       if (fr_time_delta_from_str(&actions->retry.mrd, value, strlen(value), FR_TIME_RES_SEC) < 0) goto error;
-
-                       CLAMP(max_rtx_duration, mrd, 20);
-
-               } else {
-                       cf_log_err(csi, "Invalid item '%s' in 'retry' configuration.", name);
-                       return false;
-               }
-       }
-
-       return true;
-}
-
-bool unlang_compile_actions(unlang_mod_actions_t *actions, CONF_SECTION *action_cs, bool module_retry)
-{
-       int i;
-       bool disallow_retry_action = false;
-       CONF_ITEM *csi;
-       CONF_SECTION *cs;
-
-       /*
-        *      Over-ride the default return codes of the module.
-        */
-       cs = cf_item_to_section(cf_section_to_item(action_cs));
-       for (csi=cf_item_next(cs, NULL);
-            csi != NULL;
-            csi=cf_item_next(cs, csi)) {
-               char const *name;
-               CONF_PAIR *cp;
-
-               if (cf_item_is_section(csi)) {
-                       CONF_SECTION *subcs = cf_item_to_section(csi);
-
-                       name = cf_section_name1(subcs);
-
-                       /*
-                        *      Look for a "retry" section.
-                        */
-                       if (name && (strcmp(name, "retry") == 0) && !cf_section_name2(subcs)) {
-                               if (!compile_retry_section(actions, csi)) return false;
-                               continue;
-                       }
-
-                       cf_log_err(csi, "Invalid subsection.  Expected 'action = value'");
-                       return false;
-               }
-
-               if (!cf_item_is_pair(csi)) continue;
-
-               cp = cf_item_to_pair(csi);
-
-               /*
-                *      Allow 'retry = path.to.retry.config'
-                */
-               name = cf_pair_attr(cp);
-               if (strcmp(name, "retry") == 0) {
-                       CONF_ITEM *subci;
-                       char const *value = cf_pair_value(cp);
-
-                       if (!value) {
-                               cf_log_err(csi, "Missing reference string");
-                               return false;
-                       }
-
-                       subci = cf_reference_item(cs, cf_root(cf_section_to_item(action_cs)), value);
-                       if (!subci) {
-                               cf_log_perr(csi, "Failed finding reference '%s'", value);
-                               return false;
-                       }
-
-                       if (!compile_retry_section(actions, subci)) return false;
-                       continue;
-               }
-
-               if (!compile_action_pair(actions, cp)) {
-                       return false;
-               }
-       }
-
-       if (module_retry) {
-               if (!fr_time_delta_ispos(actions->retry.irt)) {
-                       cf_log_err(csi, "initial_rtx_time MUST be non-zero for modules which support retries.");
-                       return false;
-               }
-       } else {
-               if (fr_time_delta_ispos(actions->retry.irt)) {
-                       cf_log_err(csi, "initial_rtx_time MUST be zero, as only max_rtx_count and max_rtx_duration are used.");
-                       return false;
-               }
-
-               if (!actions->retry.mrc && !fr_time_delta_ispos(actions->retry.mrd)) {
-                       disallow_retry_action = true;
-               }
-       }
-
-       /*
-        *      Sanity check that "fail = retry", we actually have a
-        *      retry section.
-        */
-       for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
-               if (actions->actions[i] != MOD_ACTION_RETRY) continue;
-
-               if (module_retry) {
-                       cf_log_err(csi, "Cannot use a '%s = retry' action for a module which has its own retries",
-                                  fr_table_str_by_value(mod_rcode_table, i, "<INVALID>"));
-                       return false;
-               }
-
-               if (disallow_retry_action) {
-                       cf_log_err(csi, "max_rtx_count and max_rtx_duration cannot both be zero when using '%s = retry'",
-                                  fr_table_str_by_value(mod_rcode_table, i, "<INVALID>"));
-                       return false;
-               }
-
-               if (!fr_time_delta_ispos(actions->retry.irt) &&
-                   !actions->retry.mrc &&
-                   !fr_time_delta_ispos(actions->retry.mrd)) {
-                       cf_log_err(csi, "Cannot use a '%s = retry' action without a 'retry { ... }' section.",
-                                  fr_table_str_by_value(mod_rcode_table, i, "<INVALID>"));
-                       return false;
-               }
-       }
-
-       return true;
-}
-
-static unlang_t *compile_empty(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type)
-{
-       unlang_group_t *g;
-       unlang_t *c;
-
-       /*
-        *      If we're compiling an empty section, then the
-        *      *interpreter* type is GROUP, even if the *debug names*
-        *      are something else.
-        */
-       g = group_allocate(parent, cs, type);
-       if (!g) return NULL;
-
-       c = unlang_group_to_generic(g);
-       if (!cs) {
-               c->name = unlang_ops[type].name;
-               c->debug_name = c->name;
-
-       } else {
-               char const *name2;
-
-               name2 = cf_section_name2(cs);
-               if (!name2) {
-                       c->name = cf_section_name1(cs);
-                       c->debug_name = c->name;
-               } else {
-                       c->name = name2;
-                       c->debug_name = talloc_typed_asprintf(c, "%s %s", cf_section_name1(cs), name2);
-               }
-       }
-
-       compile_action_defaults(c, unlang_ctx);
-       return c;
-}
-
-
-static unlang_t *compile_item(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM *ci);
-
-/*
- *     compile 'actions { ... }' inside of another group.
- */
-static bool compile_action_subsection(unlang_t *c, CONF_SECTION *cs, CONF_SECTION *subcs)
-{
-       CONF_ITEM *ci, *next;
-
-       ci = cf_section_to_item(subcs);
-
-       next = cf_item_next(cs, ci);
-       if (next && (cf_item_is_pair(next) || cf_item_is_section(next))) {
-               cf_log_err(ci, "'actions' MUST be the last block in a section");
-               return false;
-       }
-
-       if (cf_section_name2(subcs) != NULL) {
-               cf_log_err(ci, "Invalid name for 'actions' section");
-               return false;
-       }
-
-       /*
-        *      Over-riding the actions can be done in certain limited
-        *      situations.  In other situations (e.g. "redundant",
-        *      "load-balance"), it doesn't make sense.
-        *
-        *      Note that this limitation also applies to "retry"
-        *      timers.  We can do retries of a "group".  We cannot do
-        *      retries of "load-balance", as the "load-balance"
-        *      section already takes care of redundancy.
-        *
-        *      We may need to loosen that limitation in the future.
-        */
-       switch (c->type) {
-       case UNLANG_TYPE_CASE:
-       case UNLANG_TYPE_IF:
-       case UNLANG_TYPE_ELSE:
-       case UNLANG_TYPE_ELSIF:
-       case UNLANG_TYPE_FOREACH:
-       case UNLANG_TYPE_GROUP:
-       case UNLANG_TYPE_LIMIT:
-       case UNLANG_TYPE_SWITCH:
-       case UNLANG_TYPE_TIMEOUT:
-       case UNLANG_TYPE_TRANSACTION:
-               break;
-
-       default:
-               cf_log_err(ci, "'actions' MUST NOT be in a '%s' block", unlang_ops[c->type].name);
-               return false;
-       }
-
-       return unlang_compile_actions(&c->actions, subcs, false);
-}
-
-
-static unlang_t *compile_children(unlang_group_t *g, unlang_compile_t *unlang_ctx_in, bool set_action_defaults)
-{
-       CONF_ITEM       *ci = NULL;
-       unlang_t        *c, *single;
-       bool            was_if = false;
-       char const      *skip_else = NULL;
-       unlang_compile_t *unlang_ctx;
-       unlang_compile_t unlang_ctx2;
-       tmpl_rules_t    t_rules;
-
-       c = unlang_group_to_generic(g);
-
-       /*
-        *      Create our own compilation context which can be edited
-        *      by a variable definition.
-        */
-       compile_copy_context(&unlang_ctx2, unlang_ctx_in);
-       unlang_ctx = &unlang_ctx2;
-       t_rules = *unlang_ctx_in->rules;
-
-       /*
-        *      Loop over the children of this group.
-        */
-       while ((ci = cf_item_next(g->cs, ci))) {
-               if (cf_item_is_data(ci)) continue;
-
-               /*
-                *      Sections are keywords, or references to
-                *      modules with updated return codes.
-                */
-               if (cf_item_is_section(ci)) {
-                       char const *name = NULL;
-                       CONF_SECTION *subcs = cf_item_to_section(ci);
-
-                       /*
-                        *      Skip precompiled blocks.  This is
-                        *      mainly for policies.
-                        */
-                       if (cf_data_find(subcs, unlang_group_t, NULL)) continue;
-
-                       /*
-                        *      "actions" apply to the current group.
-                        *      It's not a subgroup.
-                        */
-                       name = cf_section_name1(subcs);
-
-                       /*
-                        *      In-line attribute editing.  Nothing else in the parse has list assignments, so this must be it.
-                        */
-                       if (fr_list_assignment_op[cf_section_name2_quote(subcs)]) {
-                               single = compile_edit_section(c, unlang_ctx, subcs);
-                               if (!single) {
-                                       talloc_free(c);
-                                       return NULL;
-                               }
-
-                               goto add_child;
-                       }
-
-                       if (strcmp(name, "actions") == 0) {
-                               if (!compile_action_subsection(c, g->cs, subcs)) {
-                                       talloc_free(c);
-                                       return NULL;
-                               }
-
-                               continue;
-                       }
-
-                       /*
-                        *      Special checks for "else" and "elsif".
-                        */
-                       if ((strcmp(name, "else") == 0) || (strcmp(name, "elsif") == 0)) {
-                               /*
-                                *      We ran into one without a preceding "if" or "elsif".
-                                *      That's not allowed.
-                                */
-                               if (!was_if) {
-                                       cf_log_err(ci, "Invalid location for '%s'.  There is no preceding "
-                                                  "'if' or 'elsif' statement", name);
-                                       talloc_free(c);
-                                       return NULL;
-                               }
-
-                               /*
-                                *      There was a previous "if" or "elsif" which was always taken.
-                                *      So we skip this "elsif" or "else".
-                                */
-                               if (skip_else) {
-                                       void *ptr;
-
-                                       /*
-                                        *      And manually free this.
-                                        */
-                                       ptr = cf_data_remove(subcs, xlat_exp_head_t, NULL);
-                                       talloc_free(ptr);
-
-                                       cf_section_free_children(subcs);
-
-                                       cf_log_debug_prefix(ci, "Skipping contents of '%s' due to previous "
-                                                           "'%s' being always being taken.",
-                                                           name, skip_else);
-                                       continue;
-                               }
-                       }
-
-                       /*
-                        *      Otherwise it's a real keyword.
-                        */
-                       single = compile_item(c, unlang_ctx, ci);
-                       if (!single) {
-                               cf_log_err(ci, "Failed to parse \"%s\" subsection", cf_section_name1(subcs));
-                               talloc_free(c);
-                               return NULL;
-                       }
-
-                       goto add_child;
-
-               } else if (cf_item_is_pair(ci)) {
-                       CONF_PAIR *cp = cf_item_to_pair(ci);
-
-                       /*
-                        *      Variable definition.
-                        */
-                       if (cf_pair_operator(cp) == T_OP_CMP_TRUE) {
-                               if (compile_variable(c, unlang_ctx, cp, &t_rules) < 0) {
-                                       talloc_free(c);
-                                       return NULL;
-                               }
-
-                               single = UNLANG_IGNORE;
-                               goto add_child;
-                       }
-
-                       if (!cf_pair_value(cp)) {
-                               single = compile_item(c, unlang_ctx, ci);
-                               if (!single) {
-                                       cf_log_err(ci, "Invalid keyword \"%s\".", cf_pair_attr(cp));
-                                       talloc_free(c);
-                                       return NULL;
-                               }
-
-                               goto add_child;
-                       }
-
-                       /*
-                        *      What remains MUST be an edit pair.  At this point, the state of the compiler
-                        *      tells us what it is, and we don't really care if there's a leading '&'.
-                        */
-                       single = compile_edit_pair(c, unlang_ctx, cp);
-                       if (!single) {
-                               talloc_free(c);
-                               return NULL;
-                       }
-
-                       goto add_child;
-               } else {
-                       cf_log_err(ci, "Asked to compile unknown conf type");
-                       talloc_free(c);
-                       return NULL;
-               }
-
-       add_child:
-               if (single == UNLANG_IGNORE) continue;
-
-               /*
-                *      Do optimizations for "if" and "elsif"
-                *      conditions.
-                */
-               switch (single->type) {
-               case UNLANG_TYPE_ELSIF:
-               case UNLANG_TYPE_IF:
-                       was_if = true;
-
-                       {
-                               unlang_group_t  *f;
-                               unlang_cond_t   *gext;
-
-                               /*
-                                *      Skip else, and/or omit things which will never be run.
-                                */
-                               f = unlang_generic_to_group(single);
-                               gext = unlang_group_to_cond(f);
-
-                               if (gext->is_truthy) {
-                                       if (gext->value) {
-                                               skip_else = single->debug_name;
-                                       } else {
-                                               /*
-                                                *      The condition never
-                                                *      matches, so we can
-                                                *      avoid putting it into
-                                                *      the unlang tree.
-                                                */
-                                               talloc_free(single);
-                                               continue;
-                                       }
-                               }
-                       }
-                       break;
-
-               default:
-                       was_if = false;
-                       skip_else = NULL;
-                       break;
-               }
-
-               /*
-                *      unlang_group_t is grown by adding a unlang_t to the end
-                */
-               fr_assert(g == talloc_parent(single));
-               fr_assert(single->parent == unlang_group_to_generic(g));
-               fr_assert(!single->next);
-
-               *g->tail = single;
-               g->tail = &single->next;
-               g->num_children++;
-
-               /*
-                *      If it's not possible to execute statement
-                *      after the current one, then just stop
-                *      processing the children.
-                */
-               if (g->self.closed) {
-                       cf_log_warn(ci, "Skipping remaining instructions due to '%s'",
-                                   single->name);
-                       break;
-               }
-       }
-
-       /*
-        *      Set the default actions, if they haven't already been
-        *      set by an "actions" section above.
-        */
-       if (set_action_defaults) compile_action_defaults(c, unlang_ctx);
-
-       return c;
-}
-
-
-/*
- *     Generic "compile a section with more unlang inside of it".
- */
-static unlang_t *compile_section(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type)
-{
-       unlang_group_t  *g;
-       unlang_t        *c;
-       char const      *name1, *name2;
-
-       fr_assert(unlang_ctx->rules != NULL);
-       fr_assert(unlang_ctx->rules->attr.list_def);
-
-       /*
-        *      We always create a group, even if the section is empty.
-        */
-       g = group_allocate(parent, cs, type);
-       if (!g) return NULL;
-
-       c = unlang_group_to_generic(g);
-
-       /*
-        *      Remember the name for printing, etc.
-        */
-       name1 = cf_section_name1(cs);
-       name2 = cf_section_name2(cs);
-       c->name = name1;
-
-       /*
-        *      Make sure to tell the user that we're running a
-        *      policy, and not anything else.
-        */
-       if (type == UNLANG_TYPE_POLICY) {
-               MEM(c->debug_name = talloc_typed_asprintf(c, "policy %s", name1));
-
-       } else if (!name2) {
-               c->debug_name = c->name;
-
-       } else {
-               MEM(c->debug_name = talloc_typed_asprintf(c, "%s %s", name1, name2));
-       }
-
-       return compile_children(g, unlang_ctx, true);
-}
-
-
-static unlang_t *compile_group(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       if (!cf_item_next(ci, NULL)) return UNLANG_IGNORE;
-
-       return compile_section(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_GROUP);
-}
-
-static fr_table_num_sorted_t transaction_keywords[] = {
-       { L("case"),            1 },
-       { L("else"),            1 },
-       { L("elsif"),           1 },
-       { L("foreach"),         1 },
-       { L("group"),           1 },
-       { L("if"),              1 },
-       { L("limit"),           1 },
-       { L("load-balance"),    1 },
-       { L("redundant"),       1 },
-       { L("redundant-load-balance"), 1 },
-       { L("switch"),          1 },
-       { L("timeout"),         1 },
-       { L("transaction"),     1 },
-};
-static int transaction_keywords_len = NUM_ELEMENTS(transaction_keywords);
-
-/** Limit the operations which can appear in a transaction.
- */
-static bool transaction_ok(CONF_SECTION *cs)
-{
-       CONF_ITEM *ci = NULL;
-
-       while ((ci = cf_item_next(cs, ci)) != NULL) {
-               char const *name;
-
-               if (cf_item_is_section(ci)) {
-                       CONF_SECTION *subcs;
-
-                       subcs = cf_item_to_section(ci);
-                       name = cf_section_name1(subcs);
-
-                       if (strcmp(name, "actions") == 0) continue;
-
-                       /*
-                        *      Definitely an attribute editing thing.
-                        */
-                       if (*name == '&') continue;
-
-                       if (fr_list_assignment_op[cf_section_name2_quote(cs)]) continue;
-
-                       if (fr_table_value_by_str(transaction_keywords, name, -1) < 0) {
-                               cf_log_err(ci, "Invalid keyword in 'transaction'");
-                               return false;
-                       }
-
-                       if (!transaction_ok(subcs)) return false;
-
-                       continue;
-
-               } else if (cf_item_is_pair(ci)) {
-                       CONF_PAIR *cp;
-
-                       cp = cf_item_to_pair(ci);
-                       name = cf_pair_attr(cp);
-
-                       /*
-                        *      If there's a value then it's not a module call.
-                        */
-                       if (cf_pair_value(cp)) continue;
-
-                       if (*name == '&') continue;
-
-                       /*
-                        *      Allow rcodes via the "always" module.
-                        */
-                       if (fr_table_value_by_str(mod_rcode_table, name, -1) >= 0) {
-                               continue;
-                       }
-
-                       cf_log_err(ci, "Invalid module reference in 'transaction'");
-                       return false;
-
-               } else {
-                       continue;
-               }
-       }
-
-       return true;
-}
-
-static unlang_t *compile_transaction(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION *cs = cf_item_to_section(ci);
-       unlang_group_t *g;
-       unlang_t *c;
-       unlang_compile_t unlang_ctx2;
-
-       if (cf_section_name2(cs) != NULL) {
-               cf_log_err(cs, "Unexpected argument to 'transaction' section");
-               cf_log_err(ci, DOC_KEYWORD_REF(transaction));
-               return NULL;
-       }
-
-       /*
-        *      The transaction is empty, ignore it.
-        */
-       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
-
-       if (!transaction_ok(cs)) return NULL;
-
-       /*
-        *      Any failure is return, not continue.
-        */
-       compile_copy_context(&unlang_ctx2, unlang_ctx);
-
-       unlang_ctx2.actions.actions[RLM_MODULE_REJECT] = MOD_ACTION_RETURN;
-       unlang_ctx2.actions.actions[RLM_MODULE_FAIL] = MOD_ACTION_RETURN;
-       unlang_ctx2.actions.actions[RLM_MODULE_INVALID] = MOD_ACTION_RETURN;
-       unlang_ctx2.actions.actions[RLM_MODULE_DISALLOW] = MOD_ACTION_RETURN;
-
-       g = group_allocate(parent, cs, UNLANG_TYPE_TRANSACTION);
-       if (!g) return NULL;
-
-       c = unlang_group_to_generic(g);
-       c->debug_name = c->name = cf_section_name1(cs);
-
-       if (!compile_children(g, &unlang_ctx2, false)) return NULL;
-
-       /*
-        *      The default for a failed transaction is to continue on
-        *      failure.
-        */
-       if (!c->actions.actions[RLM_MODULE_FAIL])     c->actions.actions[RLM_MODULE_FAIL] = 1;
-       if (!c->actions.actions[RLM_MODULE_INVALID])  c->actions.actions[RLM_MODULE_INVALID] = 1;
-       if (!c->actions.actions[RLM_MODULE_DISALLOW]) c->actions.actions[RLM_MODULE_DISALLOW] = 1;
-
-       compile_action_defaults(c, unlang_ctx);
-
-       return c;
-}
-
-static unlang_t *compile_try(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION *cs = cf_item_to_section(ci);
-       unlang_group_t *g;
-       unlang_t *c;
-       CONF_ITEM *next;
-
-       /*
-        *      The transaction is empty, ignore it.
-        */
-       if (!cf_item_next(cs, NULL)) {
-               cf_log_err(cs, "'try' sections cannot be empty");
-       print_url:
-               cf_log_err(ci, DOC_KEYWORD_REF(try));
-               return NULL;
-       }
-
-       if (cf_section_name2(cs) != NULL) {
-               cf_log_err(cs, "Unexpected argument to 'try' section");
-               goto print_url;
-       }
-
-       next = cf_item_next(cf_parent(cs), ci);
-       while (next && cf_item_is_data(next)) next = cf_item_next(cf_parent(cs), next);
-
-       if (!next || !cf_item_is_section(next) ||
-           (strcmp(cf_section_name1(cf_item_to_section(next)), "catch") != 0)) {
-               cf_log_err(cs, "'try' sections must be followed by a 'catch'");
-               goto print_url;
-       }
-
-       g = group_allocate(parent, cs, UNLANG_TYPE_TRY);
-       if (!g) return NULL;
-
-       c = unlang_group_to_generic(g);
-       c->debug_name = c->name = cf_section_name1(cs);
-
-       return compile_children(g, unlang_ctx, true);
-}
-
-static int catch_argv(CONF_SECTION *cs, unlang_catch_t *ca, char const *name)
-{
-       int rcode;
-
-       rcode = fr_table_value_by_str(mod_rcode_table, name, -1);
-       if (rcode < 0) {
-               cf_log_err(cs, "Unknown rcode '%s'.", name);
-               return -1;
-       }
-
-       if (ca->catching[rcode]) {
-               cf_log_err(cs, "Duplicate rcode '%s'.", name);
-               return -1;
-       }
-
-       ca->catching[rcode] = true;
-
-       return 0;
-}
-
-static unlang_t *compile_catch(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION *cs = cf_item_to_section(ci);
-       unlang_group_t *g;
-       unlang_t *c;
-       unlang_catch_t *ca;
-       CONF_ITEM *prev;
-       char const *name;
-
-       prev = cf_item_prev(cf_parent(ci), ci);
-       while (prev && cf_item_is_data(prev)) prev = cf_item_prev(cf_parent(ci), prev);
-
-       if (!prev || !cf_item_is_section(prev)) {
-       fail:
-               cf_log_err(cs, "Found 'catch' section with no previous 'try'");
-               cf_log_err(ci, DOC_KEYWORD_REF(catch));
-               return NULL;
-       }
-
-       name = cf_section_name1(cf_item_to_section(prev));
-       fr_assert(name != NULL);
-
-       if ((strcmp(name, "try") != 0) && (strcmp(name, "catch") != 0)) {
-               /*
-                *      The previous thing has to be a section.  And it has to
-                *      be either a "try" or a "catch".
-                */
-               goto fail;
-       }
-
-       g = group_allocate(parent, cs, UNLANG_TYPE_CATCH);
-       if (!g) return NULL;
-
-       c = unlang_group_to_generic(g);
-
-       /*
-        *      Want to log what we caught
-        */
-       c->debug_name = c->name = talloc_typed_asprintf(c, "%s %s", cf_section_name1(cs), cf_section_name2(cs));
-
-       ca = unlang_group_to_catch(g);
-       if (!cf_section_name2(cs)) {
-               /*
-                *      No arg2: catch errors
-                */
-               ca->catching[RLM_MODULE_REJECT] = true;
-               ca->catching[RLM_MODULE_FAIL] = true;
-               ca->catching[RLM_MODULE_INVALID] = true;
-               ca->catching[RLM_MODULE_DISALLOW] = true;
-
-       } else {
-               int i;
-
-               name = cf_section_name2(cs);
-
-               if (catch_argv(cs, ca, name) < 0) {
-                       talloc_free(c);
-                       return NULL;
-               }
-
-               for (i = 0; (name = cf_section_argv(cs, i)) != NULL; i++) {
-                       if (catch_argv(cs, ca, name) < 0) {
-                               talloc_free(c);
-                               return NULL;
-                       }
-               }
-       }
-
-       /*
-        *      @todo - Else parse and limit the things we catch
-        */
-
-       return compile_children(g, unlang_ctx, true);
-}
-
-
-static int8_t case_cmp(void const *one, void const *two)
-{
-       unlang_case_t const *a = (unlang_case_t const *) one; /* may not be talloc'd! See switch.c */
-       unlang_case_t const *b = (unlang_case_t const *) two; /* may not be talloc'd! */
-
-       return fr_value_box_cmp(tmpl_value(a->vpt), tmpl_value(b->vpt));
-}
-
-static uint32_t case_hash(void const *data)
-{
-       unlang_case_t const *a = (unlang_case_t const *) data; /* may not be talloc'd! */
-
-       return fr_value_box_hash(tmpl_value(a->vpt));
-}
-
-static int case_to_key(uint8_t **out, size_t *outlen, void const *data)
-{
-       unlang_case_t const *a = (unlang_case_t const *) data; /* may not be talloc'd! */
-
-       return fr_value_box_to_key(out, outlen, tmpl_value(a->vpt));
-}
-
-static unlang_t *compile_case(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci);
-
-static unlang_t *compile_switch(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION            *cs = cf_item_to_section(ci);
-       CONF_ITEM               *subci;
-       fr_token_t              token;
-       char const              *name1, *name2;
-       char const              *type_name;
-
-       unlang_group_t          *g;
-       unlang_switch_t         *gext;
-
-       unlang_t                *c;
-       ssize_t                 slen;
-
-       tmpl_rules_t            t_rules;
-
-       fr_type_t               type;
-       fr_htrie_type_t         htype;
-
-       /*
-        *      We allow unknown attributes here.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.allow_unknown = true;
-       RULES_VERIFY(&t_rules);
-
-       name2 = cf_section_name2(cs);
-       if (!name2) {
-               cf_log_err(cs, "You must specify a variable to switch over for 'switch'");
-       print_url:
-               cf_log_err(ci, DOC_KEYWORD_REF(switch));
-               return NULL;
-       }
-
-       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
-
-       g = group_allocate(parent, cs, UNLANG_TYPE_SWITCH);
-       if (!g) return NULL;
-
-       gext = unlang_group_to_switch(g);
-
-       /*
-        *      Create the template.  All attributes and xlats are
-        *      defined by now.
-        *
-        *      The 'case' statements need g->vpt filled out to ensure
-        *      that the data types match.
-        */
-       token = cf_section_name2_quote(cs);
-
-       if ((token == T_BARE_WORD) && (name2[0] != '%')) {
-               slen = tmpl_afrom_attr_substr(gext, NULL, &gext->vpt,
-                                             &FR_SBUFF_IN(name2, strlen(name2)),
-                                             NULL,
-                                             &t_rules);
-       } else {
-               slen = tmpl_afrom_substr(gext, &gext->vpt,
-                                        &FR_SBUFF_IN(name2, strlen(name2)),
-                                        token,
-                                        NULL,
-                                        &t_rules);
-       }
-       if (!gext->vpt) {
-               cf_canonicalize_error(cs, slen, "Failed parsing argument to 'switch'", name2);
-               talloc_free(g);
-               return NULL;
-       }
-
-       c = unlang_group_to_generic(g);
-       c->name = "switch";
-       c->debug_name = talloc_typed_asprintf(c, "switch %s", name2);
-
-       /*
-        *      Fixup the template before compiling the children.
-        *      This is so that compile_case() can do attribute type
-        *      checks / casts against us.
-        */
-       if (!pass2_fixup_tmpl(g, &gext->vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
-               talloc_free(g);
-               return NULL;
-       }
-
-       if (tmpl_is_list(gext->vpt)) {
-               cf_log_err(cs, "Cannot use list for 'switch' statement");
-       error:
-               talloc_free(g);
-               goto print_url;
-       }
-
-       if (tmpl_contains_regex(gext->vpt)) {
-               cf_log_err(cs, "Cannot use regular expression for 'switch' statement");
-               goto error;
-       }
-
-       if (tmpl_is_data(gext->vpt)) {
-               cf_log_err(cs, "Cannot use constant data for 'switch' statement");
-               goto error;
-       }
-
-       if (tmpl_is_xlat(gext->vpt)) {
-               xlat_exp_head_t *xlat = tmpl_xlat(gext->vpt);
-
-               if (xlat->flags.constant || xlat->flags.pure) {
-                       cf_log_err(cs, "Cannot use constant data for 'switch' statement");
-                       goto error;
-               }
-       }
-
-
-       if (tmpl_needs_resolving(gext->vpt)) {
-               cf_log_err(cs, "Cannot resolve key for 'switch' statement");
-               goto error;
-       }
-
-       type_name = cf_section_argv(cs, 0); /* AFTER name1, name2 */
-       if (type_name) {
-               type = fr_table_value_by_str(fr_type_table, type_name, FR_TYPE_NULL);
-
-               /*
-                *      Should have been caught in cf_file.c, process_switch()
-                */
-               fr_assert(type != FR_TYPE_NULL);
-               fr_assert(fr_type_is_leaf(type));
-
-       do_cast:
-               if (tmpl_cast_set(gext->vpt, type) < 0) {
-                       cf_log_perr(cs, "Failed setting cast type");
-                       goto error;
-               }
-
-       } else {
-               /*
-                *      Get the return type of the tmpl.  If we don't know,
-                *      mash it all to string.
-                */
-               type = tmpl_data_type(gext->vpt);
-               if ((type == FR_TYPE_NULL) || (type == FR_TYPE_VOID)) {
-                       type = FR_TYPE_STRING;
-                       goto do_cast;
-               }
-       }
-
-       htype = fr_htrie_hint(type);
-       if (htype == FR_HTRIE_INVALID) {
-               cf_log_err(cs, "Invalid data type '%s' used for 'switch' statement",
-                           fr_type_to_str(type));
-               goto error;
-       }
-
-       gext->ht = fr_htrie_alloc(gext, htype,
-                                 (fr_hash_t) case_hash,
-                                 (fr_cmp_t) case_cmp,
-                                 (fr_trie_key_t) case_to_key,
-                                 NULL);
-       if (!gext->ht) {
-               cf_log_err(cs, "Failed initializing internal data structures");
-               talloc_free(g);
-               return NULL;
-       }
-
-       /*
-        *      Walk through the children of the switch section,
-        *      ensuring that they're all 'case' statements, and then compiling them.
-        */
-       for (subci = cf_item_next(cs, NULL);
-            subci != NULL;
-            subci = cf_item_next(cs, subci)) {
-               CONF_SECTION *subcs;
-               unlang_t *single;
-               unlang_case_t   *case_gext;
-
-               if (!cf_item_is_section(subci)) {
-                       if (!cf_item_is_pair(subci)) continue;
-
-                       cf_log_err(subci, "\"switch\" sections can only have \"case\" subsections");
-                       goto error;
-               }
-
-               subcs = cf_item_to_section(subci);      /* can't return NULL */
-               name1 = cf_section_name1(subcs);
-
-               if (strcmp(name1, "case") != 0) {
-                       /*
-                        *      We finally support "default" sections for "switch".
-                        */
-                       if (strcmp(name1, "default") == 0) {
-                               if (cf_section_name2(subcs) != 0) {
-                                       cf_log_err(subci, "\"default\" sections cannot have a match argument");
-                                       goto error;
-                               }
-                               goto handle_default;
-                       }
-
-                       cf_log_err(subci, "\"switch\" sections can only have \"case\" subsections");
-                       goto error;
-               }
-
-               name2 = cf_section_name2(subcs);
-               if (!name2) {
-               handle_default:
-                       if (gext->default_case) {
-                               cf_log_err(subci, "Cannot have two 'default' case statements");
-                               goto error;
-                       }
-               }
-
-               /*
-                *      Compile the subsection.
-                */
-               single = compile_case(c, unlang_ctx, subci);
-               if (!single) goto error;
-
-               fr_assert(single->type == UNLANG_TYPE_CASE);
-
-               /*
-                *      Remember the "default" section, and insert the
-                *      non-default "case" into the htrie.
-                */
-               case_gext = unlang_group_to_case(unlang_generic_to_group(single));
-               if (!case_gext->vpt) {
-                       gext->default_case = single;
-
-               } else if (!fr_htrie_insert(gext->ht, single)) {
-                       single = fr_htrie_find(gext->ht, single);
-
-                       /*
-                        *      @todo - look up the key and get the previous one?
-                        */
-                       cf_log_err(subci, "Failed inserting 'case' statement.  Is there a duplicate?");
-
-                       if (single) cf_log_err(unlang_generic_to_group(single)->cs, "Duplicate may be here.");
-
-                       goto error;
-               }
-
-               *g->tail = single;
-               g->tail = &single->next;
-               g->num_children++;
-       }
-
-       compile_action_defaults(c, unlang_ctx);
-
-       return c;
-}
-
-static unlang_t *compile_case(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION            *cs = cf_item_to_section(ci);
-       int                     i;
-       char const              *name2;
-       unlang_t                *c;
-       unlang_group_t          *case_g;
-       unlang_case_t           *case_gext;
-       tmpl_t                  *vpt = NULL;
-       tmpl_rules_t            t_rules;
-
-       /*
-        *      We allow unknown attributes here.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.allow_unknown = true;
-       RULES_VERIFY(&t_rules);
-
-       if (!parent || (parent->type != UNLANG_TYPE_SWITCH)) {
-               cf_log_err(cs, "\"case\" statements may only appear within a \"switch\" section");
-               cf_log_err(ci, DOC_KEYWORD_REF(case));
-               return NULL;
-       }
-
-       /*
-        *      case THING means "match THING"
-        *      case       means "match anything"
-        */
-       name2 = cf_section_name2(cs);
-       if (name2) {
-               ssize_t                 slen;
-               fr_token_t              quote;
-               unlang_group_t          *switch_g;
-               unlang_switch_t         *switch_gext;
-
-               switch_g = unlang_generic_to_group(parent);
-               switch_gext = unlang_group_to_switch(switch_g);
-
-               fr_assert(switch_gext->vpt != NULL);
-
-               /*
-                *      We need to cast case values to match
-                *      what we're switching over, otherwise
-                *      integers of different widths won't
-                *      match.
-                */
-               t_rules.cast = tmpl_expanded_type(switch_gext->vpt);
-
-               /*
-                *      Need to pass the attribute from switch
-                *      to tmpl rules so we can convert the
-                *      case string to an integer value.
-                */
-               if (tmpl_is_attr(switch_gext->vpt)) {
-                       fr_dict_attr_t const *da = tmpl_attr_tail_da(switch_gext->vpt);
-                       if (da->flags.has_value) t_rules.enumv = da;
-               }
-
-               quote = cf_section_name2_quote(cs);
-
-               slen = tmpl_afrom_substr(cs, &vpt,
-                                        &FR_SBUFF_IN(name2, strlen(name2)),
-                                        quote,
-                                        NULL,
-                                        &t_rules);
-               if (!vpt) {
-                       cf_canonicalize_error(cs, slen, "Failed parsing argument to 'case'", name2);
-                       return NULL;
-               }
-
-               /*
-                *      Bare word strings are attribute references
-                */
-               if (tmpl_is_attr(vpt) || tmpl_is_attr_unresolved(vpt)) {
-               fail_attr:
-                       cf_log_err(cs, "arguments to 'case' statements MUST NOT be attribute references.");
-                       goto fail;
-               }
-
-               if (!tmpl_is_data(vpt) || tmpl_is_data_unresolved(vpt)) {
-                       cf_log_err(cs, "arguments to 'case' statements MUST be static data.");
-               fail:
-                       talloc_free(vpt);
-                       return NULL;
-               }
-
-               /*
-                *      References to unresolved attributes are forbidden.  They are no longer "bare word
-                *      strings".
-                */
-               if ((quote == T_BARE_WORD) && (tmpl_value_type(vpt) == FR_TYPE_STRING)) {
-                       goto fail_attr;
-               }
-
-       } /* else it's a default 'case' statement */
-
-       /*
-        *      If we were asked to match something, then we MUST
-        *      match it, even if the section is empty.  Otherwise we
-        *      will silently skip the match, and then fall through to
-        *      the "default" statement.
-        */
-       c = compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_CASE);
-       if (!c) {
-               talloc_free(vpt);
-               return NULL;
-       }
-
-       case_g = unlang_generic_to_group(c);
-       case_gext = unlang_group_to_case(case_g);
-       case_gext->vpt = talloc_steal(case_gext, vpt);
-
-       /*
-        *      Set all of it's codes to return, so that
-        *      when we pick a 'case' statement, we don't
-        *      fall through to processing the next one.
-        */
-       for (i = 0; i < RLM_MODULE_NUMCODES; i++) c->actions.actions[i] = MOD_ACTION_RETURN;
-
-       return c;
-}
-
-static unlang_t *compile_timeout(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION            *cs = cf_item_to_section(ci);
-       char const              *name2;
-       unlang_t                *c;
-       unlang_group_t          *g;
-       unlang_timeout_t        *gext;
-       fr_time_delta_t         timeout = fr_time_delta_from_sec(0);
-       tmpl_t                  *vpt = NULL;
-       fr_token_t              token;
-
-       /*
-        *      Timeout <time ref>
-        */
-       name2 = cf_section_name2(cs);
-       if (!name2) {
-               cf_log_err(cs, "You must specify a time value for 'timeout'");
-       print_url:
-               cf_log_err(ci, DOC_KEYWORD_REF(timeout));
-               return NULL;
-       }
-
-       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
-
-       g = group_allocate(parent, cs, UNLANG_TYPE_TIMEOUT);
-       if (!g) return NULL;
-
-       gext = unlang_group_to_timeout(g);
-
-       token = cf_section_name2_quote(cs);
-
-       if ((token == T_BARE_WORD) && isdigit((uint8_t) *name2)) {
-               if (fr_time_delta_from_str(&timeout, name2, strlen(name2), FR_TIME_RES_SEC) < 0) {
-                       cf_log_err(cs, "Failed parsing time delta %s - %s",
-                                  name2, fr_strerror());
-                       return NULL;
-               }
-       } else {
-               ssize_t         slen;
-               tmpl_rules_t    t_rules;
-
-               /*
-                *      We don't allow unknown attributes here.
-                */
-               t_rules = *(unlang_ctx->rules);
-               t_rules.attr.allow_unknown = false;
-               RULES_VERIFY(&t_rules);
-
-               slen = tmpl_afrom_substr(gext, &vpt,
-                                        &FR_SBUFF_IN(name2, strlen(name2)),
-                                        token,
-                                        NULL,
-                                        &t_rules);
-               if (!vpt) {
-                       cf_canonicalize_error(cs, slen, "Failed parsing argument to 'timeout'", name2);
-                       talloc_free(g);
-                       return NULL;
-               }
-
-               /*
-                *      Fixup the tmpl so that we know it's somewhat sane.
-                */
-               if (!pass2_fixup_tmpl(gext, &vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
-                       talloc_free(g);
-                       return NULL;
-               }
-
-               if (tmpl_is_list(vpt)) {
-                       cf_log_err(cs, "Cannot use list as argument for 'timeout' statement");
-               error:
-                       talloc_free(g);
-                       goto print_url;
-               }
-
-               if (tmpl_contains_regex(vpt)) {
-                       cf_log_err(cs, "Cannot use regular expression as argument for 'timeout' statement");
-                       goto error;
-               }
-
-               /*
-                *      Attribute or data MUST be cast to TIME_DELTA.
-                */
-               if (tmpl_cast_set(vpt, FR_TYPE_TIME_DELTA) < 0) {
-                       cf_log_perr(cs, "Failed setting cast type");
-                       goto error;
-               }
-       }
-
-       /*
-        *      Compile the contents of a "timeout".
-        */
-       c = compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_TIMEOUT);
-       if (!c) return NULL;
-
-       g = unlang_generic_to_group(c);
-       gext = unlang_group_to_timeout(g);
-       gext->timeout = timeout;
-       gext->vpt = vpt;
-
-       return c;
-}
-
-static unlang_t *compile_limit(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION            *cs = cf_item_to_section(ci);
-       char const              *name2;
-       unlang_t                *c;
-       unlang_group_t          *g;
-       unlang_limit_t          *gext;
-       tmpl_t                  *vpt = NULL;
-       uint32_t                limit = 0;
-       fr_token_t              token;
-       ssize_t                 slen;
-       tmpl_rules_t            t_rules;
-
-       /*
-        *      limit <number>
-        */
-       name2 = cf_section_name2(cs);
-       if (!name2) {
-               cf_log_err(cs, "You must specify a value for 'limit'");
-       print_url:
-               cf_log_err(ci, DOC_KEYWORD_REF(limit));
-               return NULL;
-       }
-
-       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
-
-       g = group_allocate(parent, cs, UNLANG_TYPE_LIMIT);
-       if (!g) return NULL;
-
-       gext = unlang_group_to_limit(g);
-
-       token = cf_section_name2_quote(cs);
-
-       /*
-        *      We don't allow unknown attributes here.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.allow_unknown = false;
-       RULES_VERIFY(&t_rules);
-
-       slen = tmpl_afrom_substr(gext, &vpt,
-                                &FR_SBUFF_IN(name2, strlen(name2)),
-                                token,
-                                NULL,
-                                &t_rules);
-       if (!vpt) {
-       syntax_error:
-               cf_canonicalize_error(cs, slen, "Failed parsing argument to 'foreach'", name2);
-               talloc_free(g);
-               return NULL;
-       }
-
-       /*
-        *      Fixup the tmpl so that we know it's somewhat sane.
-        */
-       if (!pass2_fixup_tmpl(gext, &vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
-               talloc_free(g);
-               return NULL;
-       }
-
-       if (tmpl_is_list(vpt)) {
-               cf_log_err(cs, "Cannot use list as argument for 'limit' statement");
-       error:
-               talloc_free(g);
-               goto print_url;
-       }
-
-       if (tmpl_contains_regex(vpt)) {
-               cf_log_err(cs, "Cannot use regular expression as argument for 'limit' statement");
-               goto error;
-       }
-
-       if (tmpl_is_data(vpt) && (token == T_BARE_WORD)) {
-               fr_value_box_t box;
-
-               if (fr_value_box_cast(NULL, &box, FR_TYPE_UINT32, NULL, tmpl_value(vpt)) < 0) goto syntax_error;
-
-               limit = box.vb_uint32;
-
-       } else {
-               /*
-                *      Attribute or data MUST be cast to a 32-bit unsigned number.
-                */
-               if (tmpl_cast_set(vpt, FR_TYPE_UINT32) < 0) {
-                       cf_log_perr(cs, "Failed setting cast type");
-                       goto syntax_error;
-               }
-       }
-
-       /*
-        *      Compile the contents of a "limit".
-        */
-       c = compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_LIMIT);
-       if (!c) return NULL;
-
-       g = unlang_generic_to_group(c);
-       gext = unlang_group_to_limit(g);
-       gext->limit = limit;
-       gext->vpt = vpt;
-
-       return c;
-}
-
-static unlang_t *compile_foreach(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION            *cs = cf_item_to_section(ci);
-       fr_token_t              token;
-       char const              *name2;
-       char const              *type_name, *variable_name;
-       fr_type_t               type;
-       unlang_t                *c;
-
-       fr_type_t               key_type;
-       char const              *key_name;
-
-       unlang_group_t          *g;
-       unlang_foreach_t        *gext;
-
-       ssize_t                 slen;
-       tmpl_t                  *vpt;
-       fr_dict_attr_t const    *da = NULL;
-
-       tmpl_rules_t            t_rules;
-       unlang_compile_t        unlang_ctx2;
-
-       /*
-        *      Ignore empty "foreach" blocks, and don't even sanity check their arguments.
-        */
-       if (!cf_item_next(cs, NULL)) {
-               return UNLANG_IGNORE;
-       }
-
-       /*
-        *      We allow unknown attributes here.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.allow_unknown = true;
-       t_rules.attr.allow_wildcard = true;
-       RULES_VERIFY(&t_rules);
-
-       name2 = cf_section_name2(cs);
-       fr_assert(name2 != NULL); /* checked in cf_file.c */
-
-       /*
-        *      Allocate a group for the "foreach" block.
-        */
-       g = group_allocate(parent, cs, UNLANG_TYPE_FOREACH);
-       if (!g) return NULL;
-
-       c = unlang_group_to_generic(g);
-
-       /*
-        *      Create the template.  If we fail, AND it's a bare word
-        *      with &Foo-Bar, it MAY be an attribute defined by a
-        *      module.  Allow it for now.  The pass2 checks below
-        *      will fix it up.
-        */
-       token = cf_section_name2_quote(cs);
-       if (token != T_BARE_WORD) {
-               cf_log_err(cs, "Data being looped over in 'foreach' must be an attribute reference or dynamic expansion, not a string");
-       print_ref:
-               cf_log_err(ci, DOC_KEYWORD_REF(foreach));
-       error:
-               talloc_free(g);
-               return NULL;
-       }
+/** Compile a variable definition.
+ *
+ *  Definitions which are adjacent to one another are automatically merged
+ *  into one larger variable definition.
+ */
+static int compile_variable(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_PAIR *cp, tmpl_rules_t *t_rules)
+{
+       unlang_variable_t *var;
+       fr_type_t       type;
+       char const      *attr, *value;
+       unlang_group_t  *group;
 
-       slen = tmpl_afrom_substr(g, &vpt,
-                                &FR_SBUFF_IN(name2, strlen(name2)),
-                                token,
-                                NULL,
-                                &t_rules);
-       if (!vpt) {
-               cf_canonicalize_error(cs, slen, "Failed parsing argument to 'foreach'", name2);
-               goto error;
-       }
+       fr_assert(debug_braces(parent->type));
 
        /*
-        *      If we don't have a negative return code, we must have a vpt
-        *      (mostly to quiet coverity).
+        *      Enforce locations for local variables.
         */
-       fr_assert(vpt);
-
-       if (tmpl_is_attr(vpt)) {
-               if (tmpl_attr_tail_num(vpt) == NUM_UNSPEC) {
-                       cf_log_warn(cs, "Attribute reference should be updated to use %s[*]", vpt->name);
-                       tmpl_attr_rewrite_leaf_num(vpt, NUM_ALL);
-               }
-
-               if (tmpl_attr_tail_num(vpt) != NUM_ALL) {
-                       cf_log_err(cs, "Attribute references must be of the form ...%s[*]", tmpl_attr_tail_da(vpt)->name);
-                       goto print_ref;
-               }
+       switch (parent->type) {
+       case UNLANG_TYPE_CASE:
+       case UNLANG_TYPE_ELSE:
+       case UNLANG_TYPE_ELSIF:
+       case UNLANG_TYPE_FOREACH:
+       case UNLANG_TYPE_GROUP:
+       case UNLANG_TYPE_IF:
+       case UNLANG_TYPE_TIMEOUT:
+       case UNLANG_TYPE_LIMIT:
+       case UNLANG_TYPE_POLICY:
+       case UNLANG_TYPE_REDUNDANT:
+       case UNLANG_TYPE_SUBREQUEST:
+       case UNLANG_TYPE_LOAD_BALANCE:
+       case UNLANG_TYPE_REDUNDANT_LOAD_BALANCE:
+               break;
 
-       } else if (!tmpl_contains_xlat(vpt)) {
-               cf_log_err(cs, "Invalid content in 'foreach (...)', it must be an attribute reference or a dynamic expansion");
-               goto print_ref;
+       default:
+               cf_log_err(cp, "Local variables cannot be used here");
+               return -1;
        }
 
-       gext = unlang_group_to_foreach(g);
-       gext->vpt = vpt;
-
-       c->name = "foreach";
-       MEM(c->debug_name = talloc_typed_asprintf(c, "foreach %s", name2));
-
-       /*
-        *      Copy over the compilation context.  This is mostly
-        *      just to ensure that retry is handled correctly.
-        *      i.e. reset.
-        */
-       compile_copy_context(&unlang_ctx2, unlang_ctx);
-
        /*
-        *      Then over-write the new compilation context.
-        */
-       unlang_ctx2.section_name1 = "foreach";
-       unlang_ctx2.section_name2 = name2;
-       unlang_ctx2.rules = &t_rules;
-       t_rules.parent = unlang_ctx->rules;
-
-       /*
-        *      If we have "type name", then define a local variable of that name.
+        *      The variables exist in the parent block.
         */
-       type_name = cf_section_argv(cs, 0); /* AFTER name1, name2 */
-
-       key_name = cf_section_argv(cs, 2);
-       if (key_name) {
-               key_type = fr_table_value_by_str(fr_type_table, key_name, FR_TYPE_VOID);
-       } else {
-               key_type = FR_TYPE_VOID;
-       }
-       key_name = cf_section_argv(cs, 3);
-
-       if (tmpl_is_xlat(vpt)) {
-               if (!type_name) {
-                       cf_log_err(cs, "Dynamic expansions MUST specify a data type for the variable");
-                       goto print_ref;
-               }
-
-               type = fr_table_value_by_str(fr_type_table, type_name, FR_TYPE_VOID);
-
-               /*
-                *      No data type was specified, see if we can get one from the function.
-                */
-               if (type == FR_TYPE_NULL) {
-                       type = xlat_data_type(tmpl_xlat(vpt));
-                       if (fr_type_is_leaf(type)) goto get_name;
-
-                       cf_log_err(cs, "Unable to determine return data type from dynamic expansion");
-                       goto print_ref;
-               }
-
-               if (!fr_type_is_leaf(type)) {
-                       cf_log_err(cs, "Dynamic expansions MUST specify a non-structural data type for the variable");
-                       goto print_ref;
-               }
-
-               if ((key_type != FR_TYPE_VOID) && !fr_type_is_numeric(key_type)) {
-                       cf_log_err(cs, "Invalid data type '%s' for 'key' variable - it should be numeric", fr_type_to_str(key_type));
-                       goto print_ref;
-               }
+       group = unlang_generic_to_group(parent);
+       if (group->variables) {
+               var = group->variables;
 
-               goto get_name;
        } else {
-               fr_assert(tmpl_is_attr(vpt));
-
-               if ((key_type != FR_TYPE_VOID) && (key_type != FR_TYPE_STRING) && (key_type != FR_TYPE_UINT32)) {
-                       cf_log_err(cs, "Invalid data type '%s' for 'key' variable - it should be 'string' or 'uint32'", fr_type_to_str(key_type));
-                       goto print_ref;
-               }
-       }
-
-       if (type_name) {
-               unlang_variable_t *var;
-
-               type = fr_table_value_by_str(fr_type_table, type_name, FR_TYPE_VOID);
-               fr_assert(type != FR_TYPE_VOID);
-
-               /*
-                *      foreach string foo (&tlv-thing.[*]) { ... }
-                */
-               if (tmpl_attr_tail_is_unspecified(vpt)) {
-                       goto get_name;
-               }
-
-               da = tmpl_attr_tail_da(vpt);
-
-               if (type == FR_TYPE_NULL) {
-                       type = da->type;
-
-               } else if (fr_type_is_leaf(type) != fr_type_is_leaf(da->type)) {
-               incompatible:
-                       cf_log_err(cs, "Incompatible data types in foreach variable (%s), and reference %s being looped over (%s)",
-                                  fr_type_to_str(type), da->name, fr_type_to_str(da->type));
-                       goto print_ref;
-
-               } else if (fr_type_is_structural(type) && (type != da->type)) {
-                       goto incompatible;
-               }
-
-       get_name:
-               variable_name = cf_section_argv(cs, 1);
-
-               /*
-                *      Define the local variables.
-                */
-               g->variables = var = talloc_zero(g, unlang_variable_t);
-               if (!var) goto error;
+               group->variables = var = talloc_zero(parent, unlang_variable_t);
+               if (!var) return -1;
 
                var->dict = fr_dict_protocol_alloc(unlang_ctx->rules->attr.dict_def);
-               if (!var->dict) goto error;
-
+               if (!var->dict) {
+                       talloc_free(var);
+                       return -1;
+               }
                var->root = fr_dict_root(var->dict);
 
                var->max_attr = 1;
 
-               if (define_local_variable(cf_section_to_item(cs), var, &t_rules, type, variable_name, da) < 0) goto error;
-
-               t_rules.attr.dict_def = var->dict;
-               t_rules.attr.namespace = NULL;
-
                /*
-                *      And ensure we have the key.
+                *      Initialize the new rules, and point them to the parent rules.
+                *
+                *      Then replace the parse rules with our rules, and our dictionary.
                 */
-               gext->value = fr_dict_attr_by_name(NULL, var->root, variable_name);
-               fr_assert(gext->value != NULL);
+               *t_rules = *unlang_ctx->rules;
+               t_rules->parent = unlang_ctx->rules;
 
-               /*
-                *      Define the local key variable.  Note that we don't copy any children.
-                */
-               if (key_type != FR_TYPE_VOID) {
-                       if (define_local_variable(cf_section_to_item(cs), var, &t_rules, key_type, key_name, NULL) < 0) goto error;
+               t_rules->attr.dict_def = var->dict;
+               t_rules->attr.namespace = NULL;
 
-                       gext->key = fr_dict_attr_by_name(NULL, var->root, key_name);
-                       fr_assert(gext->key != NULL);
-               }
+               unlang_ctx->rules = t_rules;
        }
 
-       if (!compile_children(g, &unlang_ctx2, true)) return NULL;
-
-       return c;
-}
-
-static unlang_t *compile_break(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       unlang_t *unlang;
-
-       for (unlang = parent; unlang != NULL; unlang = unlang->parent) {
-               /*
-                *      "break" doesn't go past a return point.
-                */
-               if ((unlang_ops[unlang->type].flag & UNLANG_OP_FLAG_RETURN_POINT) != 0) goto error;
-
-               if ((unlang_ops[unlang->type].flag & UNLANG_OP_FLAG_BREAK_POINT) != 0) break;
-       }
+       attr = cf_pair_attr(cp);        /* data type */
+       value = cf_pair_value(cp);      /* variable name */
 
-       if (!unlang) {
-       error:
-               cf_log_err(ci, "Invalid location for 'break' - it can only be used inside 'foreach' or 'switch'");
-               cf_log_err(ci, DOC_KEYWORD_REF(break));
-               return NULL;
+       type = fr_table_value_by_str(fr_type_table, attr, FR_TYPE_NULL);
+       if (type == FR_TYPE_NULL) {
+invalid_type:
+               cf_log_err(cp, "Invalid data type '%s'", attr);
+               return -1;
        }
 
-       parent->closed = true;
+       /*
+        *      Leaf and group are OK.  TLV, Vendor, Struct, VSA, etc. are not.
+        */
+       if (!(fr_type_is_leaf(type) || (type == FR_TYPE_GROUP))) goto invalid_type;
 
-       return compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_BREAK);
+       return unlang_define_local_variable(cf_pair_to_item(cp), var, t_rules, type, value, NULL);
 }
 
-static unlang_t *compile_continue(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
+/*
+ *     Compile action && rcode for later use.
+ */
+static int compile_action_pair(unlang_mod_actions_t *actions, CONF_PAIR *cp)
 {
-       unlang_t *unlang;
-
-       for (unlang = parent; unlang != NULL; unlang = unlang->parent) {
-               /*
-                *      "continue" doesn't go past a return point.
-                */
-               if ((unlang_ops[unlang->type].flag & UNLANG_OP_FLAG_RETURN_POINT) != 0) goto error;
-
-               if (unlang->type == UNLANG_TYPE_FOREACH) break;
-       }
-
-       if (!unlang) {
-       error:
-               cf_log_err(ci, "Invalid location for 'continue' - it can only be used inside 'foreach'");
-               cf_log_err(ci, DOC_KEYWORD_REF(break));
-               return NULL;
-       }
-
-       parent->closed = true;
-
-       return compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_CONTINUE);
-}
+       int action;
+       char const *attr, *value;
 
+       attr = cf_pair_attr(cp);
+       value = cf_pair_value(cp);
+       if (!value) return 0;
 
-static unlang_t *compile_detach(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       unlang_t *subrequest;
+       if (!strcasecmp(value, "return"))
+               action = MOD_ACTION_RETURN;
 
-       for (subrequest = parent;
-            subrequest != NULL;
-            subrequest = subrequest->parent) {
-               if (subrequest->type == UNLANG_TYPE_SUBREQUEST) break;
-       }
+       else if (!strcasecmp(value, "break"))
+               action = MOD_ACTION_RETURN;
 
-       if (!subrequest) {
-               cf_log_err(ci, "'detach' can only be used inside of a 'subrequest' section.");
-               cf_log_err(ci, DOC_KEYWORD_REF(detach));
-               return NULL;
-       }
+       else if (!strcasecmp(value, "reject"))
+               action = MOD_ACTION_REJECT;
 
-       /*
-        *      This really overloads the functionality of
-        *      cf_item_next().
-        */
-       if ((parent == subrequest) && !cf_item_next(ci, ci)) {
-               cf_log_err(ci, "'detach' cannot be used as the last entry in a section, as there is nothing more to do");
-               return NULL;
-       }
+       else if (!strcasecmp(value, "retry"))
+               action = MOD_ACTION_RETRY;
 
-       return compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_DETACH);
-}
+       else if (strspn(value, "0123456789")==strlen(value)) {
+               action = atoi(value);
 
-static unlang_t *compile_return(unlang_t *parent, unlang_compile_t *unlang_ctx, UNUSED CONF_ITEM const *ci)
-{
-       /*
-        *      These types are all parallel, and therefore can have a "return" in them.
-        */
-       switch (parent->type) {
-       case UNLANG_TYPE_LOAD_BALANCE:
-       case UNLANG_TYPE_REDUNDANT_LOAD_BALANCE:
-       case UNLANG_TYPE_PARALLEL:
-               break;
+               if (!action || (action > MOD_PRIORITY_MAX)) {
+                       cf_log_err(cp, "Priorities MUST be between 1 and 64.");
+                       return 0;
+               }
 
-       default:
-               parent->closed = true;
-               break;
+       } else {
+               cf_log_err(cp, "Unknown action '%s'.\n",
+                          value);
+               return 0;
        }
 
-       return compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_RETURN);
-}
+       if (strcasecmp(attr, "default") != 0) {
+               int rcode;
 
-static unlang_t *compile_tmpl(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM *ci)
-{
-       CONF_PAIR       *cp = cf_item_to_pair(ci);
-       unlang_t        *c;
-       unlang_tmpl_t   *ut;
-       ssize_t         slen;
-       char const      *p = cf_pair_attr(cp);
-       tmpl_t          *vpt;
+               rcode = fr_table_value_by_str(mod_rcode_table, attr, -1);
+               if (rcode < 0) {
+                       cf_log_err(cp,
+                                  "Unknown module rcode '%s'.",
+                                  attr);
+                       return 0;
+               }
+               actions->actions[rcode] = action;
 
-       MEM(ut = talloc_zero(parent, unlang_tmpl_t));
-       c = unlang_tmpl_to_generic(ut);
-       c->parent = parent;
-       c->next = NULL;
-       c->name = p;
-       c->debug_name = c->name;
-       c->type = UNLANG_TYPE_TMPL;
-       c->ci = CF_TO_ITEM(cp);
+       } else {                /* set all unset values to the default */
+               int i;
 
-       RULES_VERIFY(unlang_ctx->rules);
-       slen = tmpl_afrom_substr(ut, &vpt,
-                                &FR_SBUFF_IN(p, talloc_array_length(p) - 1),
-                                cf_pair_attr_quote(cp),
-                                NULL,
-                                unlang_ctx->rules);
-       if (!vpt) {
-               cf_canonicalize_error(cp, slen, "Failed parsing expansion", p);
-               talloc_free(ut);
-               return NULL;
+               for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
+                       if (!actions->actions[i]) actions->actions[i] = action;
+               }
        }
-       ut->tmpl = vpt; /* const issues */
 
-       compile_action_defaults(c, unlang_ctx);
-       return c;
+       return 1;
 }
 
-static const fr_sbuff_term_t if_terminals = FR_SBUFF_TERMS(
-       L(""),
-       L("{"),
-);
-
-static unlang_t *compile_if_subsection(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type)
+static bool compile_retry_section(unlang_mod_actions_t *actions, CONF_ITEM *ci)
 {
-       unlang_t                *c;
-
-       unlang_group_t          *g;
-       unlang_cond_t           *gext;
-       CONF_ITEM               *ci;
-
-       xlat_exp_head_t         *head = NULL;
-       bool                    is_truthy = false, value = false;
-       xlat_res_rules_t        xr_rules = {
-               .tr_rules = &(tmpl_res_rules_t) {
-                       .dict_def = unlang_ctx->rules->attr.dict_def,
-               },
-       };
+       CONF_ITEM *csi;
+       CONF_SECTION *cs;
 
-       if (!cf_section_name2(cs)) {
-               cf_log_err(cs, "'%s' without condition", unlang_ops[type].name);
-               return NULL;
-       }
+       cs = cf_item_to_section(ci);
+       for (csi=cf_item_next(cs, NULL);
+            csi != NULL;
+            csi=cf_item_next(cs, csi)) {
+               CONF_PAIR *cp;
+               char const *name, *value;
 
-       /*
-        *      Migration support.
-        */
-       {
-               char const *name2 = cf_section_name2(cs);
-               ssize_t slen;
-
-               tmpl_rules_t t_rules = (tmpl_rules_t) {
-                       .parent = unlang_ctx->rules->parent,
-                       .attr = {
-                               .dict_def = xr_rules.tr_rules->dict_def,
-                               .list_def = request_attr_request,
-                               .allow_unresolved = false,
-                               .allow_unknown = false,
-                               .allow_wildcard = true,
-               },
-                       .literals_safe_for = unlang_ctx->rules->literals_safe_for,
-               };
-
-               fr_sbuff_parse_rules_t p_rules = { };
-
-               p_rules.terminals = &if_terminals;
-
-               slen = xlat_tokenize_condition(cs, &head, &FR_SBUFF_IN(name2, strlen(name2)), &p_rules, &t_rules);
-               if (slen == 0) {
-                       cf_canonicalize_error(cs, slen, "Empty conditions are invalid", name2);
-                       return NULL;
+               if (cf_item_is_section(csi)) {
+                       cf_log_err(csi, "Invalid subsection in 'retry' configuration.");
+                       return false;
                }
 
-               if (slen < 0) {
-                       slen++; /* fr_slen_t vs ssize_t */
-                       cf_canonicalize_error(cs, slen, "Failed parsing condition", name2);
-                       return NULL;
-               }
+               if (!cf_item_is_pair(csi)) continue;
 
-               /*
-                *      Resolve the xlat first.
-                */
-               if (xlat_resolve(head, &xr_rules) < 0) {
-                       cf_log_err(cs, "Failed resolving condition - %s", fr_strerror());
-                       return NULL;
-               }
+               cp = cf_item_to_pair(csi);
+               name = cf_pair_attr(cp);
+               value = cf_pair_value(cp);
 
-               fr_assert(!xlat_needs_resolving(head));
+               if (!value) {
+                       cf_log_err(csi, "Retry configuration must specify a value");
+                       return false;
+               }
 
-               is_truthy = xlat_is_truthy(head, &value);
+#define CLAMP(_name, _field, _limit) do { \
+                       if (!fr_time_delta_ispos(actions->retry._field)) { \
+                               cf_log_err(csi, "Invalid value for '" STRINGIFY(_name) " = %s' - value must be positive", \
+                                          value); \
+                               return false; \
+                       } \
+                       if (fr_time_delta_cmp(actions->retry._field, fr_time_delta_from_sec(_limit)) > 0) { \
+                               cf_log_err(csi, "Invalid value for '" STRINGIFY(_name) " = %s' - value must be less than " STRINGIFY(_limit) "s", \
+                                          value); \
+                               return false; \
+                       } \
+       } while (0)
 
                /*
-                *      If the condition is always false, we don't compile the
-                *      children.
+                *      We don't use conf_parser_t here for various
+                *      magical reasons.
                 */
-               if (is_truthy && !value) {
-                       cf_log_debug_prefix(cs, "Skipping contents of '%s' as it is always 'false'",
-                                           unlang_ops[type].name);
-
-                       /*
-                        *      Free the children, which frees any xlats,
-                        *      conditions, etc. which were defined, but are
-                        *      now entirely unused.
-                        *
-                        *      However, we still need to cache the conditions, as they will be accessed at run-time.
-                        */
-                       c = compile_empty(parent, unlang_ctx, cs, type);
-                       cf_section_free_children(cs);
-               } else {
-                       c = compile_section(parent, unlang_ctx, cs, type);
-               }
-       }
+               if (strcmp(name, "initial_rtx_time") == 0) {
+                       if (fr_time_delta_from_str(&actions->retry.irt, value, strlen(value), FR_TIME_RES_SEC) < 0) {
+                       error:
+                               cf_log_err(csi, "Failed parsing '%s = %s' - %s",
+                                          name, value, fr_strerror());
+                               return false;
+                       }
+                       CLAMP(initial_rtx_time, irt, 2);
 
-       if (!c) return NULL;
-       fr_assert(c != UNLANG_IGNORE);
+               } else if (strcmp(name, "max_rtx_time") == 0) {
+                       if (fr_time_delta_from_str(&actions->retry.mrt, value, strlen(value), FR_TIME_RES_SEC) < 0) goto error;
 
-       g = unlang_generic_to_group(c);
-       gext = unlang_group_to_cond(g);
+                       CLAMP(max_rtx_time, mrt, 10);
 
-       gext->head = head;
-       gext->is_truthy = is_truthy;
-       gext->value = value;
+               } else if (strcmp(name, "max_rtx_count") == 0) {
+                       unsigned long v = strtoul(value, 0, 0);
 
-       ci = cf_section_to_item(cs);
-       while ((ci = cf_item_next(parent->ci, ci)) != NULL) {
-               if (cf_item_is_data(ci)) continue;
+                       if (v > 10) {
+                               cf_log_err(csi, "Invalid value for 'max_rtx_count = %s' - value must be between 0 and 10",
+                                          value);
+                               return false;
+                       }
 
-               break;
-       }
+                       actions->retry.mrc = v;
 
-       /*
-        *      If there's an 'if' without an 'else', then remember that.
-        */
-       if (ci && cf_item_is_section(ci)) {
-               char const *name;
+               } else if (strcmp(name, "max_rtx_duration") == 0) {
+                       if (fr_time_delta_from_str(&actions->retry.mrd, value, strlen(value), FR_TIME_RES_SEC) < 0) goto error;
 
-               name = cf_section_name1(cf_item_to_section(ci));
-               fr_assert(name != NULL);
+                       CLAMP(max_rtx_duration, mrd, 20);
 
-               gext->has_else = (strcmp(name, "else") == 0) || (strcmp(name, "elsif") == 0);
+               } else {
+                       cf_log_err(csi, "Invalid item '%s' in 'retry' configuration.", name);
+                       return false;
+               }
        }
 
-       return c;
+       return true;
 }
 
-static unlang_t *compile_if(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
+bool unlang_compile_actions(unlang_mod_actions_t *actions, CONF_SECTION *action_cs, bool module_retry)
 {
-       return compile_if_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_IF);
-}
+       int i;
+       bool disallow_retry_action = false;
+       CONF_ITEM *csi;
+       CONF_SECTION *cs;
 
-static unlang_t *compile_elsif(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       return compile_if_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_ELSIF);
-}
+       /*
+        *      Over-ride the default return codes of the module.
+        */
+       cs = cf_item_to_section(cf_section_to_item(action_cs));
+       for (csi=cf_item_next(cs, NULL);
+            csi != NULL;
+            csi=cf_item_next(cs, csi)) {
+               char const *name;
+               CONF_PAIR *cp;
 
-static unlang_t *compile_else(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION *cs = cf_item_to_section(ci);
+               if (cf_item_is_section(csi)) {
+                       CONF_SECTION *subcs = cf_item_to_section(csi);
 
-       if (cf_section_name2(cs)) {
-               cf_log_err(cs, "'else' cannot have a condition");
-               return NULL;
-       }
+                       name = cf_section_name1(subcs);
 
-       return compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_ELSE);
-}
+                       /*
+                        *      Look for a "retry" section.
+                        */
+                       if (name && (strcmp(name, "retry") == 0) && !cf_section_name2(subcs)) {
+                               if (!compile_retry_section(actions, csi)) return false;
+                               continue;
+                       }
 
-/*
- *     redundant, load-balance and parallel have limits on what can
- *     go in them.
- */
-static bool validate_limited_subsection(CONF_SECTION *cs, char const *name)
-{
-       CONF_ITEM *ci;
+                       cf_log_err(csi, "Invalid subsection.  Expected 'action = value'");
+                       return false;
+               }
+
+               if (!cf_item_is_pair(csi)) continue;
+
+               cp = cf_item_to_pair(csi);
 
-       for (ci=cf_item_next(cs, NULL);
-            ci != NULL;
-            ci=cf_item_next(cs, ci)) {
                /*
-                *      If we're a redundant, etc. group, then the
-                *      intention is to call modules, rather than
-                *      processing logic.  These checks aren't
-                *      *strictly* necessary, but they keep the users
-                *      from doing crazy things.
+                *      Allow 'retry = path.to.retry.config'
                 */
-               if (cf_item_is_section(ci)) {
-                       CONF_SECTION *subcs = cf_item_to_section(ci);
-                       char const *name1 = cf_section_name1(subcs);
+               name = cf_pair_attr(cp);
+               if (strcmp(name, "retry") == 0) {
+                       CONF_ITEM *subci;
+                       char const *value = cf_pair_value(cp);
 
-                       /*
-                        *      Allow almost anything except "else"
-                        *      statements.  The normal processing
-                        *      falls through from "if" to "else", and
-                        *      we can't do that for redundant and
-                        *      load-balance sections.
-                        */
-                       if ((strcmp(name1, "else") == 0) ||
-                           (strcmp(name1, "elsif") == 0)) {
-                               cf_log_err(ci, "%s sections cannot contain a \"%s\" statement",
-                                      name, name1);
+                       if (!value) {
+                               cf_log_err(csi, "Missing reference string");
                                return false;
                        }
+
+                       subci = cf_reference_item(cs, cf_root(cf_section_to_item(action_cs)), value);
+                       if (!subci) {
+                               cf_log_perr(csi, "Failed finding reference '%s'", value);
+                               return false;
+                       }
+
+                       if (!compile_retry_section(actions, subci)) return false;
                        continue;
                }
 
-               if (cf_item_is_pair(ci)) {
-                       CONF_PAIR *cp = cf_item_to_pair(ci);
+               if (!compile_action_pair(actions, cp)) {
+                       return false;
+               }
+       }
 
-                       if (cf_pair_operator(cp) == T_OP_CMP_TRUE) return true;
+       if (module_retry) {
+               if (!fr_time_delta_ispos(actions->retry.irt)) {
+                       cf_log_err(csi, "initial_rtx_time MUST be non-zero for modules which support retries.");
+                       return false;
+               }
+       } else {
+               if (fr_time_delta_ispos(actions->retry.irt)) {
+                       cf_log_err(csi, "initial_rtx_time MUST be zero, as only max_rtx_count and max_rtx_duration are used.");
+                       return false;
+               }
 
-                       if (cf_pair_value(cp) != NULL) {
-                               cf_log_err(cp, "Unknown keyword '%s', or invalid location", cf_pair_attr(cp));
-                               return false;
-                       }
+               if (!actions->retry.mrc && !fr_time_delta_ispos(actions->retry.mrd)) {
+                       disallow_retry_action = true;
                }
        }
 
-       return true;
-}
-
+       /*
+        *      Sanity check that "fail = retry", we actually have a
+        *      retry section.
+        */
+       for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
+               if (actions->actions[i] != MOD_ACTION_RETRY) continue;
 
-static unlang_t *compile_redundant(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION                    *cs = cf_item_to_section(ci);
-       unlang_t                        *c;
+               if (module_retry) {
+                       cf_log_err(csi, "Cannot use a '%s = retry' action for a module which has its own retries",
+                                  fr_table_str_by_value(mod_rcode_table, i, "<INVALID>"));
+                       return false;
+               }
 
-       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+               if (disallow_retry_action) {
+                       cf_log_err(csi, "max_rtx_count and max_rtx_duration cannot both be zero when using '%s = retry'",
+                                  fr_table_str_by_value(mod_rcode_table, i, "<INVALID>"));
+                       return false;
+               }
 
-       if (!validate_limited_subsection(cs, cf_section_name1(cs))) {
-               return NULL;
+               if (!fr_time_delta_ispos(actions->retry.irt) &&
+                   !actions->retry.mrc &&
+                   !fr_time_delta_ispos(actions->retry.mrd)) {
+                       cf_log_err(csi, "Cannot use a '%s = retry' action without a 'retry { ... }' section.",
+                                  fr_table_str_by_value(mod_rcode_table, i, "<INVALID>"));
+                       return false;
+               }
        }
 
-       c = compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_REDUNDANT);
-       if (!c) return NULL;
+       return true;
+}
+
+unlang_t *unlang_compile_empty(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type)
+{
+       unlang_group_t *g;
+       unlang_t *c;
 
        /*
-        *      We no longer care if "redundant" sections have a name.  If they do, it's ignored.
+        *      If we're compiling an empty section, then the
+        *      *interpreter* type is GROUP, even if the *debug names*
+        *      are something else.
         */
+       g = unlang_group_allocate(parent, cs, type);
+       if (!g) return NULL;
+
+       c = unlang_group_to_generic(g);
+       if (!cs) {
+               c->name = unlang_ops[type].name;
+               c->debug_name = c->name;
+
+       } else {
+               char const *name2;
+
+               name2 = cf_section_name2(cs);
+               if (!name2) {
+                       c->name = cf_section_name1(cs);
+                       c->debug_name = c->name;
+               } else {
+                       c->name = name2;
+                       c->debug_name = talloc_typed_asprintf(c, "%s %s", cf_section_name1(cs), name2);
+               }
+       }
 
+       unlang_compile_action_defaults(c, unlang_ctx);
        return c;
 }
 
-static unlang_t *compile_load_balance_subsection(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs,
-                                                unlang_type_t type)
+
+static unlang_t *compile_item(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM *ci);
+
+/*
+ *     compile 'actions { ... }' inside of another group.
+ */
+static bool compile_action_subsection(unlang_t *c, CONF_SECTION *cs, CONF_SECTION *subcs)
 {
-       char const                      *name2;
-       unlang_t                        *c;
-       unlang_group_t                  *g;
-       unlang_load_balance_t           *gext;
+       CONF_ITEM *ci, *next;
 
-       tmpl_rules_t                    t_rules;
+       ci = cf_section_to_item(subcs);
 
-       /*
-        *      We allow unknown attributes here.
-        */
-       t_rules = *(unlang_ctx->rules);
-       t_rules.attr.allow_unknown = true;
-       RULES_VERIFY(&t_rules);
+       next = cf_item_next(cs, ci);
+       if (next && (cf_item_is_pair(next) || cf_item_is_section(next))) {
+               cf_log_err(ci, "'actions' MUST be the last block in a section");
+               return false;
+       }
+
+       if (cf_section_name2(subcs) != NULL) {
+               cf_log_err(ci, "Invalid name for 'actions' section");
+               return false;
+       }
 
        /*
-        *      No children?  Die!
+        *      Over-riding the actions can be done in certain limited
+        *      situations.  In other situations (e.g. "redundant",
+        *      "load-balance"), it doesn't make sense.
+        *
+        *      Note that this limitation also applies to "retry"
+        *      timers.  We can do retries of a "group".  We cannot do
+        *      retries of "load-balance", as the "load-balance"
+        *      section already takes care of redundancy.
+        *
+        *      We may need to loosen that limitation in the future.
         */
-       if (!cf_item_next(cs, NULL)) {
-               cf_log_err(cs, "%s sections cannot be empty", unlang_ops[type].name);
-               return NULL;
+       switch (c->type) {
+       case UNLANG_TYPE_CASE:
+       case UNLANG_TYPE_IF:
+       case UNLANG_TYPE_ELSE:
+       case UNLANG_TYPE_ELSIF:
+       case UNLANG_TYPE_FOREACH:
+       case UNLANG_TYPE_GROUP:
+       case UNLANG_TYPE_LIMIT:
+       case UNLANG_TYPE_SWITCH:
+       case UNLANG_TYPE_TIMEOUT:
+       case UNLANG_TYPE_TRANSACTION:
+               break;
+
+       default:
+               cf_log_err(ci, "'actions' MUST NOT be in a '%s' block", unlang_ops[c->type].name);
+               return false;
        }
 
-       if (!validate_limited_subsection(cs, cf_section_name1(cs))) return NULL;
+       return unlang_compile_actions(&c->actions, subcs, false);
+}
 
-       c = compile_section(parent, unlang_ctx, cs, type);
-       if (!c) return NULL;
 
-       g = unlang_generic_to_group(c);
+unlang_t *unlang_compile_children(unlang_group_t *g, unlang_compile_ctx_t *unlang_ctx_in, bool set_action_defaults)
+{
+       CONF_ITEM       *ci = NULL;
+       unlang_t        *c, *single;
+       bool            was_if = false;
+       char const      *skip_else = NULL;
+       unlang_compile_ctx_t *unlang_ctx;
+       unlang_compile_ctx_t unlang_ctx2;
+       tmpl_rules_t    t_rules;
+
+       c = unlang_group_to_generic(g);
 
        /*
-        *      Allow for keyed load-balance / redundant-load-balance sections.
+        *      Create our own compilation context which can be edited
+        *      by a variable definition.
         */
-       name2 = cf_section_name2(cs);
+       unlang_compile_ctx_copy(&unlang_ctx2, unlang_ctx_in);
+       unlang_ctx = &unlang_ctx2;
+       t_rules = *unlang_ctx_in->rules;
 
        /*
-        *      Inside of the "modules" section, it's a virtual
-        *      module.  The name is a module name, not a key.
+        *      Loop over the children of this group.
         */
-       if (name2) {
-               if (strcmp(cf_section_name1(cf_item_to_section(cf_parent(cs))), "modules") == 0) name2 = NULL;
-       }
-
-       if (name2) {
-               fr_token_t quote;
-               ssize_t slen;
-
-               /*
-                *      Create the template.  All attributes and xlats are
-                *      defined by now.
-                */
-               quote = cf_section_name2_quote(cs);
-               gext = unlang_group_to_load_balance(g);
-               slen = tmpl_afrom_substr(gext, &gext->vpt,
-                                        &FR_SBUFF_IN(name2, strlen(name2)),
-                                        quote,
-                                        NULL,
-                                        &t_rules);
-               if (!gext->vpt) {
-                       cf_canonicalize_error(cs, slen, "Failed parsing argument", name2);
-                       talloc_free(g);
-                       return NULL;
-               }
-
-               fr_assert(gext->vpt != NULL);
+       while ((ci = cf_item_next(g->cs, ci))) {
+               if (cf_item_is_data(ci)) continue;
 
                /*
-                *      Fixup the templates
+                *      Sections are keywords, or references to
+                *      modules with updated return codes.
                 */
-               if (!pass2_fixup_tmpl(g, &gext->vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
-                       talloc_free(g);
-                       return NULL;
-               }
-
-               switch (gext->vpt->type) {
-               default:
-                       cf_log_err(cs, "Invalid type in '%s': data will not result in a load-balance key", name2);
-                       talloc_free(g);
-                       return NULL;
+               if (cf_item_is_section(ci)) {
+                       char const *name = NULL;
+                       CONF_SECTION *subcs = cf_item_to_section(ci);
 
                        /*
-                        *      Allow only these ones.
+                        *      Skip precompiled blocks.  This is
+                        *      mainly for policies.
                         */
-               case TMPL_TYPE_XLAT:
-               case TMPL_TYPE_ATTR:
-               case TMPL_TYPE_EXEC:
-                       break;
-               }
-       }
-
-       return c;
-}
-
-static unlang_t *compile_load_balance(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       return compile_load_balance_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_LOAD_BALANCE);
-}
-
-
-static unlang_t *compile_redundant_load_balance(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       return compile_load_balance_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_REDUNDANT_LOAD_BALANCE);
-}
-
-static unlang_t *compile_parallel(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION                    *cs = cf_item_to_section(ci);
-       unlang_t                        *c;
-       char const                      *name2;
-
-       unlang_group_t                  *g;
-       unlang_parallel_t               *gext;
-
-       bool                            clone = true;
-       bool                            detach = false;
+                       if (cf_data_find(subcs, unlang_group_t, NULL)) continue;
 
-       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+                       /*
+                        *      "actions" apply to the current group.
+                        *      It's not a subgroup.
+                        */
+                       name = cf_section_name1(subcs);
 
-       /*
-        *      Parallel sections can create empty children, if the
-        *      admin demands it.  Otherwise, the principle of least
-        *      surprise is to copy the whole request, reply, and
-        *      config items.
-        */
-       name2 = cf_section_name2(cs);
-       if (name2) {
-               if (strcmp(name2, "empty") == 0) {
-                       clone = false;
+                       /*
+                        *      In-line attribute editing.  Nothing else in the parse has list assignments, so this must be it.
+                        */
+                       if (fr_list_assignment_op[cf_section_name2_quote(subcs)]) {
+                               single = compile_edit_section(c, unlang_ctx, subcs);
+                               if (!single) {
+                                       talloc_free(c);
+                                       return NULL;
+                               }
 
-               } else if (strcmp(name2, "detach") == 0) {
-                       detach = true;
+                               goto add_child;
+                       }
 
-               } else {
-                       cf_log_err(cs, "Invalid argument '%s'", name2);
-                       cf_log_err(ci, DOC_KEYWORD_REF(parallel));
-                       return NULL;
-               }
+                       if (strcmp(name, "actions") == 0) {
+                               if (!compile_action_subsection(c, g->cs, subcs)) {
+                                       talloc_free(c);
+                                       return NULL;
+                               }
 
-       }
+                               continue;
+                       }
 
-       /*
-        *      We can do "if" in parallel with other "if", but we
-        *      cannot do "else" in parallel with "if".
-        */
-       if (!validate_limited_subsection(cs, cf_section_name1(cs))) {
-               return NULL;
-       }
+                       /*
+                        *      Special checks for "else" and "elsif".
+                        */
+                       if ((strcmp(name, "else") == 0) || (strcmp(name, "elsif") == 0)) {
+                               /*
+                                *      We ran into one without a preceding "if" or "elsif".
+                                *      That's not allowed.
+                                */
+                               if (!was_if) {
+                                       cf_log_err(ci, "Invalid location for '%s'.  There is no preceding "
+                                                  "'if' or 'elsif' statement", name);
+                                       talloc_free(c);
+                                       return NULL;
+                               }
 
-       c = compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_PARALLEL);
-       if (!c) return NULL;
+                               /*
+                                *      There was a previous "if" or "elsif" which was always taken.
+                                *      So we skip this "elsif" or "else".
+                                */
+                               if (skip_else) {
+                                       void *ptr;
 
-       g = unlang_generic_to_group(c);
-       gext = unlang_group_to_parallel(g);
-       gext->clone = clone;
-       gext->detach = detach;
+                                       /*
+                                        *      And manually free this.
+                                        */
+                                       ptr = cf_data_remove(subcs, xlat_exp_head_t, NULL);
+                                       talloc_free(ptr);
 
-       return c;
-}
+                                       cf_section_free_children(subcs);
 
-static unlang_t *compile_subrequest(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
-{
-       CONF_SECTION                    *cs = cf_item_to_section(ci);
-       char const                      *name2;
+                                       cf_log_debug_prefix(ci, "Skipping contents of '%s' due to previous "
+                                                           "'%s' being always being taken.",
+                                                           name, skip_else);
+                                       continue;
+                               }
+                       }
 
-       unlang_t                        *c;
+                       /*
+                        *      Otherwise it's a real keyword.
+                        */
+                       single = compile_item(c, unlang_ctx, ci);
+                       if (!single) {
+                               cf_log_err(ci, "Failed to parse \"%s\" subsection", cf_section_name1(subcs));
+                               talloc_free(c);
+                               return NULL;
+                       }
 
-       unlang_group_t                  *g;
-       unlang_subrequest_t             *gext;
+                       goto add_child;
 
-       unlang_compile_t                unlang_ctx2;
+               } else if (cf_item_is_pair(ci)) {
+                       CONF_PAIR *cp = cf_item_to_pair(ci);
 
-       tmpl_rules_t                    t_rules;
-       fr_dict_autoload_talloc_t       *dict_ref = NULL;
+                       /*
+                        *      Variable definition.
+                        */
+                       if (cf_pair_operator(cp) == T_OP_CMP_TRUE) {
+                               if (compile_variable(c, unlang_ctx, cp, &t_rules) < 0) {
+                                       talloc_free(c);
+                                       return NULL;
+                               }
 
-       fr_dict_t const                 *dict;
-       fr_dict_attr_t const            *da = NULL;
-       fr_dict_enum_value_t const      *type_enum = NULL;
+                               single = UNLANG_IGNORE;
+                               goto add_child;
+                       }
 
-       ssize_t                         slen;
-       char                            *namespace = NULL;
-       char const                      *packet_name = NULL;
+                       if (!cf_pair_value(cp)) {
+                               single = compile_item(c, unlang_ctx, ci);
+                               if (!single) {
+                                       cf_log_err(ci, "Invalid keyword \"%s\".", cf_pair_attr(cp));
+                                       talloc_free(c);
+                                       return NULL;
+                               }
 
-       tmpl_t                          *vpt = NULL, *src_vpt = NULL, *dst_vpt = NULL;
+                               goto add_child;
+                       }
 
-       /*
-        *      subrequest { ... }
-        *
-        *      Create a subrequest which is of the same dictionary
-        *      and packet type as the current request.
-        *
-        *      We assume that the Packet-Type attribute exists.
-        */
-       name2 = cf_section_name2(cs);
-       if (!name2) {
-               dict = unlang_ctx->rules->attr.dict_def;
-               packet_name = name2 = unlang_ctx->section_name2;
-               goto get_packet_type;
-       }
+                       /*
+                        *      What remains MUST be an edit pair.  At this point, the state of the compiler
+                        *      tells us what it is, and we don't really care if there's a leading '&'.
+                        */
+                       single = compile_edit_pair(c, unlang_ctx, cp);
+                       if (!single) {
+                               talloc_free(c);
+                               return NULL;
+                       }
 
-       if (cf_section_name2_quote(cs) != T_BARE_WORD) {
-               cf_log_err(cs, "The arguments to 'subrequest' must be a name or an attribute reference");
-       print_url:
-               cf_log_err(ci, DOC_KEYWORD_REF(subrequest));
-               return NULL;
-       }
+                       goto add_child;
+               } else {
+                       cf_log_err(ci, "Asked to compile unknown conf type");
+                       talloc_free(c);
+                       return NULL;
+               }
 
-       dict = unlang_ctx->rules->attr.dict_def;
+       add_child:
+               if (single == UNLANG_IGNORE) continue;
 
-       /*
-        *      @foo is "dictionary foo", as with references in the dictionaries.
-        *
-        *      @foo::bar is "dictionary foo, Packet-Type = ::bar"
-        *
-        *      foo::bar is "dictionary foo, Packet-Type = ::bar"
-        *
-        *      ::bar is "this dictionary, Packet-Type = ::bar", BUT
-        *      we don't try to parse the new dictionary name, as it
-        *      doesn't exist.
-        */
-       if ((name2[0] == '@') ||
-           ((name2[0] != ':') && (name2[0] != '&') && (strchr(name2 + 1, ':') != NULL))) {
-               char *q;
+               /*
+                *      Do optimizations for "if" and "elsif"
+                *      conditions.
+                */
+               switch (single->type) {
+               case UNLANG_TYPE_ELSIF:
+               case UNLANG_TYPE_IF:
+                       was_if = true;
 
-               if (name2[0] == '@') name2++;
+                       {
+                               unlang_group_t  *f;
+                               unlang_cond_t   *gext;
 
-               MEM(namespace = talloc_strdup(parent, name2));
-               q = namespace;
+                               /*
+                                *      Skip else, and/or omit things which will never be run.
+                                */
+                               f = unlang_generic_to_group(single);
+                               gext = unlang_group_to_cond(f);
 
-               while (fr_dict_attr_allowed_chars[(unsigned int) *q]) {
-                       q++;
-               }
-               *q = '\0';
-
-               dict = fr_dict_by_protocol_name(namespace);
-               if (!dict) {
-                       dict_ref = fr_dict_autoload_talloc(NULL, &dict, namespace);
-                       if (!dict_ref) {
-                               cf_log_err(cs, "Unknown namespace in '%s'", name2);
-                               talloc_free(namespace);
-                               return NULL;
+                               if (gext->is_truthy) {
+                                       if (gext->value) {
+                                               skip_else = single->debug_name;
+                                       } else {
+                                               /*
+                                                *      The condition never
+                                                *      matches, so we can
+                                                *      avoid putting it into
+                                                *      the unlang tree.
+                                                */
+                                               talloc_free(single);
+                                               continue;
+                                       }
+                               }
                        }
+                       break;
+
+               default:
+                       was_if = false;
+                       skip_else = NULL;
+                       break;
                }
 
                /*
-                *      Skip the dictionary name, and go to the thing
-                *      right after it.
+                *      unlang_group_t is grown by adding a unlang_t to the end
                 */
-               name2 += (q - namespace);
-               TALLOC_FREE(namespace);
-       }
-
-       /*
-        *      @dict::enum is "other dictionary, Packet-Type = ::enum"
-        *      ::enum is this dictionary, "Packet-Type = ::enum"
-        */
-       if ((name2[0] == ':') && (name2[1] == ':')) {
-               packet_name = name2;
-               goto get_packet_type;
-       }
-
-       /*
-        *      Can't do foo.bar.baz::foo, the enums are only used for Packet-Type.
-        */
-       if (strchr(name2, ':') != NULL) {
-               cf_log_err(cs, "Reference cannot contain enum value in '%s'", name2);
-               return NULL;
-       }
-
-       /*
-        *      '&' means "attribute reference"
-        *
-        *      Or, bare word an require_enum_prefix means "attribute reference".
-        */
-       slen = tmpl_afrom_attr_substr(parent, NULL, &vpt,
-                                     &FR_SBUFF_IN(name2, talloc_array_length(name2) - 1),
-                                     NULL, unlang_ctx->rules);
-       if (slen <= 0) {
-               cf_log_perr(cs, "Invalid argument to 'subrequest', failed parsing packet-type");
-               goto print_url;
-       }
-
-       fr_assert(tmpl_is_attr(vpt));
-
-       /*
-        *      Anything resembling an integer or string is
-        *      OK.  Nothing else makes sense.
-        */
-       switch (tmpl_attr_tail_da(vpt)->type) {
-       case FR_TYPE_INTEGER_EXCEPT_BOOL:
-       case FR_TYPE_STRING:
-               break;
-
-       default:
-               talloc_free(vpt);
-               cf_log_err(cs, "Invalid data type for attribute %s.  "
-                          "Must be an integer type or string", name2 + 1);
-               goto print_url;
-       }
-
-       dict = unlang_ctx->rules->attr.dict_def;
-       packet_name = NULL;
-
-get_packet_type:
-       /*
-        *      Local attributes cannot be used in a subrequest.  They belong to the parent.  Local attributes
-        *      are NOT copied to the subrequest.
-        *
-        *      @todo - maybe we want to copy local variables, too?  But there may be multiple nested local
-        *      variables, each with their own dictionary.
-        */
-       dict = fr_dict_proto_dict(dict);
+               fr_assert(g == talloc_parent(single));
+               fr_assert(single->parent == unlang_group_to_generic(g));
+               fr_assert(!single->next);
 
-       /*
-        *      Use dict name instead of "namespace", because "namespace" can be omitted.
-        */
-       da = fr_dict_attr_by_name(NULL, fr_dict_root(dict), "Packet-Type");
-       if (!da) {
-               cf_log_err(cs, "No such attribute 'Packet-Type' in namespace '%s'", fr_dict_root(dict)->name);
-       error:
-               talloc_free(namespace);
-               talloc_free(vpt);
-               talloc_free(dict_ref);
-               goto print_url;
-       }
+               *g->tail = single;
+               g->tail = &single->next;
+               g->num_children++;
 
-       if (packet_name) {
                /*
-                *      Allow ::enum-name for packet types
+                *      If it's not possible to execute statement
+                *      after the current one, then just stop
+                *      processing the children.
                 */
-               if ((packet_name[0] == ':') && (packet_name[1] == ':')) packet_name += 2;
-
-               type_enum = fr_dict_enum_by_name(da, packet_name, -1);
-               if (!type_enum) {
-                       cf_log_err(cs, "No such value '%s' for attribute 'Packet-Type' in namespace '%s'",
-                                  packet_name, fr_dict_root(dict)->name);
-                       goto error;
+               if (g->self.closed) {
+                       cf_log_warn(ci, "Skipping remaining instructions due to '%s'",
+                                   single->name);
+                       break;
                }
        }
 
        /*
-        *      No longer needed
-        */
-       talloc_free(namespace);
-
-       /*
-        *      Source and destination arguments
+        *      Set the default actions, if they haven't already been
+        *      set by an "actions" section above.
         */
-       {
-               char const      *dst, *src;
-
-               src = cf_section_argv(cs, 0);
-               if (src) {
-                       RULES_VERIFY(unlang_ctx->rules);
-
-                       (void) tmpl_afrom_substr(parent, &src_vpt,
-                                                &FR_SBUFF_IN(src, talloc_array_length(src) - 1),
-                                                cf_section_argv_quote(cs, 0), NULL, unlang_ctx->rules);
-                       if (!src_vpt) {
-                               cf_log_perr(cs, "Invalid argument to 'subrequest', failed parsing src");
-                               goto error;
-                       }
-
-                       if (!tmpl_contains_attr(src_vpt)) {
-                               cf_log_err(cs, "Invalid argument to 'subrequest' src must be an attr or list, got %s",
-                                          tmpl_type_to_str(src_vpt->type));
-                               talloc_free(src_vpt);
-                               goto error;
-                       }
+       if (set_action_defaults) unlang_compile_action_defaults(c, unlang_ctx);
 
-                       dst = cf_section_argv(cs, 1);
-                       if (dst) {
-                               RULES_VERIFY(unlang_ctx->rules);
-
-                               (void) tmpl_afrom_substr(parent, &dst_vpt,
-                                                        &FR_SBUFF_IN(dst, talloc_array_length(dst) - 1),
-                                                        cf_section_argv_quote(cs, 1), NULL, unlang_ctx->rules);
-                               if (!dst_vpt) {
-                                       cf_log_perr(cs, "Invalid argument to 'subrequest', failed parsing dst");
-                                       goto error;
-                               }
+       return c;
+}
 
-                               if (!tmpl_contains_attr(dst_vpt)) {
-                                       cf_log_err(cs, "Invalid argument to 'subrequest' dst must be an "
-                                                  "attr or list, got %s",
-                                                  tmpl_type_to_str(src_vpt->type));
-                                       talloc_free(src_vpt);
-                                       talloc_free(dst_vpt);
-                                       goto error;
-                               }
-                       }
-               }
-       }
 
-       if (!cf_item_next(cs, NULL)) {
-               talloc_free(vpt);
-               talloc_free(src_vpt);
-               talloc_free(dst_vpt);
-               return UNLANG_IGNORE;
-       }
+/*
+ *     Generic "compile a section with more unlang inside of it".
+ */
+unlang_t *unlang_compile_section(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type)
+{
+       unlang_group_t  *g;
+       unlang_t        *c;
+       char const      *name1, *name2;
 
-       t_rules = *unlang_ctx->rules;
-       t_rules.parent = unlang_ctx->rules;
-       t_rules.attr.dict_def = dict;
-       t_rules.attr.allow_foreign = false;
+       fr_assert(unlang_ctx->rules != NULL);
+       fr_assert(unlang_ctx->rules->attr.list_def);
 
        /*
-        *      Copy over the compilation context.  This is mostly
-        *      just to ensure that retry is handled correctly.
-        *      i.e. reset.
+        *      We always create a group, even if the section is empty.
         */
-       compile_copy_context(&unlang_ctx2, unlang_ctx);
+       g = unlang_group_allocate(parent, cs, type);
+       if (!g) return NULL;
 
-       /*
-        *      Then over-write the new compilation context.
-        */
-       unlang_ctx2.section_name1 = "subrequest";
-       unlang_ctx2.section_name2 = name2;
-       unlang_ctx2.rules = &t_rules;
+       c = unlang_group_to_generic(g);
 
        /*
-        *      Compile the subsection with a *different* default dictionary.
+        *      Remember the name for printing, etc.
         */
-       c = compile_section(parent, &unlang_ctx2, cs, UNLANG_TYPE_SUBREQUEST);
-       if (!c) return NULL;
+       name1 = cf_section_name1(cs);
+       name2 = cf_section_name2(cs);
+       c->name = name1;
 
        /*
-        *      Set the dictionary and packet information, which tells
-        *      unlang_subrequest() how to process the request.
+        *      Make sure to tell the user that we're running a
+        *      policy, and not anything else.
         */
-       g = unlang_generic_to_group(c);
-       gext = unlang_group_to_subrequest(g);
+       if (type == UNLANG_TYPE_POLICY) {
+               MEM(c->debug_name = talloc_typed_asprintf(c, "policy %s", name1));
 
-       if (dict_ref) {
-               /*
-                *      Parent the dictionary reference correctly now that we
-                *      have the section with the dependency.  This should
-                *      be fast as dict_ref has no siblings.
-                */
-               talloc_steal(gext, dict_ref);
-       }
-       if (vpt) gext->vpt = talloc_steal(gext, vpt);
+       } else if (!name2) {
+               c->debug_name = c->name;
 
-       gext->dict = dict;
-       gext->attr_packet_type = da;
-       gext->type_enum = type_enum;
-       gext->src = src_vpt;
-       gext->dst = dst_vpt;
+       } else {
+               MEM(c->debug_name = talloc_typed_asprintf(c, "%s %s", name1, name2));
+       }
 
-       return c;
+       return unlang_compile_children(g, unlang_ctx, true);
 }
 
 
-static unlang_t *compile_call(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
+static unlang_t *compile_tmpl(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM *ci)
 {
-       CONF_SECTION                    *cs = cf_item_to_section(ci);
-       virtual_server_t const          *vs;
-       unlang_t                        *c;
-
-       unlang_group_t                  *g;
-       unlang_call_t                   *gext;
-
-       fr_token_t                      type;
-       char const                      *server;
-       CONF_SECTION                    *server_cs;
-       fr_dict_t const                 *dict;
-       fr_dict_attr_t const            *attr_packet_type;
-
-       server = cf_section_name2(cs);
-       if (!server) {
-               cf_log_err(cs, "You MUST specify a server name for 'call <server> { ... }'");
-       print_url:
-               cf_log_err(ci, DOC_KEYWORD_REF(call));
-               return NULL;
-       }
-
-       type = cf_section_name2_quote(cs);
-       if (type != T_BARE_WORD) {
-               cf_log_err(cs, "The arguments to 'call' cannot be a quoted string or a dynamic value");
-               goto print_url;
-       }
-
-       vs = virtual_server_find(server);
-       if (!vs) {
-               cf_log_err(cs, "Unknown virtual server '%s'", server);
-               return NULL;
-       }
-
-       server_cs = virtual_server_cs(vs);
+       CONF_PAIR       *cp = cf_item_to_pair(ci);
+       unlang_t        *c;
+       unlang_tmpl_t   *ut;
+       ssize_t         slen;
+       char const      *p = cf_pair_attr(cp);
+       tmpl_t          *vpt;
 
-       /*
-        *      The dictionaries are not compatible, forbid it.
-        */
-       dict = virtual_server_dict_by_name(server);
-       if (!dict) {
-               cf_log_err(cs, "Cannot call virtual server '%s', failed retrieving its namespace",
-                          server);
-               return NULL;
-       }
-       if ((dict != fr_dict_internal()) && fr_dict_internal() &&
-           unlang_ctx->rules->attr.dict_def && !fr_dict_compatible(unlang_ctx->rules->attr.dict_def, dict)) {
-               cf_log_err(cs, "Cannot call server %s with namespace '%s' from namespaces '%s' - they have incompatible protocols",
-                          server, fr_dict_root(dict)->name, fr_dict_root(unlang_ctx->rules->attr.dict_def)->name);
-               return NULL;
-       }
+       MEM(ut = talloc_zero(parent, unlang_tmpl_t));
+       c = unlang_tmpl_to_generic(ut);
+       c->parent = parent;
+       c->next = NULL;
+       c->name = p;
+       c->debug_name = c->name;
+       c->type = UNLANG_TYPE_TMPL;
+       c->ci = CF_TO_ITEM(cp);
 
-       attr_packet_type = fr_dict_attr_by_name(NULL, fr_dict_root(dict), "Packet-Type");
-       if (!attr_packet_type) {
-               cf_log_err(cs, "Cannot call server %s with namespace '%s' - it has no Packet-Type attribute",
-                          server, fr_dict_root(dict)->name);
+       RULES_VERIFY(unlang_ctx->rules);
+       slen = tmpl_afrom_substr(ut, &vpt,
+                                &FR_SBUFF_IN(p, talloc_array_length(p) - 1),
+                                cf_pair_attr_quote(cp),
+                                NULL,
+                                unlang_ctx->rules);
+       if (!vpt) {
+               cf_canonicalize_error(cp, slen, "Failed parsing expansion", p);
+               talloc_free(ut);
                return NULL;
        }
+       ut->tmpl = vpt; /* const issues */
 
-       c = compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_CALL);
-       if (!c) return NULL;
-
-       /*
-        *      Set the virtual server name, which tells unlang_call()
-        *      which virtual server to call.
-        */
-       g = unlang_generic_to_group(c);
-       gext = unlang_group_to_call(g);
-       gext->server_cs = server_cs;
-       gext->attr_packet_type = attr_packet_type;
-
+       unlang_compile_action_defaults(c, unlang_ctx);
        return c;
 }
 
-
-static unlang_t *compile_caller(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci)
+/*
+ *     redundant, load-balance and parallel have limits on what can
+ *     go in them.
+ */
+bool unlang_compile_limit_subsection(CONF_SECTION *cs, char const *name)
 {
-       CONF_SECTION                    *cs = cf_item_to_section(ci);
-       unlang_t                        *c;
-
-       unlang_group_t                  *g;
-       unlang_caller_t                 *gext;
-
-       fr_token_t                      type;
-       char const                      *name;
-       fr_dict_t const                 *dict;
-       unlang_compile_t                unlang_ctx2;
-       tmpl_rules_t                    parent_rules, t_rules;
-
-       fr_dict_autoload_talloc_t       *dict_ref = NULL;
-
-       name = cf_section_name2(cs);
-       if (!name) {
-               cf_log_err(cs, "You MUST specify a protocol name for 'caller <protocol> { ... }'");
-       print_url:
-               cf_log_err(ci, DOC_KEYWORD_REF(caller));
-               return NULL;
-       }
+       CONF_ITEM *ci;
 
-       type = cf_section_name2_quote(cs);
-       if (type != T_BARE_WORD) {
-               cf_log_err(cs, "The argument to 'caller' cannot be a quoted string or a dynamic value");
-               goto print_url;
-       }
+       for (ci=cf_item_next(cs, NULL);
+            ci != NULL;
+            ci=cf_item_next(cs, ci)) {
+               /*
+                *      If we're a redundant, etc. group, then the
+                *      intention is to call modules, rather than
+                *      processing logic.  These checks aren't
+                *      *strictly* necessary, but they keep the users
+                *      from doing crazy things.
+                */
+               if (cf_item_is_section(ci)) {
+                       CONF_SECTION *subcs = cf_item_to_section(ci);
+                       char const *name1 = cf_section_name1(subcs);
 
-       dict = fr_dict_by_protocol_name(name);
-       if (!dict) {
-               dict_ref = fr_dict_autoload_talloc(NULL, &dict, name);
-               if (!dict_ref) {
-                       cf_log_perr(cs, "Unknown protocol '%s'", name);
-                       goto print_url;
+                       /*
+                        *      Allow almost anything except "else"
+                        *      statements.  The normal processing
+                        *      falls through from "if" to "else", and
+                        *      we can't do that for redundant and
+                        *      load-balance sections.
+                        */
+                       if ((strcmp(name1, "else") == 0) ||
+                           (strcmp(name1, "elsif") == 0)) {
+                               cf_log_err(ci, "%s sections cannot contain a \"%s\" statement",
+                                      name, name1);
+                               return false;
+                       }
+                       continue;
                }
-       }
 
-       /*
-        *      Create a new parent context with the new dictionary.
-        */
-       memcpy(&parent_rules, unlang_ctx->rules, sizeof(parent_rules));
-       memcpy(&t_rules, unlang_ctx->rules, sizeof(t_rules));
-       parent_rules.attr.dict_def = dict;
-       t_rules.parent = &parent_rules;
-
-       /*
-        *      We don't want to modify the context we were passed, so
-        *      we just clone it
-        */
-       memcpy(&unlang_ctx2, unlang_ctx, sizeof(unlang_ctx2));
-       unlang_ctx2.rules = &t_rules;
-       unlang_ctx2.section_name1 = "caller";
-       unlang_ctx2.section_name2 = name;
-
-       c = compile_section(parent, &unlang_ctx2, cs, UNLANG_TYPE_CALLER);
-       if (!c) {
-               talloc_free(dict_ref);
-               return NULL;
-       }
-
-       /*
-        *      Set the virtual server name, which tells unlang_call()
-        *      which virtual server to call.
-        */
-       g = unlang_generic_to_group(c);
-       gext = unlang_group_to_caller(g);
-       gext->dict = dict;
+               if (cf_item_is_pair(ci)) {
+                       CONF_PAIR *cp = cf_item_to_pair(ci);
 
-       if (dict_ref) {
-               /*
-                *      Parent the dictionary reference correctly now that we
-                *      have the section with the dependency.  This should
-                *      be fast as dict_ref has no siblings.
-                */
-               talloc_steal(gext, dict_ref);
-       }
+                       if (cf_pair_operator(cp) == T_OP_CMP_TRUE) return true;
 
-       if (!g->num_children) {
-               talloc_free(c);
-               return UNLANG_IGNORE;
+                       if (cf_pair_value(cp) != NULL) {
+                               cf_log_err(cp, "Unknown keyword '%s', or invalid location", cf_pair_attr(cp));
+                               return false;
+                       }
+               }
        }
 
-       return c;
+       return true;
 }
 
-static unlang_t *compile_function(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM *ci,
+
+static unlang_t *compile_function(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM *ci,
                                  CONF_SECTION *subcs,
                                  bool policy)
 {
-       unlang_compile_t                unlang_ctx2;
+       unlang_compile_ctx_t            unlang_ctx2;
        unlang_t                        *c;
 
        /*
@@ -4400,7 +1734,7 @@ static unlang_t *compile_function(unlang_t *parent, unlang_compile_t *unlang_ctx
                 *
                 *      group foo { ...
                 */
-               c = compile_section(parent, &unlang_ctx2, subcs,
+               c = unlang_compile_section(parent, &unlang_ctx2, subcs,
                                    policy ? UNLANG_TYPE_POLICY : UNLANG_TYPE_GROUP);
        }
        if (!c) return NULL;
@@ -4438,7 +1772,7 @@ static unlang_t *compile_function(unlang_t *parent, unlang_compile_t *unlang_ctx
  * @return the CONF_SECTION specifying the virtual module.
  */
 static CONF_SECTION *virtual_module_find_cs(CONF_ITEM *ci, UNUSED char const *real_name, char const *virtual_name,
-                                           char const *method_name, unlang_compile_t *unlang_ctx, bool *policy)
+                                           char const *method_name, unlang_compile_ctx_t *unlang_ctx, bool *policy)
 {
        CONF_SECTION *cs, *subcs, *conf_root;
        CONF_ITEM *loop;
@@ -4519,7 +1853,7 @@ check_for_loop:
        return subcs;
 }
 
-static unlang_t *compile_module(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM *ci, char const *name)
+static unlang_t *compile_module(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM *ci, char const *name)
 {
        unlang_t        *c;
        unlang_module_t *m;
@@ -4564,7 +1898,7 @@ static unlang_t *compile_module(unlang_t *parent, unlang_compile_t *unlang_ctx,
        /*
         *      Add in the default actions for this section.
         */
-       compile_action_defaults(c, unlang_ctx);
+       unlang_compile_action_defaults(c, unlang_ctx);
 
        /*
         *      Parse the method environment for this module / method
@@ -4605,48 +1939,25 @@ static unlang_t *compile_module(unlang_t *parent, unlang_compile_t *unlang_ctx,
        return c;
 }
 
-typedef unlang_t *(*unlang_op_compile_t)(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM const *ci);
-
-static fr_table_ptr_sorted_t unlang_section_keywords[] = {
-       { L("call"),            (void *) compile_call },
-       { L("caller"),          (void *) compile_caller },
-       { L("case"),            (void *) compile_case },
-       { L("catch"),           (void *) compile_catch },
-       { L("else"),            (void *) compile_else },
-       { L("elsif"),           (void *) compile_elsif },
-       { L("foreach"),         (void *) compile_foreach },
-       { L("group"),           (void *) compile_group },
-       { L("if"),              (void *) compile_if },
-       { L("limit"),           (void *) compile_limit },
-       { L("load-balance"),    (void *) compile_load_balance },
-       { L("map"),             (void *) compile_map },
-       { L("parallel"),        (void *) compile_parallel },
-       { L("redundant"),       (void *) compile_redundant },
-       { L("redundant-load-balance"), (void *) compile_redundant_load_balance },
-       { L("subrequest"),      (void *) compile_subrequest },
-       { L("switch"),          (void *) compile_switch },
-       { L("timeout"),         (void *) compile_timeout },
-       { L("transaction"),     (void *) compile_transaction },
-       { L("try"),             (void *) compile_try },
-       { L("update"),          (void *) compile_update },
-};
-static int unlang_section_keywords_len = NUM_ELEMENTS(unlang_section_keywords);
+extern int dict_attr_acopy_children(fr_dict_t *dict, fr_dict_attr_t *dst, fr_dict_attr_t const *src);
 
-static fr_table_ptr_sorted_t unlang_pair_keywords[] = {
-       { L("break"),           (void *) compile_break },
-       { L("continue"),        (void *) compile_continue },
-       { L("detach"),          (void *) compile_detach },
-       { L("return"),          (void *) compile_return },
-};
-static int unlang_pair_keywords_len = NUM_ELEMENTS(unlang_pair_keywords);
+static inline CC_HINT(always_inline) unlang_op_t const *name_to_op(char const *name)
+{
+       unlang_op_t const *op;
 
-extern int dict_attr_acopy_children(fr_dict_t *dict, fr_dict_attr_t *dst, fr_dict_attr_t const *src);
+       op = fr_hash_table_find(unlang_op_table, &(unlang_op_t) { .name = name });
+       if (op) return op;
+
+       DEBUG("NOT KEYWORD ::%s::", name);
+       return NULL;
+}
 
-static int define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rules_t *t_rules, fr_type_t type, char const *name,
+int unlang_define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rules_t *t_rules, fr_type_t type, char const *name,
                                 fr_dict_attr_t const *ref)
 {
        fr_dict_attr_t const *da;
        fr_slen_t len;
+       unlang_op_t const *op;
 
        fr_dict_attr_flags_t flags = {
                .internal = true,
@@ -4668,17 +1979,12 @@ static int define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rul
        /*
         *      No keyword section names.
         */
-       if (fr_table_value_by_str(unlang_section_keywords, name, NULL) != NULL) {
-       fail_unlang:
+       op = name_to_op(name);
+       if (op) {
                cf_log_err(ci, "Local variable '%s' cannot be an unlang keyword.", name);
                return -1;
        }
 
-       /*
-        *      No simple keyword names.
-        */
-       if (fr_table_value_by_str(unlang_pair_keywords, name, NULL) != NULL) goto fail_unlang;
-
        /*
         *      No protocol names.
         */
@@ -4741,26 +2047,33 @@ static int define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rul
 /*
  *     Compile one unlang instruction
  */
-static unlang_t *compile_item(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_ITEM *ci)
+static unlang_t *compile_item(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM *ci)
 {
        char const              *name, *p;
        CONF_SECTION            *cs, *subcs, *modules;
        char const              *realname;
-       unlang_compile_t        unlang_ctx2;
+       unlang_compile_ctx_t    unlang_ctx2;
        bool                    policy;
-       unlang_op_compile_t     compile;
        unlang_t                *c;
        bool                    ignore_notfound = false;
+       unlang_op_t const       *op;
 
        if (cf_item_is_section(ci)) {
                cs = cf_item_to_section(ci);
                name = cf_section_name1(cs);
+               op = name_to_op(name);
 
-               compile = (unlang_op_compile_t) fr_table_value_by_str(unlang_section_keywords, name, NULL);
-               if (compile) {
-                       unlang_op_t *op;
+               if (op) {
+                       /*
+                        *      Forbid pair keywords as section names,
+                        *      e.g. "break { ... }"
+                        */
+                       if ((op->flag & UNLANG_OP_FLAG_SINGLE_WORD) != 0) {
+                               cf_log_err(ci, "Syntax error after keyword '%s' - unexpected '{'", name);
+                               return NULL;
+                       }
 
-                       c = compile(parent, unlang_ctx, ci);
+                       c = op->compile(parent, unlang_ctx, ci);
                allocate_number:
                        if (!c) return NULL;
                        if (c == UNLANG_IGNORE) return UNLANG_IGNORE;
@@ -4783,18 +2096,11 @@ static unlang_t *compile_item(unlang_t *parent, unlang_compile_t *unlang_ctx, CO
                        return c;
                }
 
-               /*
-                *      Forbid pair keywords as section names, e.g. "break { ... }"
-                */
-               if (fr_table_value_by_str(unlang_pair_keywords, name, NULL) != NULL) {
-                       cf_log_err(ci, "Syntax error after keyword '%s' - unexpected '{'", name);
-                       return NULL;
-               }
-
                /* else it's something like sql { fail = 1 ...} */
                goto check_for_module;
 
        } else if (cf_item_is_pair(ci)) {
+
                /*
                 *      Else it's a module reference such as "sql", OR
                 *      one of the few bare keywords that we allow.
@@ -4802,6 +2108,15 @@ static unlang_t *compile_item(unlang_t *parent, unlang_compile_t *unlang_ctx, CO
                CONF_PAIR *cp = cf_item_to_pair(ci);
 
                name = cf_pair_attr(cp);
+               op = name_to_op(name);
+
+               /*
+                *      Forbid section keywords as pair names, e.g. bare "update"
+                */
+               if (op && ((op->flag & UNLANG_OP_FLAG_SINGLE_WORD) == 0)) {
+                       cf_log_err(ci, "Syntax error after keyword '%s' - missing '{'", name);
+                       return NULL;
+               }
 
                /*
                 *      We cannot have assignments or actions here.
@@ -4828,21 +2143,10 @@ static unlang_t *compile_item(unlang_t *parent, unlang_compile_t *unlang_ctx, CO
                        goto allocate_number;
                }
 
-               compile = (unlang_op_compile_t)fr_table_value_by_str(unlang_pair_keywords, name, NULL); /* Cast for -Wpedantic */
-               if (compile) {
-                       c = compile(parent, unlang_ctx, ci);
-                       goto allocate_number;
-               }
-
-               /*
-                *      Forbid section keywords as pair names, e.g. bare "update"
-                */
-               if (fr_table_value_by_str(unlang_section_keywords, name, NULL) != NULL) {
-                       cf_log_err(ci, "Syntax error after keyword '%s' - expected '{'", name);
-                       return NULL;
-               }
+               if (!op) goto check_for_module;
 
-               goto check_for_module;
+               c = op->compile(parent, unlang_ctx, ci);
+               goto allocate_number;
        } else {
                cf_log_err(ci, "Asked to compile unknown conf type");
                return NULL;    /* who knows what it is... */
@@ -4989,8 +2293,8 @@ int unlang_compile(virtual_server_t const *vs,
                rules = &my_rules;
        }
 
-       c = compile_section(NULL,
-                           &(unlang_compile_t){
+       c = unlang_compile_section(NULL,
+                           &(unlang_compile_ctx_t){
                                .vs = vs,
                                .section_name1 = cf_section_name1(cs),
                                .section_name2 = cf_section_name2(cs),
@@ -5025,9 +2329,7 @@ bool unlang_compile_is_keyword(const char *name)
 {
        if (!name || !*name) return false;
 
-       if (fr_table_value_by_str(unlang_section_keywords, name, NULL) != NULL) return true;
-
-       return (fr_table_value_by_str(unlang_pair_keywords, name, NULL) != NULL);
+       return (name_to_op(name) != 0);
 }
 
 /*
index 135cdd8506c0b65e42eec945bc1986c2487cdd14..206cf2a5cb75381e99d1d9dcce1e80b3aaf91603 100644 (file)
@@ -112,6 +112,157 @@ static unlang_action_t unlang_if(unlang_result_t *p_result, request_t *request,
        return UNLANG_ACTION_PUSHED_CHILD;
 }
 
+
+static const fr_sbuff_term_t if_terminals = FR_SBUFF_TERMS(
+       L(""),
+       L("{"),
+);
+
+static unlang_t *compile_if_subsection(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type)
+{
+       unlang_t                *c;
+
+       unlang_group_t          *g;
+       unlang_cond_t           *gext;
+       CONF_ITEM               *ci;
+
+       xlat_exp_head_t         *head = NULL;
+       bool                    is_truthy = false, value = false;
+       xlat_res_rules_t        xr_rules = {
+               .tr_rules = &(tmpl_res_rules_t) {
+                       .dict_def = unlang_ctx->rules->attr.dict_def,
+               },
+       };
+
+       if (!cf_section_name2(cs)) {
+               cf_log_err(cs, "'%s' without condition", unlang_ops[type].name);
+               return NULL;
+       }
+
+       /*
+        *      Migration support.
+        */
+       {
+               char const *name2 = cf_section_name2(cs);
+               ssize_t slen;
+
+               tmpl_rules_t t_rules = (tmpl_rules_t) {
+                       .parent = unlang_ctx->rules->parent,
+                       .attr = {
+                               .dict_def = xr_rules.tr_rules->dict_def,
+                               .list_def = request_attr_request,
+                               .allow_unresolved = false,
+                               .allow_unknown = false,
+                               .allow_wildcard = true,
+               },
+                       .literals_safe_for = unlang_ctx->rules->literals_safe_for,
+               };
+
+               fr_sbuff_parse_rules_t p_rules = { };
+
+               p_rules.terminals = &if_terminals;
+
+               slen = xlat_tokenize_condition(cs, &head, &FR_SBUFF_IN(name2, strlen(name2)), &p_rules, &t_rules);
+               if (slen == 0) {
+                       cf_canonicalize_error(cs, slen, "Empty conditions are invalid", name2);
+                       return NULL;
+               }
+
+               if (slen < 0) {
+                       slen++; /* fr_slen_t vs ssize_t */
+                       cf_canonicalize_error(cs, slen, "Failed parsing condition", name2);
+                       return NULL;
+               }
+
+               /*
+                *      Resolve the xlat first.
+                */
+               if (xlat_resolve(head, &xr_rules) < 0) {
+                       cf_log_err(cs, "Failed resolving condition - %s", fr_strerror());
+                       return NULL;
+               }
+
+               fr_assert(!xlat_needs_resolving(head));
+
+               is_truthy = xlat_is_truthy(head, &value);
+
+               /*
+                *      If the condition is always false, we don't compile the
+                *      children.
+                */
+               if (is_truthy && !value) {
+                       cf_log_debug_prefix(cs, "Skipping contents of '%s' as it is always 'false'",
+                                           unlang_ops[type].name);
+
+                       /*
+                        *      Free the children, which frees any xlats,
+                        *      conditions, etc. which were defined, but are
+                        *      now entirely unused.
+                        *
+                        *      However, we still need to cache the conditions, as they will be accessed at run-time.
+                        */
+                       c = unlang_compile_empty(parent, unlang_ctx, cs, type);
+                       cf_section_free_children(cs);
+               } else {
+                       c = unlang_compile_section(parent, unlang_ctx, cs, type);
+               }
+       }
+
+       if (!c) return NULL;
+       fr_assert(c != UNLANG_IGNORE);
+
+       g = unlang_generic_to_group(c);
+       gext = unlang_group_to_cond(g);
+
+       gext->head = head;
+       gext->is_truthy = is_truthy;
+       gext->value = value;
+
+       ci = cf_section_to_item(cs);
+       while ((ci = cf_item_next(parent->ci, ci)) != NULL) {
+               if (cf_item_is_data(ci)) continue;
+
+               break;
+       }
+
+       /*
+        *      If there's an 'if' without an 'else', then remember that.
+        */
+       if (ci && cf_item_is_section(ci)) {
+               char const *name;
+
+               name = cf_section_name1(cf_item_to_section(ci));
+               fr_assert(name != NULL);
+
+               gext->has_else = (strcmp(name, "else") == 0) || (strcmp(name, "elsif") == 0);
+       }
+
+       return c;
+}
+
+static unlang_t *unlang_compile_if(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       return compile_if_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_IF);
+}
+
+static unlang_t *unlang_compile_elsif(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       return compile_if_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_ELSIF);
+}
+
+static unlang_t *unlang_compile_else(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION *cs = cf_item_to_section(ci);
+
+       if (cf_section_name2(cs)) {
+               cf_log_err(cs, "'else' cannot have a condition");
+               return NULL;
+       }
+
+       return unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_ELSE);
+}
+
+
 void unlang_condition_init(void)
 {
        unlang_register(UNLANG_TYPE_IF,
@@ -120,6 +271,7 @@ void unlang_condition_init(void)
                                .type = UNLANG_TYPE_IF,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_if,
                                .interpret = unlang_if,
 
                                .unlang_size = sizeof(unlang_cond_t),
@@ -137,6 +289,7 @@ void unlang_condition_init(void)
                                .type = UNLANG_TYPE_ELSE,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_else,
                                .interpret = unlang_group,
 
                                .unlang_size = sizeof(unlang_group_t),
@@ -145,10 +298,11 @@ void unlang_condition_init(void)
 
        unlang_register(UNLANG_TYPE_ELSIF,
                           &(unlang_op_t){
-                               .name = "elseif",
+                               .name = "elsif",
                                .type = UNLANG_TYPE_ELSIF,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_elsif,
                                .interpret = unlang_if,
 
                                .unlang_size = sizeof(unlang_cond_t),
index 6431e5677ef12e59396b4461c0ffc397d7122cc4..76c112e93dae5ec5f56893b66f0c7a0d42bc41f2 100644 (file)
@@ -48,6 +48,34 @@ static unlang_action_t unlang_detach(unlang_result_t *p_result, request_t *reque
        return UNLANG_ACTION_CALCULATE_RESULT;
 }
 
+static unlang_t *unlang_compile_detach(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       unlang_t *subrequest;
+
+       for (subrequest = parent;
+            subrequest != NULL;
+            subrequest = subrequest->parent) {
+               if (subrequest->type == UNLANG_TYPE_SUBREQUEST) break;
+       }
+
+       if (!subrequest) {
+               cf_log_err(ci, "'detach' can only be used inside of a 'subrequest' section.");
+               cf_log_err(ci, DOC_KEYWORD_REF(detach));
+               return NULL;
+       }
+
+       /*
+        *      This really overloads the functionality of
+        *      cf_item_next().
+        */
+       if ((parent == subrequest) && !cf_item_next(ci, ci)) {
+               cf_log_err(ci, "'detach' cannot be used as the last entry in a section, as there is nothing more to do");
+               return NULL;
+       }
+
+       return unlang_compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_DETACH);
+}
+
 /** Initialise subrequest ops
  *
  */
@@ -57,7 +85,9 @@ void unlang_detach_init(void)
                        &(unlang_op_t){
                                .name = "detach",
                                .type = UNLANG_TYPE_DETACH,
+                               .flag = UNLANG_OP_FLAG_SINGLE_WORD,
 
+                               .compile = unlang_compile_detach,
                                .interpret = unlang_detach,
 
                                .unlang_size = sizeof(unlang_group_t),
index 64dfd1db8131ab9deaa2362424daa5ee6beac541..240b9f81c131bfd76abc0e186e8a6aca1b8430f5 100644 (file)
@@ -1763,7 +1763,11 @@ void unlang_edit_init(void)
        unlang_register(UNLANG_TYPE_EDIT,
                           &(unlang_op_t){
                                .name = "edit",
+                               .type = UNLANG_TYPE_EDIT,
+                               .flag = UNLANG_OP_FLAG_INTERNAL,
+
                                .interpret = unlang_edit_state_init,
+
                                .frame_state_size = sizeof(unlang_frame_state_edit_t),
                                .frame_state_type = "unlang_frame_state_edit_t",
                           });
index f65ba4085c06e70f0af5928499c59235da853247..000f66c15d90deb0e290df9d964a403c3a0023a2 100644 (file)
@@ -188,8 +188,17 @@ void unlang_finally_init(void)
        unlang_register(UNLANG_TYPE_FINALLY,
                        &(unlang_op_t){
                                .name = "finally",
+                               .type = UNLANG_TYPE_FINALLY,
+
                                .interpret = unlang_finally,
-                               .flag = UNLANG_OP_FLAG_NO_FORCE_UNWIND, /* No debug braces, the thing that's pushed in unlang finally should have braces */
+
+                               /*
+                                 *     No debug braces, the thing
+                                 *     that's pushed in unlang
+                                 *     finally should have braces
+                                 */
+                               .flag = UNLANG_OP_FLAG_NO_FORCE_UNWIND | UNLANG_OP_FLAG_INTERNAL,
+
                                .frame_state_size = sizeof(unlang_frame_state_finally_t),
                                .frame_state_type = "unlang_frame_state_finally_t",
                        });
index d8e086089e3051a4b920671719c343471b0c8ce0..c02fe49dbd2b1e2ee03e4d1c2f19950818c71039 100644 (file)
@@ -547,6 +547,297 @@ static unlang_action_t unlang_continue(UNUSED unlang_result_t *p_result, request
        return unwind_to_op_flag(NULL, stack, UNLANG_OP_FLAG_CONTINUE_POINT);
 }
 
+static unlang_t *unlang_compile_foreach(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION            *cs = cf_item_to_section(ci);
+       fr_token_t              token;
+       char const              *name2;
+       char const              *type_name, *variable_name;
+       fr_type_t               type;
+       unlang_t                *c;
+
+       fr_type_t               key_type;
+       char const              *key_name;
+
+       unlang_group_t          *g;
+       unlang_foreach_t        *gext;
+
+       ssize_t                 slen;
+       tmpl_t                  *vpt;
+       fr_dict_attr_t const    *da = NULL;
+
+       tmpl_rules_t            t_rules;
+       unlang_compile_ctx_t    unlang_ctx2;
+
+       /*
+        *      Ignore empty "foreach" blocks, and don't even sanity check their arguments.
+        */
+       if (!cf_item_next(cs, NULL)) {
+               return UNLANG_IGNORE;
+       }
+
+       /*
+        *      We allow unknown attributes here.
+        */
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.allow_unknown = true;
+       t_rules.attr.allow_wildcard = true;
+       RULES_VERIFY(&t_rules);
+
+       name2 = cf_section_name2(cs);
+       fr_assert(name2 != NULL); /* checked in cf_file.c */
+
+       /*
+        *      Allocate a group for the "foreach" block.
+        */
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_FOREACH);
+       if (!g) return NULL;
+
+       c = unlang_group_to_generic(g);
+
+       /*
+        *      Create the template.  If we fail, AND it's a bare word
+        *      with &Foo-Bar, it MAY be an attribute defined by a
+        *      module.  Allow it for now.  The pass2 checks below
+        *      will fix it up.
+        */
+       token = cf_section_name2_quote(cs);
+       if (token != T_BARE_WORD) {
+               cf_log_err(cs, "Data being looped over in 'foreach' must be an attribute reference or dynamic expansion, not a string");
+       print_ref:
+               cf_log_err(ci, DOC_KEYWORD_REF(foreach));
+       error:
+               talloc_free(g);
+               return NULL;
+       }
+
+       slen = tmpl_afrom_substr(g, &vpt,
+                                &FR_SBUFF_IN(name2, strlen(name2)),
+                                token,
+                                NULL,
+                                &t_rules);
+       if (!vpt) {
+               cf_canonicalize_error(cs, slen, "Failed parsing argument to 'foreach'", name2);
+               goto error;
+       }
+
+       /*
+        *      If we don't have a negative return code, we must have a vpt
+        *      (mostly to quiet coverity).
+        */
+       fr_assert(vpt);
+
+       if (tmpl_is_attr(vpt)) {
+               if (tmpl_attr_tail_num(vpt) == NUM_UNSPEC) {
+                       cf_log_warn(cs, "Attribute reference should be updated to use %s[*]", vpt->name);
+                       tmpl_attr_rewrite_leaf_num(vpt, NUM_ALL);
+               }
+
+               if (tmpl_attr_tail_num(vpt) != NUM_ALL) {
+                       cf_log_err(cs, "Attribute references must be of the form ...%s[*]", tmpl_attr_tail_da(vpt)->name);
+                       goto print_ref;
+               }
+
+       } else if (!tmpl_contains_xlat(vpt)) {
+               cf_log_err(cs, "Invalid content in 'foreach (...)', it must be an attribute reference or a dynamic expansion");
+               goto print_ref;
+       }
+
+       gext = unlang_group_to_foreach(g);
+       gext->vpt = vpt;
+
+       c->name = "foreach";
+       MEM(c->debug_name = talloc_typed_asprintf(c, "foreach %s", name2));
+
+       /*
+        *      Copy over the compilation context.  This is mostly
+        *      just to ensure that retry is handled correctly.
+        *      i.e. reset.
+        */
+       unlang_compile_ctx_copy(&unlang_ctx2, unlang_ctx);
+
+       /*
+        *      Then over-write the new compilation context.
+        */
+       unlang_ctx2.section_name1 = "foreach";
+       unlang_ctx2.section_name2 = name2;
+       unlang_ctx2.rules = &t_rules;
+       t_rules.parent = unlang_ctx->rules;
+
+       /*
+        *      If we have "type name", then define a local variable of that name.
+        */
+       type_name = cf_section_argv(cs, 0); /* AFTER name1, name2 */
+
+       key_name = cf_section_argv(cs, 2);
+       if (key_name) {
+               key_type = fr_table_value_by_str(fr_type_table, key_name, FR_TYPE_VOID);
+       } else {
+               key_type = FR_TYPE_VOID;
+       }
+       key_name = cf_section_argv(cs, 3);
+
+       if (tmpl_is_xlat(vpt)) {
+               if (!type_name) {
+                       cf_log_err(cs, "Dynamic expansions MUST specify a data type for the variable");
+                       goto print_ref;
+               }
+
+               type = fr_table_value_by_str(fr_type_table, type_name, FR_TYPE_VOID);
+
+               /*
+                *      No data type was specified, see if we can get one from the function.
+                */
+               if (type == FR_TYPE_NULL) {
+                       type = xlat_data_type(tmpl_xlat(vpt));
+                       if (fr_type_is_leaf(type)) goto get_name;
+
+                       cf_log_err(cs, "Unable to determine return data type from dynamic expansion");
+                       goto print_ref;
+               }
+
+               if (!fr_type_is_leaf(type)) {
+                       cf_log_err(cs, "Dynamic expansions MUST specify a non-structural data type for the variable");
+                       goto print_ref;
+               }
+
+               if ((key_type != FR_TYPE_VOID) && !fr_type_is_numeric(key_type)) {
+                       cf_log_err(cs, "Invalid data type '%s' for 'key' variable - it should be numeric", fr_type_to_str(key_type));
+                       goto print_ref;
+               }
+
+               goto get_name;
+       } else {
+               fr_assert(tmpl_is_attr(vpt));
+
+               if ((key_type != FR_TYPE_VOID) && (key_type != FR_TYPE_STRING) && (key_type != FR_TYPE_UINT32)) {
+                       cf_log_err(cs, "Invalid data type '%s' for 'key' variable - it should be 'string' or 'uint32'", fr_type_to_str(key_type));
+                       goto print_ref;
+               }
+       }
+
+       if (type_name) {
+               unlang_variable_t *var;
+
+               type = fr_table_value_by_str(fr_type_table, type_name, FR_TYPE_VOID);
+               fr_assert(type != FR_TYPE_VOID);
+
+               /*
+                *      foreach string foo (&tlv-thing.[*]) { ... }
+                */
+               if (tmpl_attr_tail_is_unspecified(vpt)) {
+                       goto get_name;
+               }
+
+               da = tmpl_attr_tail_da(vpt);
+
+               if (type == FR_TYPE_NULL) {
+                       type = da->type;
+
+               } else if (fr_type_is_leaf(type) != fr_type_is_leaf(da->type)) {
+               incompatible:
+                       cf_log_err(cs, "Incompatible data types in foreach variable (%s), and reference %s being looped over (%s)",
+                                  fr_type_to_str(type), da->name, fr_type_to_str(da->type));
+                       goto print_ref;
+
+               } else if (fr_type_is_structural(type) && (type != da->type)) {
+                       goto incompatible;
+               }
+
+       get_name:
+               variable_name = cf_section_argv(cs, 1);
+
+               /*
+                *      Define the local variables.
+                */
+               g->variables = var = talloc_zero(g, unlang_variable_t);
+               if (!var) goto error;
+
+               var->dict = fr_dict_protocol_alloc(unlang_ctx->rules->attr.dict_def);
+               if (!var->dict) goto error;
+
+               var->root = fr_dict_root(var->dict);
+
+               var->max_attr = 1;
+
+               if (unlang_define_local_variable(cf_section_to_item(cs), var, &t_rules, type, variable_name, da) < 0) goto error;
+
+               t_rules.attr.dict_def = var->dict;
+               t_rules.attr.namespace = NULL;
+
+               /*
+                *      And ensure we have the key.
+                */
+               gext->value = fr_dict_attr_by_name(NULL, var->root, variable_name);
+               fr_assert(gext->value != NULL);
+
+               /*
+                *      Define the local key variable.  Note that we don't copy any children.
+                */
+               if (key_type != FR_TYPE_VOID) {
+                       if (unlang_define_local_variable(cf_section_to_item(cs), var, &t_rules, key_type, key_name, NULL) < 0) goto error;
+
+                       gext->key = fr_dict_attr_by_name(NULL, var->root, key_name);
+                       fr_assert(gext->key != NULL);
+               }
+       }
+
+       if (!unlang_compile_children(g, &unlang_ctx2, true)) return NULL;
+
+       return c;
+}
+
+
+static unlang_t *unlang_compile_break(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       unlang_t *unlang;
+
+       for (unlang = parent; unlang != NULL; unlang = unlang->parent) {
+               /*
+                *      "break" doesn't go past a return point.
+                */
+               if ((unlang_ops[unlang->type].flag & UNLANG_OP_FLAG_RETURN_POINT) != 0) goto error;
+
+               if ((unlang_ops[unlang->type].flag & UNLANG_OP_FLAG_BREAK_POINT) != 0) break;
+       }
+
+       if (!unlang) {
+       error:
+               cf_log_err(ci, "Invalid location for 'break' - it can only be used inside 'foreach' or 'switch'");
+               cf_log_err(ci, DOC_KEYWORD_REF(break));
+               return NULL;
+       }
+
+       parent->closed = true;
+
+       return unlang_compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_BREAK);
+}
+
+static unlang_t *unlang_compile_continue(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       unlang_t *unlang;
+
+       for (unlang = parent; unlang != NULL; unlang = unlang->parent) {
+               /*
+                *      "continue" doesn't go past a return point.
+                */
+               if ((unlang_ops[unlang->type].flag & UNLANG_OP_FLAG_RETURN_POINT) != 0) goto error;
+
+               if (unlang->type == UNLANG_TYPE_FOREACH) break;
+       }
+
+       if (!unlang) {
+       error:
+               cf_log_err(ci, "Invalid location for 'continue' - it can only be used inside 'foreach'");
+               cf_log_err(ci, DOC_KEYWORD_REF(break));
+               return NULL;
+       }
+
+       parent->closed = true;
+
+       return unlang_compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_CONTINUE);
+}
+
 void unlang_foreach_init(void)
 {
        unlang_register(UNLANG_TYPE_FOREACH,
@@ -555,6 +846,7 @@ void unlang_foreach_init(void)
                                .type = UNLANG_TYPE_FOREACH,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_BREAK_POINT | UNLANG_OP_FLAG_CONTINUE_POINT,
 
+                               .compile = unlang_compile_foreach,
                                .interpret = unlang_foreach,
 
                                .unlang_size = sizeof(unlang_foreach_t),
@@ -568,7 +860,9 @@ void unlang_foreach_init(void)
                           &(unlang_op_t){
                                .name = "break",
                                .type = UNLANG_TYPE_BREAK,
-
+                               .flag = UNLANG_OP_FLAG_SINGLE_WORD
+,
+                               .compile = unlang_compile_break,
                                .interpret = unlang_break,
 
                                .unlang_size = sizeof(unlang_group_t),
@@ -579,7 +873,9 @@ void unlang_foreach_init(void)
                           &(unlang_op_t){
                                .name = "continue",
                                .type = UNLANG_TYPE_CONTINUE,
+                               .flag = UNLANG_OP_FLAG_SINGLE_WORD,
 
+                               .compile = unlang_compile_continue,
                                .interpret = unlang_continue,
 
                                .unlang_size = sizeof(unlang_group_t),
index 261a4566384974a13aa8becc03a2c7e9732a261b..63c425688b58b6c330ecae6cec7606af9b55b54f 100644 (file)
@@ -582,10 +582,12 @@ void unlang_function_init(void)
        unlang_register(UNLANG_TYPE_FUNCTION,
                        &(unlang_op_t){
                                .name = "function",
+                               .type = UNLANG_TYPE_FUNCTION,
+                               .flag = UNLANG_OP_FLAG_RETURN_POINT | UNLANG_OP_FLAG_INTERNAL,
+
                                .interpret = call_no_result,
                                .signal = unlang_function_signal,
                                .dump = unlang_function_dump,
-                               .flag = UNLANG_OP_FLAG_RETURN_POINT,
                                .frame_state_size = sizeof(unlang_frame_state_func_t),
                                .frame_state_type = "unlang_frame_state_func_t",
                        });
index b44b9e90ba2c892838bb5dc06a998da7df2c1ace..f6eb11b915feacf078ae1ab25454a444cb06c00c 100644 (file)
@@ -39,6 +39,35 @@ static unlang_action_t unlang_policy(unlang_result_t *result, request_t *request
 }
 
 
+static unlang_t *unlang_compile_group(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       if (!cf_item_next(ci, NULL)) return UNLANG_IGNORE;
+
+       return unlang_compile_section(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_GROUP);
+}
+
+static unlang_t *unlang_compile_redundant(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION                    *cs = cf_item_to_section(ci);
+       unlang_t                        *c;
+
+       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+
+       if (!unlang_compile_limit_subsection(cs, cf_section_name1(cs))) {
+               return NULL;
+       }
+
+       c = unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_REDUNDANT);
+       if (!c) return NULL;
+
+       /*
+        *      We no longer care if "redundant" sections have a name.  If they do, it's ignored.
+        */
+
+       return c;
+}
+
+
 void unlang_group_init(void)
 {
        unlang_register(UNLANG_TYPE_GROUP,
@@ -47,6 +76,7 @@ void unlang_group_init(void)
                                .type = UNLANG_TYPE_GROUP,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_group,
                                .interpret = unlang_group,
 
                                .unlang_size = sizeof(unlang_group_t),
@@ -59,6 +89,7 @@ void unlang_group_init(void)
                                .type = UNLANG_TYPE_REDUNDANT,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_redundant,
                                .interpret = unlang_group,
 
                                .unlang_size = sizeof(unlang_group_t),
index b59ad654c0494c160710721c2804f266902f84cb..436aa685c8741f16aa9f5925590a52a6873b8863 100644 (file)
@@ -122,6 +122,109 @@ static unlang_action_t unlang_limit(unlang_result_t *p_result, request_t *reques
 }
 
 
+static unlang_t *unlang_compile_limit(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION            *cs = cf_item_to_section(ci);
+       char const              *name2;
+       unlang_t                *c;
+       unlang_group_t          *g;
+       unlang_limit_t          *gext;
+       tmpl_t                  *vpt = NULL;
+       uint32_t                limit = 0;
+       fr_token_t              token;
+       ssize_t                 slen;
+       tmpl_rules_t            t_rules;
+
+       /*
+        *      limit <number>
+        */
+       name2 = cf_section_name2(cs);
+       if (!name2) {
+               cf_log_err(cs, "You must specify a value for 'limit'");
+       print_url:
+               cf_log_err(ci, DOC_KEYWORD_REF(limit));
+               return NULL;
+       }
+
+       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_LIMIT);
+       if (!g) return NULL;
+
+       gext = unlang_group_to_limit(g);
+
+       token = cf_section_name2_quote(cs);
+
+       /*
+        *      We don't allow unknown attributes here.
+        */
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.allow_unknown = false;
+       RULES_VERIFY(&t_rules);
+
+       slen = tmpl_afrom_substr(gext, &vpt,
+                                &FR_SBUFF_IN(name2, strlen(name2)),
+                                token,
+                                NULL,
+                                &t_rules);
+       if (!vpt) {
+       syntax_error:
+               cf_canonicalize_error(cs, slen, "Failed parsing argument to 'foreach'", name2);
+               talloc_free(g);
+               return NULL;
+       }
+
+       /*
+        *      Fixup the tmpl so that we know it's somewhat sane.
+        */
+       if (!pass2_fixup_tmpl(gext, &vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
+               talloc_free(g);
+               return NULL;
+       }
+
+       if (tmpl_is_list(vpt)) {
+               cf_log_err(cs, "Cannot use list as argument for 'limit' statement");
+       error:
+               talloc_free(g);
+               goto print_url;
+       }
+
+       if (tmpl_contains_regex(vpt)) {
+               cf_log_err(cs, "Cannot use regular expression as argument for 'limit' statement");
+               goto error;
+       }
+
+       if (tmpl_is_data(vpt) && (token == T_BARE_WORD)) {
+               fr_value_box_t box;
+
+               if (fr_value_box_cast(NULL, &box, FR_TYPE_UINT32, NULL, tmpl_value(vpt)) < 0) goto syntax_error;
+
+               limit = box.vb_uint32;
+
+       } else {
+               /*
+                *      Attribute or data MUST be cast to a 32-bit unsigned number.
+                */
+               if (tmpl_cast_set(vpt, FR_TYPE_UINT32) < 0) {
+                       cf_log_perr(cs, "Failed setting cast type");
+                       goto syntax_error;
+               }
+       }
+
+       /*
+        *      Compile the contents of a "limit".
+        */
+       c = unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_LIMIT);
+       if (!c) return NULL;
+
+       g = unlang_generic_to_group(c);
+       gext = unlang_group_to_limit(g);
+       gext->limit = limit;
+       gext->vpt = vpt;
+
+       return c;
+}
+
 void unlang_limit_init(void)
 {
        unlang_register(UNLANG_TYPE_LIMIT,
@@ -130,6 +233,7 @@ void unlang_limit_init(void)
                                .type = UNLANG_TYPE_LIMIT,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_limit,
                                .interpret = unlang_limit,
                                .signal = unlang_limit_signal,
 
index 90b841dc3af9597f5b54e85ff9aacdc2be0b6f92..4f6cc533fdf9cfb55e32d048f0fcc8578daa21ca 100644 (file)
@@ -242,14 +242,123 @@ static unlang_action_t unlang_load_balance(unlang_result_t *p_result, request_t
        return unlang_load_balance_next(p_result, request, frame);
 }
 
+
+static unlang_t *compile_load_balance_subsection(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs,
+                                                unlang_type_t type)
+{
+       char const                      *name2;
+       unlang_t                        *c;
+       unlang_group_t                  *g;
+       unlang_load_balance_t           *gext;
+
+       tmpl_rules_t                    t_rules;
+
+       /*
+        *      We allow unknown attributes here.
+        */
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.allow_unknown = true;
+       RULES_VERIFY(&t_rules);
+
+       /*
+        *      No children?  Die!
+        */
+       if (!cf_item_next(cs, NULL)) {
+               cf_log_err(cs, "%s sections cannot be empty", unlang_ops[type].name);
+               return NULL;
+       }
+
+       if (!unlang_compile_limit_subsection(cs, cf_section_name1(cs))) return NULL;
+
+       c = unlang_compile_section(parent, unlang_ctx, cs, type);
+       if (!c) return NULL;
+
+       g = unlang_generic_to_group(c);
+
+       /*
+        *      Allow for keyed load-balance / redundant-load-balance sections.
+        */
+       name2 = cf_section_name2(cs);
+
+       /*
+        *      Inside of the "modules" section, it's a virtual
+        *      module.  The name is a module name, not a key.
+        */
+       if (name2) {
+               if (strcmp(cf_section_name1(cf_item_to_section(cf_parent(cs))), "modules") == 0) name2 = NULL;
+       }
+
+       if (name2) {
+               fr_token_t quote;
+               ssize_t slen;
+
+               /*
+                *      Create the template.  All attributes and xlats are
+                *      defined by now.
+                */
+               quote = cf_section_name2_quote(cs);
+               gext = unlang_group_to_load_balance(g);
+               slen = tmpl_afrom_substr(gext, &gext->vpt,
+                                        &FR_SBUFF_IN(name2, strlen(name2)),
+                                        quote,
+                                        NULL,
+                                        &t_rules);
+               if (!gext->vpt) {
+                       cf_canonicalize_error(cs, slen, "Failed parsing argument", name2);
+                       talloc_free(g);
+                       return NULL;
+               }
+
+               fr_assert(gext->vpt != NULL);
+
+               /*
+                *      Fixup the templates
+                */
+               if (!pass2_fixup_tmpl(g, &gext->vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
+                       talloc_free(g);
+                       return NULL;
+               }
+
+               switch (gext->vpt->type) {
+               default:
+                       cf_log_err(cs, "Invalid type in '%s': data will not result in a load-balance key", name2);
+                       talloc_free(g);
+                       return NULL;
+
+                       /*
+                        *      Allow only these ones.
+                        */
+               case TMPL_TYPE_XLAT:
+               case TMPL_TYPE_ATTR:
+               case TMPL_TYPE_EXEC:
+                       break;
+               }
+       }
+
+       return c;
+}
+
+static unlang_t *unlang_compile_load_balance(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       return compile_load_balance_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_LOAD_BALANCE);
+}
+
+
+static unlang_t *unlang_compile_redundant_load_balance(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       return compile_load_balance_subsection(parent, unlang_ctx, cf_item_to_section(ci), UNLANG_TYPE_REDUNDANT_LOAD_BALANCE);
+}
+
+
 void unlang_load_balance_init(void)
 {
        unlang_register(UNLANG_TYPE_LOAD_BALANCE,
                           &(unlang_op_t){
-                               .name = "load-balance group",
+                               .name = "load-balance",
                                .type = UNLANG_TYPE_LOAD_BALANCE,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET,
 
+                               .compile = unlang_compile_load_balance,
                                .interpret = unlang_load_balance,
 
                                .unlang_size = sizeof(unlang_load_balance_t),
@@ -261,10 +370,11 @@ void unlang_load_balance_init(void)
 
        unlang_register(UNLANG_TYPE_REDUNDANT_LOAD_BALANCE,
                           &(unlang_op_t){
-                               .name = "redundant-load-balance group",
+                               .name = "redundant-load-balance",
                                .type = UNLANG_TYPE_REDUNDANT_LOAD_BALANCE,     
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET,
 
+                               .compile = unlang_compile_redundant_load_balance,
                                .interpret = unlang_redundant_load_balance,
 
                                .unlang_size = sizeof(unlang_load_balance_t),
index f5c91eabf6d310bf370972039099bab1cd7cfd87..46904a59c347c27b00a37735fb63ed98361a98ad 100644 (file)
@@ -444,6 +444,620 @@ static unlang_action_t unlang_map_state_init(unlang_result_t *p_result, request_
        return map_proc_apply(p_result, request, frame);
 }
 
+static int edit_section_alloc(CONF_SECTION *parent, CONF_SECTION **child, char const *name1, fr_token_t op)
+{
+       CONF_SECTION *cs;
+
+       cs = cf_section_alloc(parent, parent, name1, NULL);
+       if (!cs) return -1;
+
+       cf_section_add_name2_quote(cs, op);
+
+       if (child) *child = cs;
+
+       return 0;
+}
+
+static int edit_pair_alloc(CONF_SECTION *cs, CONF_PAIR *original, char const *attr, fr_token_t op, char const *value, fr_token_t list_op)
+{
+       CONF_PAIR *cp;
+       fr_token_t rhs_quote;
+
+       if (original) {
+               rhs_quote = cf_pair_value_quote(original);
+       } else {
+               rhs_quote = T_BARE_WORD;
+       }
+
+       cp = cf_pair_alloc(cs, attr, value, op, T_BARE_WORD, rhs_quote);
+       if (!cp) return -1;
+
+       if (!original) return 0;
+
+       cf_filename_set(cp, cf_filename(original));
+       cf_lineno_set(cp, cf_lineno(original));
+
+       if (fr_debug_lvl >= 3) {
+               if (list_op == T_INVALID) {
+                       cf_log_err(original, "%s %s %s --> %s %s %s",
+                                  cf_pair_attr(original), fr_tokens[cf_pair_operator(original)], cf_pair_value(original),
+                                  attr, fr_tokens[op], value);
+               } else {
+                       if (*attr == '&') attr++;
+                       cf_log_err(original, "%s %s %s --> %s %s { %s %s %s }",
+                                  cf_pair_attr(original), fr_tokens[cf_pair_operator(original)], cf_pair_value(original),
+                                  cf_section_name1(cs), fr_tokens[list_op], attr, fr_tokens[op], value);
+               }
+       } else if (fr_debug_lvl >= 2) {
+               if (list_op == T_INVALID) {
+                       cf_log_err(original, "--> %s %s %s",
+                                  attr, fr_tokens[op], value);
+               } else {
+                       cf_log_err(original, "--> %s %s { %s %s %s }",
+                                  cf_section_name1(cs), fr_tokens[list_op], attr, fr_tokens[op], value);
+               }
+       }
+
+       return 0;
+}
+
+/*
+ *     Convert "update" to "edit" using evil spells and sorcery.
+ */
+static unlang_t *compile_update_to_edit(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs)
+{
+       char const              *name2 = cf_section_name2(cs);
+       CONF_ITEM               *ci;
+       CONF_SECTION            *group;
+       unlang_group_t          *g;
+       char                    list_buffer[32];
+       char                    value_buffer[256];
+       char                    attr_buffer[256];
+       char const              *list;
+
+       g = unlang_generic_to_group(parent);
+
+       /*
+        *      Wrap it all in a group, no matter what.  Because of
+        *      limitations in the cf_pair_alloc() API.
+        */
+       group = cf_section_alloc(g->cs, g->cs, "group", NULL);
+       if (!group) return NULL;
+
+       (void) cf_item_remove(g->cs, group); /* was added at the end */
+       cf_item_insert_after(g->cs, cs, group);
+
+       /*
+        *      Hoist this out of the loop, and make sure it never has a '&' prefix.
+        */
+       if (name2) {
+               if (*name2 == '&') name2++;
+               snprintf(list_buffer, sizeof(list_buffer), "%s", name2);
+       } else {
+               snprintf(list_buffer, sizeof(list_buffer), "%s", tmpl_list_name(unlang_ctx->rules->attr.list_def, "<INVALID>"));
+
+       }
+
+       /*
+        *      Loop over the entries, rewriting them.
+        */
+       for (ci = cf_item_next(cs, NULL);
+            ci != NULL;
+            ci = cf_item_next(cs, ci)) {
+               CONF_PAIR       *cp;
+               CONF_SECTION    *child;
+               int             rcode;
+               fr_token_t      op;
+               char const      *attr, *value, *end;
+
+               if (cf_item_is_section(ci)) {
+                       cf_log_err(ci, "Cannot specify subsections for 'update'");
+                       return NULL;
+               }
+
+               if (!cf_item_is_pair(ci)) continue;
+
+               cp = cf_item_to_pair(ci);
+
+               attr = cf_pair_attr(cp);
+               value = cf_pair_value(cp);
+               op = cf_pair_operator(cp);
+
+               fr_assert(attr);
+               fr_assert(value);
+
+               list = list_buffer;
+
+               if (*attr == '&') attr++;
+
+               end = strchr(attr, '.');
+               if (!end) end = attr + strlen(attr);
+
+               /*
+                *      Separate out the various possibilities for the "name", which could be a list, an
+                *      attribute name, or a list followed by an attribute name.
+                *
+                *      Note that even if we have "update request { ....}", the v3 parser allowed the contents
+                *      of the "update" section to still specify parent / lists.  Which makes parsing it all
+                *      annoying.
+                *
+                *      The good news is that all we care about is whether or not there's a parent / list ref.
+                *      We don't care what that ref is.
+                */
+               {
+                       fr_dict_attr_t const *tmpl_list;
+
+                       /*
+                        *      Allow for a "parent" or "outer" reference.  There may be multiple
+                        *      "parent.parent", so we keep processing them until we get a list reference.
+                        */
+                       if (fr_table_value_by_substr(tmpl_request_ref_table, attr, end - attr, REQUEST_UNKNOWN) != REQUEST_UNKNOWN) {
+
+                               /*
+                                *      Catch one more case where the behavior is different.
+                                *
+                                *      &request += &config[*]
+                                */
+                               if ((cf_pair_value_quote(cp) == T_BARE_WORD) && (*value == '&') &&
+                                   (strchr(value, '.') == NULL) && (strchr(value, '[') != NULL)) {
+                                       char const *p = strchr(value, '[');
+
+                                       cf_log_err(cp, "Cannot do array assignments for lists.  Just use '%s %s %.*s'",
+                                                  list, fr_tokens[op], (int) (p - value), value);
+                                       return NULL;
+                               }
+
+                               goto attr_is_list;
+
+                       /*
+                        *      Doesn't have a parent ref, maybe it's a list ref?
+                        */
+                       } else if (tmpl_attr_list_from_substr(&tmpl_list, &FR_SBUFF_IN(attr, (end - attr))) > 0) {
+                               char *p;
+
+                       attr_is_list:
+                               snprintf(attr_buffer, sizeof(attr_buffer), "%s", attr);
+                               list = attr_buffer;
+                               attr = NULL;
+
+                               p = strchr(attr_buffer, '.');
+                               if (p) {
+                                       *(p++) = '\0';
+                                       attr = p;
+                               }
+                       }
+               }
+
+               switch (op) {
+                       /*
+                        *      FOO !* ANY
+                        *
+                        *      The RHS doesn't matter, so we ignore it.
+                        */
+               case T_OP_CMP_FALSE:
+                       if (!attr) {
+                               /*
+                                *      Set list to empty value.
+                                */
+                               rcode = edit_section_alloc(group, NULL, list, T_OP_SET);
+
+                       } else {
+                               if (strchr(attr, '[') == NULL) {
+                                       snprintf(value_buffer, sizeof(value_buffer), "%s[*]", attr);
+                               } else {
+                                       snprintf(value_buffer, sizeof(value_buffer), "%s", attr);
+                               }
+
+                               rcode = edit_pair_alloc(group, cp, list, T_OP_SUB_EQ, value_buffer, T_INVALID);
+                       }
+                       break;
+
+               case T_OP_SET:
+                       /*
+                        *      Must be a list-to-list operation
+                        */
+                       if (!attr) {
+                       list_op:
+                               rcode = edit_pair_alloc(group, cp, list, op, value, T_INVALID);
+                               break;
+                       }
+                       goto pair_op;
+
+               case T_OP_EQ:
+                       /*
+                        *      Allow &list = "foo"
+                        */
+                       if (!attr) {
+                               if (!value) {
+                                       cf_log_err(cp, "Missing value");
+                                       return NULL;
+                               }
+
+                               rcode = edit_pair_alloc(group, cp, list, op, value, T_INVALID);
+                               break;
+                       }
+
+               pair_op:
+                       fr_assert(*attr != '&');
+                       if (snprintf(value_buffer, sizeof(value_buffer), "%s.%s", list, attr) < 0) {
+                               cf_log_err(cp, "RHS of update too long to convert to edit automatically");
+                               return NULL;
+                       }
+
+                       rcode = edit_pair_alloc(group, cp, value_buffer, op, value, T_INVALID);
+                       break;
+
+               case T_OP_ADD_EQ:
+               case T_OP_PREPEND:
+                       if (!attr) goto list_op;
+
+                       rcode = edit_section_alloc(group, &child, list, op);
+                       if (rcode < 0) break;
+
+                       rcode = edit_pair_alloc(child, cp, attr, T_OP_EQ, value, op);
+                       break;
+
+                       /*
+                        *      Remove matching attributes
+                        */
+               case T_OP_SUB_EQ:
+                       op = T_OP_CMP_EQ;
+
+               filter:
+                       if (!attr) {
+                               cf_log_err(cp, "Invalid operator for list assignment");
+                               return NULL;
+                       }
+
+                       rcode = edit_section_alloc(group, &child, list, T_OP_SUB_EQ);
+                       if (rcode < 0) break;
+
+                       if (strchr(attr, '[') != 0) {
+                               cf_log_err(cp, "Cannot do filtering with array indexes");
+                               return NULL;
+                       }
+
+                       rcode = edit_pair_alloc(child, cp, attr, op, value, T_OP_SUB_EQ);
+                       break;
+
+                       /*
+                        *      Keep matching attributes, i.e. remove non-matching ones.
+                        */
+               case T_OP_CMP_EQ:
+                       op = T_OP_NE;
+                       goto filter;
+
+               case T_OP_NE:
+                       op = T_OP_CMP_EQ;
+                       goto filter;
+
+               case T_OP_LT:
+                       op = T_OP_GE;
+                       goto filter;
+
+               case T_OP_LE:
+                       op = T_OP_GT;
+                       goto filter;
+
+               case T_OP_GT:
+                       op = T_OP_LE;
+                       goto filter;
+
+               case T_OP_GE:
+                       op = T_OP_LT;
+                       goto filter;
+
+               default:
+                       cf_log_err(cp, "Unsupported operator - cannot auto-convert to edit section");
+                       return NULL;
+               }
+
+               if (rcode < 0) {
+                       cf_log_err(cp, "Failed converting entry");
+                       return NULL;
+               }
+       }
+
+       return UNLANG_IGNORE;
+}
+
+static unlang_t *unlang_compile_update(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION            *cs = cf_item_to_section(ci);
+       int                     rcode;
+
+       unlang_group_t          *g;
+       unlang_map_t    *gext;
+
+       unlang_t                *c;
+       char const              *name2 = cf_section_name2(cs);
+
+       tmpl_rules_t            t_rules;
+
+       if (main_config_migrate_option_get("forbid_update")) {
+               cf_log_err(cs, "The use of 'update' sections is forbidden by the server configuration");
+               return NULL;
+       }
+
+       /*
+        *      If we're migrating "update" sections to edit, then go
+        *      do that now.
+        */
+       if (main_config_migrate_option_get("rewrite_update")) {
+               return compile_update_to_edit(parent, unlang_ctx, cs);
+       }
+
+       /*
+        *      We allow unknown attributes here.
+        */
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.allow_unknown = true;
+       t_rules.attr.allow_wildcard = true;
+       RULES_VERIFY(&t_rules);
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_UPDATE);
+       if (!g) return NULL;
+
+       gext = unlang_group_to_map(g);
+
+       /*
+        *      This looks at cs->name2 to determine which list to update
+        */
+       map_list_init(&gext->map);
+       rcode = map_afrom_cs(gext, &gext->map, cs, &t_rules, &t_rules, unlang_fixup_update, NULL, 128);
+       if (rcode < 0) return NULL; /* message already printed */
+       if (map_list_empty(&gext->map)) {
+               cf_log_err(cs, "'update' sections cannot be empty");
+       error:
+               talloc_free(g);
+               return NULL;
+       }
+
+       c = unlang_group_to_generic(g);
+       if (name2) {
+               c->name = name2;
+               c->debug_name = talloc_typed_asprintf(c, "update %s", name2);
+       } else {
+               c->name = "update";
+               c->debug_name = c->name;
+       }
+
+       if (!pass2_fixup_update(g, unlang_ctx->rules)) goto error;
+
+       unlang_compile_action_defaults(c, unlang_ctx);
+
+       return c;
+}
+
+static int compile_map_name(unlang_group_t *g)
+{
+       unlang_map_t    *gext = unlang_group_to_map(g);
+
+       /*
+        *      map <module-name> <arg>
+        */
+       if (gext->vpt) {
+               char    quote;
+               size_t  quoted_len;
+               char    *quoted_str;
+
+               switch (cf_section_argv_quote(g->cs, 0)) {
+               case T_DOUBLE_QUOTED_STRING:
+                       quote = '"';
+                       break;
+
+               case T_SINGLE_QUOTED_STRING:
+                       quote = '\'';
+                       break;
+
+               case T_BACK_QUOTED_STRING:
+                       quote = '`';
+                       break;
+
+               default:
+                       quote = '\0';
+                       break;
+               }
+
+               quoted_len = fr_snprint_len(gext->vpt->name, gext->vpt->len, quote);
+               quoted_str = talloc_array(g, char, quoted_len);
+               fr_snprint(quoted_str, quoted_len, gext->vpt->name, gext->vpt->len, quote);
+
+               g->self.name = talloc_typed_asprintf(g, "map %s %s", cf_section_name2(g->cs), quoted_str);
+               g->self.debug_name = g->self.name;
+               talloc_free(quoted_str);
+
+               return 0;
+       }
+
+       g->self.name = talloc_typed_asprintf(g, "map %s", cf_section_name2(g->cs));
+       g->self.debug_name = g->self.name;
+
+       return 0;
+}
+
+/** Validate and fixup a map that's part of an map section.
+ *
+ * @param map to validate.
+ * @param ctx data to pass to fixup function (currently unused).
+ * @return 0 if valid else -1.
+ */
+static int fixup_map_cb(map_t *map, UNUSED void *ctx)
+{
+       switch (map->lhs->type) {
+       case TMPL_TYPE_ATTR:
+       case TMPL_TYPE_XLAT_UNRESOLVED:
+       case TMPL_TYPE_XLAT:
+               break;
+
+       default:
+               cf_log_err(map->ci, "Left side of map must be an attribute "
+                          "or an xlat (that expands to an attribute), not a %s",
+                          tmpl_type_to_str(map->lhs->type));
+               return -1;
+       }
+
+       switch (map->rhs->type) {
+       case TMPL_TYPE_XLAT_UNRESOLVED:
+       case TMPL_TYPE_DATA_UNRESOLVED:
+       case TMPL_TYPE_DATA:
+       case TMPL_TYPE_XLAT:
+       case TMPL_TYPE_ATTR:
+       case TMPL_TYPE_EXEC:
+               break;
+
+       default:
+               cf_log_err(map->ci, "Right side of map must be an attribute, literal, xlat or exec, got type %s",
+                          tmpl_type_to_str(map->rhs->type));
+               return -1;
+       }
+
+       if (!fr_assignment_op[map->op] && !fr_comparison_op[map->op]) {
+               cf_log_err(map->ci, "Invalid operator \"%s\" in map section.  "
+                          "Only assignment or filter operators are allowed",
+                          fr_table_str_by_value(fr_tokens_table, map->op, "<INVALID>"));
+               return -1;
+       }
+
+       return 0;
+}
+
+static unlang_t *unlang_compile_map(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION            *cs = cf_item_to_section(ci);
+       int                     rcode;
+
+       unlang_group_t          *g;
+       unlang_map_t    *gext;
+
+       unlang_t                *c;
+       CONF_SECTION            *modules;
+       char const              *tmpl_str;
+
+       tmpl_t                  *vpt = NULL;
+
+       map_proc_t              *proc;
+       map_proc_inst_t         *proc_inst;
+
+       char const              *name2 = cf_section_name2(cs);
+
+       tmpl_rules_t            t_rules;
+
+       /*
+        *      The RHS is NOT resolved in the context of the LHS.
+        */
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.disallow_rhs_resolve = true;
+       RULES_VERIFY(&t_rules);
+
+       modules = cf_section_find(cf_root(cs), "modules", NULL);
+       if (!modules) {
+               cf_log_err(cs, "'map' sections require a 'modules' section");
+               return NULL;
+       }
+
+       proc = map_proc_find(name2);
+       if (!proc) {
+               cf_log_err(cs, "Failed to find map processor '%s'", name2);
+               return NULL;
+       }
+       t_rules.literals_safe_for = map_proc_literals_safe_for(proc);
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_MAP);
+       if (!g) return NULL;
+
+       gext = unlang_group_to_map(g);
+
+       /*
+        *      If there's a third string, it's the map src.
+        *
+        *      Convert it into a template.
+        */
+       tmpl_str = cf_section_argv(cs, 0); /* AFTER name1, name2 */
+       if (tmpl_str) {
+               fr_token_t type;
+
+               type = cf_section_argv_quote(cs, 0);
+
+               /*
+                *      Try to parse the template.
+                */
+               (void) tmpl_afrom_substr(gext, &vpt,
+                                        &FR_SBUFF_IN(tmpl_str, talloc_array_length(tmpl_str) - 1),
+                                        type,
+                                        NULL,
+                                        &t_rules);
+               if (!vpt) {
+                       cf_log_perr(cs, "Failed parsing map");
+               error:
+                       talloc_free(g);
+                       return NULL;
+               }
+
+               /*
+                *      Limit the allowed template types.
+                */
+               switch (vpt->type) {
+               case TMPL_TYPE_DATA_UNRESOLVED:
+               case TMPL_TYPE_ATTR:
+               case TMPL_TYPE_ATTR_UNRESOLVED:
+               case TMPL_TYPE_XLAT:
+               case TMPL_TYPE_XLAT_UNRESOLVED:
+               case TMPL_TYPE_EXEC:
+               case TMPL_TYPE_EXEC_UNRESOLVED:
+               case TMPL_TYPE_DATA:
+                       break;
+
+               default:
+                       talloc_free(vpt);
+                       cf_log_err(cs, "Invalid third argument for map");
+                       return NULL;
+               }
+       }
+
+       /*
+        *      This looks at cs->name2 to determine which list to update
+        */
+       map_list_init(&gext->map);
+       rcode = map_afrom_cs(gext, &gext->map, cs, unlang_ctx->rules, &t_rules, fixup_map_cb, NULL, 256);
+       if (rcode < 0) return NULL; /* message already printed */
+       if (map_list_empty(&gext->map)) {
+               cf_log_err(cs, "'map' sections cannot be empty");
+               goto error;
+       }
+
+
+       /*
+        *      Call the map's instantiation function to validate
+        *      the map and perform any caching required.
+        */
+       proc_inst = map_proc_instantiate(gext, proc, cs, vpt, &gext->map);
+       if (!proc_inst) {
+               cf_log_err(cs, "Failed instantiating map function '%s'", name2);
+               goto error;
+       }
+       c = unlang_group_to_generic(g);
+
+       gext->vpt = vpt;
+       gext->proc_inst = proc_inst;
+
+       compile_map_name(g);
+
+       /*
+        *      Cache the module in the unlang_group_t struct.
+        *
+        *      Ensure that the module has a "map" entry in its module
+        *      header?  Or ensure that the map is registered in the
+        *      "bootstrap" phase, so that it's always available here.
+        */
+       if (!pass2_fixup_map_rhs(g, unlang_ctx->rules)) goto error;
+
+       unlang_compile_action_defaults(c, unlang_ctx);
+
+       return c;
+}
+
+
 void unlang_map_init(void)
 {
        unlang_register(UNLANG_TYPE_UPDATE,
@@ -452,6 +1066,7 @@ void unlang_map_init(void)
                                .type = UNLANG_TYPE_UPDATE,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_update,
                                .interpret = unlang_update_state_init,
 
                                .unlang_size = sizeof(unlang_map_t),
@@ -464,6 +1079,7 @@ void unlang_map_init(void)
                                .type = UNLANG_TYPE_MAP,
                                .flag = UNLANG_OP_FLAG_RCODE_SET,
 
+                               .compile = unlang_compile_map,
                                .interpret = unlang_map_state_init,
 
                                .unlang_size = sizeof(unlang_map_t),
index b7a74b4d3f76f84a86aea9e7c37bc7e46b3c8d4d..08404ef10f42a264b14b828ae3ba20597bfec398 100644 (file)
@@ -998,7 +998,8 @@ void unlang_module_init(void)
        unlang_register(UNLANG_TYPE_MODULE,
                           &(unlang_op_t){
                                .name = "module",
-                               .interpret = unlang_module,
+                               .type = UNLANG_TYPE_MODULE,
+
                                /*
                                 *      - UNLANG_OP_FLAG_RCODE_SET
                                 *        Set request->rcode to be the rcode from the module.
@@ -1006,8 +1007,12 @@ void unlang_module_init(void)
                                 *        Set the return point to be the module.
                                 */
                                .flag = UNLANG_OP_FLAG_RCODE_SET |
-                                       UNLANG_OP_FLAG_RETURN_POINT,
+                                       UNLANG_OP_FLAG_RETURN_POINT |
+                                       UNLANG_OP_FLAG_INTERNAL,
+
+                               .interpret = unlang_module,
                                .signal = unlang_module_signal,
+
                                .frame_state_size = sizeof(unlang_frame_state_module_t),
                                .frame_state_type = "unlang_frame_state_module_t",
                           });
index 35f1032aa73a449a2adec3433124a78ce318a3fe..257f9343d9e9dde90e5cfb6ebf0ed7bf473f5554 100644 (file)
@@ -377,6 +377,61 @@ static unlang_action_t unlang_parallel(unlang_result_t *p_result, request_t *req
        return UNLANG_ACTION_YIELD;
 }
 
+static unlang_t *unlang_compile_parallel(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION                    *cs = cf_item_to_section(ci);
+       unlang_t                        *c;
+       char const                      *name2;
+
+       unlang_group_t                  *g;
+       unlang_parallel_t               *gext;
+
+       bool                            clone = true;
+       bool                            detach = false;
+
+       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+
+       /*
+        *      Parallel sections can create empty children, if the
+        *      admin demands it.  Otherwise, the principle of least
+        *      surprise is to copy the whole request, reply, and
+        *      config items.
+        */
+       name2 = cf_section_name2(cs);
+       if (name2) {
+               if (strcmp(name2, "empty") == 0) {
+                       clone = false;
+
+               } else if (strcmp(name2, "detach") == 0) {
+                       detach = true;
+
+               } else {
+                       cf_log_err(cs, "Invalid argument '%s'", name2);
+                       cf_log_err(ci, DOC_KEYWORD_REF(parallel));
+                       return NULL;
+               }
+
+       }
+
+       /*
+        *      We can do "if" in parallel with other "if", but we
+        *      cannot do "else" in parallel with "if".
+        */
+       if (!unlang_compile_limit_subsection(cs, cf_section_name1(cs))) {
+               return NULL;
+       }
+
+       c = unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_PARALLEL);
+       if (!c) return NULL;
+
+       g = unlang_generic_to_group(c);
+       gext = unlang_group_to_parallel(g);
+       gext->clone = clone;
+       gext->detach = detach;
+
+       return c;
+}
+
 void unlang_parallel_init(void)
 {
        unlang_register(UNLANG_TYPE_PARALLEL,
@@ -385,6 +440,7 @@ void unlang_parallel_init(void)
                                .type = UNLANG_TYPE_PARALLEL,   
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET | UNLANG_OP_FLAG_NO_FORCE_UNWIND,
 
+                               .compile = unlang_compile_parallel,
                                .interpret = unlang_parallel,
                                .signal = unlang_parallel_signal,
 
index b43a420e14c4944fd7734eee65d4c18b3f389307..8fec974e6969b87969752b76393a2d92d082bfc6 100644 (file)
@@ -46,13 +46,34 @@ unlang_action_t unlang_return(unlang_result_t *p_result, request_t *request, unl
        return unwind_to_op_flag(NULL, request->stack, UNLANG_OP_FLAG_RETURN_POINT);
 }
 
+static unlang_t *unlang_compile_return(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, UNUSED CONF_ITEM const *ci)
+{
+       /*
+        *      These types are all parallel, and therefore can have a "return" in them.
+        */
+       switch (parent->type) {
+       case UNLANG_TYPE_LOAD_BALANCE:
+       case UNLANG_TYPE_REDUNDANT_LOAD_BALANCE:
+       case UNLANG_TYPE_PARALLEL:
+               break;
+
+       default:
+               parent->closed = true;
+               break;
+       }
+
+       return unlang_compile_empty(parent, unlang_ctx, NULL, UNLANG_TYPE_RETURN);
+}
+
 void unlang_return_init(void)
 {
        unlang_register(UNLANG_TYPE_RETURN,
                           &(unlang_op_t){
                                .name = "return",
                                .type = UNLANG_TYPE_RETURN,
+                               .flag = UNLANG_OP_FLAG_SINGLE_WORD,
 
+                               .compile = unlang_compile_return,
                                .interpret = unlang_return,
 
                                .unlang_size = sizeof(unlang_group_t),
index 652a59fe9ba84e07280f38ebd2e30738ab391644..c16e629825e1c4138a985f02981a14fbaa443048 100644 (file)
@@ -515,6 +515,299 @@ int unlang_subrequest_child_push_and_detach(request_t *request)
        return 0;
 }
 
+static unlang_t *unlang_compile_subrequest(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION                    *cs = cf_item_to_section(ci);
+       char const                      *name2;
+
+       unlang_t                        *c;
+
+       unlang_group_t                  *g;
+       unlang_subrequest_t             *gext;
+
+       unlang_compile_ctx_t            unlang_ctx2;
+
+       tmpl_rules_t                    t_rules;
+       fr_dict_autoload_talloc_t       *dict_ref = NULL;
+
+       fr_dict_t const                 *dict;
+       fr_dict_attr_t const            *da = NULL;
+       fr_dict_enum_value_t const      *type_enum = NULL;
+
+       ssize_t                         slen;
+       char                            *namespace = NULL;
+       char const                      *packet_name = NULL;
+
+       tmpl_t                          *vpt = NULL, *src_vpt = NULL, *dst_vpt = NULL;
+
+       /*
+        *      subrequest { ... }
+        *
+        *      Create a subrequest which is of the same dictionary
+        *      and packet type as the current request.
+        *
+        *      We assume that the Packet-Type attribute exists.
+        */
+       name2 = cf_section_name2(cs);
+       if (!name2) {
+               dict = unlang_ctx->rules->attr.dict_def;
+               packet_name = name2 = unlang_ctx->section_name2;
+               goto get_packet_type;
+       }
+
+       if (cf_section_name2_quote(cs) != T_BARE_WORD) {
+               cf_log_err(cs, "The arguments to 'subrequest' must be a name or an attribute reference");
+       print_url:
+               cf_log_err(ci, DOC_KEYWORD_REF(subrequest));
+               return NULL;
+       }
+
+       dict = unlang_ctx->rules->attr.dict_def;
+
+       /*
+        *      @foo is "dictionary foo", as with references in the dictionaries.
+        *
+        *      @foo::bar is "dictionary foo, Packet-Type = ::bar"
+        *
+        *      foo::bar is "dictionary foo, Packet-Type = ::bar"
+        *
+        *      ::bar is "this dictionary, Packet-Type = ::bar", BUT
+        *      we don't try to parse the new dictionary name, as it
+        *      doesn't exist.
+        */
+       if ((name2[0] == '@') ||
+           ((name2[0] != ':') && (name2[0] != '&') && (strchr(name2 + 1, ':') != NULL))) {
+               char *q;
+
+               if (name2[0] == '@') name2++;
+
+               MEM(namespace = talloc_strdup(parent, name2));
+               q = namespace;
+
+               while (fr_dict_attr_allowed_chars[(unsigned int) *q]) {
+                       q++;
+               }
+               *q = '\0';
+
+               dict = fr_dict_by_protocol_name(namespace);
+               if (!dict) {
+                       dict_ref = fr_dict_autoload_talloc(NULL, &dict, namespace);
+                       if (!dict_ref) {
+                               cf_log_err(cs, "Unknown namespace in '%s'", name2);
+                               talloc_free(namespace);
+                               return NULL;
+                       }
+               }
+
+               /*
+                *      Skip the dictionary name, and go to the thing
+                *      right after it.
+                */
+               name2 += (q - namespace);
+               TALLOC_FREE(namespace);
+       }
+
+       /*
+        *      @dict::enum is "other dictionary, Packet-Type = ::enum"
+        *      ::enum is this dictionary, "Packet-Type = ::enum"
+        */
+       if ((name2[0] == ':') && (name2[1] == ':')) {
+               packet_name = name2;
+               goto get_packet_type;
+       }
+
+       /*
+        *      Can't do foo.bar.baz::foo, the enums are only used for Packet-Type.
+        */
+       if (strchr(name2, ':') != NULL) {
+               cf_log_err(cs, "Reference cannot contain enum value in '%s'", name2);
+               return NULL;
+       }
+
+       /*
+        *      '&' means "attribute reference"
+        *
+        *      Or, bare word an require_enum_prefix means "attribute reference".
+        */
+       slen = tmpl_afrom_attr_substr(parent, NULL, &vpt,
+                                     &FR_SBUFF_IN(name2, talloc_array_length(name2) - 1),
+                                     NULL, unlang_ctx->rules);
+       if (slen <= 0) {
+               cf_log_perr(cs, "Invalid argument to 'subrequest', failed parsing packet-type");
+               goto print_url;
+       }
+
+       fr_assert(tmpl_is_attr(vpt));
+
+       /*
+        *      Anything resembling an integer or string is
+        *      OK.  Nothing else makes sense.
+        */
+       switch (tmpl_attr_tail_da(vpt)->type) {
+       case FR_TYPE_INTEGER_EXCEPT_BOOL:
+       case FR_TYPE_STRING:
+               break;
+
+       default:
+               talloc_free(vpt);
+               cf_log_err(cs, "Invalid data type for attribute %s.  "
+                          "Must be an integer type or string", name2 + 1);
+               goto print_url;
+       }
+
+       dict = unlang_ctx->rules->attr.dict_def;
+       packet_name = NULL;
+
+get_packet_type:
+       /*
+        *      Local attributes cannot be used in a subrequest.  They belong to the parent.  Local attributes
+        *      are NOT copied to the subrequest.
+        *
+        *      @todo - maybe we want to copy local variables, too?  But there may be multiple nested local
+        *      variables, each with their own dictionary.
+        */
+       dict = fr_dict_proto_dict(dict);
+
+       /*
+        *      Use dict name instead of "namespace", because "namespace" can be omitted.
+        */
+       da = fr_dict_attr_by_name(NULL, fr_dict_root(dict), "Packet-Type");
+       if (!da) {
+               cf_log_err(cs, "No such attribute 'Packet-Type' in namespace '%s'", fr_dict_root(dict)->name);
+       error:
+               talloc_free(namespace);
+               talloc_free(vpt);
+               talloc_free(dict_ref);
+               goto print_url;
+       }
+
+       if (packet_name) {
+               /*
+                *      Allow ::enum-name for packet types
+                */
+               if ((packet_name[0] == ':') && (packet_name[1] == ':')) packet_name += 2;
+
+               type_enum = fr_dict_enum_by_name(da, packet_name, -1);
+               if (!type_enum) {
+                       cf_log_err(cs, "No such value '%s' for attribute 'Packet-Type' in namespace '%s'",
+                                  packet_name, fr_dict_root(dict)->name);
+                       goto error;
+               }
+       }
+
+       /*
+        *      No longer needed
+        */
+       talloc_free(namespace);
+
+       /*
+        *      Source and destination arguments
+        */
+       {
+               char const      *dst, *src;
+
+               src = cf_section_argv(cs, 0);
+               if (src) {
+                       RULES_VERIFY(unlang_ctx->rules);
+
+                       (void) tmpl_afrom_substr(parent, &src_vpt,
+                                                &FR_SBUFF_IN(src, talloc_array_length(src) - 1),
+                                                cf_section_argv_quote(cs, 0), NULL, unlang_ctx->rules);
+                       if (!src_vpt) {
+                               cf_log_perr(cs, "Invalid argument to 'subrequest', failed parsing src");
+                               goto error;
+                       }
+
+                       if (!tmpl_contains_attr(src_vpt)) {
+                               cf_log_err(cs, "Invalid argument to 'subrequest' src must be an attr or list, got %s",
+                                          tmpl_type_to_str(src_vpt->type));
+                               talloc_free(src_vpt);
+                               goto error;
+                       }
+
+                       dst = cf_section_argv(cs, 1);
+                       if (dst) {
+                               RULES_VERIFY(unlang_ctx->rules);
+
+                               (void) tmpl_afrom_substr(parent, &dst_vpt,
+                                                        &FR_SBUFF_IN(dst, talloc_array_length(dst) - 1),
+                                                        cf_section_argv_quote(cs, 1), NULL, unlang_ctx->rules);
+                               if (!dst_vpt) {
+                                       cf_log_perr(cs, "Invalid argument to 'subrequest', failed parsing dst");
+                                       goto error;
+                               }
+
+                               if (!tmpl_contains_attr(dst_vpt)) {
+                                       cf_log_err(cs, "Invalid argument to 'subrequest' dst must be an "
+                                                  "attr or list, got %s",
+                                                  tmpl_type_to_str(src_vpt->type));
+                                       talloc_free(src_vpt);
+                                       talloc_free(dst_vpt);
+                                       goto error;
+                               }
+                       }
+               }
+       }
+
+       if (!cf_item_next(cs, NULL)) {
+               talloc_free(vpt);
+               talloc_free(src_vpt);
+               talloc_free(dst_vpt);
+               return UNLANG_IGNORE;
+       }
+
+       t_rules = *unlang_ctx->rules;
+       t_rules.parent = unlang_ctx->rules;
+       t_rules.attr.dict_def = dict;
+       t_rules.attr.allow_foreign = false;
+
+       /*
+        *      Copy over the compilation context.  This is mostly
+        *      just to ensure that retry is handled correctly.
+        *      i.e. reset.
+        */
+       unlang_compile_ctx_copy(&unlang_ctx2, unlang_ctx);
+
+       /*
+        *      Then over-write the new compilation context.
+        */
+       unlang_ctx2.section_name1 = "subrequest";
+       unlang_ctx2.section_name2 = name2;
+       unlang_ctx2.rules = &t_rules;
+
+       /*
+        *      Compile the subsection with a *different* default dictionary.
+        */
+       c = unlang_compile_section(parent, &unlang_ctx2, cs, UNLANG_TYPE_SUBREQUEST);
+       if (!c) return NULL;
+
+       /*
+        *      Set the dictionary and packet information, which tells
+        *      unlang_subrequest() how to process the request.
+        */
+       g = unlang_generic_to_group(c);
+       gext = unlang_group_to_subrequest(g);
+
+       if (dict_ref) {
+               /*
+                *      Parent the dictionary reference correctly now that we
+                *      have the section with the dependency.  This should
+                *      be fast as dict_ref has no siblings.
+                */
+               talloc_steal(gext, dict_ref);
+       }
+       if (vpt) gext->vpt = talloc_steal(gext, vpt);
+
+       gext->dict = dict;
+       gext->attr_packet_type = da;
+       gext->type_enum = type_enum;
+       gext->src = src_vpt;
+       gext->dst = dst_vpt;
+
+       return c;
+}
+
+
 /** Initialise subrequest ops
  *
  */
@@ -538,6 +831,7 @@ int unlang_subrequest_op_init(void)
                                 */
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET | UNLANG_OP_FLAG_NO_FORCE_UNWIND,
 
+                               .compile = unlang_compile_subrequest,
                                .interpret = unlang_subrequest_init,
                                .signal = unlang_subrequest_signal,
 
index 5c68bfbbb3311735ba9623311f6a1281798e349b..64fb3783c224979bcf167269a1f2ede604d66708 100644 (file)
@@ -27,6 +27,7 @@ RCSID("$Id$")
 #include <freeradius-devel/server/rcode.h>
 #include "group_priv.h"
 #include "switch_priv.h"
+#include "xlat_priv.h"
 
 static unlang_action_t unlang_switch(UNUSED unlang_result_t *p_result, request_t *request, unlang_stack_frame_t *frame)
 {
@@ -128,6 +129,396 @@ static unlang_action_t unlang_case(unlang_result_t *p_result, request_t *request
        return unlang_group(p_result, request, frame);
 }
 
+
+static unlang_t *unlang_compile_case(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION            *cs = cf_item_to_section(ci);
+       int                     i;
+       char const              *name2;
+       unlang_t                *c;
+       unlang_group_t          *case_g;
+       unlang_case_t           *case_gext;
+       tmpl_t                  *vpt = NULL;
+       tmpl_rules_t            t_rules;
+
+       /*
+        *      We allow unknown attributes here.
+        */
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.allow_unknown = true;
+       RULES_VERIFY(&t_rules);
+
+       if (!parent || (parent->type != UNLANG_TYPE_SWITCH)) {
+               cf_log_err(cs, "\"case\" statements may only appear within a \"switch\" section");
+               cf_log_err(ci, DOC_KEYWORD_REF(case));
+               return NULL;
+       }
+
+       /*
+        *      case THING means "match THING"
+        *      case       means "match anything"
+        */
+       name2 = cf_section_name2(cs);
+       if (name2) {
+               ssize_t                 slen;
+               fr_token_t              quote;
+               unlang_group_t          *switch_g;
+               unlang_switch_t         *switch_gext;
+
+               switch_g = unlang_generic_to_group(parent);
+               switch_gext = unlang_group_to_switch(switch_g);
+
+               fr_assert(switch_gext->vpt != NULL);
+
+               /*
+                *      We need to cast case values to match
+                *      what we're switching over, otherwise
+                *      integers of different widths won't
+                *      match.
+                */
+               t_rules.cast = tmpl_expanded_type(switch_gext->vpt);
+
+               /*
+                *      Need to pass the attribute from switch
+                *      to tmpl rules so we can convert the
+                *      case string to an integer value.
+                */
+               if (tmpl_is_attr(switch_gext->vpt)) {
+                       fr_dict_attr_t const *da = tmpl_attr_tail_da(switch_gext->vpt);
+                       if (da->flags.has_value) t_rules.enumv = da;
+               }
+
+               quote = cf_section_name2_quote(cs);
+
+               slen = tmpl_afrom_substr(cs, &vpt,
+                                        &FR_SBUFF_IN(name2, strlen(name2)),
+                                        quote,
+                                        NULL,
+                                        &t_rules);
+               if (!vpt) {
+                       cf_canonicalize_error(cs, slen, "Failed parsing argument to 'case'", name2);
+                       return NULL;
+               }
+
+               /*
+                *      Bare word strings are attribute references
+                */
+               if (tmpl_is_attr(vpt) || tmpl_is_attr_unresolved(vpt)) {
+               fail_attr:
+                       cf_log_err(cs, "arguments to 'case' statements MUST NOT be attribute references.");
+                       goto fail;
+               }
+
+               if (!tmpl_is_data(vpt) || tmpl_is_data_unresolved(vpt)) {
+                       cf_log_err(cs, "arguments to 'case' statements MUST be static data.");
+               fail:
+                       talloc_free(vpt);
+                       return NULL;
+               }
+
+               /*
+                *      References to unresolved attributes are forbidden.  They are no longer "bare word
+                *      strings".
+                */
+               if ((quote == T_BARE_WORD) && (tmpl_value_type(vpt) == FR_TYPE_STRING)) {
+                       goto fail_attr;
+               }
+
+       } /* else it's a default 'case' statement */
+
+       /*
+        *      If we were asked to match something, then we MUST
+        *      match it, even if the section is empty.  Otherwise we
+        *      will silently skip the match, and then fall through to
+        *      the "default" statement.
+        */
+       c = unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_CASE);
+       if (!c) {
+               talloc_free(vpt);
+               return NULL;
+       }
+
+       case_g = unlang_generic_to_group(c);
+       case_gext = unlang_group_to_case(case_g);
+       case_gext->vpt = talloc_steal(case_gext, vpt);
+
+       /*
+        *      Set all of it's codes to return, so that
+        *      when we pick a 'case' statement, we don't
+        *      fall through to processing the next one.
+        */
+       for (i = 0; i < RLM_MODULE_NUMCODES; i++) c->actions.actions[i] = MOD_ACTION_RETURN;
+
+       return c;
+}
+
+static int8_t case_cmp(void const *one, void const *two)
+{
+       unlang_case_t const *a = (unlang_case_t const *) one; /* may not be talloc'd! See switch.c */
+       unlang_case_t const *b = (unlang_case_t const *) two; /* may not be talloc'd! */
+
+       return fr_value_box_cmp(tmpl_value(a->vpt), tmpl_value(b->vpt));
+}
+
+static uint32_t case_hash(void const *data)
+{
+       unlang_case_t const *a = (unlang_case_t const *) data; /* may not be talloc'd! */
+
+       return fr_value_box_hash(tmpl_value(a->vpt));
+}
+
+static int case_to_key(uint8_t **out, size_t *outlen, void const *data)
+{
+       unlang_case_t const *a = (unlang_case_t const *) data; /* may not be talloc'd! */
+
+       return fr_value_box_to_key(out, outlen, tmpl_value(a->vpt));
+}
+
+static unlang_t *unlang_compile_switch(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION            *cs = cf_item_to_section(ci);
+       CONF_ITEM               *subci;
+       fr_token_t              token;
+       char const              *name1, *name2;
+       char const              *type_name;
+
+       unlang_group_t          *g;
+       unlang_switch_t         *gext;
+
+       unlang_t                *c;
+       ssize_t                 slen;
+
+       tmpl_rules_t            t_rules;
+
+       fr_type_t               type;
+       fr_htrie_type_t         htype;
+
+       /*
+        *      We allow unknown attributes here.
+        */
+       t_rules = *(unlang_ctx->rules);
+       t_rules.attr.allow_unknown = true;
+       RULES_VERIFY(&t_rules);
+
+       name2 = cf_section_name2(cs);
+       if (!name2) {
+               cf_log_err(cs, "You must specify a variable to switch over for 'switch'");
+       print_url:
+               cf_log_err(ci, DOC_KEYWORD_REF(switch));
+               return NULL;
+       }
+
+       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_SWITCH);
+       if (!g) return NULL;
+
+       gext = unlang_group_to_switch(g);
+
+       /*
+        *      Create the template.  All attributes and xlats are
+        *      defined by now.
+        *
+        *      The 'case' statements need g->vpt filled out to ensure
+        *      that the data types match.
+        */
+       token = cf_section_name2_quote(cs);
+
+       if ((token == T_BARE_WORD) && (name2[0] != '%')) {
+               slen = tmpl_afrom_attr_substr(gext, NULL, &gext->vpt,
+                                             &FR_SBUFF_IN(name2, strlen(name2)),
+                                             NULL,
+                                             &t_rules);
+       } else {
+               slen = tmpl_afrom_substr(gext, &gext->vpt,
+                                        &FR_SBUFF_IN(name2, strlen(name2)),
+                                        token,
+                                        NULL,
+                                        &t_rules);
+       }
+       if (!gext->vpt) {
+               cf_canonicalize_error(cs, slen, "Failed parsing argument to 'switch'", name2);
+               talloc_free(g);
+               return NULL;
+       }
+
+       c = unlang_group_to_generic(g);
+       c->name = "switch";
+       c->debug_name = talloc_typed_asprintf(c, "switch %s", name2);
+
+       /*
+        *      Fixup the template before compiling the children.
+        *      This is so that compile_case() can do attribute type
+        *      checks / casts against us.
+        */
+       if (!pass2_fixup_tmpl(g, &gext->vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
+               talloc_free(g);
+               return NULL;
+       }
+
+       if (tmpl_is_list(gext->vpt)) {
+               cf_log_err(cs, "Cannot use list for 'switch' statement");
+       error:
+               talloc_free(g);
+               goto print_url;
+       }
+
+       if (tmpl_contains_regex(gext->vpt)) {
+               cf_log_err(cs, "Cannot use regular expression for 'switch' statement");
+               goto error;
+       }
+
+       if (tmpl_is_data(gext->vpt)) {
+               cf_log_err(cs, "Cannot use constant data for 'switch' statement");
+               goto error;
+       }
+
+       if (tmpl_is_xlat(gext->vpt)) {
+               xlat_exp_head_t *xlat = tmpl_xlat(gext->vpt);
+
+               if (xlat->flags.constant || xlat->flags.pure) {
+                       cf_log_err(cs, "Cannot use constant data for 'switch' statement");
+                       goto error;
+               }
+       }
+
+
+       if (tmpl_needs_resolving(gext->vpt)) {
+               cf_log_err(cs, "Cannot resolve key for 'switch' statement");
+               goto error;
+       }
+
+       type_name = cf_section_argv(cs, 0); /* AFTER name1, name2 */
+       if (type_name) {
+               type = fr_table_value_by_str(fr_type_table, type_name, FR_TYPE_NULL);
+
+               /*
+                *      Should have been caught in cf_file.c, process_switch()
+                */
+               fr_assert(type != FR_TYPE_NULL);
+               fr_assert(fr_type_is_leaf(type));
+
+       do_cast:
+               if (tmpl_cast_set(gext->vpt, type) < 0) {
+                       cf_log_perr(cs, "Failed setting cast type");
+                       goto error;
+               }
+
+       } else {
+               /*
+                *      Get the return type of the tmpl.  If we don't know,
+                *      mash it all to string.
+                */
+               type = tmpl_data_type(gext->vpt);
+               if ((type == FR_TYPE_NULL) || (type == FR_TYPE_VOID)) {
+                       type = FR_TYPE_STRING;
+                       goto do_cast;
+               }
+       }
+
+       htype = fr_htrie_hint(type);
+       if (htype == FR_HTRIE_INVALID) {
+               cf_log_err(cs, "Invalid data type '%s' used for 'switch' statement",
+                           fr_type_to_str(type));
+               goto error;
+       }
+
+       gext->ht = fr_htrie_alloc(gext, htype,
+                                 (fr_hash_t) case_hash,
+                                 (fr_cmp_t) case_cmp,
+                                 (fr_trie_key_t) case_to_key,
+                                 NULL);
+       if (!gext->ht) {
+               cf_log_err(cs, "Failed initializing internal data structures");
+               talloc_free(g);
+               return NULL;
+       }
+
+       /*
+        *      Walk through the children of the switch section,
+        *      ensuring that they're all 'case' statements, and then compiling them.
+        */
+       for (subci = cf_item_next(cs, NULL);
+            subci != NULL;
+            subci = cf_item_next(cs, subci)) {
+               CONF_SECTION *subcs;
+               unlang_t *single;
+               unlang_case_t   *case_gext;
+
+               if (!cf_item_is_section(subci)) {
+                       if (!cf_item_is_pair(subci)) continue;
+
+                       cf_log_err(subci, "\"switch\" sections can only have \"case\" subsections");
+                       goto error;
+               }
+
+               subcs = cf_item_to_section(subci);      /* can't return NULL */
+               name1 = cf_section_name1(subcs);
+
+               if (strcmp(name1, "case") != 0) {
+                       /*
+                        *      We finally support "default" sections for "switch".
+                        */
+                       if (strcmp(name1, "default") == 0) {
+                               if (cf_section_name2(subcs) != 0) {
+                                       cf_log_err(subci, "\"default\" sections cannot have a match argument");
+                                       goto error;
+                               }
+                               goto handle_default;
+                       }
+
+                       cf_log_err(subci, "\"switch\" sections can only have \"case\" subsections");
+                       goto error;
+               }
+
+               name2 = cf_section_name2(subcs);
+               if (!name2) {
+               handle_default:
+                       if (gext->default_case) {
+                               cf_log_err(subci, "Cannot have two 'default' case statements");
+                               goto error;
+                       }
+               }
+
+               /*
+                *      Compile the subsection.
+                */
+               single = unlang_compile_case(c, unlang_ctx, subci);
+               if (!single) goto error;
+
+               fr_assert(single->type == UNLANG_TYPE_CASE);
+
+               /*
+                *      Remember the "default" section, and insert the
+                *      non-default "case" into the htrie.
+                */
+               case_gext = unlang_group_to_case(unlang_generic_to_group(single));
+               if (!case_gext->vpt) {
+                       gext->default_case = single;
+
+               } else if (!fr_htrie_insert(gext->ht, single)) {
+                       single = fr_htrie_find(gext->ht, single);
+
+                       /*
+                        *      @todo - look up the key and get the previous one?
+                        */
+                       cf_log_err(subci, "Failed inserting 'case' statement.  Is there a duplicate?");
+
+                       if (single) cf_log_err(unlang_generic_to_group(single)->cs, "Duplicate may be here.");
+
+                       goto error;
+               }
+
+               *g->tail = single;
+               g->tail = &single->next;
+               g->num_children++;
+       }
+
+       unlang_compile_action_defaults(c, unlang_ctx); /* why is this here???? */
+
+       return c;
+}
+
 void unlang_switch_init(void)
 {
        unlang_register(UNLANG_TYPE_SWITCH,
@@ -136,6 +527,7 @@ void unlang_switch_init(void)
                                .type = UNLANG_TYPE_SWITCH,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_switch,
                                .interpret = unlang_switch,
                                        
 
@@ -153,6 +545,7 @@ void unlang_switch_init(void)
                                .type = UNLANG_TYPE_CASE,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_BREAK_POINT,
 
+                               .compile = unlang_compile_case,
                                .interpret = unlang_case,
 
                                .unlang_size = sizeof(unlang_case_t),
index 73c90d260feb4afd1235eed0b865229fa8daaba4..b93cba8b32e7799f0a8aea0954ad3e5adb8b6969 100644 (file)
@@ -265,6 +265,108 @@ int unlang_timeout_section_push(request_t *request, CONF_SECTION *cs, fr_time_de
 
 }
 
+static unlang_t *unlang_compile_timeout(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION            *cs = cf_item_to_section(ci);
+       char const              *name2;
+       unlang_t                *c;
+       unlang_group_t          *g;
+       unlang_timeout_t        *gext;
+       fr_time_delta_t         timeout = fr_time_delta_from_sec(0);
+       tmpl_t                  *vpt = NULL;
+       fr_token_t              token;
+
+       /*
+        *      Timeout <time ref>
+        */
+       name2 = cf_section_name2(cs);
+       if (!name2) {
+               cf_log_err(cs, "You must specify a time value for 'timeout'");
+       print_url:
+               cf_log_err(ci, DOC_KEYWORD_REF(timeout));
+               return NULL;
+       }
+
+       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_TIMEOUT);
+       if (!g) return NULL;
+
+       gext = unlang_group_to_timeout(g);
+
+       token = cf_section_name2_quote(cs);
+
+       if ((token == T_BARE_WORD) && isdigit((uint8_t) *name2)) {
+               if (fr_time_delta_from_str(&timeout, name2, strlen(name2), FR_TIME_RES_SEC) < 0) {
+                       cf_log_err(cs, "Failed parsing time delta %s - %s",
+                                  name2, fr_strerror());
+                       return NULL;
+               }
+       } else {
+               ssize_t         slen;
+               tmpl_rules_t    t_rules;
+
+               /*
+                *      We don't allow unknown attributes here.
+                */
+               t_rules = *(unlang_ctx->rules);
+               t_rules.attr.allow_unknown = false;
+               RULES_VERIFY(&t_rules);
+
+               slen = tmpl_afrom_substr(gext, &vpt,
+                                        &FR_SBUFF_IN(name2, strlen(name2)),
+                                        token,
+                                        NULL,
+                                        &t_rules);
+               if (!vpt) {
+                       cf_canonicalize_error(cs, slen, "Failed parsing argument to 'timeout'", name2);
+                       talloc_free(g);
+                       return NULL;
+               }
+
+               /*
+                *      Fixup the tmpl so that we know it's somewhat sane.
+                */
+               if (!pass2_fixup_tmpl(gext, &vpt, cf_section_to_item(cs), unlang_ctx->rules->attr.dict_def)) {
+                       talloc_free(g);
+                       return NULL;
+               }
+
+               if (tmpl_is_list(vpt)) {
+                       cf_log_err(cs, "Cannot use list as argument for 'timeout' statement");
+               error:
+                       talloc_free(g);
+                       goto print_url;
+               }
+
+               if (tmpl_contains_regex(vpt)) {
+                       cf_log_err(cs, "Cannot use regular expression as argument for 'timeout' statement");
+                       goto error;
+               }
+
+               /*
+                *      Attribute or data MUST be cast to TIME_DELTA.
+                */
+               if (tmpl_cast_set(vpt, FR_TYPE_TIME_DELTA) < 0) {
+                       cf_log_perr(cs, "Failed setting cast type");
+                       goto error;
+               }
+       }
+
+       /*
+        *      Compile the contents of a "timeout".
+        */
+       c = unlang_compile_section(parent, unlang_ctx, cs, UNLANG_TYPE_TIMEOUT);
+       if (!c) return NULL;
+
+       g = unlang_generic_to_group(c);
+       gext = unlang_group_to_timeout(g);
+       gext->timeout = timeout;
+       gext->vpt = vpt;
+
+       return c;
+}
+
 void unlang_timeout_init(void)
 {
        unlang_register(UNLANG_TYPE_TIMEOUT,
@@ -273,6 +375,7 @@ void unlang_timeout_init(void)
                                .type = UNLANG_TYPE_TIMEOUT,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET,
 
+                               .compile = unlang_compile_timeout,
                                .interpret = unlang_timeout,
                                .signal = unlang_timeout_signal,
 
index d40d72252acfe868ab25337d60776f25b995db8a..0b1c249c44c0d058440fb9ec95788900b1ca30c1 100644 (file)
@@ -330,8 +330,12 @@ void unlang_tmpl_init(void)
        unlang_register(UNLANG_TYPE_TMPL,
                           &(unlang_op_t){
                                .name = "tmpl",
+                               .type = UNLANG_TYPE_TMPL,
+                               .flag = UNLANG_OP_FLAG_INTERNAL,
+
                                .interpret = unlang_tmpl,
                                .signal = unlang_tmpl_signal,
+
                                .frame_state_size = sizeof(unlang_frame_state_tmpl_t),
                                .frame_state_type = "unlang_frame_state_tmpl_t",
                           });
index 746d91503c11b95e02cee1d984487d960df75d9f..6e02ab3c353e4ee93a2c575ce202ac1a411b2e3e 100644 (file)
@@ -134,6 +134,138 @@ fr_edit_list_t *unlang_interpret_edit_list(request_t *request)
        return NULL;
 }
 
+static fr_table_num_sorted_t transaction_keywords[] = {
+       { L("case"),            1 },
+       { L("else"),            1 },
+       { L("elsif"),           1 },
+       { L("foreach"),         1 },
+       { L("group"),           1 },
+       { L("if"),              1 },
+       { L("limit"),           1 },
+       { L("load-balance"),    1 },
+       { L("redundant"),       1 },
+       { L("redundant-load-balance"), 1 },
+       { L("switch"),          1 },
+       { L("timeout"),         1 },
+       { L("transaction"),     1 },
+};
+static int transaction_keywords_len = NUM_ELEMENTS(transaction_keywords);
+
+/** Limit the operations which can appear in a transaction.
+ */
+static bool transaction_ok(CONF_SECTION *cs)
+{
+       CONF_ITEM *ci = NULL;
+
+       while ((ci = cf_item_next(cs, ci)) != NULL) {
+               char const *name;
+
+               if (cf_item_is_section(ci)) {
+                       CONF_SECTION *subcs;
+
+                       subcs = cf_item_to_section(ci);
+                       name = cf_section_name1(subcs);
+
+                       if (strcmp(name, "actions") == 0) continue;
+
+                       /*
+                        *      Definitely an attribute editing thing.
+                        */
+                       if (*name == '&') continue;
+
+                       if (fr_list_assignment_op[cf_section_name2_quote(cs)]) continue;
+
+                       if (fr_table_value_by_str(transaction_keywords, name, -1) < 0) {
+                               cf_log_err(ci, "Invalid keyword in 'transaction'");
+                               return false;
+                       }
+
+                       if (!transaction_ok(subcs)) return false;
+
+                       continue;
+
+               } else if (cf_item_is_pair(ci)) {
+                       CONF_PAIR *cp;
+
+                       cp = cf_item_to_pair(ci);
+                       name = cf_pair_attr(cp);
+
+                       /*
+                        *      If there's a value then it's not a module call.
+                        */
+                       if (cf_pair_value(cp)) continue;
+
+                       if (*name == '&') continue;
+
+                       /*
+                        *      Allow rcodes via the "always" module.
+                        */
+                       if (fr_table_value_by_str(mod_rcode_table, name, -1) >= 0) {
+                               continue;
+                       }
+
+                       cf_log_err(ci, "Invalid module reference in 'transaction'");
+                       return false;
+
+               } else {
+                       continue;
+               }
+       }
+
+       return true;
+}
+
+static unlang_t *unlang_compile_transaction(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION *cs = cf_item_to_section(ci);
+       unlang_group_t *g;
+       unlang_t *c;
+       unlang_compile_ctx_t unlang_ctx2;
+
+       if (cf_section_name2(cs) != NULL) {
+               cf_log_err(cs, "Unexpected argument to 'transaction' section");
+               cf_log_err(ci, DOC_KEYWORD_REF(transaction));
+               return NULL;
+       }
+
+       /*
+        *      The transaction is empty, ignore it.
+        */
+       if (!cf_item_next(cs, NULL)) return UNLANG_IGNORE;
+
+       if (!transaction_ok(cs)) return NULL;
+
+       /*
+        *      Any failure is return, not continue.
+        */
+       unlang_compile_ctx_copy(&unlang_ctx2, unlang_ctx);
+
+       unlang_ctx2.actions.actions[RLM_MODULE_REJECT] = MOD_ACTION_RETURN;
+       unlang_ctx2.actions.actions[RLM_MODULE_FAIL] = MOD_ACTION_RETURN;
+       unlang_ctx2.actions.actions[RLM_MODULE_INVALID] = MOD_ACTION_RETURN;
+       unlang_ctx2.actions.actions[RLM_MODULE_DISALLOW] = MOD_ACTION_RETURN;
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_TRANSACTION);
+       if (!g) return NULL;
+
+       c = unlang_group_to_generic(g);
+       c->debug_name = c->name = cf_section_name1(cs);
+
+       if (!unlang_compile_children(g, &unlang_ctx2, false)) return NULL;
+
+       /*
+        *      The default for a failed transaction is to continue on
+        *      failure.
+        */
+       if (!c->actions.actions[RLM_MODULE_FAIL])     c->actions.actions[RLM_MODULE_FAIL] = 1;
+       if (!c->actions.actions[RLM_MODULE_INVALID])  c->actions.actions[RLM_MODULE_INVALID] = 1;
+       if (!c->actions.actions[RLM_MODULE_DISALLOW]) c->actions.actions[RLM_MODULE_DISALLOW] = 1;
+
+       unlang_compile_action_defaults(c, unlang_ctx); /* why is this here???? */
+
+       return c;
+}
+
 void unlang_transaction_init(void)
 {
        unlang_register(UNLANG_TYPE_TRANSACTION,
@@ -142,6 +274,7 @@ void unlang_transaction_init(void)
                                .type = UNLANG_TYPE_TRANSACTION,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_transaction,
                                .interpret = unlang_transaction,
                                .signal = unlang_transaction_signal,
 
index b76b97494b0c0634602471ceb895529121d9bcb7..398433ebf062b034387d101cb011b1a2d8421611 100644 (file)
@@ -40,6 +40,47 @@ static unlang_action_t unlang_try(UNUSED unlang_result_t *p_result, request_t *r
        return unlang_interpret_push_children(NULL, request, RLM_MODULE_NOT_SET, UNLANG_NEXT_SIBLING);
 }
 
+static unlang_t *unlang_compile_try(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci)
+{
+       CONF_SECTION *cs = cf_item_to_section(ci);
+       unlang_group_t *g;
+       unlang_t *c;
+       CONF_ITEM *next;
+
+       /*
+        *      The transaction is empty, ignore it.
+        */
+       if (!cf_item_next(cs, NULL)) {
+               cf_log_err(cs, "'try' sections cannot be empty");
+       print_url:
+               cf_log_err(ci, DOC_KEYWORD_REF(try));
+               return NULL;
+       }
+
+       if (cf_section_name2(cs) != NULL) {
+               cf_log_err(cs, "Unexpected argument to 'try' section");
+               goto print_url;
+       }
+
+       next = cf_item_next(cf_parent(cs), ci);
+       while (next && cf_item_is_data(next)) next = cf_item_next(cf_parent(cs), next);
+
+       if (!next || !cf_item_is_section(next) ||
+           (strcmp(cf_section_name1(cf_item_to_section(next)), "catch") != 0)) {
+               cf_log_err(cs, "'try' sections must be followed by a 'catch'");
+               goto print_url;
+       }
+
+       g = unlang_group_allocate(parent, cs, UNLANG_TYPE_TRY);
+       if (!g) return NULL;
+
+       c = unlang_group_to_generic(g);
+       c->debug_name = c->name = cf_section_name1(cs);
+
+       return unlang_compile_children(g, unlang_ctx, true);
+}
+
+
 void unlang_try_init(void)
 {
        unlang_register(UNLANG_TYPE_TRY,
@@ -48,6 +89,7 @@ void unlang_try_init(void)
                                .type = UNLANG_TYPE_TRY,
                                .flag = UNLANG_OP_FLAG_DEBUG_BRACES,
 
+                               .compile = unlang_compile_try,
                                .interpret = unlang_try,
 
                                .unlang_size = sizeof(unlang_try_t),
index 273fe2f570bb9834cad7f28a7484b03f8a79ccb0..5054fa296c0c9c89edd2d2cefacbd251d69ecc88 100644 (file)
@@ -203,6 +203,89 @@ typedef void (*unlang_dump_t)(request_t *request, unlang_stack_frame_t *frame);
 
 typedef int (*unlang_thread_instantiate_t)(unlang_t const *instruction, void *thread_inst);
 
+typedef struct {
+       virtual_server_t const          *vs;                    //!< Virtual server we're compiling in the context of.
+                                                               ///< This shouldn't change during the compilation of
+                                                               ///< a single unlang section.
+       char const                      *section_name1;
+       char const                      *section_name2;
+       unlang_mod_actions_t            actions;
+       tmpl_rules_t const              *rules;
+} unlang_compile_ctx_t;
+
+typedef unlang_t *(*unlang_compile_t)(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_ITEM const *ci);
+
+#define UNLANG_IGNORE ((unlang_t *) -1)
+
+unlang_t *unlang_compile_empty(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type);
+
+unlang_t *unlang_compile_section(unlang_t *parent, unlang_compile_ctx_t *unlang_ctx, CONF_SECTION *cs, unlang_type_t type);
+
+unlang_t *unlang_compile_children(unlang_group_t *g, unlang_compile_ctx_t *unlang_ctx_in, bool set_action_defaults);
+
+unlang_group_t *unlang_group_allocate(unlang_t *parent, CONF_SECTION *cs, unlang_type_t type);
+
+int unlang_define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rules_t *t_rules, fr_type_t type, char const *name,
+                                fr_dict_attr_t const *ref);
+
+bool unlang_compile_limit_subsection(CONF_SECTION *cs, char const *name);
+
+/*
+ *     @todo - arguably this should be part of the core compiler, and
+ *     never called by any keyword.
+ */
+void unlang_compile_action_defaults(unlang_t *c, unlang_compile_ctx_t *unlang_ctx);
+
+/*
+ *     @todo - These functions should be made private once all of they keywords have been moved to foo(args) syntax.
+ */
+bool pass2_fixup_tmpl(UNUSED TALLOC_CTX *ctx, tmpl_t **vpt_p, CONF_ITEM const *ci, fr_dict_t const *dict);
+bool pass2_fixup_map(map_t *map, tmpl_rules_t const *rules, fr_dict_attr_t const *parent);
+bool pass2_fixup_update(unlang_group_t *g, tmpl_rules_t const *rules);
+bool pass2_fixup_map_rhs(unlang_group_t *g, tmpl_rules_t const *rules);
+
+/*
+ *     When we switch to a new unlang ctx, we use the new component
+ *     name and number, but we use the CURRENT actions.
+ */
+static inline CC_HINT(always_inline)
+void unlang_compile_ctx_copy(unlang_compile_ctx_t *dst, unlang_compile_ctx_t const *src)
+{
+       int i;
+
+       *dst = *src;
+
+       /*
+        *      Ensure that none of the actions are RETRY.
+        */
+       for (i = 0; i < RLM_MODULE_NUMCODES; i++) {
+               if (dst->actions.actions[i] == MOD_ACTION_RETRY) dst->actions.actions[i] = MOD_PRIORITY_MIN;
+       }
+       memset(&dst->actions.retry, 0, sizeof(dst->actions.retry)); \
+}
+
+
+#ifndef NDEBUG
+static inline CC_HINT(always_inline) int unlang_attr_rules_verify(tmpl_attr_rules_t const *rules)
+{
+       if (!fr_cond_assert_msg(rules->dict_def, "No protocol dictionary set")) return -1;
+       if (!fr_cond_assert_msg(rules->dict_def != fr_dict_internal(), "rules->attr.dict_def must not be the internal dictionary")) return -1;
+       if (!fr_cond_assert_msg(!rules->allow_foreign, "rules->attr.allow_foreign must be false")) return -1;
+
+       return 0;
+}
+
+static inline CC_HINT(always_inline) int unlang_rules_verify(tmpl_rules_t const *rules)
+{
+       if (!fr_cond_assert_msg(!rules->at_runtime, "rules->at_runtime must be false")) return -1;
+       return unlang_attr_rules_verify(&rules->attr);
+}
+
+#define RULES_VERIFY(_rules) do { if (unlang_rules_verify(_rules) < 0) return NULL; } while (0)
+#else
+#define RULES_VERIFY(_rules)
+#endif
+
 DIAG_OFF(attributes)
 typedef enum CC_HINT(flag_enum) {
        UNLANG_OP_FLAG_NONE                     = 0x00,                 //!< No flags.
@@ -216,7 +299,11 @@ typedef enum CC_HINT(flag_enum) {
                                                                        ///< is desired, but can be ignored.
        UNLANG_OP_FLAG_BREAK_POINT              = 0x08,                 //!< Break point.
        UNLANG_OP_FLAG_RETURN_POINT             = 0x10,                 //!< Return point.
-       UNLANG_OP_FLAG_CONTINUE_POINT           = 0x20                  //!< Continue point.
+       UNLANG_OP_FLAG_CONTINUE_POINT           = 0x20,                 //!< Continue point.
+
+       UNLANG_OP_FLAG_SINGLE_WORD              = 0x1000,               //!< the operation is parsed and compiled as a single word
+       UNLANG_OP_FLAG_INTERNAL                 = 0x2000,               //!< it's not a real keyword
+
 } unlang_op_flag_t;
 DIAG_ON(attributes)
 
@@ -229,6 +316,8 @@ typedef struct {
        char const              *name;                          //!< Name of the keyword
        unlang_type_t           type;                           //!< enum value for the keyword
 
+       unlang_compile_t        compile;                        //!< compile the keyword
+
        unlang_process_t        interpret;                      //!< Function to interpret the keyword
 
        unlang_signal_t         signal;                         //!< Function to signal stop / dup / whatever
@@ -246,7 +335,7 @@ typedef struct {
        size_t                  thread_inst_size;
        char const              *thread_inst_type;
 
-       unlang_op_flag_t        flag;                           //!< Flags for this operation.
+       unlang_op_flag_t        flag;                           //!< Interpreter flags for this operation.
 
        size_t                  frame_state_size;               //!< size of instance data in the stack frame
 
@@ -376,6 +465,7 @@ typedef struct {
 /** Different operations the interpreter can execute
  */
 extern unlang_op_t unlang_ops[];
+extern fr_hash_table_t *unlang_op_table;
 
 #define MOD_NUM_TYPES (UNLANG_TYPE_XLAT + 1)
 
index a90773df51b5a15d578ad6dadb3c2c227bdb70a3..b389921599306d0a1b789ca6eaaf2695c414baa8 100644 (file)
@@ -823,10 +823,14 @@ void unlang_xlat_init(void)
 {
        unlang_register(UNLANG_TYPE_XLAT,
                           &(unlang_op_t){
-                               .name = "xlat_eval",
+                               .name = "xlat",
+                               .type = UNLANG_TYPE_XLAT,
+                               .flag = UNLANG_OP_FLAG_INTERNAL,
+
                                .interpret = unlang_xlat,
                                .signal = unlang_xlat_signal,
                                .dump = unlang_xlat_dump,
+
                                .frame_state_size = sizeof(unlang_frame_state_xlat_t),
                                .frame_state_type = "unlang_frame_state_xlat_t",
                           });