[ovs-dev] [PATCH v4 1/8] ovn: Rewrite logical action parsing and encoding library.

Ben Pfaff blp at ovn.org
Sat Aug 13 16:30:23 UTC 2016


Until now, parsing logical actions and encoding them into OpenFlow has
happened in a single step.  An upcoming commit will want to examine
actions after parsing without encoding them into OpenFlow.  This commit
refactors OVN logical actions to make this possible.

The new form of the OVN action handling is closely modeled on ofp-actions
in the OVS core library.  Notable differences are that OVN actions are
always fixed-length and that individual OVN actions can have destructors
(and thus can contain pointers to data that need to be freed when the
actions are destroyed).

Signed-off-by: Ben Pfaff <blp at ovn.org>
Acked-by: Ryan Moats <rmoats at us.ibm.com>
---
 include/ovn/actions.h  |  297 ++++++-
 include/ovn/expr.h     |   41 +-
 ovn/controller/lflow.c |   43 +-
 ovn/lib/actions.c      | 2120 ++++++++++++++++++++++++++++++++----------------
 ovn/lib/expr.c         |  463 ++++-------
 ovn/ovn-sb.xml         |    6 +-
 tests/ovn.at           |  479 +++++++----
 tests/test-ovn.c       |   88 +-
 8 files changed, 2299 insertions(+), 1238 deletions(-)

diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index fb2d6a9..d4a87e9 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -20,17 +20,262 @@
 #include <stdbool.h>
 #include <stdint.h>
 #include "compiler.h"
-#include "openvswitch/hmap.h"
+#include "expr.h"
 #include "openvswitch/dynamic-string.h"
+#include "openvswitch/hmap.h"
 #include "openvswitch/uuid.h"
 #include "util.h"
 
-struct expr;
 struct lexer;
 struct ofpbuf;
 struct shash;
 struct simap;
 
+/* List of OVN logical actions.
+ *
+ * This macro is used directly only internally by this header and its
+ * corresponding .c file, but the list is still of interest to developers.
+ *
+ * Each OVNACT invocation has the following parameters:
+ *
+ * 1. <ENUM>, used below in the enum definition of OVNACT_<ENUM>, and
+ *    elsewhere.
+ *
+ * 2. <STRUCT> corresponding to a structure "struct <STRUCT>", that must be a
+ *    defined below.  This structure must be an abstract definition of the
+ *    action.  Its first member must have type "struct ovnact" and name
+ *    "ovnact".  The structure must have a fixed length, that is, it may not
+ *    end with a flexible array member.
+ */
+#define OVNACTS                                 \
+    OVNACT(OUTPUT,        ovnact_null)          \
+    OVNACT(NEXT,          ovnact_next)          \
+    OVNACT(LOAD,          ovnact_load)          \
+    OVNACT(MOVE,          ovnact_move)          \
+    OVNACT(EXCHANGE,      ovnact_move)          \
+    OVNACT(DEC_TTL,       ovnact_null)          \
+    OVNACT(CT_NEXT,       ovnact_next)          \
+    OVNACT(CT_COMMIT,     ovnact_ct_commit)     \
+    OVNACT(CT_DNAT,       ovnact_ct_nat)        \
+    OVNACT(CT_SNAT,       ovnact_ct_nat)        \
+    OVNACT(CT_LB,         ovnact_ct_lb)         \
+    OVNACT(ARP,           ovnact_nest)          \
+    OVNACT(ND_NA,         ovnact_nest)          \
+    OVNACT(GET_ARP,       ovnact_get_mac_bind)  \
+    OVNACT(PUT_ARP,       ovnact_put_mac_bind)  \
+    OVNACT(GET_ND,        ovnact_get_mac_bind)  \
+    OVNACT(PUT_ND,        ovnact_put_mac_bind)  \
+    OVNACT(PUT_DHCP_OPTS, ovnact_put_dhcp_opts)
+
+/* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
+enum OVS_PACKED_ENUM ovnact_type {
+#define OVNACT(ENUM, STRUCT) OVNACT_##ENUM,
+    OVNACTS
+#undef OVNACT
+};
+
+/* Define N_OVNACTS to the number of types of ovnacts. */
+enum {
+#define OVNACT(ENUM, STRUCT) + 1
+    N_OVNACTS = OVNACTS
+#undef OVNACT
+};
+
+/* Header for an action.
+ *
+ * Each action is a structure "struct ovnact_*" that begins with "struct
+ * ovnact", usually followed by other data that describes the action.  Actions
+ * are padded out to a multiple of OVNACT_ALIGNTO bytes in length.
+ */
+struct ovnact {
+    /* We want the space advantage of an 8-bit type here on every
+     * implementation, without giving up the advantage of having a useful type
+     * on implementations that support packed enums. */
+#ifdef HAVE_PACKED_ENUM
+    enum ovnact_type type;      /* OVNACT_*. */
+#else
+    uint8_t type;               /* OVNACT_* */
+#endif
+    uint8_t pad;                /* Pad to multiple of 16 bits. */
+
+    uint16_t len;               /* Length of the action, in bytes, including
+                                 * struct ovnact, excluding padding. */
+};
+BUILD_ASSERT_DECL(sizeof(struct ovnact) == 4);
+
+/* Alignment. */
+#define OVNACT_ALIGNTO 8
+#define OVNACT_ALIGN(SIZE) ROUND_UP(SIZE, OVNACT_ALIGNTO)
+
+/* Returns the ovnact following 'ovnact'. */
+static inline struct ovnact *
+ovnact_next(const struct ovnact *ovnact)
+{
+    return (void *) ((uint8_t *) ovnact + OVNACT_ALIGN(ovnact->len));
+}
+
+struct ovnact *ovnact_next_flattened(const struct ovnact *);
+
+static inline struct ovnact *
+ovnact_end(const struct ovnact *ovnacts, size_t ovnacts_len)
+{
+    return (void *) ((uint8_t *) ovnacts + ovnacts_len);
+}
+
+/* Assigns POS to each ovnact, in turn, in the OVNACTS_LEN bytes of ovnacts
+ * starting at OVNACTS. */
+#define OVNACT_FOR_EACH(POS, OVNACTS, OVNACTS_LEN)                      \
+    for ((POS) = (OVNACTS); (POS) < ovnact_end(OVNACTS, OVNACTS_LEN);  \
+         (POS) = ovnact_next(POS))
+
+/* Action structure for each OVNACT_*. */
+
+/* Action structure for actions that do not have any extra data beyond the
+ * action type. */
+struct ovnact_null {
+    struct ovnact ovnact;
+};
+
+/* OVNACT_NEXT. */
+struct ovnact_next {
+    struct ovnact ovnact;
+    uint8_t ltable;             /* Logical table ID of next table. */
+};
+
+/* OVNACT_LOAD. */
+struct ovnact_load {
+    struct ovnact ovnact;
+    struct expr_field dst;
+    union expr_constant imm;
+};
+
+/* OVNACT_MOVE, OVNACT_EXCHANGE. */
+struct ovnact_move {
+    struct ovnact ovnact;
+    struct expr_field lhs;
+    struct expr_field rhs;
+};
+
+/* OVNACT_CT_COMMIT. */
+struct ovnact_ct_commit {
+    struct ovnact ovnact;
+    uint32_t ct_mark, ct_mark_mask;
+    ovs_be128 ct_label, ct_label_mask;
+};
+
+/* OVNACT_CT_DNAT, OVNACT_CT_SNAT. */
+struct ovnact_ct_nat {
+    struct ovnact ovnact;
+    ovs_be32 ip;
+    uint8_t ltable;             /* Logical table ID of next table. */
+};
+
+struct ovnact_ct_lb_dst {
+    ovs_be32 ip;
+    uint16_t port;
+};
+
+/* OVNACT_CT_LB. */
+struct ovnact_ct_lb {
+    struct ovnact ovnact;
+    struct ovnact_ct_lb_dst *dsts;
+    size_t n_dsts;
+    uint8_t ltable;             /* Logical table ID of next table. */
+};
+
+/* OVNACT_ARP, OVNACT_NA. */
+struct ovnact_nest {
+    struct ovnact ovnact;
+    struct ovnact *nested;
+    size_t nested_len;
+};
+
+/* OVNACT_GET_ARP, OVNACT_GET_ND. */
+struct ovnact_get_mac_bind {
+    struct ovnact ovnact;
+    struct expr_field port;     /* Logical port name. */
+    struct expr_field ip;       /* 32-bit or 128-bit IP address. */
+};
+
+/* OVNACT_PUT_ARP, ONVACT_PUT_ND. */
+struct ovnact_put_mac_bind {
+    struct ovnact ovnact;
+    struct expr_field port;     /* Logical port name. */
+    struct expr_field ip;       /* 32-bit or 128-bit IP address. */
+    struct expr_field mac;      /* 48-bit Ethernet address. */
+};
+
+struct ovnact_dhcp_option {
+    const struct dhcp_opts_map *option;
+    struct expr_constant_set value;
+};
+
+/* OVNACT_PUT_DHCP_OPTS. */
+struct ovnact_put_dhcp_opts {
+    struct ovnact ovnact;
+    struct expr_field dst;      /* 1-bit destination field. */
+    struct ovnact_dhcp_option *options;
+    size_t n_options;
+};
+
+/* Internal use by the helpers below. */
+void ovnact_init(struct ovnact *, enum ovnact_type, size_t len);
+void *ovnact_put(struct ofpbuf *, enum ovnact_type, size_t len);
+
+/* For each OVNACT_<ENUM> with a corresponding struct <STRUCT>, this defines
+ * the following commonly useful functions:
+ *
+ *   struct <STRUCT> *ovnact_put_<ENUM>(struct ofpbuf *ovnacts);
+ *
+ *     Appends a new 'ovnact', of length OVNACT_<ENUM>_SIZE, to 'ovnacts',
+ *     initializes it with ovnact_init_<ENUM>(), and returns it.  Also sets
+ *     'ovnacts->header' to the returned action.
+ *
+ *   struct <STRUCT> *ovnact_get_<ENUM>(const struct ovnact *ovnact);
+ *
+ *     Returns 'ovnact' cast to "struct <STRUCT> *".  'ovnact->type' must be
+ *     OVNACT_<ENUM>.
+ *
+ * as well as the following more rarely useful definitions:
+ *
+ *   void ovnact_init_<ENUM>(struct <STRUCT> *ovnact);
+ *
+ *     Initializes the parts of 'ovnact' that identify it as having type
+ *     OVNACT_<ENUM> and length OVNACT_<ENUM>_SIZE and zeros the rest.
+ *
+ *   <ENUM>_SIZE
+ *
+ *     The size of the action structure, that is, sizeof(struct <STRUCT>)
+ *     rounded up to a multiple of OVNACT_ALIGNTO.
+ */
+#define OVNACT(ENUM, STRUCT)                                            \
+    BUILD_ASSERT_DECL(offsetof(struct STRUCT, ovnact) == 0);            \
+                                                                        \
+    enum { OVNACT_##ENUM##_SIZE = OVNACT_ALIGN(sizeof(struct STRUCT)) }; \
+                                                                        \
+    static inline struct STRUCT *                                       \
+    ovnact_get_##ENUM(const struct ovnact *ovnact)                      \
+    {                                                                   \
+        ovs_assert(ovnact->type == OVNACT_##ENUM);                      \
+        return ALIGNED_CAST(struct STRUCT *, ovnact);                   \
+    }                                                                   \
+                                                                        \
+    static inline struct STRUCT *                                       \
+    ovnact_put_##ENUM(struct ofpbuf *ovnacts)                           \
+    {                                                                   \
+        return ovnact_put(ovnacts, OVNACT_##ENUM,                       \
+                          OVNACT_##ENUM##_SIZE);                        \
+    }                                                                   \
+                                                                        \
+    static inline void                                                  \
+    ovnact_init_##ENUM(struct STRUCT *ovnact)                           \
+    {                                                                   \
+        ovnact_init(&ovnact->ovnact, OVNACT_##ENUM,                     \
+                    OVNACT_##ENUM##_SIZE);                              \
+    }
+OVNACTS
+#undef OVNACT
+
 #define MAX_OVN_GROUPS 65535
 
 struct group_table {
@@ -99,14 +344,45 @@ struct action_header {
 };
 BUILD_ASSERT_DECL(sizeof(struct action_header) == 8);
 
-struct action_params {
+struct ovnact_parse_params {
     /* A table of "struct expr_symbol"s to support (as one would provide to
      * expr_parse()). */
     const struct shash *symtab;
 
-    /* hmap of 'struct dhcp_opts_map'  to support 'put_dhcp_opts' action */
+    /* hmap of 'struct dhcp_opts_map' to support 'put_dhcp_opts' action */
     const struct hmap *dhcp_opts;
 
+    /* OVN maps each logical flow table (ltable), one-to-one, onto a physical
+     * OpenFlow flow table (ptable).  A number of parameters describe this
+     * mapping and data related to flow tables:
+     *
+     *     - 'first_ptable' and 'n_tables' define the range of OpenFlow tables
+     *        to which the logical "next" action should be able to jump.
+     *        Logical table 0 maps to OpenFlow table 'first_ptable', logical
+     *        table 1 to 'first_ptable + 1', and so on.  If 'n_tables' is 0
+     *        then "next" is disallowed entirely.
+     *
+     *     - 'cur_ltable' is an offset from 'first_ptable' (e.g. 0 <=
+     *       cur_ltable < n_ptables) of the logical flow that contains the
+     *       actions.  If cur_ltable + 1 < n_tables, then this defines the
+     *       default table that "next" will jump to.
+     *
+     *     - 'output_ptable' should be the OpenFlow table to which the logical
+     *       "output" action will resubmit. */
+    uint8_t n_tables;           /* Number of flow tables. */
+    uint8_t cur_ltable;         /* 0 <= cur_ltable < n_tables. */
+};
+
+char *ovnacts_parse(struct lexer *, const struct ovnact_parse_params *,
+                    struct ofpbuf *ovnacts, struct expr **prereqsp)
+    OVS_WARN_UNUSED_RESULT;
+char *ovnacts_parse_string(const char *s, const struct ovnact_parse_params *,
+                           struct ofpbuf *ovnacts, struct expr **prereqsp)
+    OVS_WARN_UNUSED_RESULT;
+
+void ovnacts_format(const struct ovnact[], size_t ovnacts_len, struct ds *);
+
+struct ovnact_encode_params {
     /* Looks up logical port 'port_name'.  If found, stores its port number in
      * '*portp' and returns true; otherwise, returns false. */
     bool (*lookup_port)(const void *aux, const char *port_name,
@@ -139,19 +415,16 @@ struct action_params {
      *
      *     - 'output_ptable' should be the OpenFlow table to which the logical
      *       "output" action will resubmit. */
-    uint8_t n_tables;           /* Number of flow tables. */
     uint8_t first_ptable;       /* First OpenFlow table. */
-    uint8_t cur_ltable;         /* 0 <= cur_ltable < n_tables. */
     uint8_t output_ptable;      /* OpenFlow table for 'output' to resubmit. */
     uint8_t mac_bind_ptable;    /* OpenFlow table for 'get_arp'/'get_nd' to
                                    resubmit. */
 };
 
-char *actions_parse(struct lexer *, const struct action_params *,
-                    struct ofpbuf *ofpacts, struct expr **prereqsp)
-    OVS_WARN_UNUSED_RESULT;
-char *actions_parse_string(const char *s, const struct action_params *,
-                           struct ofpbuf *ofpacts, struct expr **prereqsp)
-    OVS_WARN_UNUSED_RESULT;
+void ovnacts_encode(const struct ovnact[], size_t ovnacts_len,
+                    const struct ovnact_encode_params *,
+                    struct ofpbuf *ofpacts);
+
+void ovnacts_free(struct ovnact[], size_t ovnacts_len);
 
 #endif /* ovn/actions.h */
diff --git a/include/ovn/expr.h b/include/ovn/expr.h
index df22895..50cb73f 100644
--- a/include/ovn/expr.h
+++ b/include/ovn/expr.h
@@ -60,6 +60,7 @@
 #include "openvswitch/meta-flow.h"
 
 struct ds;
+struct expr;
 struct ofpbuf;
 struct shash;
 struct simap;
@@ -264,6 +265,9 @@ struct expr_field {
     int n_bits;                       /* Number of bits. */
 };
 
+char *expr_field_parse(struct lexer *lexer, const struct shash *symtab,
+                       struct expr_field *field, struct expr **prereqsp)
+    OVS_WARN_UNUSED_RESULT;
 void expr_field_format(const struct expr_field *, struct ds *);
 
 struct expr_symbol *expr_symtab_add_field(struct shash *symtab,
@@ -401,30 +405,9 @@ void expr_matches_print(const struct hmap *matches, FILE *);
 
 /* Action parsing helper. */
 
-char *expr_parse_assignment(struct lexer *lexer, struct expr_field *dst,
-                            const struct shash *symtab,
-                            bool (*lookup_port)(const void *aux,
-                                                const char *port_name,
-                                                unsigned int *portp),
-                            const void *aux,
-                            struct ofpbuf *ofpacts, struct expr **prereqsp)
-    OVS_WARN_UNUSED_RESULT;
-char *expr_parse_exchange(struct lexer *lexer, struct expr_field *dst,
-                          const struct shash *symtab,
-                          bool (*lookup_port)(const void *aux,
-                                              const char *port_name,
-                                              unsigned int *portp),
-                          const void *aux,
-                          struct ofpbuf *ofpacts, struct expr **prereqsp)
-    OVS_WARN_UNUSED_RESULT;
-char *expr_parse_field(struct lexer *lexer, const struct shash *symtab,
-                       struct expr_field *field)
-    OVS_WARN_UNUSED_RESULT;
-char *expr_expand_field(struct lexer *lexer, const struct shash *symtab,
-                        const struct expr_field *orig_field,
-                        int n_bits, bool rw,
-                        struct mf_subfield *sf, struct expr **prereqsp)
+char *expr_type_check(const struct expr_field *, int n_bits, bool rw)
     OVS_WARN_UNUSED_RESULT;
+struct mf_subfield expr_resolve_field(const struct expr_field *);
 
 /* Type of a "union expr_constant" or "struct expr_constant_set". */
 enum expr_constant_type {
@@ -451,6 +434,14 @@ union expr_constant {
     char *string;
 };
 
+char *expr_constant_parse(struct lexer *, const struct expr_field *,
+                          union expr_constant *)
+    OVS_WARN_UNUSED_RESULT;
+void expr_constant_format(const union expr_constant *,
+                          enum expr_constant_type, struct ds *);
+void expr_constant_destroy(const union expr_constant *,
+                           enum expr_constant_type);
+
 /* A collection of "union expr_constant"s of the same type. */
 struct expr_constant_set {
     union expr_constant *values;  /* Constants. */
@@ -459,9 +450,9 @@ struct expr_constant_set {
     bool in_curlies;              /* Whether the constants were in {}. */
 };
 
-char *expr_parse_constant_set(struct lexer *, const struct shash *symtab,
-                              struct expr_constant_set *cs)
+char *expr_constant_set_parse(struct lexer *, struct expr_constant_set *cs)
     OVS_WARN_UNUSED_RESULT;
+void expr_constant_set_format(const struct expr_constant_set *, struct ds *);
 void expr_constant_set_destroy(struct expr_constant_set *cs);
 
 
diff --git a/ovn/controller/lflow.c b/ovn/controller/lflow.c
index 9024cc0..b17646d 100644
--- a/ovn/controller/lflow.c
+++ b/ovn/controller/lflow.c
@@ -371,43 +371,52 @@ consider_logical_flow(const struct lport_index *lports,
                              ? OFTABLE_REMOTE_OUTPUT
                              : OFTABLE_SAVE_INPORT);
 
-    /* Translate OVN actions into OpenFlow actions.
+    /* Parse OVN logical actions.
      *
      * XXX Deny changes to 'outport' in egress pipeline. */
-    uint64_t ofpacts_stub[64 / 8];
-    struct ofpbuf ofpacts;
+    uint64_t ovnacts_stub[1024 / 8];
+    struct ofpbuf ovnacts = OFPBUF_STUB_INITIALIZER(ovnacts_stub);
+    struct ovnact_parse_params pp = {
+        .symtab = &symtab,
+        .dhcp_opts = dhcp_opts_p,
+
+        .n_tables = LOG_PIPELINE_LEN,
+        .cur_ltable = lflow->table_id,
+    };
     struct expr *prereqs;
     char *error;
 
-    ofpbuf_use_stub(&ofpacts, ofpacts_stub, sizeof ofpacts_stub);
+    error = ovnacts_parse_string(lflow->actions, &pp, &ovnacts, &prereqs);
+    if (error) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "error parsing actions \"%s\": %s",
+                     lflow->actions, error);
+        free(error);
+        ofpbuf_uninit(&ovnacts);
+        return;
+    }
+
+    /* Encode OVN logical actions into OpenFlow. */
+    uint64_t ofpacts_stub[1024 / 8];
+    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
     struct lookup_port_aux aux = {
         .lports = lports,
         .mcgroups = mcgroups,
         .dp = lflow->logical_datapath
     };
-    struct action_params ap = {
-        .symtab = &symtab,
-        .dhcp_opts = dhcp_opts_p,
+    struct ovnact_encode_params ep = {
         .lookup_port = lookup_port_cb,
         .aux = &aux,
         .ct_zones = ct_zones,
         .group_table = group_table,
         .lflow_uuid = lflow->header_.uuid,
 
-        .n_tables = LOG_PIPELINE_LEN,
         .first_ptable = first_ptable,
-        .cur_ltable = lflow->table_id,
         .output_ptable = output_ptable,
         .mac_bind_ptable = OFTABLE_MAC_BINDING,
     };
-    error = actions_parse_string(lflow->actions, &ap, &ofpacts, &prereqs);
-    if (error) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-        VLOG_WARN_RL(&rl, "error parsing actions \"%s\": %s",
-                     lflow->actions, error);
-        free(error);
-        return;
-    }
+    ovnacts_encode(ovnacts.data, ovnacts.size, &ep, &ofpacts);
+    ofpbuf_uninit(&ovnacts);
 
     /* Translate OVN match into table of OpenFlow matches. */
     struct hmap matches;
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index ec82ec3..66648e5 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -22,12 +22,14 @@
 #include "compiler.h"
 #include "ovn-dhcp.h"
 #include "hash.h"
-#include "openvswitch/hmap.h"
 #include "logical-fields.h"
 #include "nx-match.h"
 #include "openvswitch/dynamic-string.h"
+#include "openvswitch/hmap.h"
+#include "openvswitch/json.h"
 #include "openvswitch/ofp-actions.h"
 #include "openvswitch/ofpbuf.h"
+#include "openvswitch/vlog.h"
 #include "ovn/actions.h"
 #include "ovn/expr.h"
 #include "ovn/lex.h"
@@ -35,18 +37,150 @@
 #include "openvswitch/shash.h"
 #include "simap.h"
 
-/* Context maintained during actions_parse(). */
+VLOG_DEFINE_THIS_MODULE(actions);
+
+/* Prototypes for functions to be defined by each action. */
+#define OVNACT(ENUM, STRUCT)                                        \
+    static void format_##ENUM(const struct STRUCT *, struct ds *);  \
+    static void encode_##ENUM(const struct STRUCT *,                \
+                              const struct ovnact_encode_params *,  \
+                              struct ofpbuf *ofpacts);              \
+    static void free_##ENUM(struct STRUCT *a);
+OVNACTS
+#undef OVNACT
+
+/* Helpers. */
+
+/* Implementation of ovnact_put_<ENUM>(). */
+void *
+ovnact_put(struct ofpbuf *ovnacts, enum ovnact_type type, size_t len)
+{
+    struct ovnact *ovnact;
+
+    ovnacts->header = ofpbuf_put_uninit(ovnacts, len);
+    ovnact = ovnacts->header;
+    ovnact_init(ovnact, type, len);
+    return ovnact;
+}
+
+/* Implementation of ovnact_init_<ENUM>(). */
+void
+ovnact_init(struct ovnact *ovnact, enum ovnact_type type, size_t len)
+{
+    memset(ovnact, 0, len);
+    ovnact->type = type;
+    ovnact->len = len;
+}
+
+static size_t
+encode_start_controller_op(enum action_opcode opcode, bool pause,
+                           struct ofpbuf *ofpacts)
+{
+    size_t ofs = ofpacts->size;
+
+    struct ofpact_controller *oc = ofpact_put_CONTROLLER(ofpacts);
+    oc->max_len = UINT16_MAX;
+    oc->reason = OFPR_ACTION;
+    oc->pause = pause;
+
+    struct action_header ah = { .opcode = htonl(opcode) };
+    ofpbuf_put(ofpacts, &ah, sizeof ah);
+
+    return ofs;
+}
+
+static void
+encode_finish_controller_op(size_t ofs, struct ofpbuf *ofpacts)
+{
+    struct ofpact_controller *oc = ofpbuf_at_assert(ofpacts, ofs, sizeof *oc);
+    ofpacts->header = oc;
+    oc->userdata_len = ofpacts->size - (ofs + sizeof *oc);
+    ofpact_finish_CONTROLLER(ofpacts, &oc);
+}
+
+static void
+encode_controller_op(enum action_opcode opcode, struct ofpbuf *ofpacts)
+{
+    size_t ofs = encode_start_controller_op(opcode, false, ofpacts);
+    encode_finish_controller_op(ofs, ofpacts);
+}
+
+static void
+init_stack(struct ofpact_stack *stack, enum mf_field_id field)
+{
+    stack->subfield.field = mf_from_id(field);
+    stack->subfield.ofs = 0;
+    stack->subfield.n_bits = stack->subfield.field->n_bits;
+}
+
+struct arg {
+    const struct mf_subfield src;
+    enum mf_field_id dst;
+};
+
+static void
+encode_setup_args(const struct arg args[], size_t n_args,
+                  struct ofpbuf *ofpacts)
+{
+    /* 1. Save all of the destinations that will be modified. */
+    for (const struct arg *a = args; a < &args[n_args]; a++) {
+        ovs_assert(a->src.n_bits == mf_from_id(a->dst)->n_bits);
+        if (a->src.field->id != a->dst) {
+            init_stack(ofpact_put_STACK_PUSH(ofpacts), a->dst);
+        }
+    }
+
+    /* 2. Push the sources, in reverse order. */
+    for (size_t i = n_args - 1; i < n_args; i--) {
+        const struct arg *a = &args[i];
+        if (a->src.field->id != a->dst) {
+            ofpact_put_STACK_PUSH(ofpacts)->subfield = a->src;
+        }
+    }
+
+    /* 3. Pop the sources into the destinations. */
+    for (const struct arg *a = args; a < &args[n_args]; a++) {
+        if (a->src.field->id != a->dst) {
+            init_stack(ofpact_put_STACK_POP(ofpacts), a->dst);
+        }
+    }
+}
+
+static void
+encode_restore_args(const struct arg args[], size_t n_args,
+                    struct ofpbuf *ofpacts)
+{
+    for (size_t i = n_args - 1; i < n_args; i--) {
+        const struct arg *a = &args[i];
+        if (a->src.field->id != a->dst) {
+            init_stack(ofpact_put_STACK_POP(ofpacts), a->dst);
+        }
+    }
+}
+
+static void
+put_load(uint64_t value, enum mf_field_id dst, int ofs, int n_bits,
+         struct ofpbuf *ofpacts)
+{
+    struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
+    sf->field = mf_from_id(dst);
+    sf->flow_has_vlan = false;
+
+    ovs_be64 n_value = htonll(value);
+    bitwise_copy(&n_value, 8, 0, &sf->value, sf->field->n_bytes, ofs, n_bits);
+    bitwise_one(&sf->mask, sf->field->n_bytes, ofs, n_bits);
+}
+
+/* Context maintained during ovnacts_parse(). */
 struct action_context {
-    const struct action_params *ap; /* Parameters. */
+    const struct ovnact_parse_params *pp; /* Parameters. */
     struct lexer *lexer;        /* Lexer for pulling more tokens. */
     char *error;                /* Error, if any, otherwise NULL. */
-    struct ofpbuf *ofpacts;     /* Actions. */
+    struct ofpbuf *ovnacts;     /* Actions. */
     struct expr *prereqs;       /* Prerequisites to apply to match. */
 };
 
 static bool parse_action(struct action_context *);
-static void parse_put_dhcp_opts_action(struct action_context *,
-                                       const struct expr_field *dst);
 
 static bool
 action_error_handle_common(struct action_context *ctx)
@@ -111,53 +245,40 @@ action_syntax_error(struct action_context *ctx, const char *message, ...)
     ctx->error = ds_steal_cstr(&s);
 }
 
-/* Parses an assignment or exchange or put_dhcp_opts action. */
-static void
-parse_set_action(struct action_context *ctx)
+static bool
+action_force_match(struct action_context *ctx, enum lex_type t)
 {
-    struct expr *prereqs = NULL;
-    struct expr_field dst;
-    char *error;
+    if (lexer_match(ctx->lexer, t)) {
+        return true;
+    } else {
+        struct lex_token token = { .type = t };
+        struct ds s = DS_EMPTY_INITIALIZER;
+        lex_token_format(&token, &s);
 
-    error = expr_parse_field(ctx->lexer, ctx->ap->symtab, &dst);
-    if (!error) {
-        if (lexer_match(ctx->lexer, LEX_T_EXCHANGE)) {
-            error = expr_parse_exchange(ctx->lexer, &dst, ctx->ap->symtab,
-                                        ctx->ap->lookup_port, ctx->ap->aux,
-                                        ctx->ofpacts, &prereqs);
-        } else if (lexer_match(ctx->lexer, LEX_T_EQUALS)) {
-            if (ctx->lexer->token.type == LEX_T_ID
-                && !strcmp(ctx->lexer->token.s, "put_dhcp_opts")
-                && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
-                lexer_get(ctx->lexer); /* Skip put_dhcp_opts. */
-                lexer_get(ctx->lexer); /* Skip '('. */
-                parse_put_dhcp_opts_action(ctx, &dst);
-            } else {
-                error = expr_parse_assignment(
-                    ctx->lexer, &dst, ctx->ap->symtab, ctx->ap->lookup_port,
-                    ctx->ap->aux, ctx->ofpacts, &prereqs);
-            }
-        } else {
-            action_syntax_error(ctx, "expecting `=' or `<->'");
-        }
-        if (!error) {
-            ctx->prereqs = expr_combine(EXPR_T_AND, ctx->prereqs, prereqs);
-        }
-    }
+        action_syntax_error(ctx, "expecting `%s'", ds_cstr(&s));
 
-    if (error) {
-        expr_destroy(prereqs);
-        action_error(ctx, "%s", error);
-        free(error);
+        ds_destroy(&s);
+
+        return false;
     }
 }
 
-static void
-emit_resubmit(struct action_context *ctx, uint8_t table_id)
+static bool
+action_parse_field(struct action_context *ctx,
+                   int n_bits, bool rw, struct expr_field *f)
 {
-    struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(ctx->ofpacts);
-    resubmit->in_port = OFPP_IN_PORT;
-    resubmit->table_id = table_id;
+    char *error = expr_field_parse(ctx->lexer, ctx->pp->symtab, f,
+                                   &ctx->prereqs);
+    if (!error) {
+        error = expr_type_check(f, n_bits, rw);
+        if (!error) {
+            return true;
+        }
+    }
+
+    action_error(ctx, "%s", error);
+    free(error);
+    return false;
 }
 
 static bool
@@ -170,10 +291,65 @@ action_get_int(struct action_context *ctx, int *value)
     return ok;
 }
 
+static bool
+action_parse_port(struct action_context *ctx, uint16_t *port)
+{
+    if (lexer_is_int(ctx->lexer)) {
+        int value = ntohll(ctx->lexer->token.value.integer);
+        if (value <= UINT16_MAX) {
+            *port = value;
+            lexer_get(ctx->lexer);
+            return true;
+        }
+    }
+    action_syntax_error(ctx, "expecting port number");
+    return false;
+}
+
+/* Parses 'prerequisite' as an expression in the context of 'ctx', then adds it
+ * as a conjunction with the existing 'ctx->prereqs'. */
+static void
+add_prerequisite(struct action_context *ctx, const char *prerequisite)
+{
+    struct expr *expr;
+    char *error;
+
+    expr = expr_parse_string(prerequisite, ctx->pp->symtab, NULL, &error);
+    ovs_assert(!error);
+    ctx->prereqs = expr_combine(EXPR_T_AND, ctx->prereqs, expr);
+}
+
+static void
+format_OUTPUT(const struct ovnact_null *a OVS_UNUSED, struct ds *s)
+{
+    ds_put_cstr(s, "output;");
+}
+
+static void
+emit_resubmit(struct ofpbuf *ofpacts, uint8_t ptable)
+{
+    struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(ofpacts);
+    resubmit->in_port = OFPP_IN_PORT;
+    resubmit->table_id = ptable;
+}
+
+static void
+encode_OUTPUT(const struct ovnact_null *a OVS_UNUSED,
+              const struct ovnact_encode_params *ep,
+              struct ofpbuf *ofpacts)
+{
+    emit_resubmit(ofpacts, ep->output_ptable);
+}
+
+static void
+free_OUTPUT(struct ovnact_null *a OVS_UNUSED)
+{
+}
+
 static void
-parse_next_action(struct action_context *ctx)
+parse_NEXT(struct action_context *ctx)
 {
-    if (!ctx->ap->n_tables) {
+    if (!ctx->pp->n_tables) {
         action_error(ctx, "\"next\" action not allowed here.");
     } else if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
         int ltable;
@@ -186,481 +362,650 @@ parse_next_action(struct action_context *ctx)
             return;
         }
 
-        if (ltable >= ctx->ap->n_tables) {
+        if (ltable >= ctx->pp->n_tables) {
             action_error(ctx, "\"next\" argument must be in range 0 to %d.",
-                         ctx->ap->n_tables - 1);
+                         ctx->pp->n_tables - 1);
             return;
         }
 
-        emit_resubmit(ctx, ctx->ap->first_ptable + ltable);
+        ovnact_put_NEXT(ctx->ovnacts)->ltable = ltable;
     } else {
-        if (ctx->ap->cur_ltable < ctx->ap->n_tables) {
-            emit_resubmit(ctx,
-                          ctx->ap->first_ptable + ctx->ap->cur_ltable + 1);
+        if (ctx->pp->cur_ltable < ctx->pp->n_tables) {
+            ovnact_put_NEXT(ctx->ovnacts)->ltable = ctx->pp->cur_ltable + 1;
         } else {
             action_error(ctx, "\"next\" action not allowed in last table.");
         }
     }
 }
 
-/* Parses 'prerequisite' as an expression in the context of 'ctx', then adds it
- * as a conjunction with the existing 'ctx->prereqs'. */
 static void
-add_prerequisite(struct action_context *ctx, const char *prerequisite)
+format_NEXT(const struct ovnact_next *next, struct ds *s)
 {
-    struct expr *expr;
-    char *error;
+    ds_put_format(s, "next(%d);", next->ltable);
+}
 
-    expr = expr_parse_string(prerequisite, ctx->ap->symtab, NULL, &error);
-    ovs_assert(!error);
-    ctx->prereqs = expr_combine(EXPR_T_AND, ctx->prereqs, expr);
+static void
+encode_NEXT(const struct ovnact_next *next,
+            const struct ovnact_encode_params *ep,
+            struct ofpbuf *ofpacts)
+{
+    emit_resubmit(ofpacts, ep->first_ptable + next->ltable);
 }
 
-static size_t
-start_controller_op(struct ofpbuf *ofpacts, enum action_opcode opcode,
-                    bool pause)
+static void
+free_NEXT(struct ovnact_next *a OVS_UNUSED)
 {
-    size_t ofs = ofpacts->size;
+}
+
+static void
+parse_LOAD(struct action_context *ctx, const struct expr_field *lhs)
+{
+    size_t ofs = ctx->ovnacts->size;
+    struct ovnact_load *load = ovnact_put_LOAD(ctx->ovnacts);
+    load->dst = *lhs;
+    char *error = expr_type_check(lhs, lhs->n_bits, true);
+    if (!error) {
+        error = expr_constant_parse(ctx->lexer, lhs, &load->imm);
+    }
+    if (error) {
+        ctx->ovnacts->size = ofs;
+        action_error(ctx, "%s", error);
+        free(error);
+    }
+}
 
-    struct ofpact_controller *oc = ofpact_put_CONTROLLER(ofpacts);
-    oc->max_len = UINT16_MAX;
-    oc->reason = OFPR_ACTION;
-    oc->pause = pause;
+static enum expr_constant_type
+load_type(const struct ovnact_load *load)
+{
+    return load->dst.symbol->width > 0 ? EXPR_C_INTEGER : EXPR_C_STRING;
 
-    struct action_header ah = { .opcode = htonl(opcode) };
-    ofpbuf_put(ofpacts, &ah, sizeof ah);
+}
 
-    return ofs;
+static void
+format_LOAD(const struct ovnact_load *load, struct ds *s)
+{
+    expr_field_format(&load->dst, s);
+    ds_put_cstr(s, " = ");
+    expr_constant_format(&load->imm, load_type(load), s);
+    ds_put_char(s, ';');
 }
 
 static void
-finish_controller_op(struct ofpbuf *ofpacts, size_t ofs)
+encode_LOAD(const struct ovnact_load *load,
+            const struct ovnact_encode_params *ep,
+            struct ofpbuf *ofpacts)
 {
-    struct ofpact_controller *oc = ofpbuf_at_assert(ofpacts, ofs, sizeof *oc);
-    ofpacts->header = oc;
-    oc->userdata_len = ofpacts->size - (ofs + sizeof *oc);
-    ofpact_finish_CONTROLLER(ofpacts, &oc);
+    const union expr_constant *c = &load->imm;
+    struct mf_subfield dst = expr_resolve_field(&load->dst);
+
+    struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
+    sf->field = dst.field;
+
+    if (load->dst.symbol->width) {
+        bitwise_copy(&c->value, sizeof c->value, 0,
+                     &sf->value, dst.field->n_bytes, dst.ofs,
+                     dst.n_bits);
+        if (c->masked) {
+            bitwise_copy(&c->mask, sizeof c->mask, 0,
+                         &sf->mask, dst.field->n_bytes, dst.ofs,
+                         dst.n_bits);
+        } else {
+            bitwise_one(&sf->mask, dst.field->n_bytes,
+                        dst.ofs, dst.n_bits);
+        }
+    } else {
+        uint32_t port;
+        if (!ep->lookup_port(ep->aux, load->imm.string, &port)) {
+            port = 0;
+        }
+        bitwise_put(port, &sf->value,
+                    sf->field->n_bytes, 0, sf->field->n_bits);
+        bitwise_one(&sf->mask, sf->field->n_bytes, 0, sf->field->n_bits);
+    }
+}
+
+static void
+free_LOAD(struct ovnact_load *load)
+{
+    expr_constant_destroy(&load->imm, load_type(load));
+}
+
+static void
+format_assignment(const struct ovnact_move *move, const char *operator,
+                  struct ds *s)
+{
+    expr_field_format(&move->lhs, s);
+    ds_put_format(s, " %s ", operator);
+    expr_field_format(&move->rhs, s);
+    ds_put_char(s, ';');
 }
 
 static void
-put_controller_op(struct ofpbuf *ofpacts, enum action_opcode opcode)
+format_MOVE(const struct ovnact_move *move, struct ds *s)
 {
-    size_t ofs = start_controller_op(ofpacts, opcode, false);
-    finish_controller_op(ofpacts, ofs);
+    format_assignment(move, "=", s);
 }
 
-/* Implements the "arp" and "nd_na" actions, which execute nested
- * actions on a packet derived fro: the one being processed. */
 static void
-parse_nested_action(struct action_context *ctx, enum action_opcode opcode,
-                    const char *prereq)
+format_EXCHANGE(const struct ovnact_move *move, struct ds *s)
+{
+    format_assignment(move, "<->", s);
+}
+
+static void
+parse_assignment_action(struct action_context *ctx, bool exchange,
+                        const struct expr_field *lhs)
 {
-    if (!lexer_match(ctx->lexer, LEX_T_LCURLY)) {
-        action_syntax_error(ctx, "expecting `{'");
+    struct expr_field rhs;
+    char *error = expr_field_parse(ctx->lexer, ctx->pp->symtab, &rhs,
+                                   &ctx->prereqs);
+    if (error) {
+        action_error(ctx, "%s", error);
+        free(error);
         return;
     }
 
-    struct ofpbuf *outer_ofpacts = ctx->ofpacts;
-    uint64_t inner_ofpacts_stub[1024 / 8];
-    struct ofpbuf inner_ofpacts = OFPBUF_STUB_INITIALIZER(inner_ofpacts_stub);
-    ctx->ofpacts = &inner_ofpacts;
-
-    /* Save prerequisites.  (XXX What is the right treatment for prereqs?) */
-    struct expr *outer_prereqs = ctx->prereqs;
-    ctx->prereqs = NULL;
-
-    /* Parse inner actions. */
-    while (!lexer_match(ctx->lexer, LEX_T_RCURLY)) {
-        if (!parse_action(ctx)) {
-            break;
+    const struct expr_symbol *ls = lhs->symbol;
+    const struct expr_symbol *rs = rhs.symbol;
+    if ((ls->width != 0) != (rs->width != 0)) {
+        if (exchange) {
+            action_error(ctx,
+                         "Can't exchange %s field (%s) with %s field (%s).",
+                         ls->width ? "integer" : "string",
+                         ls->name,
+                         rs->width ? "integer" : "string",
+                         rs->name);
+        } else {
+            action_error(ctx,
+                         "Can't assign %s field (%s) to %s field (%s).",
+                         rs->width ? "integer" : "string",
+                         rs->name,
+                         ls->width ? "integer" : "string",
+                         ls->name);
         }
+        return;
     }
 
-    ctx->ofpacts = outer_ofpacts;
-
-    /* Add a "controller" OpenFlow action with the actions nested inside the
-     * requested OVN action's "{...}", converted to OpenFlow, as its userdata.
-     * ovn-controller will convert the packet to the requested type and
-     * then send the packet and actions back to the switch inside an
-     * OFPT_PACKET_OUT message. */
-    size_t oc_offset = start_controller_op(ctx->ofpacts, opcode, false);
-    ofpacts_put_openflow_actions(inner_ofpacts.data, inner_ofpacts.size,
-                                 ctx->ofpacts, OFP13_VERSION);
-    finish_controller_op(ctx->ofpacts, oc_offset);
-
-    /* Restore prerequisites. */
-    expr_destroy(ctx->prereqs);
-    ctx->prereqs = outer_prereqs;
-    add_prerequisite(ctx, prereq);
+    if (lhs->n_bits != rhs.n_bits) {
+        if (exchange) {
+            action_error(ctx, "Can't exchange %d-bit field with %d-bit field.",
+                         lhs->n_bits, rhs.n_bits);
+        } else {
+            action_error(ctx,
+                         "Can't assign %d-bit value to %d-bit destination.",
+                         rhs.n_bits, lhs->n_bits);
+        }
+        return;
+    } else if (!lhs->n_bits &&
+               ls->field->n_bits != rs->field->n_bits) {
+        action_error(ctx, "String fields %s and %s are incompatible for "
+                     "%s.", ls->name, rs->name,
+                     exchange ? "exchange" : "assignment");
+        return;
+    }
 
-    /* Free memory. */
-    ofpbuf_uninit(&inner_ofpacts);
+    error = expr_type_check(lhs, lhs->n_bits, true);
+    if (!error) {
+        error = expr_type_check(&rhs, rhs.n_bits, true);
+    }
+    if (error) {
+        action_error(ctx, "%s", error);
+        free(error);
+        return;
+    }
+
+    struct ovnact_move *move;
+    move = (exchange
+            ? ovnact_put_EXCHANGE(ctx->ovnacts)
+            : ovnact_put_MOVE(ctx->ovnacts));
+    move->lhs = *lhs;
+    move->rhs = rhs;
 }
 
-static bool
-action_force_match(struct action_context *ctx, enum lex_type t)
+static void
+encode_MOVE(const struct ovnact_move *move,
+            const struct ovnact_encode_params *ep OVS_UNUSED,
+            struct ofpbuf *ofpacts)
 {
-    if (lexer_match(ctx->lexer, t)) {
-        return true;
-    } else {
-        struct lex_token token = { .type = t };
-        struct ds s = DS_EMPTY_INITIALIZER;
-        lex_token_format(&token, &s);
+    struct ofpact_reg_move *orm = ofpact_put_REG_MOVE(ofpacts);
+    orm->src = expr_resolve_field(&move->rhs);
+    orm->dst = expr_resolve_field(&move->lhs);
+}
 
-        action_syntax_error(ctx, "expecting `%s'", ds_cstr(&s));
+static void
+encode_EXCHANGE(const struct ovnact_move *xchg,
+                const struct ovnact_encode_params *ep OVS_UNUSED,
+                struct ofpbuf *ofpacts)
+{
+    ofpact_put_STACK_PUSH(ofpacts)->subfield = expr_resolve_field(&xchg->rhs);
+    ofpact_put_STACK_PUSH(ofpacts)->subfield = expr_resolve_field(&xchg->lhs);
+    ofpact_put_STACK_POP(ofpacts)->subfield = expr_resolve_field(&xchg->rhs);
+    ofpact_put_STACK_POP(ofpacts)->subfield = expr_resolve_field(&xchg->lhs);
+}
 
-        ds_destroy(&s);
+static void
+free_MOVE(struct ovnact_move *move OVS_UNUSED)
+{
+}
 
-        return false;
-    }
+static void
+free_EXCHANGE(struct ovnact_move *xchg OVS_UNUSED)
+{
+}
+
+static void
+parse_DEC_TTL(struct action_context *ctx)
+{
+    action_force_match(ctx, LEX_T_DECREMENT);
+    ovnact_put_DEC_TTL(ctx->ovnacts);
+    add_prerequisite(ctx, "ip");
 }
 
-static bool
-action_parse_field(struct action_context *ctx,
-                   int n_bits, struct mf_subfield *sf)
+static void
+format_DEC_TTL(const struct ovnact_null *null OVS_UNUSED, struct ds *s)
 {
-    struct expr_field field;
-    char *error;
+    ds_put_cstr(s, "ip.ttl--;");
+}
 
-    error = expr_parse_field(ctx->lexer, ctx->ap->symtab, &field);
-    if (!error) {
-        struct expr *prereqs;
-        error = expr_expand_field(ctx->lexer, ctx->ap->symtab,
-                                  &field, n_bits, false, sf, &prereqs);
-        if (!error) {
-            ctx->prereqs = expr_combine(EXPR_T_AND, ctx->prereqs, prereqs);
-            return true;
-        }
+static void
+encode_DEC_TTL(const struct ovnact_null *null OVS_UNUSED,
+               const struct ovnact_encode_params *ep OVS_UNUSED,
+               struct ofpbuf *ofpacts)
+{
+    ofpact_put_DEC_TTL(ofpacts);
+}
+
+static void
+free_DEC_TTL(struct ovnact_null *null OVS_UNUSED)
+{
+}
+
+static void
+parse_CT_NEXT(struct action_context *ctx)
+{
+    if (ctx->pp->cur_ltable >= ctx->pp->n_tables) {
+        action_error(ctx, "\"ct_next\" action not allowed in last table.");
+        return;
     }
 
-    action_error(ctx, "%s", error);
-    free(error);
-    return false;
+    add_prerequisite(ctx, "ip");
+    ovnact_put_CT_NEXT(ctx->ovnacts)->ltable = ctx->pp->cur_ltable + 1;
 }
 
 static void
-init_stack(struct ofpact_stack *stack, enum mf_field_id field)
+format_CT_NEXT(const struct ovnact_next *next OVS_UNUSED, struct ds *s)
 {
-    stack->subfield.field = mf_from_id(field);
-    stack->subfield.ofs = 0;
-    stack->subfield.n_bits = stack->subfield.field->n_bits;
+    ds_put_cstr(s, "ct_next;");
 }
 
-struct arg {
-    const struct mf_subfield *src;
-    enum mf_field_id dst;
-};
+static void
+encode_CT_NEXT(const struct ovnact_next *next,
+                const struct ovnact_encode_params *ep,
+                struct ofpbuf *ofpacts)
+{
+    struct ofpact_conntrack *ct = ofpact_put_CT(ofpacts);
+    ct->recirc_table = ep->first_ptable + next->ltable;
+    ct->zone_src.field = mf_from_id(MFF_LOG_CT_ZONE);
+    ct->zone_src.ofs = 0;
+    ct->zone_src.n_bits = 16;
+    ofpact_finish(ofpacts, &ct->ofpact);
+}
 
 static void
-setup_args(struct action_context *ctx,
-           const struct arg args[], size_t n_args)
+free_CT_NEXT(struct ovnact_next *next OVS_UNUSED)
 {
-    /* 1. Save all of the destinations that will be modified. */
-    for (const struct arg *a = args; a < &args[n_args]; a++) {
-        ovs_assert(a->src->n_bits == mf_from_id(a->dst)->n_bits);
-        if (a->src->field->id != a->dst) {
-            init_stack(ofpact_put_STACK_PUSH(ctx->ofpacts), a->dst);
+}
+
+static void
+parse_ct_commit_arg(struct action_context *ctx,
+                    struct ovnact_ct_commit *cc)
+{
+    if (lexer_match_id(ctx->lexer, "ct_mark")) {
+        if (!action_force_match(ctx, LEX_T_EQUALS)) {
+            return;
         }
-    }
-
-    /* 2. Push the sources, in reverse order. */
-    for (size_t i = n_args - 1; i < n_args; i--) {
-        const struct arg *a = &args[i];
-        if (a->src->field->id != a->dst) {
-            ofpact_put_STACK_PUSH(ctx->ofpacts)->subfield = *a->src;
+        if (ctx->lexer->token.type == LEX_T_INTEGER) {
+            cc->ct_mark = ntohll(ctx->lexer->token.value.integer);
+            cc->ct_mark_mask = UINT32_MAX;
+        } else if (ctx->lexer->token.type == LEX_T_MASKED_INTEGER) {
+            cc->ct_mark = ntohll(ctx->lexer->token.value.integer);
+            cc->ct_mark_mask = ntohll(ctx->lexer->token.mask.integer);
+        } else {
+            action_syntax_error(ctx, "expecting integer");
+            return;
         }
-    }
-
-    /* 3. Pop the sources into the destinations. */
-    for (const struct arg *a = args; a < &args[n_args]; a++) {
-        if (a->src->field->id != a->dst) {
-            init_stack(ofpact_put_STACK_POP(ctx->ofpacts), a->dst);
+        lexer_get(ctx->lexer);
+    } else if (lexer_match_id(ctx->lexer, "ct_label")) {
+        if (!action_force_match(ctx, LEX_T_EQUALS)) {
+            return;
+        }
+        if (ctx->lexer->token.type == LEX_T_INTEGER) {
+            cc->ct_label = ctx->lexer->token.value.be128_int;
+            cc->ct_label_mask = OVS_BE128_MAX;
+        } else if (ctx->lexer->token.type == LEX_T_MASKED_INTEGER) {
+            cc->ct_label = ctx->lexer->token.value.be128_int;
+            cc->ct_label_mask = ctx->lexer->token.mask.be128_int;
+        } else {
+            action_syntax_error(ctx, "expecting integer");
+            return;
         }
+        lexer_get(ctx->lexer);
+    } else {
+        action_syntax_error(ctx, NULL);
     }
 }
 
 static void
-restore_args(struct action_context *ctx,
-             const struct arg args[], size_t n_args)
+parse_CT_COMMIT(struct action_context *ctx)
 {
-    for (size_t i = n_args - 1; i < n_args; i--) {
-        const struct arg *a = &args[i];
-        if (a->src->field->id != a->dst) {
-            init_stack(ofpact_put_STACK_POP(ctx->ofpacts), a->dst);
+    add_prerequisite(ctx, "ip");
+
+    struct ovnact_ct_commit *ct_commit = ovnact_put_CT_COMMIT(ctx->ovnacts);
+    if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
+        while (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
+            parse_ct_commit_arg(ctx, ct_commit);
+            if (ctx->error) {
+                return;
+            }
+            lexer_match(ctx->lexer, LEX_T_COMMA);
         }
     }
 }
 
 static void
-put_load(uint64_t value, enum mf_field_id dst, int ofs, int n_bits,
-         struct ofpbuf *ofpacts)
+format_CT_COMMIT(const struct ovnact_ct_commit *cc, struct ds *s)
 {
-    struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
-    sf->field = mf_from_id(dst);
-    sf->flow_has_vlan = false;
+    ds_put_cstr(s, "ct_commit(");
+    if (cc->ct_mark_mask) {
+        ds_put_format(s, "ct_mark=%#"PRIx32, cc->ct_mark);
+        if (cc->ct_mark_mask != UINT32_MAX) {
+            ds_put_format(s, "/%#"PRIx32, cc->ct_mark_mask);
+        }
+    }
+    if (!ovs_be128_is_zero(cc->ct_label_mask)) {
+        if (ds_last(s) != '(') {
+            ds_put_cstr(s, ", ");
+        }
 
-    ovs_be64 n_value = htonll(value);
-    bitwise_copy(&n_value, 8, 0, &sf->value, sf->field->n_bytes, ofs, n_bits);
-    bitwise_one(&sf->mask, sf->field->n_bytes, ofs, n_bits);
+        ds_put_format(s, "ct_label=");
+        ds_put_hex(s, &cc->ct_label, sizeof cc->ct_label);
+        if (!ovs_be128_equals(cc->ct_label_mask, OVS_BE128_MAX)) {
+            ds_put_char(s, '/');
+            ds_put_hex(s, &cc->ct_label_mask, sizeof cc->ct_label_mask);
+        }
+    }
+    if (!ds_chomp(s, '(')) {
+        ds_put_char(s, ')');
+    }
+    ds_put_char(s, ';');
 }
 
 static void
-parse_get_arp_action(struct action_context *ctx)
+encode_CT_COMMIT(const struct ovnact_ct_commit *cc,
+                 const struct ovnact_encode_params *ep OVS_UNUSED,
+                 struct ofpbuf *ofpacts)
 {
-    struct mf_subfield port, ip;
+    struct ofpact_conntrack *ct = ofpact_put_CT(ofpacts);
+    ct->flags = NX_CT_F_COMMIT;
+    ct->recirc_table = NX_CT_RECIRC_NONE;
+    ct->zone_src.field = mf_from_id(MFF_LOG_CT_ZONE);
+    ct->zone_src.ofs = 0;
+    ct->zone_src.n_bits = 16;
 
-    if (!action_force_match(ctx, LEX_T_LPAREN)
-        || !action_parse_field(ctx, 0, &port)
-        || !action_force_match(ctx, LEX_T_COMMA)
-        || !action_parse_field(ctx, 32, &ip)
-        || !action_force_match(ctx, LEX_T_RPAREN)) {
-        return;
-    }
+    size_t set_field_offset = ofpacts->size;
+    ofpbuf_pull(ofpacts, set_field_offset);
 
-    const struct arg args[] = {
-        { &port, MFF_LOG_OUTPORT },
-        { &ip, MFF_REG0 },
-    };
-    setup_args(ctx, args, ARRAY_SIZE(args));
+    if (cc->ct_mark_mask) {
+        struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
+        sf->field = mf_from_id(MFF_CT_MARK);
+        sf->value.be32 = htonl(cc->ct_mark);
+        sf->mask.be32 = htonl(cc->ct_mark_mask);
+    }
 
-    put_load(0, MFF_ETH_DST, 0, 48, ctx->ofpacts);
-    emit_resubmit(ctx, ctx->ap->mac_bind_ptable);
+    if (!ovs_be128_is_zero(cc->ct_label_mask)) {
+        struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
+        sf->field = mf_from_id(MFF_CT_LABEL);
+        sf->value.be128 = cc->ct_label;
+        sf->mask.be128 = cc->ct_label_mask;
+    }
 
-    restore_args(ctx, args, ARRAY_SIZE(args));
+    ofpacts->header = ofpbuf_push_uninit(ofpacts, set_field_offset);
+    ct = ofpacts->header;
+    ofpact_finish(ofpacts, &ct->ofpact);
 }
 
 static void
-parse_put_arp_action(struct action_context *ctx)
+free_CT_COMMIT(struct ovnact_ct_commit *cc OVS_UNUSED)
 {
-    struct mf_subfield port, ip, mac;
-
-    if (!action_force_match(ctx, LEX_T_LPAREN)
-        || !action_parse_field(ctx, 0, &port)
-        || !action_force_match(ctx, LEX_T_COMMA)
-        || !action_parse_field(ctx, 32, &ip)
-        || !action_force_match(ctx, LEX_T_COMMA)
-        || !action_parse_field(ctx, 48, &mac)
-        || !action_force_match(ctx, LEX_T_RPAREN)) {
-        return;
-    }
-
-    const struct arg args[] = {
-        { &port, MFF_LOG_INPORT },
-        { &ip, MFF_REG0 },
-        { &mac, MFF_ETH_SRC }
-    };
-    setup_args(ctx, args, ARRAY_SIZE(args));
-    put_controller_op(ctx->ofpacts, ACTION_OPCODE_PUT_ARP);
-    restore_args(ctx, args, ARRAY_SIZE(args));
 }
-
+
 static void
-parse_dhcp_opt(struct action_context *ctx, struct ofpbuf *ofpacts)
+parse_ct_nat(struct action_context *ctx, const char *name,
+             struct ovnact_ct_nat *cn)
 {
-    if (ctx->lexer->token.type != LEX_T_ID) {
-        action_syntax_error(ctx, NULL);
-        return;
-    }
-    const struct dhcp_opts_map *dhcp_opt = dhcp_opts_find(
-        ctx->ap->dhcp_opts, ctx->lexer->token.s);
-    if (!dhcp_opt) {
-        action_syntax_error(ctx, "expecting DHCP option name");
-        return;
-    }
-    lexer_get(ctx->lexer);
-
-    if (!action_force_match(ctx, LEX_T_EQUALS)) {
-        return;
-    }
+    add_prerequisite(ctx, "ip");
 
-    struct expr_constant_set cs;
-    memset(&cs, 0, sizeof(struct expr_constant_set));
-    char *error = expr_parse_constant_set(ctx->lexer, NULL, &cs);
-    if (error) {
-        action_error(ctx, "%s", error);
-        free(error);
+    if (ctx->pp->cur_ltable >= ctx->pp->n_tables) {
+        action_error(ctx, "\"%s\" action not allowed in last table.", name);
         return;
     }
+    cn->ltable = ctx->pp->cur_ltable + 1;
 
-    if (!strcmp(dhcp_opt->type, "str")) {
-        if (cs.type != EXPR_C_STRING) {
-            action_error(ctx, "DHCP option %s requires string value.",
-                         dhcp_opt->name);
+    if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
+        if (ctx->lexer->token.type != LEX_T_INTEGER
+            || ctx->lexer->token.format != LEX_F_IPV4) {
+            action_syntax_error(ctx, "expecting IPv4 address");
             return;
         }
-    } else {
-        if (cs.type != EXPR_C_INTEGER) {
-            action_error(ctx, "DHCP option %s requires numeric value.",
-                         dhcp_opt->name);
+        cn->ip = ctx->lexer->token.value.ipv4;
+        lexer_get(ctx->lexer);
+
+        if (!action_force_match(ctx, LEX_T_RPAREN)) {
             return;
         }
     }
+}
 
-    if (!lexer_match(ctx->lexer, LEX_T_COMMA) && (
-        ctx->lexer->token.type != LEX_T_RPAREN)) {
-        action_syntax_error(ctx, NULL);
-        return;
-    }
+static void
+parse_CT_DNAT(struct action_context *ctx)
+{
+    parse_ct_nat(ctx, "ct_dnat", ovnact_put_CT_DNAT(ctx->ovnacts));
+}
 
+static void
+parse_CT_SNAT(struct action_context *ctx)
+{
+    parse_ct_nat(ctx, "ct_snat", ovnact_put_CT_SNAT(ctx->ovnacts));
+}
 
-    if (dhcp_opt->code == 0) {
-        /* offer-ip */
-        ofpbuf_put(ofpacts, &cs.values[0].value.ipv4, sizeof(ovs_be32));
-        goto exit;
+static void
+format_ct_nat(const struct ovnact_ct_nat *cn, const char *name, struct ds *s)
+{
+    ds_put_cstr(s, name);
+    if (cn->ip) {
+        ds_put_format(s, "("IP_FMT")", IP_ARGS(cn->ip));
     }
+    ds_put_char(s, ';');
+}
 
-    uint8_t *opt_header = ofpbuf_put_uninit(ofpacts, 2);
-    opt_header[0] = dhcp_opt->code;
+static void
+format_CT_DNAT(const struct ovnact_ct_nat *cn, struct ds *s)
+{
+    format_ct_nat(cn, "ct_dnat", s);
+}
 
-    if (!strcmp(dhcp_opt->type, "bool") || !strcmp(dhcp_opt->type, "uint8")) {
-        opt_header[1] = 1;
-        ofpbuf_put(ofpacts, &cs.values[0].value.u8_val, 1);
-    } else if (!strcmp(dhcp_opt->type, "uint16")) {
-        opt_header[1] = 2;
-        ofpbuf_put(ofpacts, &cs.values[0].value.be16_int, 2);
-    } else if (!strcmp(dhcp_opt->type, "uint32")) {
-        opt_header[1] = 4;
-        ofpbuf_put(ofpacts, &cs.values[0].value.be32_int, 4);
-    } else if (!strcmp(dhcp_opt->type, "ipv4")) {
-        opt_header[1] = cs.n_values * sizeof(ovs_be32);
-        for (size_t i = 0; i < cs.n_values; i++) {
-            ofpbuf_put(ofpacts, &cs.values[i].value.ipv4, sizeof(ovs_be32));
-        }
-    } else if (!strcmp(dhcp_opt->type, "static_routes")) {
-        size_t no_of_routes = cs.n_values;
-        if (no_of_routes % 2) {
-            no_of_routes -= 1;
-        }
-        opt_header[1] = 0;
+static void
+format_CT_SNAT(const struct ovnact_ct_nat *cn, struct ds *s)
+{
+    format_ct_nat(cn, "ct_snat", s);
+}
 
-        /* Calculating the length of this option first because when
-         * we call ofpbuf_put, it might reallocate the buffer if the
-         * tail room is short making "opt_header" pointer invalid.
-         * So running the for loop twice.
-         */
-        for (size_t i = 0; i < no_of_routes; i += 2) {
-            uint8_t plen = 32;
-            if (cs.values[i].masked) {
-                plen = (uint8_t) ip_count_cidr_bits(cs.values[i].mask.ipv4);
-            }
-            opt_header[1] += (1 + (plen / 8) + sizeof(ovs_be32)) ;
-        }
+static void
+encode_ct_nat(const struct ovnact_ct_nat *cn,
+              const struct ovnact_encode_params *ep,
+              bool snat, struct ofpbuf *ofpacts)
+{
+    const size_t ct_offset = ofpacts->size;
+    ofpbuf_pull(ofpacts, ct_offset);
 
-        /* Copied from RFC 3442. Please refer to this RFC for the format of
-         * the classless static route option.
-         *
-         *  The following table contains some examples of how various subnet
-         *  number/mask combinations can be encoded:
-         *
-         *  Subnet number   Subnet mask      Destination descriptor
-         *  0               0                0
-         *  10.0.0.0        255.0.0.0        8.10
-         *  10.0.0.0        255.255.255.0    24.10.0.0
-         *  10.17.0.0       255.255.0.0      16.10.17
-         *  10.27.129.0     255.255.255.0    24.10.27.129
-         *  10.229.0.128    255.255.255.128  25.10.229.0.128
-         *  10.198.122.47   255.255.255.255  32.10.198.122.47
-         */
+    struct ofpact_conntrack *ct = ofpact_put_CT(ofpacts);
+    ct->recirc_table = cn->ltable + ep->first_ptable;
+    if (snat) {
+        ct->zone_src.field = mf_from_id(MFF_LOG_SNAT_ZONE);
+    } else {
+        ct->zone_src.field = mf_from_id(MFF_LOG_DNAT_ZONE);
+    }
+    ct->zone_src.ofs = 0;
+    ct->zone_src.n_bits = 16;
+    ct->flags = 0;
+    ct->alg = 0;
 
-        for (size_t i = 0; i < no_of_routes; i += 2) {
-            uint8_t plen = 32;
-            if (cs.values[i].masked) {
-                plen = ip_count_cidr_bits(cs.values[i].mask.ipv4);
-            }
-            ofpbuf_put(ofpacts, &plen, 1);
-            ofpbuf_put(ofpacts, &cs.values[i].value.ipv4, plen / 8);
-            ofpbuf_put(ofpacts, &cs.values[i + 1].value.ipv4,
-                       sizeof(ovs_be32));
+    struct ofpact_nat *nat;
+    size_t nat_offset;
+    nat_offset = ofpacts->size;
+    ofpbuf_pull(ofpacts, nat_offset);
+
+    nat = ofpact_put_NAT(ofpacts);
+    nat->flags = 0;
+    nat->range_af = AF_UNSPEC;
+
+    if (cn->ip) {
+        nat->range_af = AF_INET;
+        nat->range.addr.ipv4.min = cn->ip;
+        if (snat) {
+            nat->flags |= NX_NAT_F_SRC;
+        } else {
+            nat->flags |= NX_NAT_F_DST;
         }
-    } else if (!strcmp(dhcp_opt->type, "str")) {
-        opt_header[1] = strlen(cs.values[0].string);
-        ofpbuf_put(ofpacts, cs.values[0].string, opt_header[1]);
     }
 
-exit:
-    expr_constant_set_destroy(&cs);
+    ofpacts->header = ofpbuf_push_uninit(ofpacts, nat_offset);
+    ct = ofpacts->header;
+    if (cn->ip) {
+        ct->flags |= NX_CT_F_COMMIT;
+    } else if (snat) {
+        /* XXX: For performance reasons, we try to prevent additional
+         * recirculations.  So far, ct_snat which is used in a gateway router
+         * does not need a recirculation. ct_snat(IP) does need a
+         * recirculation.  Should we consider a method to let the actions
+         * specify whether a action needs recirculation if there more use
+         * cases?. */
+        ct->recirc_table = NX_CT_RECIRC_NONE;
+    }
+    ofpact_finish(ofpacts, &ct->ofpact);
+    ofpbuf_push_uninit(ofpacts, ct_offset);
 }
 
-/* Parses the "put_dhcp_opts" action.  The result should be stored into 'dst'.
- *
- * The caller has already consumed "put_dhcp_opts(", so this just parses the
- * rest. */
 static void
-parse_put_dhcp_opts_action(struct action_context *ctx,
-                           const struct expr_field *dst)
+encode_CT_DNAT(const struct ovnact_ct_nat *cn,
+               const struct ovnact_encode_params *ep,
+               struct ofpbuf *ofpacts)
 {
-    /* Validate that the destination is a 1-bit, modifiable field. */
-    struct mf_subfield sf;
-    struct expr *prereqs;
-    char *error = expr_expand_field(ctx->lexer, ctx->ap->symtab,
-                                    dst, 1, true, &sf, &prereqs);
-    if (error) {
-        action_error(ctx, "%s", error);
-        free(error);
-        return;
-    }
-    ctx->prereqs = expr_combine(EXPR_T_AND, ctx->prereqs, prereqs);
+    encode_ct_nat(cn, ep, false, ofpacts);
+}
 
-    /* Make sure the first option is "offer_ip" */
-    if (ctx->lexer->token.type != LEX_T_ID) {
-        action_syntax_error(ctx, NULL);
-        return;
-    }
-    const struct dhcp_opts_map *dhcp_opt = dhcp_opts_find(
-        ctx->ap->dhcp_opts, ctx->lexer->token.s);
-    if (!dhcp_opt || dhcp_opt->code != 0) {
-        action_syntax_error(ctx, "expecting offerip option");
+static void
+encode_CT_SNAT(const struct ovnact_ct_nat *cn,
+               const struct ovnact_encode_params *ep,
+               struct ofpbuf *ofpacts)
+{
+    encode_ct_nat(cn, ep, true, ofpacts);
+}
+
+static void
+free_CT_DNAT(struct ovnact_ct_nat *ct_nat OVS_UNUSED)
+{
+}
+
+static void
+free_CT_SNAT(struct ovnact_ct_nat *ct_nat OVS_UNUSED)
+{
+}
+
+static void
+parse_ct_lb_action(struct action_context *ctx)
+{
+    if (ctx->pp->cur_ltable >= ctx->pp->n_tables) {
+        action_error(ctx, "\"ct_lb\" action not allowed in last table.");
         return;
     }
 
-    /* controller. */
-    size_t oc_offset = start_controller_op(ctx->ofpacts,
-                                           ACTION_OPCODE_PUT_DHCP_OPTS, true);
-    nx_put_header(ctx->ofpacts, sf.field->id, OFP13_VERSION, false);
-    ovs_be32 ofs = htonl(sf.ofs);
-    ofpbuf_put(ctx->ofpacts, &ofs, sizeof ofs);
-    while (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
-        parse_dhcp_opt(ctx, ctx->ofpacts);
-        if (ctx->error) {
-            return;
+    add_prerequisite(ctx, "ip");
+
+    struct ovnact_ct_lb_dst *dsts = NULL;
+    size_t allocated_dsts = 0;
+    size_t n_dsts = 0;
+
+    if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
+        while (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
+            if (ctx->lexer->token.type != LEX_T_INTEGER
+                || mf_subvalue_width(&ctx->lexer->token.value) > 32) {
+                action_syntax_error(ctx, "expecting IPv4 address");
+                return;
+            }
+
+            /* Parse IP. */
+            ovs_be32 ip = ctx->lexer->token.value.ipv4;
+            lexer_get(ctx->lexer);
+
+            /* Parse optional port. */
+            uint16_t port = 0;
+            if (lexer_match(ctx->lexer, LEX_T_COLON)
+                && !action_parse_port(ctx, &port)) {
+                free(dsts);
+                return;
+            }
+            lexer_match(ctx->lexer, LEX_T_COMMA);
+
+            /* Append to dsts. */
+            if (n_dsts >= allocated_dsts) {
+                dsts = x2nrealloc(dsts, &allocated_dsts, sizeof *dsts);
+            }
+            dsts[n_dsts++] = (struct ovnact_ct_lb_dst) { ip, port };
         }
     }
-    finish_controller_op(ctx->ofpacts, oc_offset);
+
+    struct ovnact_ct_lb *cl = ovnact_put_CT_LB(ctx->ovnacts);
+    cl->ltable = ctx->pp->cur_ltable + 1;
+    cl->dsts = dsts;
+    cl->n_dsts = n_dsts;
 }
 
-static bool
-action_parse_port(struct action_context *ctx, uint16_t *port)
+static void
+format_CT_LB(const struct ovnact_ct_lb *cl, struct ds *s)
 {
-    if (lexer_is_int(ctx->lexer)) {
-        int value = ntohll(ctx->lexer->token.value.integer);
-        if (value <= UINT16_MAX) {
-            *port = value;
-            lexer_get(ctx->lexer);
-            return true;
+    ds_put_cstr(s, "ct_lb");
+    if (cl->n_dsts) {
+        ds_put_char(s, '(');
+        for (size_t i = 0; i < cl->n_dsts; i++) {
+            if (i) {
+                ds_put_cstr(s, ", ");
+            }
+
+            const struct ovnact_ct_lb_dst *dst = &cl->dsts[i];
+            ds_put_format(s, IP_FMT, IP_ARGS(dst->ip));
+            if (dst->port) {
+                ds_put_format(s, ":%"PRIu16, dst->port);
+            }
         }
+        ds_put_char(s, ')');
     }
-    action_syntax_error(ctx, "expecting port number");
-    return false;
+    ds_put_char(s, ';');
 }
 
 static void
-parse_ct_lb_action(struct action_context *ctx)
+encode_CT_LB(const struct ovnact_ct_lb *cl,
+             const struct ovnact_encode_params *ep,
+             struct ofpbuf *ofpacts)
 {
-    uint8_t recirc_table;
-    if (ctx->ap->cur_ltable < ctx->ap->n_tables) {
-        recirc_table = ctx->ap->first_ptable + ctx->ap->cur_ltable + 1;
-    } else {
-        action_error(ctx, "\"ct_lb\" action not allowed in last table.");
-        return;
-    }
-
-    if (!lexer_match(ctx->lexer, LEX_T_LPAREN)) {
-        /* ct_lb without parentheses means that this is an established
+    uint8_t recirc_table = cl->ltable + ep->first_ptable;
+    if (!cl->n_dsts) {
+        /* ct_lb without any destinations means that this is an established
          * connection and we just need to do a NAT. */
-        const size_t ct_offset = ctx->ofpacts->size;
-        ofpbuf_pull(ctx->ofpacts, ct_offset);
+        const size_t ct_offset = ofpacts->size;
+        ofpbuf_pull(ofpacts, ct_offset);
 
-        struct ofpact_conntrack *ct = ofpact_put_CT(ctx->ofpacts);
+        struct ofpact_conntrack *ct = ofpact_put_CT(ofpacts);
         struct ofpact_nat *nat;
         size_t nat_offset;
         ct->zone_src.field = mf_from_id(MFF_LOG_CT_ZONE);
@@ -670,23 +1015,21 @@ parse_ct_lb_action(struct action_context *ctx)
         ct->recirc_table = recirc_table;
         ct->alg = 0;
 
-        add_prerequisite(ctx, "ip");
+        nat_offset = ofpacts->size;
+        ofpbuf_pull(ofpacts, nat_offset);
 
-        nat_offset = ctx->ofpacts->size;
-        ofpbuf_pull(ctx->ofpacts, nat_offset);
-
-        nat = ofpact_put_NAT(ctx->ofpacts);
+        nat = ofpact_put_NAT(ofpacts);
         nat->flags = 0;
         nat->range_af = AF_UNSPEC;
 
-        ctx->ofpacts->header = ofpbuf_push_uninit(ctx->ofpacts, nat_offset);
-        ct = ctx->ofpacts->header;
-        ofpact_finish(ctx->ofpacts, &ct->ofpact);
-        ofpbuf_push_uninit(ctx->ofpacts, ct_offset);
+        ofpacts->header = ofpbuf_push_uninit(ofpacts, nat_offset);
+        ct = ofpacts->header;
+        ofpact_finish(ofpacts, &ct->ofpact);
+        ofpbuf_push_uninit(ofpacts, ct_offset);
         return;
     }
 
-    uint32_t group_id = 0, bucket_id = 0, hash;
+    uint32_t group_id = 0, hash;
     struct group_info *group_info;
     struct ofpact_group *og;
 
@@ -695,41 +1038,22 @@ parse_ct_lb_action(struct action_context *ctx)
 
     BUILD_ASSERT(MFF_LOG_CT_ZONE >= MFF_REG0);
     BUILD_ASSERT(MFF_LOG_CT_ZONE < MFF_REG0 + FLOW_N_REGS);
-    do {
-        if (ctx->lexer->token.type != LEX_T_INTEGER
-            || mf_subvalue_width(&ctx->lexer->token.value) > 32) {
-            action_syntax_error(ctx, "expecting IPv4 address");
-            ds_destroy(&ds);
-            return;
-        }
-        ovs_be32 ip = ctx->lexer->token.value.ipv4;
-        lexer_get(ctx->lexer);
-
-        uint16_t port = 0;
-        if (lexer_match(ctx->lexer, LEX_T_COLON)
-            && !action_parse_port(ctx, &port)) {
-            ds_destroy(&ds);
-            return;
-        }
-
-        bucket_id++;
-        ds_put_format(&ds, ",bucket=bucket_id=%u,weight:100,actions="
-                      "ct(nat(dst="IP_FMT, bucket_id, IP_ARGS(ip));
-        if (port) {
-            ds_put_format(&ds, ":%"PRIu16, port);
+    for (size_t bucket_id = 0; bucket_id < cl->n_dsts; bucket_id++) {
+        const struct ovnact_ct_lb_dst *dst = &cl->dsts[bucket_id];
+        ds_put_format(&ds, ",bucket=bucket_id=%"PRIuSIZE",weight:100,actions="
+                      "ct(nat(dst="IP_FMT, bucket_id, IP_ARGS(dst->ip));
+        if (dst->port) {
+            ds_put_format(&ds, ":%"PRIu16, dst->port);
         }
         ds_put_format(&ds, "),commit,table=%d,zone=NXM_NX_REG%d[0..15])",
                       recirc_table, MFF_LOG_CT_ZONE - MFF_REG0);
-
-        lexer_match(ctx->lexer, LEX_T_COMMA);
-    } while (!lexer_match(ctx->lexer, LEX_T_RPAREN));
-    add_prerequisite(ctx, "ip");
+    }
 
     hash = hash_string(ds_cstr(&ds), 0);
 
     /* Check whether we have non installed but allocated group_id. */
     HMAP_FOR_EACH_WITH_HASH (group_info, hmap_node, hash,
-                             &ctx->ap->group_table->desired_groups) {
+                             &ep->group_table->desired_groups) {
         if (!strcmp(ds_cstr(&group_info->group), ds_cstr(&ds))) {
             group_id = group_info->group_id;
             break;
@@ -740,7 +1064,7 @@ parse_ct_lb_action(struct action_context *ctx)
         /* Check whether we already have an installed entry for this
          * combination. */
         HMAP_FOR_EACH_WITH_HASH (group_info, hmap_node, hash,
-                                 &ctx->ap->group_table->existing_groups) {
+                                 &ep->group_table->existing_groups) {
             if (!strcmp(ds_cstr(&group_info->group), ds_cstr(&ds))) {
                 group_id = group_info->group_id;
             }
@@ -748,337 +1072,606 @@ parse_ct_lb_action(struct action_context *ctx)
 
         if (!group_id) {
             /* Reserve a new group_id. */
-            group_id = bitmap_scan(ctx->ap->group_table->group_ids, 0, 1,
+            group_id = bitmap_scan(ep->group_table->group_ids, 0, 1,
                                    MAX_OVN_GROUPS + 1);
         }
 
         if (group_id == MAX_OVN_GROUPS + 1) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+            VLOG_ERR_RL(&rl, "out of group ids");
+
             ds_destroy(&ds);
-            action_error(ctx, "out of group ids.");
             return;
         }
-        bitmap_set1(ctx->ap->group_table->group_ids, group_id);
+        bitmap_set1(ep->group_table->group_ids, group_id);
 
         group_info = xmalloc(sizeof *group_info);
         group_info->group = ds;
         group_info->group_id = group_id;
-        group_info->lflow_uuid = ctx->ap->lflow_uuid;
+        group_info->lflow_uuid = ep->lflow_uuid;
         group_info->hmap_node.hash = hash;
 
-        hmap_insert(&ctx->ap->group_table->desired_groups,
+        hmap_insert(&ep->group_table->desired_groups,
                     &group_info->hmap_node, group_info->hmap_node.hash);
     } else {
         ds_destroy(&ds);
     }
 
-    /* Create an action to set the group. */
-    og = ofpact_put_GROUP(ctx->ofpacts);
-    og->group_id = group_id;
+    /* Create an action to set the group. */
+    og = ofpact_put_GROUP(ofpacts);
+    og->group_id = group_id;
+}
+
+static void
+free_CT_LB(struct ovnact_ct_lb *ct_lb)
+{
+    free(ct_lb->dsts);
+}
+
+/* Implements the "arp" and "nd_na" actions, which execute nested actions on a
+ * packet derived from the one being processed. */
+static void
+parse_nested_action(struct action_context *ctx, enum ovnact_type type,
+                    const char *prereq)
+{
+    if (!action_force_match(ctx, LEX_T_LCURLY)) {
+        return;
+    }
+
+    uint64_t stub[1024 / 8];
+    struct ofpbuf nested = OFPBUF_STUB_INITIALIZER(stub);
+
+    struct action_context inner_ctx = {
+        .pp = ctx->pp,
+        .lexer = ctx->lexer,
+        .error = NULL,
+        .ovnacts = &nested,
+        .prereqs = NULL
+    };
+    while (!lexer_match(ctx->lexer, LEX_T_RCURLY)) {
+        if (!parse_action(&inner_ctx)) {
+            break;
+        }
+    }
+
+    expr_destroy(inner_ctx.prereqs); /* XXX what is the correct behavior? */
+    if (inner_ctx.error) {
+        ctx->error = inner_ctx.error;
+        ovnacts_free(nested.data, nested.size);
+        ofpbuf_uninit(&nested);
+        return;
+    }
+
+    add_prerequisite(ctx, prereq);
+
+    struct ovnact_nest *on = ovnact_put(ctx->ovnacts, type, sizeof *on);
+    on->nested_len = nested.size;
+    on->nested = ofpbuf_steal_data(&nested);
+}
+
+static void
+parse_ARP(struct action_context *ctx)
+{
+    parse_nested_action(ctx, OVNACT_ARP, "ip4");
+}
+
+static void
+parse_ND_NA(struct action_context *ctx)
+{
+    parse_nested_action(ctx, OVNACT_ND_NA, "nd_ns");
+}
+
+static void
+format_nested_action(const struct ovnact_nest *on, const char *name,
+                     struct ds *s)
+{
+    ds_put_format(s, "%s { ", name);
+    ovnacts_format(on->nested, on->nested_len, s);
+    ds_put_format(s, " };");
+}
+
+static void
+format_ARP(const struct ovnact_nest *nest, struct ds *s)
+{
+    format_nested_action(nest, "arp", s);
+}
+
+static void
+format_ND_NA(const struct ovnact_nest *nest, struct ds *s)
+{
+    format_nested_action(nest, "nd_na", s);
+}
+
+static void
+encode_nested_actions(const struct ovnact_nest *on,
+                      const struct ovnact_encode_params *ep,
+                      enum action_opcode opcode,
+                      struct ofpbuf *ofpacts)
+{
+    /* Convert nested actions into ofpacts. */
+    uint64_t inner_ofpacts_stub[1024 / 8];
+    struct ofpbuf inner_ofpacts = OFPBUF_STUB_INITIALIZER(inner_ofpacts_stub);
+    ovnacts_encode(on->nested, on->nested_len, ep, &inner_ofpacts);
+
+    /* Add a "controller" action with the actions nested inside "{...}",
+     * converted to OpenFlow, as its userdata.  ovn-controller will convert the
+     * packet to ARP or NA and then send the packet and actions back to the
+     * switch inside an OFPT_PACKET_OUT message. */
+    size_t oc_offset = encode_start_controller_op(opcode, false, ofpacts);
+    ofpacts_put_openflow_actions(inner_ofpacts.data, inner_ofpacts.size,
+                                 ofpacts, OFP13_VERSION);
+    encode_finish_controller_op(oc_offset, ofpacts);
+
+    /* Free memory. */
+    ofpbuf_uninit(&inner_ofpacts);
+}
+
+static void
+encode_ARP(const struct ovnact_nest *on,
+           const struct ovnact_encode_params *ep,
+           struct ofpbuf *ofpacts)
+{
+    encode_nested_actions(on, ep, ACTION_OPCODE_ARP, ofpacts);
+}
+
+static void
+encode_ND_NA(const struct ovnact_nest *on,
+             const struct ovnact_encode_params *ep,
+             struct ofpbuf *ofpacts)
+{
+    encode_nested_actions(on, ep, ACTION_OPCODE_ND_NA, ofpacts);
+}
+
+static void
+free_nested_actions(struct ovnact_nest *on)
+{
+    ovnacts_free(on->nested, on->nested_len);
+    free(on->nested);
+}
+
+static void
+free_ARP(struct ovnact_nest *nest)
+{
+    free_nested_actions(nest);
+}
+
+static void
+free_ND_NA(struct ovnact_nest *nest)
+{
+    free_nested_actions(nest);
+}
+
+static void
+parse_get_mac_bind(struct action_context *ctx, int width,
+                   struct ovnact_get_mac_bind *get_mac)
+{
+    action_force_match(ctx, LEX_T_LPAREN);
+    action_parse_field(ctx, 0, false, &get_mac->port);
+    action_force_match(ctx, LEX_T_COMMA);
+    action_parse_field(ctx, width, false, &get_mac->ip);
+    action_force_match(ctx, LEX_T_RPAREN);
+}
+
+static void
+format_get_mac_bind(const struct ovnact_get_mac_bind *get_mac,
+                    const char *name, struct ds *s)
+{
+    ds_put_format(s, "%s(", name);
+    expr_field_format(&get_mac->port, s);
+    ds_put_cstr(s, ", ");
+    expr_field_format(&get_mac->ip, s);
+    ds_put_cstr(s, ");");
+}
+
+static void
+format_GET_ARP(const struct ovnact_get_mac_bind *get_mac, struct ds *s)
+{
+    format_get_mac_bind(get_mac, "get_arp", s);
+}
+
+static void
+format_GET_ND(const struct ovnact_get_mac_bind *get_mac, struct ds *s)
+{
+    format_get_mac_bind(get_mac, "get_nd", s);
+}
+
+static void
+encode_get_mac(const struct ovnact_get_mac_bind *get_mac,
+               enum mf_field_id ip_field,
+               const struct ovnact_encode_params *ep,
+               struct ofpbuf *ofpacts)
+{
+    const struct arg args[] = {
+        { expr_resolve_field(&get_mac->port), MFF_LOG_OUTPORT },
+        { expr_resolve_field(&get_mac->ip), ip_field },
+    };
+    encode_setup_args(args, ARRAY_SIZE(args), ofpacts);
+
+    put_load(0, MFF_ETH_DST, 0, 48, ofpacts);
+    emit_resubmit(ofpacts, ep->mac_bind_ptable);
+
+    encode_restore_args(args, ARRAY_SIZE(args), ofpacts);
+}
+
+static void
+encode_GET_ARP(const struct ovnact_get_mac_bind *get_mac,
+               const struct ovnact_encode_params *ep,
+               struct ofpbuf *ofpacts)
+{
+    encode_get_mac(get_mac, MFF_REG0, ep, ofpacts);
+}
+
+static void
+encode_GET_ND(const struct ovnact_get_mac_bind *get_mac,
+              const struct ovnact_encode_params *ep,
+              struct ofpbuf *ofpacts)
+{
+    encode_get_mac(get_mac, MFF_XXREG0, ep, ofpacts);
+}
+
+static void
+free_GET_ARP(struct ovnact_get_mac_bind *get_mac OVS_UNUSED)
+{
+}
+
+static void
+free_GET_ND(struct ovnact_get_mac_bind *get_mac OVS_UNUSED)
+{
+}
+
+static void
+parse_put_mac_bind(struct action_context *ctx, int width,
+                   struct ovnact_put_mac_bind *put_mac)
+{
+    action_force_match(ctx, LEX_T_LPAREN);
+    action_parse_field(ctx, 0, false, &put_mac->port);
+    action_force_match(ctx, LEX_T_COMMA);
+    action_parse_field(ctx, width, false, &put_mac->ip);
+    action_force_match(ctx, LEX_T_COMMA);
+    action_parse_field(ctx, 48, false, &put_mac->mac);
+    action_force_match(ctx, LEX_T_RPAREN);
+}
+
+static void
+format_put_mac_bind(const struct ovnact_put_mac_bind *put_mac,
+                    const char *name, struct ds *s)
+{
+    ds_put_format(s, "%s(", name);
+    expr_field_format(&put_mac->port, s);
+    ds_put_cstr(s, ", ");
+    expr_field_format(&put_mac->ip, s);
+    ds_put_cstr(s, ", ");
+    expr_field_format(&put_mac->mac, s);
+    ds_put_cstr(s, ");");
 }
 
 static void
-parse_get_nd_action(struct action_context *ctx)
+format_PUT_ARP(const struct ovnact_put_mac_bind *put_mac, struct ds *s)
 {
-    struct mf_subfield port, ip6;
+    format_put_mac_bind(put_mac, "put_arp", s);
+}
 
-    if (!action_force_match(ctx, LEX_T_LPAREN)
-        || !action_parse_field(ctx, 0, &port)
-        || !action_force_match(ctx, LEX_T_COMMA)
-        || !action_parse_field(ctx, 128, &ip6)
-        || !action_force_match(ctx, LEX_T_RPAREN)) {
-        return;
-    }
+static void
+format_PUT_ND(const struct ovnact_put_mac_bind *put_mac, struct ds *s)
+{
+    format_put_mac_bind(put_mac, "put_nd", s);
+}
 
+static void
+encode_put_mac(const struct ovnact_put_mac_bind *put_mac,
+               enum mf_field_id ip_field, enum action_opcode opcode,
+               struct ofpbuf *ofpacts)
+{
     const struct arg args[] = {
-        { &port, MFF_LOG_OUTPORT },
-        { &ip6, MFF_XXREG0 },
+        { expr_resolve_field(&put_mac->port), MFF_LOG_INPORT },
+        { expr_resolve_field(&put_mac->ip), ip_field },
+        { expr_resolve_field(&put_mac->mac), MFF_ETH_SRC }
     };
-    setup_args(ctx, args, ARRAY_SIZE(args));
-
-    put_load(0, MFF_ETH_DST, 0, 48, ctx->ofpacts);
-    emit_resubmit(ctx, ctx->ap->mac_bind_ptable);
-
-    restore_args(ctx, args, ARRAY_SIZE(args));
+    encode_setup_args(args, ARRAY_SIZE(args), ofpacts);
+    encode_controller_op(opcode, ofpacts);
+    encode_restore_args(args, ARRAY_SIZE(args), ofpacts);
 }
 
 static void
-parse_put_nd_action(struct action_context *ctx)
+encode_PUT_ARP(const struct ovnact_put_mac_bind *put_mac,
+               const struct ovnact_encode_params *ep OVS_UNUSED,
+               struct ofpbuf *ofpacts)
 {
-    struct mf_subfield port, ip6, mac;
-
-    if (!action_force_match(ctx, LEX_T_LPAREN)
-        || !action_parse_field(ctx, 0, &port)
-        || !action_force_match(ctx, LEX_T_COMMA)
-        || !action_parse_field(ctx, 128, &ip6)
-        || !action_force_match(ctx, LEX_T_COMMA)
-        || !action_parse_field(ctx, 48, &mac)
-        || !action_force_match(ctx, LEX_T_RPAREN)) {
-        return;
-    }
+    encode_put_mac(put_mac, MFF_REG0, ACTION_OPCODE_PUT_ARP, ofpacts);
+}
 
-    const struct arg args[] = {
-        { &port, MFF_LOG_INPORT },
-        { &ip6, MFF_XXREG0 },
-        { &mac, MFF_ETH_SRC }
-    };
-    setup_args(ctx, args, ARRAY_SIZE(args));
-    put_controller_op(ctx->ofpacts, ACTION_OPCODE_PUT_ND);
-    restore_args(ctx, args, ARRAY_SIZE(args));
+static void
+encode_PUT_ND(const struct ovnact_put_mac_bind *put_mac,
+              const struct ovnact_encode_params *ep OVS_UNUSED,
+              struct ofpbuf *ofpacts)
+{
+    encode_put_mac(put_mac, MFF_XXREG0, ACTION_OPCODE_PUT_ND, ofpacts);
 }
 
 static void
-emit_ct(struct action_context *ctx, bool recirc_next, bool commit,
-        int *ct_mark, int *ct_mark_mask,
-        ovs_be128 *ct_label, ovs_be128 *ct_label_mask)
+free_PUT_ARP(struct ovnact_put_mac_bind *put_mac OVS_UNUSED)
 {
-    struct ofpact_conntrack *ct = ofpact_put_CT(ctx->ofpacts);
-    ct->flags |= commit ? NX_CT_F_COMMIT : 0;
+}
 
-    /* If "recirc" is set, we automatically go to the next table. */
-    if (recirc_next) {
-        if (ctx->ap->cur_ltable < ctx->ap->n_tables) {
-            ct->recirc_table = ctx->ap->first_ptable + ctx->ap->cur_ltable + 1;
-        } else {
-            action_error(ctx, "\"ct_next\" action not allowed in last table.");
-            return;
-        }
-    } else {
-        ct->recirc_table = NX_CT_RECIRC_NONE;
+static void
+free_PUT_ND(struct ovnact_put_mac_bind *put_mac OVS_UNUSED)
+{
+}
+
+static void
+parse_dhcp_opt(struct action_context *ctx, struct ovnact_dhcp_option *o)
+{
+    if (ctx->lexer->token.type != LEX_T_ID) {
+        action_syntax_error(ctx, NULL);
+        return;
     }
-
-    ct->zone_src.field = mf_from_id(MFF_LOG_CT_ZONE);
-    ct->zone_src.ofs = 0;
-    ct->zone_src.n_bits = 16;
-
-    /* We do not support ALGs yet. */
-    ct->alg = 0;
-
-    /* CT only works with IP, so set up a prerequisite. */
-    add_prerequisite(ctx, "ip");
-
-    size_t set_field_offset = ctx->ofpacts->size;
-    ofpbuf_pull(ctx->ofpacts, set_field_offset);
-
-    if (ct_mark) {
-        struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ctx->ofpacts);
-        sf->field = mf_from_id(MFF_CT_MARK);
-        sf->value.be32 = htonl(*ct_mark);
-        sf->mask.be32 = ct_mark_mask ? htonl(*ct_mark_mask) : OVS_BE32_MAX;
+    o->option = dhcp_opts_find(ctx->pp->dhcp_opts, ctx->lexer->token.s);
+    if (!o->option) {
+        action_syntax_error(ctx, "expecting DHCP option name");
+        return;
     }
+    lexer_get(ctx->lexer);
 
-    if (ct_label) {
-        struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ctx->ofpacts);
-        sf->field = mf_from_id(MFF_CT_LABEL);
-        sf->value.be128 = *ct_label;
-        sf->mask.be128 = ct_label_mask ? *ct_label_mask : OVS_BE128_MAX;
+    if (!action_force_match(ctx, LEX_T_EQUALS)) {
+        return;
     }
 
-    ctx->ofpacts->header = ofpbuf_push_uninit(ctx->ofpacts, set_field_offset);
-    ct = ctx->ofpacts->header;
-    ofpact_finish(ctx->ofpacts, &ct->ofpact);
-}
-
-/* Parse an argument to the ct_commit(); action.  Supported arguments include:
- *
- *      ct_mark=<value>[/<mask>]
- *      ct_label=<value>[/<mask>]
- *
- * If a comma separates the current argument from the next argument, this
- * function will consume it.
- *
- * set_mark - This will be set to true if a value for ct_mark was successfully
- *            parsed. Otherwise, it will be unchanged.
- * mark_value - If set_mark was set to true, this will contain the value
- *              parsed for ct_mark.
- * mark_mask - If set_mark was set to true, this will contain the mask
- *             for ct_mark if one was found.  Otherwise, it will be
- *             unchanged, so the caller should initialize this to an
- *             appropriate value.
- * set_label - This will be set to true if a value for ct_label was successfully
- *             parsed. Otherwise, it will be unchanged.
- * label_value - If set_label was set to true, this will contain the value
- *               parsed for ct_label.
- * label_mask - If set_label was set to true, this will contain the mask
- *              for ct_label if one was found.  Otherwise, it will be
- *              unchanged, so the caller should initialize this to an
- *              appropriate value.
- *
- * Return true after successfully parsing an argument.  false on failure. */
-static bool
-parse_ct_commit_arg(struct action_context *ctx,
-                    bool *set_mark, int *mark_value, int *mark_mask,
-                    bool *set_label, ovs_be128 *label_value,
-                    ovs_be128 *label_mask)
-{
-    if (lexer_match_id(ctx->lexer, "ct_mark")) {
-        if (!lexer_match(ctx->lexer, LEX_T_EQUALS)) {
-            action_error(ctx, "Expected '=' after argument to ct_commit");
-            return false;
-        }
-        if (ctx->lexer->token.type == LEX_T_INTEGER) {
-            *mark_value = ntohll(ctx->lexer->token.value.integer);
-        } else if (ctx->lexer->token.type == LEX_T_MASKED_INTEGER) {
-            *mark_value = ntohll(ctx->lexer->token.value.integer);
-            *mark_mask = ntohll(ctx->lexer->token.mask.integer);
-        } else {
-            action_error(ctx, "Expected integer after 'ct_mark='");
-            return false;
-        }
-        lexer_get(ctx->lexer);
-        *set_mark = true;
-    } else if (lexer_match_id(ctx->lexer, "ct_label")) {
-        if (!lexer_match(ctx->lexer, LEX_T_EQUALS)) {
-            action_error(ctx, "Expected '=' after argument to ct_commit");
-            return false;
-        }
+    char *error = expr_constant_set_parse(ctx->lexer, &o->value);
+    if (error) {
+        action_error(ctx, "%s", error);
+        free(error);
+        return;
+    }
 
-        /* ct_label is a 128-bit field.  The lexer supports 128-bit
-         * integers if its a hex string. The ct_label value should be specified
-         * in hex string if > 64-bits are to be used */
-        if (ctx->lexer->token.type == LEX_T_INTEGER) {
-            label_value->be64.lo = ctx->lexer->token.value.be128_int.be64.lo;
-            label_value->be64.hi = ctx->lexer->token.value.be128_int.be64.hi;
-        } else if (ctx->lexer->token.type == LEX_T_MASKED_INTEGER) {
-            label_value->be64.lo = ctx->lexer->token.value.be128_int.be64.lo;
-            label_value->be64.hi = ctx->lexer->token.value.be128_int.be64.hi;
-            label_mask->be64.lo = ctx->lexer->token.mask.be128_int.be64.lo;
-            label_mask->be64.hi = ctx->lexer->token.mask.be128_int.be64.hi;
-        } else {
-            action_error(ctx, "Expected integer after 'ct_label='");
-            return false;
+    if (!strcmp(o->option->type, "str")) {
+        if (o->value.type != EXPR_C_STRING) {
+            action_error(ctx, "DHCP option %s requires string value.",
+                         o->option->name);
+            expr_constant_set_destroy(&o->value);
+            return;
         }
-        lexer_get(ctx->lexer);
-        *set_label = true;
     } else {
-        action_error(ctx, "Expected argument to ct_commit()");
-        return false;
+        if (o->value.type != EXPR_C_INTEGER) {
+            action_error(ctx, "DHCP option %s requires numeric value.",
+                         o->option->name);
+            expr_constant_set_destroy(&o->value);
+            return;
+        }
     }
+}
 
-    if (lexer_match(ctx->lexer, LEX_T_COMMA)) {
-        /* A comma is valid after an argument, but only if another
-         * argument is present (not a closing paren) */
-        if (lexer_lookahead(ctx->lexer) == LEX_T_RPAREN) {
-            action_error(ctx, "Another argument to ct_commit() expected "
-                              "after comma.");
-            return false;
+static const struct ovnact_dhcp_option *
+find_offerip(const struct ovnact_dhcp_option *options, size_t n)
+{
+    for (const struct ovnact_dhcp_option *o = options; o < &options[n]; o++) {
+        if (o->option->code == 0) {
+            return o;
         }
     }
+    return NULL;
+}
 
-    return true;
+static void
+free_dhcp_options(struct ovnact_dhcp_option *options, size_t n)
+{
+    for (struct ovnact_dhcp_option *o = options; o < &options[n]; o++) {
+        expr_constant_set_destroy(&o->value);
+    }
+    free(options);
 }
 
+/* Parses the "put_dhcp_opts" action.
+ *
+ * The caller has already consumed "<dst> = put_dhcp_opts(", so this just
+ * parses the rest. */
 static void
-parse_ct_commit_action(struct action_context *ctx)
+parse_PUT_DHCP_OPTS(struct action_context *ctx, const struct expr_field *dst)
 {
-    if (!lexer_match(ctx->lexer, LEX_T_LPAREN)) {
-        /* ct_commit; */
-        emit_ct(ctx, false, true, NULL, NULL, NULL, NULL);
+    /* Validate that the destination is a 1-bit, modifiable field. */
+    char *error = expr_type_check(dst, 1, true);
+    if (error) {
+        action_error(ctx, "%s", error);
+        free(error);
         return;
     }
 
-    /* ct_commit();
-     * ct_commit(ct_mark=0);
-     * ct_commit(ct_label=0);
-     * ct_commit(ct_mark=0, ct_label=0); */
+    struct ovnact_dhcp_option *options = NULL;
+    size_t allocated_options = 0;
+    size_t n_options = 0;
 
-    bool set_mark = false;
-    bool set_label = false;
-    int mark_value = 0;
-    int mark_mask = ~0;
-    ovs_be128 label_value = { .be32 = { 0, }, };
-    ovs_be128 label_mask = OVS_BE128_MAX;
+    do {
+        if (n_options >= allocated_options) {
+            options = x2nrealloc(options, &allocated_options, sizeof *options);
+        }
 
-    while (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
-        if (!parse_ct_commit_arg(ctx, &set_mark, &mark_value, &mark_mask,
-                                 &set_label, &label_value, &label_mask)) {
+        struct ovnact_dhcp_option *o = &options[n_options];
+        parse_dhcp_opt(ctx, o);
+        if (ctx->error) {
+            free_dhcp_options(options, n_options);
             return;
         }
+        n_options++;
+
+        lexer_match(ctx->lexer, LEX_T_COMMA);
+    } while (!lexer_match(ctx->lexer, LEX_T_RPAREN));
+
+    if (!find_offerip(options, n_options)) {
+        action_error(ctx, "put_dhcp_opts requires offerip to be specified.");
+        free_dhcp_options(options, n_options);
+        return;
     }
 
-    emit_ct(ctx, false, true,
-            set_mark ? &mark_value : NULL,
-            set_mark ? &mark_mask : NULL,
-            set_label ? &label_value : NULL,
-            set_label ? &label_mask : NULL);
+    struct ovnact_put_dhcp_opts *pdo = ovnact_put_PUT_DHCP_OPTS(ctx->ovnacts);
+    pdo->dst = *dst;
+    pdo->options = options;
+    pdo->n_options = n_options;
 }
 
 static void
-parse_ct_nat(struct action_context *ctx, bool snat)
+format_PUT_DHCP_OPTS(const struct ovnact_put_dhcp_opts *pdo, struct ds *s)
 {
-    const size_t ct_offset = ctx->ofpacts->size;
-    ofpbuf_pull(ctx->ofpacts, ct_offset);
-
-    struct ofpact_conntrack *ct = ofpact_put_CT(ctx->ofpacts);
-
-    if (ctx->ap->cur_ltable < ctx->ap->n_tables) {
-        ct->recirc_table = ctx->ap->first_ptable + ctx->ap->cur_ltable + 1;
-    } else {
-        action_error(ctx,
-                     "\"ct_[sd]nat\" action not allowed in last table.");
-        return;
+    expr_field_format(&pdo->dst, s);
+    ds_put_cstr(s, " = put_dhcp_opts(");
+    for (const struct ovnact_dhcp_option *o = pdo->options;
+         o < &pdo->options[pdo->n_options]; o++) {
+        if (o != pdo->options) {
+            ds_put_cstr(s, ", ");
+        }
+        ds_put_format(s, "%s = ", o->option->name);
+        expr_constant_set_format(&o->value, s);
     }
+    ds_put_cstr(s, ");");
+}
 
-    if (snat) {
-        ct->zone_src.field = mf_from_id(MFF_LOG_SNAT_ZONE);
-    } else {
-        ct->zone_src.field = mf_from_id(MFF_LOG_DNAT_ZONE);
-    }
-    ct->zone_src.ofs = 0;
-    ct->zone_src.n_bits = 16;
-    ct->flags = 0;
-    ct->alg = 0;
+static void
+encode_put_dhcp_option(const struct ovnact_dhcp_option *o,
+                       struct ofpbuf *ofpacts)
+{
+    uint8_t *opt_header = ofpbuf_put_zeros(ofpacts, 2);
+    opt_header[0] = o->option->code;
 
-    add_prerequisite(ctx, "ip");
+    const union expr_constant *c = o->value.values;
+    size_t n_values = o->value.n_values;
+    if (!strcmp(o->option->type, "bool") ||
+        !strcmp(o->option->type, "uint8")) {
+        opt_header[1] = 1;
+        ofpbuf_put(ofpacts, &c->value.u8_val, 1);
+    } else if (!strcmp(o->option->type, "uint16")) {
+        opt_header[1] = 2;
+        ofpbuf_put(ofpacts, &c->value.be16_int, 2);
+    } else if (!strcmp(o->option->type, "uint32")) {
+        opt_header[1] = 4;
+        ofpbuf_put(ofpacts, &c->value.be32_int, 4);
+    } else if (!strcmp(o->option->type, "ipv4")) {
+        opt_header[1] = n_values * sizeof(ovs_be32);
+        for (size_t i = 0; i < n_values; i++) {
+            ofpbuf_put(ofpacts, &c[i].value.ipv4, sizeof(ovs_be32));
+        }
+    } else if (!strcmp(o->option->type, "static_routes")) {
+        size_t no_of_routes = n_values;
+        if (no_of_routes % 2) {
+            no_of_routes -= 1;
+        }
+        opt_header[1] = 0;
 
-    struct ofpact_nat *nat;
-    size_t nat_offset;
-    nat_offset = ctx->ofpacts->size;
-    ofpbuf_pull(ctx->ofpacts, nat_offset);
+        /* Calculating the length of this option first because when
+         * we call ofpbuf_put, it might reallocate the buffer if the
+         * tail room is short making "opt_header" pointer invalid.
+         * So running the for loop twice.
+         */
+        for (size_t i = 0; i < no_of_routes; i += 2) {
+            uint8_t plen = 32;
+            if (c[i].masked) {
+                plen = (uint8_t) ip_count_cidr_bits(c[i].mask.ipv4);
+            }
+            opt_header[1] += (1 + (plen / 8) + sizeof(ovs_be32)) ;
+        }
 
-    nat = ofpact_put_NAT(ctx->ofpacts);
-    nat->flags = 0;
-    nat->range_af = AF_UNSPEC;
+        /* Copied from RFC 3442. Please refer to this RFC for the format of
+         * the classless static route option.
+         *
+         *  The following table contains some examples of how various subnet
+         *  number/mask combinations can be encoded:
+         *
+         *  Subnet number   Subnet mask      Destination descriptor
+         *  0               0                0
+         *  10.0.0.0        255.0.0.0        8.10
+         *  10.0.0.0        255.255.255.0    24.10.0.0
+         *  10.17.0.0       255.255.0.0      16.10.17
+         *  10.27.129.0     255.255.255.0    24.10.27.129
+         *  10.229.0.128    255.255.255.128  25.10.229.0.128
+         *  10.198.122.47   255.255.255.255  32.10.198.122.47
+         */
 
-    int commit = 0;
-    if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
-        ovs_be32 ip;
-        if (ctx->lexer->token.type == LEX_T_INTEGER
-            && ctx->lexer->token.format == LEX_F_IPV4) {
-            ip = ctx->lexer->token.value.ipv4;
-        } else {
-            action_syntax_error(ctx, "invalid ip");
-            return;
+        for (size_t i = 0; i < no_of_routes; i += 2) {
+            uint8_t plen = 32;
+            if (c[i].masked) {
+                plen = ip_count_cidr_bits(c[i].mask.ipv4);
+            }
+            ofpbuf_put(ofpacts, &plen, 1);
+            ofpbuf_put(ofpacts, &c[i].value.ipv4, plen / 8);
+            ofpbuf_put(ofpacts, &c[i + 1].value.ipv4,
+                       sizeof(ovs_be32));
         }
+    } else if (!strcmp(o->option->type, "str")) {
+        opt_header[1] = strlen(c->string);
+        ofpbuf_put(ofpacts, c->string, opt_header[1]);
+    }
+}
 
-        nat->range_af = AF_INET;
-        nat->range.addr.ipv4.min = ip;
-        if (snat) {
-            nat->flags |= NX_NAT_F_SRC;
+static void
+encode_PUT_DHCP_OPTS(const struct ovnact_put_dhcp_opts *pdo,
+                     const struct ovnact_encode_params *ep OVS_UNUSED,
+                     struct ofpbuf *ofpacts)
+{
+    struct mf_subfield dst = expr_resolve_field(&pdo->dst);
+
+    size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_PUT_DHCP_OPTS,
+                                                  true, ofpacts);
+    nx_put_header(ofpacts, dst.field->id, OFP13_VERSION, false);
+    ovs_be32 ofs = htonl(dst.ofs);
+    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
+
+    /* Encode the offerip option first, because it's a special case and needs
+     * to be first in the actual DHCP response, and then encode the rest
+     * (skipping offerip the second time around). */
+    const struct ovnact_dhcp_option *offerip_opt = find_offerip(
+        pdo->options, pdo->n_options);
+    ovs_be32 offerip = offerip_opt->value.values[0].value.ipv4;
+    ofpbuf_put(ofpacts, &offerip, sizeof offerip);
+
+    for (const struct ovnact_dhcp_option *o = pdo->options;
+         o < &pdo->options[pdo->n_options]; o++) {
+        if (o != offerip_opt) {
+            encode_put_dhcp_option(o, ofpacts);
+        }
+    }
+
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+static void
+free_PUT_DHCP_OPTS(struct ovnact_put_dhcp_opts *pdo)
+{
+    free_dhcp_options(pdo->options, pdo->n_options);
+}
+
+/* Parses an assignment or exchange or put_dhcp_opts action. */
+static void
+parse_set_action(struct action_context *ctx)
+{
+    struct expr *prereqs = NULL;
+    struct expr_field lhs;
+    char *error;
+
+    error = expr_field_parse(ctx->lexer, ctx->pp->symtab, &lhs, &ctx->prereqs);
+    if (!error) {
+        if (lexer_match(ctx->lexer, LEX_T_EXCHANGE)) {
+            parse_assignment_action(ctx, true, &lhs);
+        } else if (lexer_match(ctx->lexer, LEX_T_EQUALS)) {
+            if (ctx->lexer->token.type != LEX_T_ID) {
+                parse_LOAD(ctx, &lhs);
+            } else if (!strcmp(ctx->lexer->token.s, "put_dhcp_opts")
+                && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
+                lexer_get(ctx->lexer); /* Skip put_dhcp_opts. */
+                lexer_get(ctx->lexer); /* Skip '('. */
+                parse_PUT_DHCP_OPTS(ctx, &lhs);
+            } else {
+                parse_assignment_action(ctx, false, &lhs);
+            }
         } else {
-            nat->flags |= NX_NAT_F_DST;
+            action_syntax_error(ctx, "expecting `=' or `<->'");
         }
-        commit = NX_CT_F_COMMIT;
-        lexer_get(ctx->lexer);
-        if (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
-            action_syntax_error(ctx, "expecting `)'");
-            return;
+        if (!error) {
+            ctx->prereqs = expr_combine(EXPR_T_AND, ctx->prereqs, prereqs);
         }
     }
 
-    ctx->ofpacts->header = ofpbuf_push_uninit(ctx->ofpacts, nat_offset);
-    ct = ctx->ofpacts->header;
-    ct->flags |= commit;
-
-    /* XXX: For performance reasons, we try to prevent additional
-     * recirculations.  So far, ct_snat which is used in a gateway router
-     * does not need a recirculation. ct_snat(IP) does need a recirculation.
-     * Should we consider a method to let the actions specify whether a action
-     * needs recirculation if there more use cases?. */
-    if (!commit && snat) {
-        ct->recirc_table = NX_CT_RECIRC_NONE;
+    if (error) {
+        expr_destroy(prereqs);
+        action_error(ctx, "%s", error);
+        free(error);
     }
-    ofpact_finish(ctx->ofpacts, &ct->ofpact);
-    ofpbuf_push_uninit(ctx->ofpacts, ct_offset);
 }
 
 static bool
@@ -1094,38 +1687,33 @@ parse_action(struct action_context *ctx)
         || lookahead == LEX_T_LSQUARE) {
         parse_set_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "next")) {
-        parse_next_action(ctx);
+        parse_NEXT(ctx);
     } else if (lexer_match_id(ctx->lexer, "output")) {
-        emit_resubmit(ctx, ctx->ap->output_ptable);
+        ovnact_put_OUTPUT(ctx->ovnacts);
     } else if (lexer_match_id(ctx->lexer, "ip.ttl")) {
-        if (lexer_match(ctx->lexer, LEX_T_DECREMENT)) {
-            add_prerequisite(ctx, "ip");
-            ofpact_put_DEC_TTL(ctx->ofpacts);
-        } else {
-            action_syntax_error(ctx, "expecting `--'");
-        }
+        parse_DEC_TTL(ctx);
     } else if (lexer_match_id(ctx->lexer, "ct_next")) {
-        emit_ct(ctx, true, false, NULL, NULL, NULL, NULL);
+        parse_CT_NEXT(ctx);
     } else if (lexer_match_id(ctx->lexer, "ct_commit")) {
-        parse_ct_commit_action(ctx);
+        parse_CT_COMMIT(ctx);
     } else if (lexer_match_id(ctx->lexer, "ct_dnat")) {
-        parse_ct_nat(ctx, false);
+        parse_CT_DNAT(ctx);
     } else if (lexer_match_id(ctx->lexer, "ct_snat")) {
-        parse_ct_nat(ctx, true);
+        parse_CT_SNAT(ctx);
     } else if (lexer_match_id(ctx->lexer, "ct_lb")) {
         parse_ct_lb_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "arp")) {
-        parse_nested_action(ctx, ACTION_OPCODE_ARP, "ip4");
+        parse_ARP(ctx);
+    } else if (lexer_match_id(ctx->lexer, "nd_na")) {
+        parse_ND_NA(ctx);
     } else if (lexer_match_id(ctx->lexer, "get_arp")) {
-        parse_get_arp_action(ctx);
+        parse_get_mac_bind(ctx, 32, ovnact_put_GET_ARP(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "put_arp")) {
-        parse_put_arp_action(ctx);
-    } else if (lexer_match_id(ctx->lexer, "nd_na")) {
-        parse_nested_action(ctx, ACTION_OPCODE_ND_NA, "nd_ns");
+        parse_put_mac_bind(ctx, 32, ovnact_put_PUT_ARP(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "get_nd")) {
-        parse_get_nd_action(ctx);
+        parse_get_mac_bind(ctx, 128, ovnact_put_GET_ND(ctx->ovnacts));
     } else if (lexer_match_id(ctx->lexer, "put_nd")) {
-        parse_put_nd_action(ctx);
+        parse_put_mac_bind(ctx, 128, ovnact_put_PUT_ND(ctx->ovnacts));
     } else {
         action_syntax_error(ctx, "expecting action");
     }
@@ -1160,9 +1748,10 @@ parse_actions(struct action_context *ctx)
 
 /* Parses OVN actions, in the format described for the "actions" column in the
  * Logical_Flow table in ovn-sb(5), and appends the parsed versions of the
- * actions to 'ofpacts' as "struct ofpact"s.
+ * actions to 'ovnacts' as "struct ovnact"s.  The caller must eventually free
+ * the parsed ovnacts with ovnacts_free().
  *
- * 'ap' provides most of the parameters for translation.
+ * 'pp' provides most of the parameters for translation.
  *
  * Some actions add extra requirements (prerequisites) to the flow's match.  If
  * so, this function sets '*prereqsp' to the actions' prerequisites; otherwise,
@@ -1170,21 +1759,21 @@ parse_actions(struct action_context *ctx)
  * eventually free it.
  *
  * Returns NULL on success, otherwise a malloc()'d error message that the
- * caller must free.  On failure, 'ofpacts' has the same contents and
+ * caller must free.  On failure, 'ovnacts' has the same contents and
  * '*prereqsp' is set to NULL, but some tokens may have been consumed from
  * 'lexer'.
   */
 char * OVS_WARN_UNUSED_RESULT
-actions_parse(struct lexer *lexer, const struct action_params *ap,
-              struct ofpbuf *ofpacts, struct expr **prereqsp)
+ovnacts_parse(struct lexer *lexer, const struct ovnact_parse_params *pp,
+              struct ofpbuf *ovnacts, struct expr **prereqsp)
 {
-    size_t ofpacts_start = ofpacts->size;
+    size_t ovnacts_start = ovnacts->size;
 
     struct action_context ctx = {
-        .ap = ap,
+        .pp = pp,
         .lexer = lexer,
         .error = NULL,
-        .ofpacts = ofpacts,
+        .ovnacts = ovnacts,
         .prereqs = NULL,
     };
     parse_actions(&ctx);
@@ -1193,16 +1782,20 @@ actions_parse(struct lexer *lexer, const struct action_params *ap,
         *prereqsp = ctx.prereqs;
         return NULL;
     } else {
-        ofpacts->size = ofpacts_start;
+        ofpbuf_pull(ovnacts, ovnacts_start);
+        ovnacts_free(ovnacts->data, ovnacts->size);
+        ofpbuf_push_uninit(ovnacts, ovnacts_start);
+
+        ovnacts->size = ovnacts_start;
         expr_destroy(ctx.prereqs);
         *prereqsp = NULL;
         return ctx.error;
     }
 }
 
-/* Like actions_parse(), but the actions are taken from 's'. */
+/* Like ovnacts_parse(), but the actions are taken from 's'. */
 char * OVS_WARN_UNUSED_RESULT
-actions_parse_string(const char *s, const struct action_params *ap,
+ovnacts_parse_string(const char *s, const struct ovnact_parse_params *pp,
                      struct ofpbuf *ofpacts, struct expr **prereqsp)
 {
     struct lexer lexer;
@@ -1210,8 +1803,113 @@ actions_parse_string(const char *s, const struct action_params *ap,
 
     lexer_init(&lexer, s);
     lexer_get(&lexer);
-    error = actions_parse(&lexer, ap, ofpacts, prereqsp);
+    error = ovnacts_parse(&lexer, pp, ofpacts, prereqsp);
     lexer_destroy(&lexer);
 
     return error;
 }
+
+/* Formatting ovnacts. */
+
+static void
+ovnact_format(const struct ovnact *a, struct ds *s)
+{
+    switch (a->type) {
+#define OVNACT(ENUM, STRUCT)                                            \
+        case OVNACT_##ENUM:                                             \
+            format_##ENUM(ALIGNED_CAST(const struct STRUCT *, a), s);   \
+            break;
+        OVNACTS
+#undef OVNACT
+    default:
+        OVS_NOT_REACHED();
+    }
+}
+
+/* Appends a string representing the 'ovnacts_len' bytes of ovnacts in
+ * 'ovnacts' to 'string'. */
+void
+ovnacts_format(const struct ovnact *ovnacts, size_t ovnacts_len,
+               struct ds *string)
+{
+    if (!ovnacts_len) {
+        ds_put_cstr(string, "drop;");
+    } else {
+        const struct ovnact *a;
+
+        OVNACT_FOR_EACH (a, ovnacts, ovnacts_len) {
+            if (a != ovnacts) {
+                ds_put_char(string, ' ');
+            }
+            ovnact_format(a, string);
+        }
+    }
+}
+
+/* Encoding ovnacts to OpenFlow. */
+
+static void
+ovnact_encode(const struct ovnact *a, const struct ovnact_encode_params *ep,
+              struct ofpbuf *ofpacts)
+{
+    switch (a->type) {
+#define OVNACT(ENUM, STRUCT)                                            \
+        case OVNACT_##ENUM:                                             \
+            encode_##ENUM(ALIGNED_CAST(const struct STRUCT *, a),       \
+                          ep, ofpacts);                                 \
+            break;
+        OVNACTS
+#undef OVNACT
+    default:
+        OVS_NOT_REACHED();
+    }
+}
+
+/* Appends ofpacts to 'ofpacts' that represent the actions in the 'ovnacts_len'
+ * bytes of actions starting at 'ovnacts'. */
+void
+ovnacts_encode(const struct ovnact *ovnacts, size_t ovnacts_len,
+               const struct ovnact_encode_params *ep,
+               struct ofpbuf *ofpacts)
+{
+    if (ovnacts) {
+        const struct ovnact *a;
+
+        OVNACT_FOR_EACH (a, ovnacts, ovnacts_len) {
+            ovnact_encode(a, ep, ofpacts);
+        }
+    }
+}
+
+/* Freeing ovnacts. */
+
+static void
+ovnact_free(struct ovnact *a)
+{
+    switch (a->type) {
+#define OVNACT(ENUM, STRUCT)                                            \
+        case OVNACT_##ENUM:                                             \
+            free_##ENUM(ALIGNED_CAST(struct STRUCT *, a));              \
+            break;
+        OVNACTS
+#undef OVNACT
+    default:
+        OVS_NOT_REACHED();
+    }
+}
+
+/* Frees each of the actions in the 'ovnacts_len' bytes of actions starting at
+ * 'ovnacts'.
+ *
+ * Does not call free(ovnacts); the caller must do so if desirable. */
+void
+ovnacts_free(struct ovnact *ovnacts, size_t ovnacts_len)
+{
+    if (ovnacts) {
+        struct ovnact *a;
+
+        OVNACT_FOR_EACH (a, ovnacts, ovnacts_len) {
+            ovnact_free(a);
+        }
+    }
+}
diff --git a/ovn/lib/expr.c b/ovn/lib/expr.c
index db00d7c..25281e0 100644
--- a/ovn/lib/expr.c
+++ b/ovn/lib/expr.c
@@ -31,6 +31,11 @@
 #include "util.h"
 
 VLOG_DEFINE_THIS_MODULE(expr);
+
+static struct expr *parse_and_annotate(const char *s,
+                                       const struct shash *symtab,
+                                       struct ovs_list *nesting,
+                                       char **errorp);
 
 /* Returns the name of measurement level 'level'. */
 const char *
@@ -833,6 +838,99 @@ parse_constant_set(struct expr_context *ctx, struct expr_constant_set *cs)
     return ok;
 }
 
+/* Parses from 'lexer' a single integer or string constant compatible with the
+ * type of 'f' into 'c'.  On success, returns NULL.  On failure, returns an
+ * xmalloc()'ed error message that the caller must free and 'c' is
+ * indeterminate. */
+char * OVS_WARN_UNUSED_RESULT
+expr_constant_parse(struct lexer *lexer, const struct expr_field *f,
+                    union expr_constant *c)
+{
+    struct expr_context ctx = { .lexer = lexer };
+
+    struct expr_constant_set cs;
+    memset(&cs, 0, sizeof cs);
+    size_t allocated_values = 0;
+    if (parse_constant(&ctx, &cs, &allocated_values)
+        && type_check(&ctx, f, &cs)) {
+        *c = cs.values[0];
+        cs.n_values = 0;
+    }
+    expr_constant_set_destroy(&cs);
+
+    return ctx.error;
+}
+
+/* Appends to 's' a re-parseable representation of constant 'c' with the given
+ * 'type'. */
+void
+expr_constant_format(const union expr_constant *c,
+                     enum expr_constant_type type, struct ds *s)
+{
+    if (type == EXPR_C_STRING) {
+        json_string_escape(c->string, s);
+    } else {
+        struct lex_token token;
+        token.type = c->masked ? LEX_T_MASKED_INTEGER : LEX_T_INTEGER;
+        token.s = NULL;
+        token.format = c->format;
+        token.value = c->value;
+        if (c->masked) {
+            token.mask = c->mask;
+        }
+
+        lex_token_format(&token, s);
+    }
+}
+
+/* Frees the contents of 'c', which has the specified 'type'.
+ *
+ * Does not free(c). */
+void
+expr_constant_destroy(const union expr_constant *c,
+                      enum expr_constant_type type)
+{
+    if (c && type == EXPR_C_STRING) {
+        free(c->string);
+    }
+}
+
+/* Parses from 'lexer' a single or {}-enclosed set of at least one integer or
+ * string constants into 'cs', which the caller need not have initialized.
+ * Returns NULL on success, in which case the caller owns 'cs', otherwise a
+ * xmalloc()'ed error message owned by the caller, in which case 'cs' is
+ * indeterminate. */
+char * OVS_WARN_UNUSED_RESULT
+expr_constant_set_parse(struct lexer *lexer, struct expr_constant_set *cs)
+{
+    struct expr_context ctx = { .lexer = lexer };
+    parse_constant_set(&ctx, cs);
+    return ctx.error;
+}
+
+/* Appends to 's' a re-parseable representation of 'cs'. */
+void
+expr_constant_set_format(const struct expr_constant_set *cs, struct ds *s)
+{
+    bool curlies = cs->in_curlies || cs->n_values != 1;
+    if (curlies) {
+        ds_put_char(s, '{');
+    }
+
+    for (const union expr_constant *c = cs->values;
+         c < &cs->values[cs->n_values]; c++) {
+        if (c != cs->values) {
+            ds_put_cstr(s, ", ");
+        }
+
+        expr_constant_format(c, cs->type, s);
+    }
+
+    if (curlies) {
+        ds_put_char(s, '}');
+    }
+}
+
 void
 expr_constant_set_destroy(struct expr_constant_set *cs)
 {
@@ -1117,6 +1215,43 @@ expr_parse_string(const char *s, const struct shash *symtab,
     return expr;
 }
 
+/* Parses a field or subfield from 'lexer' into 'field', obtaining field names
+ * from 'symtab'.  Returns NULL if successful, otherwise an error message owned
+ * by the caller. */
+char * OVS_WARN_UNUSED_RESULT
+expr_field_parse(struct lexer *lexer, const struct shash *symtab,
+                 struct expr_field *field, struct expr **prereqsp)
+{
+    struct expr_context ctx = { .lexer = lexer, .symtab = symtab };
+    if (parse_field(&ctx, field) && field->symbol->predicate) {
+        ctx.error = xasprintf("Predicate symbol %s used where lvalue "
+                              "required.", field->symbol->name);
+    }
+    if (!ctx.error) {
+        const struct expr_symbol *symbol = field->symbol;
+        while (symbol) {
+            if (symbol->prereqs) {
+                struct ovs_list nesting = OVS_LIST_INITIALIZER(&nesting);
+                struct expr *e = parse_and_annotate(symbol->prereqs, symtab,
+                                                    &nesting, &ctx.error);
+                if (ctx.error) {
+                    break;
+                }
+                *prereqsp = expr_combine(EXPR_T_AND, *prereqsp, e);
+            }
+
+            if (!symbol->parent) {
+                break;
+            }
+            symbol = symbol->parent;
+        }
+    }
+    if (ctx.error) {
+        memset(field, 0, sizeof *field);
+    }
+    return ctx.error;
+}
+
 /* Appends to 's' a re-parseable representation of 'field'. */
 void
 expr_field_format(const struct expr_field *field, struct ds *s)
@@ -2717,312 +2852,48 @@ expr_is_normalized(const struct expr *expr)
 
 /* Action parsing helper. */
 
-/* Expands 'f' repeatedly as long as it has an expansion, that is, as long as
- * it is a subfield or a predicate.  Adds any prerequisites for 'f' to
- * '*prereqs'.
- *
- * If 'rw', verifies that 'f' is a read/write field.
- *
- * Returns true if successful, false if an error was encountered (in which case
- * 'ctx->error' reports the particular error). */
-static bool
-expand_symbol(struct expr_context *ctx, bool rw,
-              struct expr_field *f, struct expr **prereqsp)
-{
-    const struct expr_symbol *orig_symbol = f->symbol;
-
-    if (f->symbol->predicate) {
-        expr_error(ctx, "Predicate symbol %s used where lvalue required.",
-                   f->symbol->name);
-        return false;
-    }
-
-    for (;;) {
-        /* Accumulate prerequisites. */
-        if (f->symbol->prereqs) {
-            struct ovs_list nesting = OVS_LIST_INITIALIZER(&nesting);
-            char *error;
-            struct expr *e;
-            e = parse_and_annotate(f->symbol->prereqs, ctx->symtab, &nesting,
-                                   &error);
-            if (error) {
-                expr_error(ctx, "%s", error);
-                free(error);
-                return false;
-            }
-            *prereqsp = expr_combine(EXPR_T_AND, *prereqsp, e);
-        }
-
-        /* If there's no expansion, we're done. */
-        if (!f->symbol->parent) {
-            break;
-        }
-
-        /* Expand. */
-        f->ofs += f->symbol->parent_ofs;
-        f->symbol = f->symbol->parent;
-    }
-
-    if (rw && !f->symbol->field->writable) {
-        expr_error(ctx, "Field %s is not modifiable.", orig_symbol->name);
-        return false;
-    }
-
-    return true;
-}
-
-static void
-mf_subfield_from_expr_field(const struct expr_field *f, struct mf_subfield *sf)
-{
-    sf->field = f->symbol->field;
-    sf->ofs = f->ofs;
-    sf->n_bits = f->n_bits ? f->n_bits : f->symbol->field->n_bits;
-}
-
-static void
-init_stack_action(const struct expr_field *f, struct ofpact_stack *stack)
-{
-    mf_subfield_from_expr_field(f, &stack->subfield);
-}
-
-static char * OVS_WARN_UNUSED_RESULT
-parse_assignment(struct lexer *lexer, struct expr_field *dst,
-                 const struct shash *symtab, bool exchange,
-                 bool (*lookup_port)(const void *aux, const char *port_name,
-                                     unsigned int *portp),
-                 const void *aux, struct ofpbuf *ofpacts,
-                 struct expr **prereqsp)
-{
-    struct expr_context ctx = { .lexer = lexer, .symtab = symtab };
-    struct expr *prereqs = NULL;
-
-    /* Parse destination and do basic checking. */
-    const struct expr_symbol *orig_dst = dst->symbol;
-    if (!expand_symbol(&ctx, true, dst, &prereqs)) {
-        goto exit;
-    }
-
-    if (exchange || ctx.lexer->token.type == LEX_T_ID) {
-        struct expr_field src;
-        if (!parse_field(&ctx, &src)) {
-            goto exit;
-        }
-        const struct expr_symbol *orig_src = src.symbol;
-        if (!expand_symbol(&ctx, exchange, &src, &prereqs)) {
-            goto exit;
-        }
-
-        if ((dst->symbol->width != 0) != (src.symbol->width != 0)) {
-            if (exchange) {
-                expr_error(&ctx,
-                           "Can't exchange %s field (%s) with %s field (%s).",
-                           orig_dst->width ? "integer" : "string",
-                           orig_dst->name,
-                           orig_src->width ? "integer" : "string",
-                           orig_src->name);
-            } else {
-                expr_error(&ctx,
-                           "Can't assign %s field (%s) to %s field (%s).",
-                           orig_src->width ? "integer" : "string",
-                           orig_src->name,
-                           orig_dst->width ? "integer" : "string",
-                           orig_dst->name);
-            }
-            goto exit;
-        }
-
-        if (dst->n_bits != src.n_bits) {
-            if (exchange) {
-                expr_error(&ctx,
-                           "Can't exchange %d-bit field with %d-bit field.",
-                           dst->n_bits, src.n_bits);
-            } else {
-                expr_error(&ctx,
-                           "Can't assign %d-bit value to %d-bit destination.",
-                           src.n_bits, dst->n_bits);
-            }
-            goto exit;
-        } else if (!dst->n_bits &&
-                   dst->symbol->field->n_bits != src.symbol->field->n_bits) {
-            expr_error(&ctx, "String fields %s and %s are incompatible for "
-                       "%s.", orig_dst->name, orig_src->name,
-                       exchange ? "exchange" : "assignment");
-            goto exit;
-        }
-
-        if (exchange) {
-            init_stack_action(&src, ofpact_put_STACK_PUSH(ofpacts));
-            init_stack_action(dst, ofpact_put_STACK_PUSH(ofpacts));
-            init_stack_action(&src, ofpact_put_STACK_POP(ofpacts));
-            init_stack_action(dst, ofpact_put_STACK_POP(ofpacts));
+/* Checks that 'f' is 'n_bits' wide (where 'n_bits == 0' means that 'f' must be
+ * a string field) and, if 'rw' is true, that 'f' is modifiable.  Returns NULL
+ * if 'f' is acceptable, otherwise an malloc()'d error message that the caller
+ * must free(). */
+char * OVS_WARN_UNUSED_RESULT
+expr_type_check(const struct expr_field *f, int n_bits, bool rw)
+{
+    if (n_bits != f->n_bits) {
+        if (n_bits && f->n_bits) {
+            return xasprintf("Cannot use %d-bit field %s[%d..%d] "
+                             "where %d-bit field is required.",
+                             f->n_bits, f->symbol->name,
+                             f->ofs, f->ofs + f->n_bits - 1,
+                             n_bits);
+        } else if (n_bits) {
+            return xasprintf("Cannot use string field %s where numeric "
+                             "field is required.", f->symbol->name);
         } else {
-            struct ofpact_reg_move *move = ofpact_put_REG_MOVE(ofpacts);
-            mf_subfield_from_expr_field(&src, &move->src);
-            mf_subfield_from_expr_field(dst, &move->dst);
-        }
-    } else {
-        struct expr_constant_set cs;
-        if (!parse_constant_set(&ctx, &cs)) {
-            goto exit;
-        }
-
-        if (!type_check(&ctx, dst, &cs)) {
-            goto exit_destroy_cs;
-        }
-        if (cs.in_curlies) {
-            expr_error(&ctx, "Assignments require a single value.");
-            goto exit_destroy_cs;
+            return xasprintf("Cannot use numeric field %s where string "
+                             "field is required.", f->symbol->name);
         }
-
-        union expr_constant *c = cs.values;
-        struct ofpact_set_field *sf = ofpact_put_SET_FIELD(ofpacts);
-        sf->field = dst->symbol->field;
-        if (dst->symbol->width) {
-            mf_subvalue_shift(&c->value, dst->ofs);
-            if (!c->masked) {
-                memset(&c->mask, 0, sizeof c->mask);
-                bitwise_one(&c->mask, sizeof c->mask, dst->ofs, dst->n_bits);
-            } else {
-                mf_subvalue_shift(&c->mask, dst->ofs);
-            }
-
-            memcpy(&sf->value,
-                   &c->value.u8[sizeof c->value - sf->field->n_bytes],
-                   sf->field->n_bytes);
-            memcpy(&sf->mask,
-                   &c->mask.u8[sizeof c->mask - sf->field->n_bytes],
-                   sf->field->n_bytes);
-        } else {
-            uint32_t port;
-            if (!lookup_port(aux, c->string, &port)) {
-                port = 0;
-            }
-            bitwise_put(port, &sf->value,
-                        sf->field->n_bytes, 0, sf->field->n_bits);
-            bitwise_one(&sf->mask, sf->field->n_bytes, 0, sf->field->n_bits);
-        }
-
-    exit_destroy_cs:
-        expr_constant_set_destroy(&cs);
     }
 
-exit:
-    if (ctx.error) {
-        expr_destroy(prereqs);
-        prereqs = NULL;
+    if (rw && !f->symbol->rw) {
+        return xasprintf("Field %s is not modifiable.", f->symbol->name);
     }
-    *prereqsp = prereqs;
-    return ctx.error;
-}
-
-/* A helper for actions_parse(), to parse an OVN assignment action in the form
- * "field = value" or "field = field2" into 'ofpacts'.  The caller must have
- * already parsed and skipped the left-hand side "field =" and pass in the
- * field as 'dst'.  Other parameters and return value match those for
- * actions_parse(). */
-char * OVS_WARN_UNUSED_RESULT
-expr_parse_assignment(struct lexer *lexer, struct expr_field *dst,
-                      const struct shash *symtab,
-                      bool (*lookup_port)(const void *aux,
-                                          const char *port_name,
-                                          unsigned int *portp),
-                      const void *aux,
-                      struct ofpbuf *ofpacts, struct expr **prereqsp)
-{
-    return parse_assignment(lexer, dst, symtab, false, lookup_port, aux,
-                            ofpacts, prereqsp);
-}
-
-/* A helper for actions_parse(), to parse an OVN exchange action in the form
- * "field1 <-> field2" into 'ofpacts'.  The caller must have already parsed and
- * skipped the left-hand side "field1 <->" and pass in 'field1'.  Other
- * parameters and return value match those for actions_parse(). */
-char * OVS_WARN_UNUSED_RESULT
-expr_parse_exchange(struct lexer *lexer, struct expr_field *field,
-                    const struct shash *symtab,
-                    bool (*lookup_port)(const void *aux,
-                                        const char *port_name,
-                                        unsigned int *portp),
-                    const void *aux,
-                    struct ofpbuf *ofpacts, struct expr **prereqsp)
-{
-    return parse_assignment(lexer, field, symtab, true, lookup_port, aux,
-                            ofpacts, prereqsp);
-}
 
-/* Parses a field or subfield from 'lexer' into 'field', obtaining field names
- * from 'symtab'.  Returns NULL if successful, otherwise an error message owned
- * by the caller. */
-char * OVS_WARN_UNUSED_RESULT
-expr_parse_field(struct lexer *lexer, const struct shash *symtab,
-                 struct expr_field *field)
-{
-    struct expr_context ctx = { .lexer = lexer, .symtab = symtab };
-    if (!parse_field(&ctx, field)) {
-        memset(field, 0, sizeof *field);
-    }
-    return ctx.error;
+    return NULL;
 }
 
-/* Takes 'field', which was presumably parsed by expr_parse_field(), and
- * converts it into mf_subfield 'sf' and a set of prerequisites in '*prereqsp'.
- *
- * 'n_bits' specifies the number of bits that the field must have, and 0
- * indicates a string field; reports an error if 'field' has a different type
- * or width.  If 'rw' is true, it is an error if 'field' is read-only.  Uses
- * 'symtab 'for expanding references and 'lexer' for error reporting.
- *
- * Returns NULL if successful, otherwise an error message owned by the
- * caller. */
-char * OVS_WARN_UNUSED_RESULT
-expr_expand_field(struct lexer *lexer, const struct shash *symtab,
-                  const struct expr_field *orig_field, int n_bits, bool rw,
-                  struct mf_subfield *sf, struct expr **prereqsp)
+/* Returns the mf_subfield that corresponds to 'f'. */
+struct mf_subfield
+expr_resolve_field(const struct expr_field *f)
 {
-    struct expr_context ctx = { .lexer = lexer, .symtab = symtab };
-    struct expr *prereqs = NULL;
-
-    struct expr_field field = *orig_field;
-    if (!expand_symbol(&ctx, rw, &field, &prereqs)) {
-        goto exit;
-    }
-    ovs_assert(field.n_bits == orig_field->n_bits);
-
-    if (n_bits != field.n_bits) {
-        if (n_bits && field.n_bits) {
-            expr_error(&ctx, "Cannot use %d-bit field %s[%d..%d] "
-                       "where %d-bit field is required.",
-                       orig_field->n_bits, orig_field->symbol->name,
-                       orig_field->ofs,
-                       orig_field->ofs + orig_field->n_bits - 1, n_bits);
-        } else if (n_bits) {
-            expr_error(&ctx, "Cannot use string field %s where numeric "
-                       "field is required.",
-                       orig_field->symbol->name);
-        } else {
-            expr_error(&ctx, "Cannot use numeric field %s where string "
-                       "field is required.",
-                       orig_field->symbol->name);
-        }
-    }
+    const struct expr_symbol *symbol = f->symbol;
+    int ofs = f->ofs;
 
-exit:
-    if (!ctx.error) {
-        mf_subfield_from_expr_field(&field, sf);
-        *prereqsp = prereqs;
-    } else {
-        memset(sf, 0, sizeof *sf);
-        expr_destroy(prereqs);
-        *prereqsp = NULL;
+    while (symbol->parent) {
+        ofs += symbol->parent_ofs;
+        symbol = symbol->parent;
     }
-    return ctx.error;
-}
 
-char * OVS_WARN_UNUSED_RESULT
-expr_parse_constant_set(struct lexer *lexer, const struct shash *symtab,
-                        struct expr_constant_set *cs)
-{
-    struct expr_context ctx = { .lexer = lexer, .symtab = symtab };
-    parse_constant_set(&ctx, cs);
-    return ctx.error;
+    int n_bits = symbol->width ? f->n_bits : symbol->field->n_bits;
+    return (struct mf_subfield) { symbol->field, ofs, n_bits };
 }
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 795be68..a190c6b 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1206,13 +1206,13 @@
         </dd>
 
         <dt>
-          <code><var>R</var> = put_dhcp_opts(<code>offerip</code> = <var>IP</var>, <var>D1</var> = <var>V1</var>, <var>D2</var> = <var>V2</var>, ..., <var>Dn</var> = <var>Vn</var>);</code>
+          <code><var>R</var> = put_dhcp_opts(<var>D1</var> = <var>V1</var>, <var>D2</var> = <var>V2</var>, ..., <var>Dn</var> = <var>Vn</var>);</code>
         </dt>
 
         <dd>
           <p>
-            <b>Parameters</b>: one or more DHCP option/value pairs, the first
-            of which must set a value for the offered IP, <code>offerip</code>.
+            <b>Parameters</b>: one or more DHCP option/value pairs, which must
+            include an <code>offerip</code> option (with code 0).
           </p>
 
           <p>
diff --git a/tests/ovn.at b/tests/ovn.at
index a95e0b2..163bd93 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -577,189 +577,364 @@ dl_src=ba:be:be:ef:de:ad
 AT_CLEANUP
 
 AT_SETUP([ovn -- action parsing])
-dnl Text before => is input, text after => is expected output.
-AT_DATA([test-cases.txt], [[
-# drop
-drop; => actions=drop, prereqs=1
-drop; next; => Syntax error at `next' expecting end of input.
-next; drop; => Syntax error at `drop' expecting action.
+dnl Unindented text is input (a set of OVN logical actions).
+dnl Indented text is expected output.
+AT_DATA([test-cases.txt],
+[[# drop
+drop;
+    encodes as drop
+drop; next;
+    Syntax error at `next' expecting end of input.
+next; drop;
+    Syntax error at `drop' expecting action.
 
 # output
-output; => actions=resubmit(,64), prereqs=1
+output;
+    encodes as resubmit(,64)
 
 # next
-next; => actions=resubmit(,27), prereqs=1
-next(0); => actions=resubmit(,16), prereqs=1
-next(15); => actions=resubmit(,31), prereqs=1
-
-next(); => Syntax error at `)' expecting small integer.
-next(10; => Syntax error at `;' expecting `)'.
-next(16); => "next" argument must be in range 0 to 15.
+next;
+    formats as next(11);
+    encodes as resubmit(,27)
+next(11);
+    encodes as resubmit(,27)
+next(0);
+    encodes as resubmit(,16)
+next(15);
+    encodes as resubmit(,31)
+
+next();
+    Syntax error at `)' expecting small integer.
+next(10;
+    Syntax error at `;' expecting `)'.
+next(16);
+    "next" argument must be in range 0 to 15.
 
 # Loading a constant value.
-tcp.dst=80; => actions=set_field:80->tcp_dst, prereqs=ip.proto == 0x6 && (eth.type == 0x800 || eth.type == 0x86dd)
-eth.dst[40] = 1; => actions=set_field:01:00:00:00:00:00/01:00:00:00:00:00->eth_dst, prereqs=1
-vlan.pcp = 2; => actions=set_field:0x4000/0xe000->vlan_tci, prereqs=vlan.tci[12]
-vlan.tci[13..15] = 2; => actions=set_field:0x4000/0xe000->vlan_tci, prereqs=1
-inport = ""; => actions=set_field:0->reg14, prereqs=1
-ip.ttl = 4; => actions=set_field:4->nw_ttl, prereqs=eth.type == 0x800 || eth.type == 0x86dd
-outport="eth0"; next; outport="LOCAL"; next; => actions=set_field:0x5->reg15,resubmit(,27),set_field:0xfffe->reg15,resubmit(,27), prereqs=1
-
-inport[1] = 1; => Cannot select subfield of string field inport.
-ip.proto[1] = 1; => Cannot select subfield of nominal field ip.proto.
-eth.dst[40] == 1; => Syntax error at `==' expecting `=' or `<->'.
-ip = 1; => Predicate symbol ip used where lvalue required.
-ip.proto = 6; => Field ip.proto is not modifiable.
-inport = {"a", "b"}; => Assignments require a single value.
-inport = {}; => Syntax error at `}' expecting constant.
-bad_prereq = 123; => Error parsing expression `xyzzy' encountered as prerequisite or predicate of initial expression: Syntax error at `xyzzy' expecting field name.
-self_recurse = 123; => Error parsing expression `self_recurse != 0' encountered as prerequisite or predicate of initial expression: Error parsing expression `self_recurse != 0' encountered as prerequisite or predicate of initial expression: Recursive expansion of symbol `self_recurse'.
-vlan.present = 0; => Predicate symbol vlan.present used where lvalue required.
+tcp.dst=80;
+    formats as tcp.dst = 80;
+    encodes as set_field:80->tcp_dst
+    has prereqs ip.proto == 0x6 && (eth.type == 0x800 || eth.type == 0x86dd)
+eth.dst[40] = 1;
+    encodes as set_field:01:00:00:00:00:00/01:00:00:00:00:00->eth_dst
+vlan.pcp = 2;
+    encodes as set_field:0x4000/0xe000->vlan_tci
+    has prereqs vlan.tci[12]
+vlan.tci[13..15] = 2;
+    encodes as set_field:0x4000/0xe000->vlan_tci
+inport = "";
+    encodes as set_field:0->reg14
+ip.ttl=4;
+    formats as ip.ttl = 4;
+    encodes as set_field:4->nw_ttl
+    has prereqs eth.type == 0x800 || eth.type == 0x86dd
+outport="eth0"; next; outport="LOCAL"; next;
+    formats as outport = "eth0"; next(11); outport = "LOCAL"; next(11);
+    encodes as set_field:0x5->reg15,resubmit(,27),set_field:0xfffe->reg15,resubmit(,27)
+
+inport[1] = 1;
+    Cannot select subfield of string field inport.
+ip.proto[1] = 1;
+    Cannot select subfield of nominal field ip.proto.
+eth.dst[40] == 1;
+    Syntax error at `==' expecting `=' or `<->'.
+ip = 1;
+    Predicate symbol ip used where lvalue required.
+ip.proto = 6;
+    Field ip.proto is not modifiable.
+inport = {"a", "b"};
+    Syntax error at `{' expecting constant.
+inport = {};
+    Syntax error at `{' expecting constant.
+bad_prereq = 123;
+    Error parsing expression `xyzzy' encountered as prerequisite or predicate of initial expression: Syntax error at `xyzzy' expecting field name.
+self_recurse = 123;
+    Error parsing expression `self_recurse != 0' encountered as prerequisite or predicate of initial expression: Error parsing expression `self_recurse != 0' encountered as prerequisite or predicate of initial expression: Recursive expansion of symbol `self_recurse'.
+vlan.present = 0;
+    Predicate symbol vlan.present used where lvalue required.
 
 # Moving one field into another.
-reg0 = reg1; => actions=move:NXM_NX_XXREG0[64..95]->NXM_NX_XXREG0[96..127], prereqs=1
-vlan.pcp = reg0[0..2]; => actions=move:NXM_NX_XXREG0[96..98]->NXM_OF_VLAN_TCI[13..15], prereqs=vlan.tci[12]
-reg0[10] = vlan.pcp[1]; => actions=move:NXM_OF_VLAN_TCI[14]->NXM_NX_XXREG0[106], prereqs=vlan.tci[12]
-outport = inport; => actions=move:NXM_NX_REG14[]->NXM_NX_REG15[], prereqs=1
-
-reg0[0] = vlan.present; => Predicate symbol vlan.present used where lvalue required.
-reg0 = reg1[0..10]; => Can't assign 11-bit value to 32-bit destination.
-inport = reg0; => Can't assign integer field (reg0) to string field (inport).
-inport = big_string; => String fields inport and big_string are incompatible for assignment.
-ip.proto = reg0[0..7]; => Field ip.proto is not modifiable.
+reg0=reg1;
+    formats as reg0 = reg1;
+    encodes as move:NXM_NX_XXREG0[64..95]->NXM_NX_XXREG0[96..127]
+vlan.pcp = reg0[0..2];
+    encodes as move:NXM_NX_XXREG0[96..98]->NXM_OF_VLAN_TCI[13..15]
+    has prereqs vlan.tci[12]
+reg0[10] = vlan.pcp[1];
+    encodes as move:NXM_OF_VLAN_TCI[14]->NXM_NX_XXREG0[106]
+    has prereqs vlan.tci[12]
+outport = inport;
+    encodes as move:NXM_NX_REG14[]->NXM_NX_REG15[]
+
+reg0[0] = vlan.present;
+    Predicate symbol vlan.present used where lvalue required.
+reg0 = reg1[0..10];
+    Can't assign 11-bit value to 32-bit destination.
+inport = reg0;
+    Can't assign integer field (reg0) to string field (inport).
+inport = big_string;
+    String fields inport and big_string are incompatible for assignment.
+ip.proto = reg0[0..7];
+    Field ip.proto is not modifiable.
 
 # Exchanging fields.
-reg0 <-> reg1; => actions=push:NXM_NX_XXREG0[64..95],push:NXM_NX_XXREG0[96..127],pop:NXM_NX_XXREG0[64..95],pop:NXM_NX_XXREG0[96..127], prereqs=1
-vlan.pcp <-> reg0[0..2]; => actions=push:NXM_NX_XXREG0[96..98],push:NXM_OF_VLAN_TCI[13..15],pop:NXM_NX_XXREG0[96..98],pop:NXM_OF_VLAN_TCI[13..15], prereqs=vlan.tci[12]
-reg0[10] <-> vlan.pcp[1]; => actions=push:NXM_OF_VLAN_TCI[14],push:NXM_NX_XXREG0[106],pop:NXM_OF_VLAN_TCI[14],pop:NXM_NX_XXREG0[106], prereqs=vlan.tci[12]
-outport <-> inport; => actions=push:NXM_NX_REG14[],push:NXM_NX_REG15[],pop:NXM_NX_REG14[],pop:NXM_NX_REG15[], prereqs=1
-
-reg0[0] <-> vlan.present; => Predicate symbol vlan.present used where lvalue required.
-reg0 <-> reg1[0..10]; => Can't exchange 32-bit field with 11-bit field.
-inport <-> reg0; => Can't exchange string field (inport) with integer field (reg0).
-inport <-> big_string; => String fields inport and big_string are incompatible for exchange.
-ip.proto <-> reg0[0..7]; => Field ip.proto is not modifiable.
-reg0[0..7] <-> ip.proto; => Field ip.proto is not modifiable.
+reg0 <-> reg1;
+    encodes as push:NXM_NX_XXREG0[64..95],push:NXM_NX_XXREG0[96..127],pop:NXM_NX_XXREG0[64..95],pop:NXM_NX_XXREG0[96..127]
+vlan.pcp <-> reg0[0..2];
+    encodes as push:NXM_NX_XXREG0[96..98],push:NXM_OF_VLAN_TCI[13..15],pop:NXM_NX_XXREG0[96..98],pop:NXM_OF_VLAN_TCI[13..15]
+    has prereqs vlan.tci[12]
+reg0[10] <-> vlan.pcp[1];
+    encodes as push:NXM_OF_VLAN_TCI[14],push:NXM_NX_XXREG0[106],pop:NXM_OF_VLAN_TCI[14],pop:NXM_NX_XXREG0[106]
+    has prereqs vlan.tci[12]
+outport <-> inport;
+    encodes as push:NXM_NX_REG14[],push:NXM_NX_REG15[],pop:NXM_NX_REG14[],pop:NXM_NX_REG15[]
+
+reg0[0] <-> vlan.present;
+    Predicate symbol vlan.present used where lvalue required.
+reg0 <-> reg1[0..10];
+    Can't exchange 32-bit field with 11-bit field.
+inport <-> reg0;
+    Can't exchange string field (inport) with integer field (reg0).
+inport <-> big_string;
+    String fields inport and big_string are incompatible for exchange.
+ip.proto <-> reg0[0..7];
+    Field ip.proto is not modifiable.
+reg0[0..7] <-> ip.proto;
+    Field ip.proto is not modifiable.
 
 # TTL decrement.
-ip.ttl--; => actions=dec_ttl, prereqs=ip
-ip.ttl => Syntax error at end of input expecting `--'.
+ip.ttl--;
+    encodes as dec_ttl
+    has prereqs ip
+ip.ttl
+    Syntax error at end of input expecting `--'.
 
 # load balancing.
-ct_lb; => actions=ct(table=27,zone=NXM_NX_REG13[0..15],nat), prereqs=ip
-ct_lb(); => Syntax error at `)' expecting IPv4 address.
-ct_lb(192.168.1.2:80, 192.168.1.3:80); => actions=group:1, prereqs=ip
-ct_lb(192.168.1.2, 192.168.1.3, ); => actions=group:2, prereqs=ip
-ct_lb(192.168.1.2:); => Syntax error at `)' expecting port number.
-ct_lb(192.168.1.2:123456); => Syntax error at `123456' expecting port number.
-ct_lb(foo); => Syntax error at `foo' expecting IPv4 address.
-
-# conntrack
-ct_next; => actions=ct(table=27,zone=NXM_NX_REG13[0..15]), prereqs=ip
-ct_commit; => actions=ct(commit,zone=NXM_NX_REG13[0..15]), prereqs=ip
-ct_commit(); => actions=ct(commit,zone=NXM_NX_REG13[0..15]), prereqs=ip
-ct_commit(ct_mark=1); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1->ct_mark)), prereqs=ip
-ct_commit(ct_mark=1/1); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1/0x1->ct_mark)), prereqs=ip
-ct_commit(ct_label=1); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1->ct_label)), prereqs=ip
-ct_commit(ct_label=1/1); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1/0x1->ct_label)), prereqs=ip
-ct_commit(ct_label=0x01020304050607080910111213141516); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1020304050607080910111213141516->ct_label)), prereqs=ip
-ct_commit(ct_label=0x181716151413121110090807060504030201); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x16151413121110090807060504030201->ct_label)), prereqs=ip
-ct_commit(ct_label=0x01000000000000000000000000000000/0x01000000000000000000000000000000); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1000000000000000000000000000000/0x1000000000000000000000000000000->ct_label)), prereqs=ip
-ct_commit(ct_label=18446744073709551615); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0xffffffffffffffff->ct_label)), prereqs=ip
-ct_commit(ct_label=18446744073709551616); => Decimal constants must be less than 2**64.
-ct_commit(ct_mark=1, ct_label=2); => actions=ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1->ct_mark,set_field:0x2->ct_label)), prereqs=ip
-
-# dnat
-ct_dnat; => actions=ct(table=27,zone=NXM_NX_REG11[0..15],nat), prereqs=ip
-ct_dnat(192.168.1.2); => actions=ct(commit,table=27,zone=NXM_NX_REG11[0..15],nat(dst=192.168.1.2)), prereqs=ip
-ct_dnat(192.168.1.2, 192.168.1.3); => Syntax error at `,' expecting `)'.
-ct_dnat(foo); => Syntax error at `foo' invalid ip.
-ct_dnat(foo, bar); => Syntax error at `foo' invalid ip.
-ct_dnat(); => Syntax error at `)' invalid ip.
-
-# snat
-ct_snat; => actions=ct(zone=NXM_NX_REG12[0..15],nat), prereqs=ip
-ct_snat(192.168.1.2); => actions=ct(commit,table=27,zone=NXM_NX_REG12[0..15],nat(src=192.168.1.2)), prereqs=ip
-ct_snat(192.168.1.2, 192.168.1.3); => Syntax error at `,' expecting `)'.
-ct_snat(foo); => Syntax error at `foo' invalid ip.
-ct_snat(foo, bar); => Syntax error at `foo' invalid ip.
-ct_snat(); => Syntax error at `)' invalid ip.
-
+ct_lb;
+    encodes as ct(table=27,zone=NXM_NX_REG13[0..15],nat)
+    has prereqs ip
+ct_lb();
+    formats as ct_lb;
+    encodes as ct(table=27,zone=NXM_NX_REG13[0..15],nat)
+    has prereqs ip
+ct_lb(192.168.1.2:80, 192.168.1.3:80);
+    encodes as group:1
+    has prereqs ip
+ct_lb(192.168.1.2, 192.168.1.3, );
+    formats as ct_lb(192.168.1.2, 192.168.1.3);
+    encodes as group:2
+    has prereqs ip
+
+ct_lb(192.168.1.2:);
+    Syntax error at `)' expecting port number.
+ct_lb(192.168.1.2:123456);
+    Syntax error at `123456' expecting port number.
+ct_lb(foo);
+    Syntax error at `foo' expecting IPv4 address.
+
+# ct_next
+ct_next;
+    encodes as ct(table=27,zone=NXM_NX_REG13[0..15])
+    has prereqs ip
+
+# ct_commit
+ct_commit;
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15])
+    has prereqs ip
+ct_commit();
+    formats as ct_commit;
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15])
+    has prereqs ip
+ct_commit(ct_mark=1);
+    formats as ct_commit(ct_mark=0x1);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1->ct_mark))
+    has prereqs ip
+ct_commit(ct_mark=1/1);
+    formats as ct_commit(ct_mark=0x1/0x1);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1/0x1->ct_mark))
+    has prereqs ip
+ct_commit(ct_label=1);
+    formats as ct_commit(ct_label=0x1);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1->ct_label))
+    has prereqs ip
+ct_commit(ct_label=1/1);
+    formats as ct_commit(ct_label=0x1/0x1);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1/0x1->ct_label))
+    has prereqs ip
+ct_commit(ct_mark=1, ct_label=2);
+    formats as ct_commit(ct_mark=0x1, ct_label=0x2);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1->ct_mark,set_field:0x2->ct_label))
+    has prereqs ip
+
+ct_commit(ct_label=0x01020304050607080910111213141516);
+    formats as ct_commit(ct_label=0x1020304050607080910111213141516);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1020304050607080910111213141516->ct_label))
+    has prereqs ip
+ct_commit(ct_label=0x181716151413121110090807060504030201);
+    formats as ct_commit(ct_label=0x16151413121110090807060504030201);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x16151413121110090807060504030201->ct_label))
+    has prereqs ip
+ct_commit(ct_label=0x1000000000000000000000000000000/0x1000000000000000000000000000000);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0x1000000000000000000000000000000/0x1000000000000000000000000000000->ct_label))
+    has prereqs ip
+ct_commit(ct_label=18446744073709551615);
+    formats as ct_commit(ct_label=0xffffffffffffffff);
+    encodes as ct(commit,zone=NXM_NX_REG13[0..15],exec(set_field:0xffffffffffffffff->ct_label))
+    has prereqs ip
+ct_commit(ct_label=18446744073709551616);
+    Decimal constants must be less than 2**64.
+
+# ct_dnat
+ct_dnat;
+    encodes as ct(table=27,zone=NXM_NX_REG11[0..15],nat)
+    has prereqs ip
+ct_dnat(192.168.1.2);
+    encodes as ct(commit,table=27,zone=NXM_NX_REG11[0..15],nat(dst=192.168.1.2))
+    has prereqs ip
+
+ct_dnat(192.168.1.2, 192.168.1.3);
+    Syntax error at `,' expecting `)'.
+ct_dnat(foo);
+    Syntax error at `foo' expecting IPv4 address.
+ct_dnat(foo, bar);
+    Syntax error at `foo' expecting IPv4 address.
+ct_dnat();
+    Syntax error at `)' expecting IPv4 address.
+
+# ct_snat
+ct_snat;
+    encodes as ct(zone=NXM_NX_REG12[0..15],nat)
+    has prereqs ip
+ct_snat(192.168.1.2);
+    encodes as ct(commit,table=27,zone=NXM_NX_REG12[0..15],nat(src=192.168.1.2))
+    has prereqs ip
+
+ct_snat(192.168.1.2, 192.168.1.3);
+    Syntax error at `,' expecting `)'.
+ct_snat(foo);
+    Syntax error at `foo' expecting IPv4 address.
+ct_snat(foo, bar);
+    Syntax error at `foo' expecting IPv4 address.
+ct_snat();
+    Syntax error at `)' expecting IPv4 address.
 
 # arp
-arp { eth.dst = ff:ff:ff:ff:ff:ff; output; }; => actions=controller(userdata=00.00.00.00.00.00.00.00.00.19.00.10.80.00.06.06.ff.ff.ff.ff.ff.ff.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00), prereqs=ip4
+arp { eth.dst = ff:ff:ff:ff:ff:ff; output; };
+    encodes as controller(userdata=00.00.00.00.00.00.00.00.00.19.00.10.80.00.06.06.ff.ff.ff.ff.ff.ff.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00)
+    has prereqs ip4
 
 # get_arp
-get_arp(outport, ip4.dst); => actions=push:NXM_NX_REG0[],push:NXM_OF_IP_DST[],pop:NXM_NX_REG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_REG0[], prereqs=eth.type == 0x800
-get_arp(inport, reg0); => actions=push:NXM_NX_REG15[],push:NXM_NX_REG0[],push:NXM_NX_XXREG0[96..127],push:NXM_NX_REG14[],pop:NXM_NX_REG15[],pop:NXM_NX_REG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_REG0[],pop:NXM_NX_REG15[], prereqs=1
-get_arp; => Syntax error at `;' expecting `('.
-get_arp(); => Syntax error at `)' expecting field name.
-get_arp(inport); => Syntax error at `)' expecting `,'.
-get_arp(inport ip4.dst); => Syntax error at `ip4.dst' expecting `,'.
-get_arp(inport, ip4.dst; => Syntax error at `;' expecting `)'.
-get_arp(inport, eth.dst); => Cannot use 48-bit field eth.dst[0..47] where 32-bit field is required.
-get_arp(inport, outport); => Cannot use string field outport where numeric field is required.
-get_arp(reg0, ip4.dst); => Cannot use numeric field reg0 where string field is required.
+get_arp(outport, ip4.dst);
+    encodes as push:NXM_NX_REG0[],push:NXM_OF_IP_DST[],pop:NXM_NX_REG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_REG0[]
+    has prereqs eth.type == 0x800
+get_arp(inport, reg0);
+    encodes as push:NXM_NX_REG15[],push:NXM_NX_REG0[],push:NXM_NX_XXREG0[96..127],push:NXM_NX_REG14[],pop:NXM_NX_REG15[],pop:NXM_NX_REG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_REG0[],pop:NXM_NX_REG15[]
+
+get_arp;
+    Syntax error at `;' expecting `('.
+get_arp();
+    Syntax error at `)' expecting field name.
+get_arp(inport);
+    Syntax error at `)' expecting `,'.
+get_arp(inport ip4.dst);
+    Syntax error at `ip4.dst' expecting `,'.
+get_arp(inport, ip4.dst;
+    Syntax error at `;' expecting `)'.
+get_arp(inport, eth.dst);
+    Cannot use 48-bit field eth.dst[0..47] where 32-bit field is required.
+get_arp(inport, outport);
+    Cannot use string field outport where numeric field is required.
+get_arp(reg0, ip4.dst);
+    Cannot use numeric field reg0 where string field is required.
 
 # put_arp
-put_arp(inport, arp.spa, arp.sha); => actions=push:NXM_NX_REG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ARP_SHA[],push:NXM_OF_ARP_SPA[],pop:NXM_NX_REG0[],pop:NXM_OF_ETH_SRC[],controller(userdata=00.00.00.01.00.00.00.00),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_REG0[], prereqs=eth.type == 0x806 && eth.type == 0x806
+put_arp(inport, arp.spa, arp.sha);
+    encodes as push:NXM_NX_REG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ARP_SHA[],push:NXM_OF_ARP_SPA[],pop:NXM_NX_REG0[],pop:NXM_OF_ETH_SRC[],controller(userdata=00.00.00.01.00.00.00.00),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_REG0[]
+    has prereqs eth.type == 0x806 && eth.type == 0x806
 
 # put_dhcp_opts
-reg1[0] = put_dhcp_opts(offerip = 1.2.3.4, router = 10.0.0.1); => actions=controller(userdata=00.00.00.02.00.00.00.00.00.01.de.10.00.00.00.40.01.02.03.04.03.04.0a.00.00.01,pause), prereqs=1
-reg2[5] = put_dhcp_opts(offerip=10.0.0.4,router=10.0.0.1,netmask=255.255.254.0,mtu=1400,domain="ovn.org"); => actions=controller(userdata=00.00.00.02.00.00.00.00.00.01.de.10.00.00.00.25.0a.00.00.04.03.04.0a.00.00.01.01.04.ff.ff.fe.00.1a.02.05.78.0f.07.6f.76.6e.2e.6f.72.67,pause), prereqs=1
-# offerip=10.0.0.4             --> 0a.00.00.04
-# router=10.0.0.1              --> 03.04.0a.00.00.01
-# netmask=255.255.255.0        --> 01.04.ff.ff.ff.00
-# mtu=1400                     --> 1a.02.05.78
-# ip_forward_enable-1          --> 13.01.01
-# default_ttl=121              --> 17.01.79
-# dns_server={8.8.8.8,7.7.7.7} --> 06.08.08.08.08.08.07.07.07.07
-# classless_static_route=      --> 79.14
-# {30.0.0.0/24,10.0.0.4        --> 18.1e.00.00.0a.00.00.04
-#  40.0.0.0/16,10.0.0.6        --> 10.28.00.0a.00.00.06
-#  0.0.0.0/0,10.0.0.1}         --> 00.0a.00.00.01
-# ethernet_encap=1             --> 24.01.01
-# router_discovery=0           --> 1f.01.00
-reg0[15] = put_dhcp_opts(offerip=10.0.0.4,router=10.0.0.1,netmask=255.255.255.0,mtu=1400,ip_forward_enable=1,default_ttl=121,dns_server={8.8.8.8,7.7.7.7},classless_static_route={30.0.0.0/24,10.0.0.4,40.0.0.0/16,10.0.0.6,0.0.0.0/0,10.0.0.1},ethernet_encap=1,router_discovery=0); => actions=controller(userdata=00.00.00.02.00.00.00.00.00.01.de.10.00.00.00.6f.0a.00.00.04.03.04.0a.00.00.01.01.04.ff.ff.ff.00.1a.02.05.78.13.01.01.17.01.79.06.08.08.08.08.08.07.07.07.07.79.14.18.1e.00.00.0a.00.00.04.10.28.00.0a.00.00.06.00.0a.00.00.01.24.01.01.1f.01.00,pause), prereqs=1
-reg1[0..1] = put_dhcp_opts(offerip = 1.2.3.4, router = 10.0.0.1); => Cannot use 2-bit field reg1[0..1] where 1-bit field is required.
-reg1[0] = put_dhcp_opts(); => Syntax error at `)'.
-reg1[0] = put_dhcp_opts(x = 1.2.3.4, router = 10.0.0.1); => Syntax error at `x' expecting offerip option.
-reg1[0] = put_dhcp_opts(offerip=1.2.3.4, "hi"); => Syntax error at `"hi"'.
-reg1[0] = put_dhcp_opts(offerip=1.2.3.4, xyzzy); => Syntax error at `xyzzy' expecting DHCP option name.
-reg1[0] = put_dhcp_opts(offerip="xyzzy"); => DHCP option offerip requires numeric value.
-reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4); => DHCP option domain requires string value.
+reg1[0] = put_dhcp_opts(offerip = 1.2.3.4, router = 10.0.0.1);
+    encodes as controller(userdata=00.00.00.02.00.00.00.00.00.01.de.10.00.00.00.40.01.02.03.04.03.04.0a.00.00.01,pause)
+reg2[5] = put_dhcp_opts(offerip=10.0.0.4,router=10.0.0.1,netmask=255.255.254.0,mtu=1400,domain="ovn.org");
+    formats as reg2[5] = put_dhcp_opts(offerip = 10.0.0.4, router = 10.0.0.1, netmask = 255.255.254.0, mtu = 1400, domain = "ovn.org");
+    encodes as controller(userdata=00.00.00.02.00.00.00.00.00.01.de.10.00.00.00.25.0a.00.00.04.03.04.0a.00.00.01.01.04.ff.ff.fe.00.1a.02.05.78.0f.07.6f.76.6e.2e.6f.72.67,pause)
+reg0[15] = put_dhcp_opts(offerip=10.0.0.4,router=10.0.0.1,netmask=255.255.255.0,mtu=1400,ip_forward_enable=1,default_ttl=121,dns_server={8.8.8.8,7.7.7.7},classless_static_route={30.0.0.0/24,10.0.0.4,40.0.0.0/16,10.0.0.6,0.0.0.0/0,10.0.0.1},ethernet_encap=1,router_discovery=0);
+    formats as reg0[15] = put_dhcp_opts(offerip = 10.0.0.4, router = 10.0.0.1, netmask = 255.255.255.0, mtu = 1400, ip_forward_enable = 1, default_ttl = 121, dns_server = {8.8.8.8, 7.7.7.7}, classless_static_route = {30.0.0.0/24, 10.0.0.4, 40.0.0.0/16, 10.0.0.6, 0.0.0.0/0, 10.0.0.1}, ethernet_encap = 1, router_discovery = 0);
+    encodes as controller(userdata=00.00.00.02.00.00.00.00.00.01.de.10.00.00.00.6f.0a.00.00.04.03.04.0a.00.00.01.01.04.ff.ff.ff.00.1a.02.05.78.13.01.01.17.01.79.06.08.08.08.08.08.07.07.07.07.79.14.18.1e.00.00.0a.00.00.04.10.28.00.0a.00.00.06.00.0a.00.00.01.24.01.01.1f.01.00,pause)
+
+reg1[0..1] = put_dhcp_opts(offerip = 1.2.3.4, router = 10.0.0.1);
+    Cannot use 2-bit field reg1[0..1] where 1-bit field is required.
+reg1[0] = put_dhcp_opts();
+    Syntax error at `)'.
+reg1[0] = put_dhcp_opts(x = 1.2.3.4, router = 10.0.0.1);
+    Syntax error at `x' expecting DHCP option name.
+reg1[0] = put_dhcp_opts(router = 10.0.0.1);
+    put_dhcp_opts requires offerip to be specified.
+reg1[0] = put_dhcp_opts(offerip=1.2.3.4, "hi");
+    Syntax error at `"hi"'.
+reg1[0] = put_dhcp_opts(offerip=1.2.3.4, xyzzy);
+    Syntax error at `xyzzy' expecting DHCP option name.
+reg1[0] = put_dhcp_opts(offerip="xyzzy");
+    DHCP option offerip requires numeric value.
+reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain=1.2.3.4);
+    DHCP option domain requires string value.
 
 # nd_na
-nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport = inport; inport = ""; /* Allow sending out inport. */ output; }; => actions=controller(userdata=00.00.00.03.00.00.00.00.00.19.00.10.80.00.08.06.12.34.56.78.9a.bc.00.00.00.19.00.10.80.00.42.06.12.34.56.78.9a.bc.00.00.ff.ff.00.18.00.00.23.20.00.06.00.20.00.00.00.00.00.01.1c.04.00.01.1e.04.00.19.00.10.00.01.1c.04.00.00.00.00.00.00.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00), prereqs=nd_ns
+nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport = inport; inport = ""; /* Allow sending out inport. */ output; };
+    formats as nd_na { eth.src = 12:34:56:78:9a:bc; nd.tll = 12:34:56:78:9a:bc; outport = inport; inport = ""; output; };
+    encodes as controller(userdata=00.00.00.03.00.00.00.00.00.19.00.10.80.00.08.06.12.34.56.78.9a.bc.00.00.00.19.00.10.80.00.42.06.12.34.56.78.9a.bc.00.00.ff.ff.00.18.00.00.23.20.00.06.00.20.00.00.00.00.00.01.1c.04.00.01.1e.04.00.19.00.10.00.01.1c.04.00.00.00.00.00.00.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00)
+    has prereqs nd_ns
 
 # get_nd
-get_nd(outport, ip6.dst); => actions=push:NXM_NX_XXREG0[],push:NXM_NX_IPV6_DST[],pop:NXM_NX_XXREG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_XXREG0[], prereqs=eth.type == 0x86dd
-get_nd(inport, xxreg0); => actions=push:NXM_NX_REG15[],push:NXM_NX_REG14[],pop:NXM_NX_REG15[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_REG15[], prereqs=1
-get_nd; => Syntax error at `;' expecting `('.
-get_nd(); => Syntax error at `)' expecting field name.
-get_nd(inport); => Syntax error at `)' expecting `,'.
-get_nd(inport ip6.dst); => Syntax error at `ip6.dst' expecting `,'.
-get_nd(inport, ip6.dst; => Syntax error at `;' expecting `)'.
-get_nd(inport, eth.dst); => Cannot use 48-bit field eth.dst[0..47] where 128-bit field is required.
-get_nd(inport, outport); => Cannot use string field outport where numeric field is required.
-get_nd(xxreg0, ip6.dst); => Cannot use numeric field xxreg0 where string field is required.
+get_nd(outport, ip6.dst);
+    encodes as push:NXM_NX_XXREG0[],push:NXM_NX_IPV6_DST[],pop:NXM_NX_XXREG0[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_XXREG0[]
+    has prereqs eth.type == 0x86dd
+get_nd(inport, xxreg0);
+    encodes as push:NXM_NX_REG15[],push:NXM_NX_REG14[],pop:NXM_NX_REG15[],set_field:00:00:00:00:00:00->eth_dst,resubmit(,65),pop:NXM_NX_REG15[]
+get_nd;
+    Syntax error at `;' expecting `('.
+get_nd();
+    Syntax error at `)' expecting field name.
+get_nd(inport);
+    Syntax error at `)' expecting `,'.
+get_nd(inport ip6.dst);
+    Syntax error at `ip6.dst' expecting `,'.
+get_nd(inport, ip6.dst;
+    Syntax error at `;' expecting `)'.
+get_nd(inport, eth.dst);
+    Cannot use 48-bit field eth.dst[0..47] where 128-bit field is required.
+get_nd(inport, outport);
+    Cannot use string field outport where numeric field is required.
+get_nd(xxreg0, ip6.dst);
+    Cannot use numeric field xxreg0 where string field is required.
 
 # put_nd
-put_nd(inport, nd.target, nd.sll); => actions=push:NXM_NX_XXREG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ND_SLL[],push:NXM_NX_ND_TARGET[],pop:NXM_NX_XXREG0[],pop:NXM_OF_ETH_SRC[],controller(userdata=00.00.00.04.00.00.00.00),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_XXREG0[], prereqs=((icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)) || (icmp6.type == 0x88 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd))) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd)
+put_nd(inport, nd.target, nd.sll);
+    encodes as push:NXM_NX_XXREG0[],push:NXM_OF_ETH_SRC[],push:NXM_NX_ND_SLL[],push:NXM_NX_ND_TARGET[],pop:NXM_NX_XXREG0[],pop:NXM_OF_ETH_SRC[],controller(userdata=00.00.00.04.00.00.00.00),pop:NXM_OF_ETH_SRC[],pop:NXM_NX_XXREG0[]
+    has prereqs ((icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd)) || (icmp6.type == 0x88 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd))) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.type == 0x87 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 0x86dd)
 
 # Contradictionary prerequisites (allowed but not useful):
-ip4.src = ip6.src[0..31]; => actions=move:NXM_NX_IPV6_SRC[0..31]->NXM_OF_IP_SRC[], prereqs=eth.type == 0x800 && eth.type == 0x86dd
-ip4.src <-> ip6.src[0..31]; => actions=push:NXM_NX_IPV6_SRC[0..31],push:NXM_OF_IP_SRC[],pop:NXM_NX_IPV6_SRC[0..31],pop:NXM_OF_IP_SRC[], prereqs=eth.type == 0x800 && eth.type == 0x86dd
-
-## Miscellaneous negative tests.
-; => Syntax error at `;'.
-xyzzy; => Syntax error at `xyzzy' expecting action.
-next; 123; => Syntax error at `123'.
-next; xyzzy; => Syntax error at `xyzzy' expecting action.
-next => Syntax error at end of input expecting ';'.
+ip4.src = ip6.src[0..31];
+    encodes as move:NXM_NX_IPV6_SRC[0..31]->NXM_OF_IP_SRC[]
+    has prereqs eth.type == 0x800 && eth.type == 0x86dd
+ip4.src <-> ip6.src[0..31];
+    encodes as push:NXM_NX_IPV6_SRC[0..31],push:NXM_OF_IP_SRC[],pop:NXM_NX_IPV6_SRC[0..31],pop:NXM_OF_IP_SRC[]
+    has prereqs eth.type == 0x800 && eth.type == 0x86dd
+
+# Miscellaneous negative tests.
+;
+    Syntax error at `;'.
+xyzzy;
+    Syntax error at `xyzzy' expecting action.
+next; 123;
+    Syntax error at `123'.
+next; xyzzy;
+    Syntax error at `xyzzy' expecting action.
+next
+    Syntax error at end of input expecting ';'.
 ]])
-sed 's/ =>.*//' test-cases.txt > input.txt
-sed 's/.* => //' test-cases.txt > expout
+sed '/^[[ 	]]/d' test-cases.txt > input.txt
+cp test-cases.txt expout
 AT_CHECK([ovstest test-ovn parse-actions < input.txt], [0], [expout])
 AT_CLEANUP
 
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index 42941eb..e62c2fe 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1210,6 +1210,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
     struct hmap dhcp_opts;
     struct simap ports, ct_zones;
     struct ds input;
+    bool ok = true;
 
     create_symtab(&symtab);
     create_dhcp_opts(&dhcp_opts);
@@ -1229,48 +1230,89 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
 
     ds_init(&input);
     while (!ds_get_test_line(&input, stdin)) {
-        struct ofpbuf ofpacts;
+        struct ofpbuf ovnacts;
         struct expr *prereqs;
         char *error;
 
-        ofpbuf_init(&ofpacts, 0);
+        puts(ds_cstr(&input));
 
-        struct action_params ap = {
+        ofpbuf_init(&ovnacts, 0);
+
+        const struct ovnact_parse_params pp = {
             .symtab = &symtab,
             .dhcp_opts = &dhcp_opts,
-            .lookup_port = lookup_port_cb,
-            .aux = &ports,
-            .ct_zones = &ct_zones,
-            .group_table = &group_table,
 
             .n_tables = 16,
-            .first_ptable = 16,
             .cur_ltable = 10,
-            .output_ptable = 64,
-            .mac_bind_ptable = 65,
         };
-        error = actions_parse_string(ds_cstr(&input), &ap, &ofpacts, &prereqs);
+        error = ovnacts_parse_string(ds_cstr(&input), &pp, &ovnacts, &prereqs);
         if (!error) {
-            struct ds output;
+            /* Convert the parsed representation back to a string and print it,
+             * if it's different from the input. */
+            struct ds ovnacts_s = DS_EMPTY_INITIALIZER;
+            ovnacts_format(ovnacts.data, ovnacts.size, &ovnacts_s);
+            if (strcmp(ds_cstr(&input), ds_cstr(&ovnacts_s))) {
+                printf("    formats as %s\n", ds_cstr(&ovnacts_s));
+            }
 
-            ds_init(&output);
-            ds_put_cstr(&output, "actions=");
-            ofpacts_format(ofpacts.data, ofpacts.size, &output);
-            ds_put_cstr(&output, ", prereqs=");
+            /* Encode the actions into OpenFlow and print. */
+            const struct ovnact_encode_params ep = {
+                .lookup_port = lookup_port_cb,
+                .aux = &ports,
+                .ct_zones = &ct_zones,
+                .group_table = &group_table,
+
+                .first_ptable = 16,
+                .output_ptable = 64,
+                .mac_bind_ptable = 65,
+            };
+            struct ofpbuf ofpacts;
+            ofpbuf_init(&ofpacts, 0);
+            ovnacts_encode(ovnacts.data, ovnacts.size, &ep, &ofpacts);
+            struct ds ofpacts_s = DS_EMPTY_INITIALIZER;
+            ofpacts_format(ofpacts.data, ofpacts.size, &ofpacts_s);
+            printf("    encodes as %s\n", ds_cstr(&ofpacts_s));
+            ds_destroy(&ofpacts_s);
+            ofpbuf_uninit(&ofpacts);
+
+            /* Print prerequisites if any. */
             if (prereqs) {
-                expr_format(prereqs, &output);
+                struct ds prereqs_s = DS_EMPTY_INITIALIZER;
+                expr_format(prereqs, &prereqs_s);
+                printf("    has prereqs %s\n", ds_cstr(&prereqs_s));
+                ds_destroy(&prereqs_s);
+            }
+
+            /* Now re-parse and re-format the string to verify that it's
+             * round-trippable. */
+            struct ofpbuf ovnacts2;
+            struct expr *prereqs2;
+            ofpbuf_init(&ovnacts2, 0);
+            error = ovnacts_parse_string(ds_cstr(&ovnacts_s), &pp, &ovnacts2,
+                                         &prereqs2);
+            if (!error) {
+                struct ds ovnacts2_s = DS_EMPTY_INITIALIZER;
+                ovnacts_format(ovnacts2.data, ovnacts2.size, &ovnacts2_s);
+                if (strcmp(ds_cstr(&ovnacts_s), ds_cstr(&ovnacts2_s))) {
+                    printf("    bad reformat: %s\n", ds_cstr(&ovnacts2_s));
+                    ok = false;
+                }
+                ds_destroy(&ovnacts2_s);
             } else {
-                ds_put_char(&output, '1');
+                printf("    reparse error: %s\n", error);
+                free(error);
+                ok = false;
             }
-            puts(ds_cstr(&output));
-            ds_destroy(&output);
+            expr_destroy(prereqs2);
+
+            ds_destroy(&ovnacts_s);
         } else {
-            puts(error);
+            printf("    %s\n", error);
             free(error);
         }
 
         expr_destroy(prereqs);
-        ofpbuf_uninit(&ofpacts);
+        ofpbuf_uninit(&ovnacts);
     }
     ds_destroy(&input);
 
@@ -1278,6 +1320,8 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
     simap_destroy(&ct_zones);
     expr_symtab_destroy(&symtab);
     shash_destroy(&symtab);
+
+    exit(ok ? EXIT_SUCCESS : EXIT_FAILURE);
 }
 
 static unsigned int
-- 
2.1.3




More information about the dev mailing list