rules: rewrite
authorRan Benita <ran234@gmail.com>
Sun, 26 Aug 2012 05:54:31 +0000 (08:54 +0300)
committerRan Benita <ran234@gmail.com>
Wed, 5 Sep 2012 13:37:02 +0000 (16:37 +0300)
Rewrite the rules file parser for clarity, performance and memory usage
reduction. The previous implementation was quite hard to navigate and
did a lot of unnecessary work and copying.
This implementation keeps along just the state necessary, and doesn't
perform any copying of the file's content (although the entire file is
mmap'ed as before). Hopefully it's also easier to understand, has better
documentation, and better error checking and reporting. We try to
reproduce the previous behavior in every case.

Note: the diff is pretty confusing; it's likely better to look at the
file directly.

Benchmarks:
    On an old 32-bit Intel processor.
    gcc -O2 -pg
    ./test/rulescomp bench
    grof test/rulescomp

    Before:
        compiled 1000 keymaps in 14.863564304s
        %   cumulative   self              self     total
        time   seconds   seconds    calls  ms/call  ms/call  name
        49.33      4.43     4.43 30610000     0.00     0.00  yylex
        17.93      6.04     1.61    31000     0.05     0.22  yyparse
        6.57      6.63     0.59     1000     0.59     0.59  load_rules
        3.23      6.92     0.29  3637000     0.00     0.00  AppendStmt
        2.45      7.14     0.22   472000     0.00     0.00  AddKeySymbols
        2.12      7.33     0.19  3591000     0.00     0.00  atom_intern
        2.12      7.52     0.19   518000     0.00     0.00  FindNamedKey
        2.00      7.70     0.18   230000     0.00     0.00  FreeStmt
        1.78      7.86     0.16     1000     0.16     0.17  UpdateModifiersFromCompat
        1.34      7.98     0.12   732000     0.00     0.00  AddKeyName
        1.34      8.10     0.12                             __x86.get_pc_thunk.bx

    After:
        compiled 1000 keymaps in 13.874666269s
        %   cumulative   self              self     total
        time   seconds   seconds    calls  ms/call  ms/call  name
        49.82      4.26     4.26 30610000     0.00     0.00  yylex
        22.22      6.16     1.90    31000     0.06     0.22  yyparse
        2.92      6.41     0.25  3591000     0.00     0.00  atom_intern
        2.57      6.63     0.22     1000     0.22     0.25  xkb_components_from_rules
        2.11      6.81     0.18  3637000     0.00     0.00  AppendStmt
        2.11      6.99     0.18   230000     0.00     0.00  FreeStmt
        1.99      7.16     0.17   518000     0.00     0.00  FindNamedKey
        1.99      7.33     0.17     1000     0.17     0.17  UpdateModifiersFromCompat
        1.99      7.50     0.17                             __x86.get_pc_thunk.bx
        1.52      7.63     0.13   150000     0.00     0.00  AddInterp
        1.40      7.75     0.12   472000     0.00     0.00  AddKeySymbols

    On a newer 64-bit Intel processor.
    gcc -O2
    ./test/rules-file bench

    Before:
        processed 20000 times in 15.940546625s

    After:
        processed 20000 times in 5.295026345s

Allocations:
    gcc -O2
    valgrind test/rulescomp

    Before:
    total heap usage: 257,519 allocs, 257,519 frees, 14,766,529 bytes allocated

    After:
    total heap usage: 240,756 allocs, 240,756 frees, 14,007,886 bytes allocated

Signed-off-by: Ran Benita <ran234@gmail.com>
src/xkbcomp/rules.c

index bd99dc1..c048605 100644 (file)
@@ -24,6 +24,7 @@
  *
  ********************************************************/
 
+#include <stdarg.h>
 #include <stdio.h>
 #include <ctype.h>
 #include <fcntl.h>
 #include "rules.h"
 #include "include.h"
 
-static const char *
-input_line_get(struct xkb_context *ctx, const char *buf, const char *end,
-               darray_char *line)
+/*
+ * The rules file
+ * ==============
+ * The purpose of this file is to map between configuration values that
+ * are easy for a user to specify and understand, and the configuration
+ * values xkbcomp uses and understands.
+ * xkbcomp uses the xkb_component_names struct, which maps directly to
+ * include statements of the appropriate sections, called for short
+ * KcCGST (see keycodes.c, types.c, compat.c, symbols.c; geometry.c was
+ * removed). These are not really intuitive or straight-forward for
+ * the uninitiated.
+ * Instead, the user passes in a xkb_rule_names struct, which consists
+ * of the name of a rules file (in Linux this is usually "evdev"), a
+ * keyboard model (e.g. "pc105"), a set of layouts (which will end up
+ * in different groups, e.g. "us,fr"), variants (used to alter/augment
+ * the respective layout, e.g. "intl,dvorak"), and a set of options
+ * (used to tweak some general behavior of the keyboard, e.g.
+ * "ctrl:nocaps,compose:menu" to make the Caps Lock key act like Ctrl
+ * and the Menu key like Compose). We call these RMLVO.
+ *
+ * Format of the file
+ * ------------------
+ * The file consists of rule sets, each consisting of rules (one per
+ * line), which match the MLVO values on the left hand side, and, if
+ * the values match to the values the user passed in, results in the
+ * values on the right hand side being added to the resulting KcCGST.
+ * Since some values are related and repeated often, it is possible
+ * to group them together and refer to them by a group name in the
+ * rules.
+ * Along with matching values by simple string equality, and for
+ * membership in a group defined previously, rules may also contain
+ * "wildcard" values - "*" - which always match. These usually appear
+ * near the end.
+ *
+ * Grammer
+ * -------
+ * (It might be helpful to look at a file like rules/evdev along with
+ * this grammer. Comments, whitespace, etc. are not shown.)
+ *
+ * File         ::= { "!" (Group | RuleSet) }
+ *
+ * Group        ::= GroupName "=" { GroupElement } "\n"
+ * GroupName    ::= "$"<ident>
+ * GroupElement ::= <ident>
+ *
+ * RuleSet      ::= Mapping { Rule }
+ *
+ * Mapping      ::= { Mlvo } "=" { Kccgst } "\n"
+ * Mlvo         ::= "model" | "option" | ("layout" | "variant") [ Index ]
+ * Index        ::= "[" 1..XkbNumKbdGroups "]"
+ * Kccgst       ::= "keycodes" | "symbols" | "types" | "compat" | "geometry"
+ *
+ * Rule         ::= { MlvoValue } "=" { KccgstValue } "\n"
+ * MlvoValue    ::= "*" | GroupName | <ident>
+ * KccgstValue  ::= <ident>
+ *
+ * Notes:
+ * - The order of values in a Rule must be the same as the Mapping it
+ *   follows. The mapping line determines the meaning of the values in
+ *   the rules which follow in the RuleSet.
+ * - If a Rule is matched, %-expansion is performed on the KccgstValue,
+ *   as follows:
+ *   %m, %l, %v:
+ *      The model, layout or variant, if only one was given (e.g.
+ *      %l for "us,il" is invalid).
+ *   %l[1], %v[1]:
+ *      Layout or variant for the specified group Index, if more than
+ *      one was given (e.g. %l[1] for "us" is invalid).
+ *   %+m, %+l, %+v, %+l[1], %+v[1]
+ *      As above, but prefixed with '+'. Similarly, '|', '-', '_' may be
+ *      used instead of '+'.
+ *   %(m), %(l), %(l[1]), %(v), %(v[1]):
+ *      As above, but prefixed by '(' and suffixed by ')'.
+ *   In case the expansion is invalid, as described above, it is
+ *   skipped (the rest of the string is still processed); this includes
+ *   the prefix and suffix (that's why you shouldn't use e.g. "(%v[1])").
+ */
+
+/* Scanner / Lexer */
+
+/* Point to some substring in the file; used to avoid copying. */
+struct sval {
+    const char *start;
+    unsigned int len;
+};
+typedef darray(struct sval) darray_sval;
+
+static inline bool
+svaleq(struct sval s1, struct sval s2)
 {
-    int ch;
-    bool space_pending;
-    bool slash_pending;
-    bool in_comment;
-
-    while (buf < end && darray_empty(*line)) {
-        space_pending = slash_pending = in_comment = false;
-
-        while (buf < end && (ch = *buf++) != '\n') {
-            if (ch == '\\') {
-                buf++;
-                if (buf >= end)
-                    break;
-                ch = *buf;
+    return s1.len == s2.len && strncmp(s1.start, s2.start, s1.len) == 0;
+}
 
-                if (ch == '\n') {
-                    in_comment = false;
-                    ch = ' ';
-                }
-            }
+static inline bool
+svaleq_prefix(struct sval s1, struct sval s2)
+{
+    return s1.len <= s2.len && strncmp(s1.start, s2.start, s1.len) == 0;
+}
 
-            if (in_comment)
-                continue;
+/* Values returned with some tokens, like yylval. */
+union lvalue {
+    struct sval string;
+};
 
-            if (ch == '/') {
-                if (slash_pending) {
-                    in_comment = true;
-                    slash_pending = false;
-                }
-                else {
-                    slash_pending = true;
-                }
+/*
+ * Holds the location in the file of the last processed token,
+ * like yylloc.
+ */
+struct location {
+    int line, column;
+};
 
-                continue;
-            }
+struct scanner {
+    const char *s;
+    size_t pos;
+    size_t len;
+    int line, column;
+    const char *file_name;
+    struct xkb_context *ctx;
+};
 
-            if (slash_pending) {
-                if (space_pending) {
-                    darray_append(*line, ' ');
-                    space_pending = false;
-                }
+enum rules_token {
+    TOK_END_OF_FILE = 0,
+    TOK_END_OF_LINE,
+    TOK_IDENTIFIER,
+    TOK_GROUP_NAME,
+    TOK_BANG,
+    TOK_EQUALS,
+    TOK_STAR,
+    TOK_ERROR
+};
 
-                darray_append(*line, '/');
-                slash_pending = false;
-            }
+static void
+scanner_init(struct scanner *s, struct xkb_context *ctx,
+             const char *string, size_t len, const char *file_name)
+{
+    s->s = string;
+    s->len = len;
+    s->pos = 0;
+    s->line = s->column = 1;
+    s->file_name = file_name;
+    s->ctx = ctx;
+}
 
-            if (isspace(ch)) {
-                while (buf < end && isspace(ch) && ch != '\n')
-                    ch = *buf++;
+/* C99 is stupid. Just use the 1 variant when there are no args. */
+#define scanner_error1(scanner, loc, msg) \
+    log_warn(scanner->ctx, "rules/%s:%d:%d: " msg "\n", \
+             scanner->file_name, loc->line, loc->column)
+#define scanner_error(scanner, loc, fmt, ...) \
+    log_warn(scanner->ctx, "rules/%s:%d:%d: " fmt "\n", \
+             scanner->file_name, loc->line, loc->column, __VA_ARGS__)
 
-                if (buf >= end)
-                    break;
+static char
+peek(struct scanner *s)
+{
+    return s->s[s->pos];
+}
 
-                if (ch != '\n' && !darray_empty(*line))
-                    space_pending = true;
+static bool
+eof(struct scanner *s)
+{
+    return s->s[s->pos] == '\0' || s->pos >= s->len;
+}
 
-                buf--;
-            }
-            else {
-                if (space_pending) {
-                    darray_append(*line, ' ');
-                    space_pending = false;
-                }
+static bool
+eol(struct scanner *s)
+{
+    return s->s[s->pos] == '\n';
+}
 
-                if (ch == '!') {
-                    if (!darray_empty(*line)) {
-                        log_warn(ctx,
-                                 "The '!' is legal only at start of line; "
-                                 "Line containing '!' ignored\n");
-                        darray_resize(*line, 0);
-                        break;
-                    }
-                }
+/*
+ * Use the check_nl variant when the current char might be a new line;
+ * just an optimization.
+ */
+static char
+next(struct scanner *s)
+{
+    s->column++;
+    return s->s[s->pos++];
+}
 
-                darray_append(*line, ch);
-            }
-        }
+static char
+next_check_nl(struct scanner *s)
+{
+    if (eol(s)) {
+        s->line++;
+        s->column = 1;
     }
+    else {
+        s->column++;
+    }
+    return s->s[s->pos++];
+}
 
-    if (darray_empty(*line) && buf >= end)
-        return NULL;
+static bool
+chr(struct scanner *s, char ch)
+{
+    if (peek(s) != ch)
+        return false;
+    s->pos++; s->column++;
+    return true;
+}
 
-    darray_append(*line, '\0');
-    return buf;
+static bool
+str(struct scanner *s, const char *string, size_t len)
+{
+    if (strncasecmp(s->s + s->pos, string, len) != 0)
+        return false;
+    s->pos += len; s->column += len;
+    return true;
 }
 
-/***====================================================================***/
+#define lit(s, literal) str(s, literal, sizeof(literal) - 1)
 
-enum {
-    /* "Parts" - the MLVO which rules file maps to components. */
-    MODEL = 0,
-    LAYOUT,
-    VARIANT,
-    OPTION,
+static enum rules_token
+lex(struct scanner *s, union lvalue *val, struct location *loc)
+{
+skip_more_whitespace_and_comments:
+    /* Skip spaces. */
+    while (chr(s, ' ') || chr(s, '\t'));
 
-#define PART_MASK \
-    ((1 << MODEL) | (1 << LAYOUT) | (1 << VARIANT) | (1 << OPTION))
+    /* Skip comments. */
+    if (lit(s, "//")) {
+        while (!eof(s) && !eol(s)) next(s);
+    }
 
-    /* Components */
-    KEYCODES,
-    SYMBOLS,
-    TYPES,
-    COMPAT,
-    GEOMETRY,
+    /* New line. */
+    if (eol(s)) {
+        while (eol(s)) next_check_nl(s);
+        return TOK_END_OF_LINE;
+    }
 
-#define COMPONENT_MASK \
-    ((1 << KEYCODES) | (1 << SYMBOLS) | (1 << TYPES) | (1 << COMPAT) | \
-     (1 << GEOMETRY))
+    /* Escaped line continuation. */
+    if (chr(s, '\\')) {
+        if (!eol(s)) {
+            scanner_error1(s, loc,
+                           "illegal new line escape; must appear at end of line");
+            return TOK_ERROR;
+        }
+        next_check_nl(s);
+        goto skip_more_whitespace_and_comments;
+    }
 
-    MAX_WORDS
+    /* See if we're done. */
+    if (eof(s)) return TOK_END_OF_FILE;
+
+    /* New token. */
+    loc->line = s->line;
+    loc->column = s->column;
+
+    /* Operators and punctuation. */
+    if (chr(s, '!')) return TOK_BANG;
+    if (chr(s, '=')) return TOK_EQUALS;
+    if (chr(s, '*')) return TOK_STAR;
+
+    /* Group name. */
+    if (chr(s, '$')) {
+        val->string.start = s->s + s->pos;
+        val->string.len = 0;
+        while (isgraph(peek(s))) {
+            next(s);
+            val->string.len++;
+        }
+        if (val->string.len == 0) {
+            scanner_error1(s, loc,
+                           "unexpected character after \'$\'; expected name");
+            return TOK_ERROR;
+        }
+        return TOK_GROUP_NAME;
+    }
+
+    /* Identifier. */
+    if (isgraph(peek(s))) {
+        val->string.start = s->s + s->pos;
+        val->string.len = 0;
+        while (isgraph(peek(s))) {
+            next(s);
+            val->string.len++;
+        }
+        return TOK_IDENTIFIER;
+    }
+
+    scanner_error1(s, loc, "unrecognized token");
+    return TOK_ERROR;
+}
+
+/***====================================================================***/
+
+enum rules_mlvo {
+    MLVO_MODEL,
+    MLVO_LAYOUT,
+    MLVO_VARIANT,
+    MLVO_OPTION,
+    _MLVO_NUM_ENTRIES
 };
 
-static const char *cname[] = {
-    [MODEL] = "model",
-    [LAYOUT] = "layout",
-    [VARIANT] = "variant",
-    [OPTION] = "option",
-
-    [KEYCODES] = "keycodes",
-    [SYMBOLS] = "symbols",
-    [TYPES] = "types",
-    [COMPAT] = "compat",
-    [GEOMETRY] = "geometry",
+#define SVAL_LIT(literal) { literal, sizeof(literal) - 1 }
+
+static const struct sval rules_mlvo_svals[_MLVO_NUM_ENTRIES] = {
+    [MLVO_MODEL] = SVAL_LIT("model"),
+    [MLVO_LAYOUT] = SVAL_LIT("layout"),
+    [MLVO_VARIANT] = SVAL_LIT("variant"),
+    [MLVO_OPTION] = SVAL_LIT("option"),
 };
 
-struct multi_defs {
-    const char *model;
-    const char *layout[XkbNumKbdGroups + 1];
-    const char *variant[XkbNumKbdGroups + 1];
-    char *options;
+enum rules_kccgst {
+    KCCGST_KEYCODES,
+    KCCGST_TYPES,
+    KCCGST_COMPAT,
+    KCCGST_SYMBOLS,
+    KCCGST_GEOMETRY,
+    _KCCGST_NUM_ENTRIES
 };
 
-struct mapping {
-    /* Sequential id for the mappings. */
-    int number;
-    size_t num_maps;
-
-    struct {
-        int word;
-        int index;
-    } map[MAX_WORDS];
+static const struct sval rules_kccgst_svals[_KCCGST_NUM_ENTRIES] = {
+    [KCCGST_KEYCODES] = SVAL_LIT("keycodes"),
+    [KCCGST_TYPES] = SVAL_LIT("types"),
+    [KCCGST_COMPAT] = SVAL_LIT("compat"),
+    [KCCGST_SYMBOLS] = SVAL_LIT("symbols"),
+    [KCCGST_GEOMETRY] = SVAL_LIT("geometry"),
 };
 
-struct var_desc {
-    char *name;
-    char *desc;
+/*
+ * A broken-down version of xkb_rule_names (without the rules,
+ * obviously).
+ */
+struct rule_names {
+    struct sval model;
+    darray_sval layouts;
+    darray_sval variants;
+    darray_sval options;
 };
 
 struct group {
-    int number;
-    char *name;
-    char *words;
+    struct sval name;
+    darray_sval elements;
 };
 
-enum rule_flag {
-    RULE_FLAG_PENDING_MATCH = (1L << 1),
-    RULE_FLAG_OPTION = (1L << 2),
-    RULE_FLAG_APPEND = (1L << 3),
-    RULE_FLAG_NORMAL = (1L << 4),
+struct mapping {
+    int mlvo_at_pos[_MLVO_NUM_ENTRIES];
+    unsigned int num_mlvo;
+    unsigned int defined_mlvo_mask;
+    xkb_group_index_t layout_idx, variant_idx;
+    int kccgst_at_pos[_KCCGST_NUM_ENTRIES];
+    unsigned int num_kccgst;
+    unsigned int defined_kccgst_mask;
+    bool skip;
 };
 
-struct rule {
-    int number;
-
-    char *model;
-    char *layout;
-    int layout_num;
-    char *variant;
-    int variant_num;
-    char *option;
-
-    /* yields */
-
-    char *keycodes;
-    char *symbols;
-    char *types;
-    char *compat;
-    unsigned flags;
+enum mlvo_match_type {
+    MLVO_MATCH_NORMAL = 0,
+    MLVO_MATCH_WILDCARD,
+    MLVO_MATCH_GROUP,
 };
 
-struct rules {
-    darray(struct rule) rules;
-    darray(struct group) groups;
+struct rule {
+    struct sval mlvo_value_at_pos[_MLVO_NUM_ENTRIES];
+    enum mlvo_match_type match_type_at_pos[_MLVO_NUM_ENTRIES];
+    unsigned int num_mlvo_values;
+    struct sval kccgst_value_at_pos[_KCCGST_NUM_ENTRIES];
+    unsigned int num_kccgst_values;
+    bool skip;
 };
 
-/***====================================================================***/
-
 /*
- * Resolve numeric index, such as "[4]" in layout[4]. Missing index
- * means zero.
+ * This is the main object used to match a given RMLVO against a rules
+ * file and aggragate the results in a KcCGST. It goes through a simple
+ * matching state machine, with tokens as transitions (see
+ * matcher_match()).
  */
-static char *
-get_index(char *str, int *ndx)
-{
-    int empty = 0, consumed = 0, num;
-
-    sscanf(str, "[%n%d]%n", &empty, &num, &consumed);
-    if (consumed > 0) {
-        *ndx = num;
-        str += consumed;
-    }
-    else if (empty > 0) {
-        *ndx = -1;
-    }
-    else {
-        *ndx = 0;
-    }
+struct matcher {
+    struct xkb_context *ctx;
+    /* Input.*/
+    struct rule_names rmlvo;
+    struct location loc;
+    struct scanner scanner;
+    darray(struct group) groups;
+    /* Current mapping. */
+    struct mapping mapping;
+    /* Current rule. */
+    struct rule rule;
+    /* Output. */
+    darray_char kccgst[_KCCGST_NUM_ENTRIES];
+};
 
-    return str;
+static struct sval
+strip_spaces(struct sval v)
+{
+    while (v.len > 0 && isspace(v.start[0])) { v.len--; v.start++; }
+    while (v.len > 0 && isspace(v.start[v.len - 1])) v.len--;
+    return v;
 }
 
-/*
- * Match a mapping line which opens a rule, e.g:
- * ! model      layout[4]       variant[4]      =       symbols       geometry
- * Which will be followed by lines such as:
- *   *          ben             basic           =       +in(ben):4    nec(pc98)
- * So if the MLVO matches the LHS of some line, we'll get the components
- * on the RHS.
- * In this example, we will get for the second and fourth columns:
- * mapping->map[1] = {.word = LAYOUT, .index = 4}
- * mapping->map[3] = {.word = SYMBOLS, .index = 0}
- */
-static void
-match_mapping_line(struct xkb_context *ctx, darray_char *line,
-                   struct mapping *mapping)
+static darray_sval
+split_comma_separated_string(const char *s)
 {
-    char *tok;
-    char *str = darray_mem(*line, 1);
-    unsigned present = 0, layout_ndx_present = 0, variant_ndx_present = 0;
-    int i, tmp;
-    size_t len;
-    int ndx;
-    char *strtok_buf;
-    bool found;
+    darray_sval arr = darray_new();
+    struct sval val = { NULL, 0 };
 
     /*
-     * Remember the last sequential mapping id (incremented if the match
-     * is successful).
+     * Make sure the array returned by this function always includes at
+     * least one value, e.g. "" -> { "" } and "," -> { "", "" }.
      */
-    tmp = mapping->number;
-    memset(mapping, 0, sizeof(*mapping));
-    mapping->number = tmp;
 
-    while ((tok = strtok_r(str, " ", &strtok_buf)) != NULL) {
-        found = false;
-        str = NULL;
+    if (!s) {
+        darray_append(arr, val);
+        return arr;
+    }
 
-        if (streq(tok, "="))
-            continue;
+    while (true) {
+        val.start = s; val.len = 0;
+        while (*s != '\0' && *s != ',') { s++; val.len++; }
+        darray_append(arr, strip_spaces(val));
+        if (*s == '\0') break;
+        if (*s == ',') s++;
+    }
 
-        for (i = 0; i < MAX_WORDS; i++) {
-            len = strlen(cname[i]);
-
-            if (strncmp(cname[i], tok, len) == 0) {
-                if (strlen(tok) > len) {
-                    char *end = get_index(tok + len, &ndx);
-
-                    if ((i != LAYOUT && i != VARIANT) ||
-                        *end != '\0' || ndx == -1) {
-                        log_warn(ctx,
-                                 "Illegal %s index: %d\n", cname[i], ndx);
-                        log_warn(ctx, "Can only index layout and variant\n");
-                        break;
-                    }
-
-                    if (ndx < 1 || ndx > XkbNumKbdGroups) {
-                        log_warn(ctx, "Illegal %s index: %d\n",
-                                 cname[i], ndx);
-                        log_warn(ctx, "Index must be in range 1..%d\n",
-                                 XkbNumKbdGroups);
-                        break;
-                    }
-                }
-                else {
-                    ndx = 0;
-                }
+    return arr;
+}
 
-                found = true;
+static struct matcher *
+matcher_new(struct xkb_context *ctx,
+            const struct xkb_rule_names *rmlvo)
+{
+    struct matcher *m = calloc(1, sizeof(*m));
+    if (!m)
+        return NULL;
 
-                if (present & (1 << i)) {
-                    if ((i == LAYOUT && layout_ndx_present & (1 << ndx)) ||
-                        (i == VARIANT && variant_ndx_present & (1 << ndx))) {
-                        log_warn(ctx,
-                                 "Component \"%s\" listed twice; "
-                                 "Second definition ignored\n", tok);
-                        break;
-                    }
-                }
+    m->ctx = ctx;
+    m->rmlvo.model.start = rmlvo->model;
+    m->rmlvo.model.len = rmlvo->model ? strlen(rmlvo->model) : 0;
+    m->rmlvo.layouts = split_comma_separated_string(rmlvo->layout);
+    m->rmlvo.variants = split_comma_separated_string(rmlvo->variant);
+    m->rmlvo.options = split_comma_separated_string(rmlvo->options);
 
-                present |= (1 << i);
-                if (i == LAYOUT)
-                    layout_ndx_present |= 1 << ndx;
-                if (i == VARIANT)
-                    variant_ndx_present |= 1 << ndx;
+    return m;
+}
 
-                mapping->map[mapping->num_maps].word = i;
-                mapping->map[mapping->num_maps].index = ndx;
-                mapping->num_maps++;
-                break;
-            }
-        }
+static void
+matcher_free(struct matcher *m)
+{
+    struct group *group;
+    if (!m)
+        return;
+    darray_free(m->rmlvo.layouts);
+    darray_free(m->rmlvo.variants);
+    darray_free(m->rmlvo.options);
+    darray_foreach(group, m->groups)
+        darray_free(group->elements);
+    darray_free(m->groups);
+    free(m);
+}
 
-        if (!found)
-            log_warn(ctx, "Unknown component \"%s\"; Ignored\n", tok);
-    }
+/* C99 is stupid. Just use the 1 variant when there are no args. */
+#define matcher_error1(matcher, msg) \
+    log_warn(matcher->ctx, "rules/%s:%d:%d: " msg "\n", \
+             matcher->scanner.file_name, matcher->loc.line, \
+             matcher->loc.column)
+#define matcher_error(matcher, fmt, ...) \
+    log_warn(matcher->ctx, "rules/%s:%d:%d: " fmt "\n", \
+             matcher->scanner.file_name, matcher->loc.line, \
+             matcher->loc.column, __VA_ARGS__)
 
-    if ((present & PART_MASK) == 0) {
-        log_warn(ctx,
-                 "Mapping needs at least one MLVO part; "
-                 "Illegal mapping ignored\n");
-        mapping->num_maps = 0;
-        return;
-    }
+static void
+matcher_group_start_new(struct matcher *m, struct sval name)
+{
+    struct group group = { .name = name, .elements = darray_new() };
+    darray_append(m->groups, group);
+}
 
-    if ((present & COMPONENT_MASK) == 0) {
-        log_warn(ctx,
-                 "Mapping needs at least one component; "
-                 "Illegal mapping ignored\n");
-        mapping->num_maps = 0;
-        return;
-    }
+static void
+matcher_group_add_element(struct matcher *m, struct sval element)
+{
+    darray_append(darray_item(m->groups, darray_size(m->groups) - 1).elements,
+                  element);
+}
 
-    mapping->number++;
+static void
+matcher_mapping_start_new(struct matcher *m)
+{
+    unsigned int i;
+    for (i = 0; i < _MLVO_NUM_ENTRIES; i++)
+        m->mapping.mlvo_at_pos[i] = -1;
+    for (i = 0; i < _KCCGST_NUM_ENTRIES; i++)
+        m->mapping.kccgst_at_pos[i] = -1;
+    m->mapping.layout_idx = m->mapping.variant_idx = XKB_GROUP_INVALID;
+    m->mapping.num_mlvo = m->mapping.num_kccgst = 0;
+    m->mapping.defined_mlvo_mask = 0;
+    m->mapping.defined_kccgst_mask = 0;
+    m->mapping.skip = false;
 }
 
-/*
- * Match a line such as:
- * ! $pcmodels = pc101 pc102 pc104 pc105
- */
-static bool
-match_group_line(darray_char *line, struct group *group)
+static int
+extract_group_index(const char *s, size_t max_len, xkb_group_index_t *out)
 {
-    int i;
-    char *name = strchr(darray_mem(*line, 0), '$');
-    char *words = strchr(name, ' ');
+    /* This function is pretty stupid, but works for now. */
+    if (max_len < 3)
+        return -1;
+    if (s[0] != '[' || !isdigit(s[1]) || s[2] != ']')
+        return -1;
+    if (s[1] - '0' < 1 || s[1] - '0' > XkbNumKbdGroups)
+        return -1;
+    /* To zero-based index. */
+    *out = s[1] - '0' - 1;
+    return 3;
+}
 
-    if (!words)
-        return false;
+static void
+matcher_mapping_set_mlvo(struct matcher *m, struct sval ident)
+{
+    enum rules_mlvo mlvo;
+    struct sval mlvo_sval;
+    xkb_group_index_t idx;
+    int consumed;
 
-    *words++ = '\0';
+    for (mlvo = 0; mlvo < _MLVO_NUM_ENTRIES; mlvo++) {
+        mlvo_sval = rules_mlvo_svals[mlvo];
 
-    for (; *words; words++) {
-        if (*words != '=' && *words != ' ')
+        if (svaleq_prefix(mlvo_sval, ident))
             break;
     }
 
-    if (*words == '\0')
-        return false;
-
-    group->name = strdup(name);
-    group->words = strdup(words);
-
-    words = group->words;
-    for (i = 1; *words; words++) {
-        if (*words == ' ') {
-            *words++ = '\0';
-            i++;
-        }
+    /* Not found. */
+    if (mlvo >= _MLVO_NUM_ENTRIES) {
+        matcher_error(m,
+                      "invalid mapping: %.*s is not a valid value here; "
+                      "ignoring rule set",
+                      ident.len, ident.start);
+        m->mapping.skip = true;
+        return;
     }
-    group->number = i;
 
-    return true;
-
-}
-
-/* Match lines following a mapping (see match_mapping_line comment). */
-static bool
-match_rule_line(struct xkb_context *ctx, darray_char *line,
-                struct mapping *mapping, struct rule *rule)
-{
-    char *str, *tok;
-    int nread, i;
-    char *strtok_buf;
-    bool append = false;
-    const char *names[MAX_WORDS] = { NULL };
-
-    if (mapping->num_maps == 0) {
-        log_warn(ctx,
-                 "Must have a mapping before first line of data; "
-                 "Illegal line of data ignored\n");
-        return false;
+    if (m->mapping.defined_mlvo_mask & (1 << mlvo)) {
+        matcher_error(m,
+                      "invalid mapping: %.*s appears twice on the same line; "
+                      "ignoring rule set",
+                      mlvo_sval.len, mlvo_sval.start);
+        m->mapping.skip = true;
+        return;
     }
 
-    str = darray_mem(*line, 0);
-
-    for (nread = 0; (tok = strtok_r(str, " ", &strtok_buf)) != NULL;
-         nread++) {
-        str = NULL;
-
-        if (streq(tok, "=")) {
-            nread--;
-            continue;
+    /* If there are leftovers still, it must be an index. */
+    if (mlvo_sval.len < ident.len) {
+        consumed = extract_group_index(ident.start + mlvo_sval.len,
+                                       ident.len - mlvo_sval.len, &idx);
+        if ((int) (ident.len - mlvo_sval.len) != consumed) {
+            matcher_error(m,
+                          "invalid mapping:\" %.*s\" may only be followed by a valid group index; "
+                          "ignoring rule set",
+                          mlvo_sval.len, mlvo_sval.start);
+            m->mapping.skip = true;
+            return;
         }
 
-        if (nread > mapping->num_maps) {
-            log_warn(ctx,
-                     "Too many words on a line; "
-                     "Extra word \"%s\" ignored\n", tok);
-            continue;
+        if (mlvo == MLVO_LAYOUT) {
+            m->mapping.layout_idx = idx;
         }
-
-        names[mapping->map[nread].word] = tok;
-        if (*tok == '+' || *tok == '|')
-            append = true;
-    }
-
-    if (nread < mapping->num_maps) {
-        log_warn(ctx, "Too few words on a line: %s; Line ignored\n",
-                 darray_mem(*line, 0));
-        return false;
-    }
-
-    rule->flags = 0;
-    rule->number = mapping->number;
-
-    if (names[OPTION])
-        rule->flags |= RULE_FLAG_OPTION;
-    else if (append)
-        rule->flags |= RULE_FLAG_APPEND;
-    else
-        rule->flags |= RULE_FLAG_NORMAL;
-
-    rule->model = strdup_safe(names[MODEL]);
-    rule->layout = strdup_safe(names[LAYOUT]);
-    rule->variant = strdup_safe(names[VARIANT]);
-    rule->option = strdup_safe(names[OPTION]);
-
-    rule->keycodes = strdup_safe(names[KEYCODES]);
-    rule->symbols = strdup_safe(names[SYMBOLS]);
-    rule->types = strdup_safe(names[TYPES]);
-    rule->compat = strdup_safe(names[COMPAT]);
-
-    rule->layout_num = rule->variant_num = 0;
-    for (i = 0; i < nread; i++) {
-        if (mapping->map[i].index) {
-            if (mapping->map[i].word == LAYOUT)
-                rule->layout_num = mapping->map[i].index;
-            if (mapping->map[i].word == VARIANT)
-                rule->variant_num = mapping->map[i].index;
+        else if (mlvo == MLVO_VARIANT) {
+            m->mapping.variant_idx = idx;
+        }
+        else {
+            matcher_error(m,
+                          "invalid mapping: \"%.*s\" cannot be followed by a group index; "
+                          "ignoring rule set",
+                          mlvo_sval.len, mlvo_sval.start);
+            m->mapping.skip = true;
+            return;
         }
     }
 
-    return true;
+    m->mapping.mlvo_at_pos[m->mapping.num_mlvo] = mlvo;
+    m->mapping.defined_mlvo_mask |= 1 << mlvo;
+    m->mapping.num_mlvo++;
 }
 
-static bool
-match_line(struct xkb_context *ctx, darray_char *line,
-           struct mapping *mapping, struct rule *rule,
-           struct group *group)
+static void
+matcher_mapping_set_kccgst(struct matcher *m, struct sval ident)
 {
-    if (darray_item(*line, 0) != '!')
-        return match_rule_line(ctx, line, mapping, rule);
+    enum rules_kccgst kccgst;
+    struct sval kccgst_sval;
 
-    if (darray_item(*line, 1) == '$' ||
-        (darray_item(*line, 1) == ' ' && darray_item(*line, 2) == '$'))
-        return match_group_line(line, group);
+    for (kccgst = 0; kccgst < _KCCGST_NUM_ENTRIES; kccgst++) {
+        kccgst_sval = rules_kccgst_svals[kccgst];
 
-    match_mapping_line(ctx, line, mapping);
-    return false;
-}
+        if (svaleq(rules_kccgst_svals[kccgst], ident))
+            break;
+    }
 
-static void
-squeeze_spaces(char *p1)
-{
-    char *p2;
+    /* Not found. */
+    if (kccgst >= _KCCGST_NUM_ENTRIES) {
+        matcher_error(m,
+                      "invalid mapping: %.*s is not a valid value here; "
+                      "ignoring rule set",
+                      ident.len, ident.start);
+        m->mapping.skip = true;
+        return;
+    }
 
-    for (p2 = p1; *p2; p2++) {
-        *p1 = *p2;
-        if (*p1 != ' ')
-            p1++;
+    if (m->mapping.defined_kccgst_mask & (1 << kccgst)) {
+        matcher_error(m,
+                      "invalid mapping: %.*s appears twice on the same line; "
+                      "ignoring rule set",
+                      kccgst_sval.len, kccgst_sval.start);
+        m->mapping.skip = true;
+        return;
     }
 
-    *p1 = '\0';
+    m->mapping.kccgst_at_pos[m->mapping.num_kccgst] = kccgst;
+    m->mapping.defined_kccgst_mask |= 1 << kccgst;
+    m->mapping.num_kccgst++;
 }
 
-/*
- * Expand the layout and variant of the rule_names and remove extraneous
- * spaces. If there's one layout/variant, it is kept in
- * .layout[0]/.variant[0], else is kept in [1], [2] and so on, and [0]
- * remains empty. For example, this rule_names:
- *      .model  = "pc105",
- *      .layout = "us,il,ru,ca"
- *      .variant = ",,,multix"
- *      .options = "grp:alts_toggle,   ctrl:nocaps,  compose:rwin"
- * Is expanded into this multi_defs:
- *      .model = "pc105"
- *      .layout = {NULL, "us", "il", "ru", "ca"},
- *      .variant = {NULL, "", "", "", "multix"},
- *      .options = "grp:alts_toggle,ctrl:nocaps,compose:rwin"
- */
-static bool
-make_multi_defs(struct multi_defs *mdefs, const struct xkb_rule_names *mlvo)
+static void
+matcher_mapping_verify(struct matcher *m)
 {
-    char *p;
-    int i;
-
-    memset(mdefs, 0, sizeof(*mdefs));
-
-    if (mlvo->model) {
-        mdefs->model = mlvo->model;
+    if (m->mapping.num_mlvo == 0) {
+        matcher_error1(m,
+                       "invalid mapping: must have at least one value on the left hand side; "
+                       "ignoring rule set");
+        goto skip;
     }
 
-    if (mlvo->options) {
-        mdefs->options = strdup(mlvo->options);
-        if (mdefs->options == NULL)
-            return false;
-
-        squeeze_spaces(mdefs->options);
+    if (m->mapping.num_kccgst == 0) {
+        matcher_error1(m,
+                       "invalid mapping: must have at least one value on the right hand side; "
+                       "ignoring rule set");
+        goto skip;
     }
 
-    if (mlvo->layout) {
-        if (!strchr(mlvo->layout, ',')) {
-            mdefs->layout[0] = mlvo->layout;
+    /*
+     * This following is very stupid, but this is how it works.
+     * See the "Notes" section in the overview above.
+     */
+
+    if (m->mapping.defined_mlvo_mask & (1 << MLVO_LAYOUT)) {
+        if (m->mapping.layout_idx == XKB_GROUP_INVALID) {
+            if (darray_size(m->rmlvo.layouts) > 1)
+                goto skip;
         }
         else {
-            p = strdup(mlvo->layout);
-            if (p == NULL)
-                return false;
-
-            squeeze_spaces(p);
-            mdefs->layout[1] = p;
-
-            for (i = 2; i <= XkbNumKbdGroups; i++) {
-                if ((p = strchr(p, ','))) {
-                    *p++ = '\0';
-                    mdefs->layout[i] = p;
-                }
-                else {
-                    break;
-                }
-            }
-
-            if (p && (p = strchr(p, ',')))
-                *p = '\0';
+            if (darray_size(m->rmlvo.layouts) == 1 ||
+                m->mapping.layout_idx >= darray_size(m->rmlvo.layouts))
+                goto skip;
         }
     }
 
-    if (mlvo->variant) {
-        if (!strchr(mlvo->variant, ',')) {
-            mdefs->variant[0] = mlvo->variant;
+    if (m->mapping.defined_mlvo_mask & (1 << MLVO_VARIANT)) {
+        if (m->mapping.variant_idx == XKB_GROUP_INVALID) {
+            if (darray_size(m->rmlvo.variants) > 1)
+                goto skip;
         }
         else {
-            p = strdup(mlvo->variant);
-            if (p == NULL)
-                return false;
-
-            squeeze_spaces(p);
-            mdefs->variant[1] = p;
-
-            for (i = 2; i <= XkbNumKbdGroups; i++) {
-                if ((p = strchr(p, ','))) {
-                    *p++ = '\0';
-                    mdefs->variant[i] = p;
-                }
-                else {
-                    break;
-                }
-            }
-
-            if (p && (p = strchr(p, ',')))
-                *p = '\0';
+            if (darray_size(m->rmlvo.variants) == 1 ||
+                m->mapping.variant_idx >= darray_size(m->rmlvo.variants))
+                goto skip;
         }
     }
 
-    return true;
+    return;
+
+skip:
+    m->mapping.skip = true;
 }
 
 static void
-free_multi_defs(struct multi_defs *defs)
+matcher_rule_start_new(struct matcher *m)
 {
-    free(defs->options);
-    /*
-     * See make_multi_defs comment for the hack; the same strdup'd
-     * string is split among the indexes, but the one in [0] is const.
-     */
-    free(UNCONSTIFY(defs->layout[1]));
-    free(UNCONSTIFY(defs->variant[1]));
+    memset(&m->rule, 0, sizeof(m->rule));
+    m->rule.skip = m->mapping.skip;
 }
 
-/* See apply_rule below. */
 static void
-apply(const char *src, char **dst)
+matcher_rule_set_mlvo_common(struct matcher *m, struct sval ident,
+                             enum mlvo_match_type match_type)
 {
-    int ret;
-    char *tmp;
-
-    if (!src)
+    if (m->rule.num_mlvo_values + 1 > m->mapping.num_mlvo) {
+        matcher_error1(m,
+                       "invalid rule: has more values than the mapping line; "
+                       "ignoring rule");
+        m->rule.skip = true;
         return;
-
-    if (*src == '+' || *src == '|') {
-        tmp = *dst;
-        ret = asprintf(dst, "%s%s", *dst, src);
-        if (ret < 0)
-            *dst = NULL;
-        free(tmp);
-    }
-    else if (*dst == NULL) {
-        *dst = strdup(src);
     }
+    m->rule.match_type_at_pos[m->rule.num_mlvo_values] = match_type;
+    m->rule.mlvo_value_at_pos[m->rule.num_mlvo_values] = ident;
+    m->rule.num_mlvo_values++;
 }
 
-/*
- * Add the info from the matching rule to the resulting
- * xkb_component_names. If we already had a match for something
- * (e.g. keycodes), and the rule is not an appending one (e.g.
- * +whatever), than we don't override but drop the new one.
- */
 static void
-apply_rule(struct rule *rule, struct xkb_component_names *kccgst)
+matcher_rule_set_mlvo_wildcard(struct matcher *m)
 {
-    /* Clear the flag because it's applied. */
-    rule->flags &= ~RULE_FLAG_PENDING_MATCH;
+    struct sval dummy = { NULL, 0 };
+    matcher_rule_set_mlvo_common(m, dummy, MLVO_MATCH_WILDCARD);
+}
 
-    apply(rule->keycodes, &kccgst->keycodes);
-    apply(rule->symbols, &kccgst->symbols);
-    apply(rule->types, &kccgst->types);
-    apply(rule->compat, &kccgst->compat);
+static void
+matcher_rule_set_mlvo_group(struct matcher *m, struct sval ident)
+{
+    matcher_rule_set_mlvo_common(m, ident, MLVO_MATCH_GROUP);
+}
+
+static void
+matcher_rule_set_mlvo(struct matcher *m, struct sval ident)
+{
+    matcher_rule_set_mlvo_common(m, ident, MLVO_MATCH_NORMAL);
+}
+
+static void
+matcher_rule_set_kccgst(struct matcher *m, struct sval ident)
+{
+    if (m->rule.num_kccgst_values + 1 > m->mapping.num_kccgst) {
+        matcher_error1(m,
+                       "invalid rule: has more values than the mapping line; "
+                       "ignoring rule");
+        m->rule.skip = true;
+        return;
+    }
+    m->rule.kccgst_value_at_pos[m->rule.num_kccgst_values] = ident;
+    m->rule.num_kccgst_values++;
 }
 
-/*
- * Match if name is part of the group, e.g. if the following
- * group is defined:
- *      ! $qwertz = al cz de hr hu ro si sk
- * then
- *      match_group_member(rules, "qwertz", "hr")
- * will return true.
- */
 static bool
-match_group_member(struct rules *rules, const char *group_name,
-                   const char *name)
+match_group(struct matcher *m, struct sval group_name, struct sval to)
 {
-    int i;
-    const char *word;
-    struct group *iter, *group = NULL;
+    struct group *group;
+    struct sval *element;
+    bool found = false;
 
-    darray_foreach(iter, rules->groups) {
-        if (streq(iter->name, group_name)) {
-            group = iter;
+    darray_foreach(group, m->groups) {
+        if (svaleq(group->name, group_name)) {
+            found = true;
             break;
         }
     }
 
-    if (!group)
+    if (!found) {
+        /*
+         * rules/evdev intentionally uses some undeclared group names
+         * in rules (e.g. commented group definitions which may be
+         * uncommented if needed). So we continue silently.
+         */
         return false;
+    }
 
-    word = group->words;
-    for (i = 0; i < group->number; i++, word += strlen(word) + 1)
-        if (streq(word, name))
+    darray_foreach(element, group->elements)
+        if (svaleq(to, *element))
             return true;
 
     return false;
 }
 
-/* Match @needle out of @sep-seperated @haystack. */
 static bool
-match_one_of(const char *haystack, const char *needle, char sep)
+match_value(struct matcher *m, struct sval val, struct sval to,
+          enum mlvo_match_type match_type)
 {
-    const char *s = haystack;
-    const size_t len = strlen(needle);
-
-    do {
-        if (strncmp(s, needle, len) == 0 && (s[len] == '\0' || s[len] == sep))
-            return true;
-        s = strchr(s, sep);
-    } while (s++);
-
-    return false;
+    if (match_type == MLVO_MATCH_WILDCARD)
+        return true;
+    if (match_type == MLVO_MATCH_GROUP)
+        return match_group(m, val, to);
+    return svaleq(val, to);
 }
 
-static int
-apply_rule_if_matches(struct rules *rules, struct rule *rule,
-                      struct multi_defs *mdefs,
-                      struct xkb_component_names *kccgst)
+/*
+ * This function performs %-expansion on @value (see overview above),
+ * and appends the result to @to.
+ */
+static bool
+append_expanded_kccgst_value(struct matcher *m, darray_char *to,
+                             struct sval value)
 {
-    bool pending = false;
-
-    if (rule->model) {
-        if (mdefs->model == NULL)
-            return 0;
+    unsigned int i;
+    size_t original_size = darray_size(*to);
+    const char *s = value.start;
+    xkb_group_index_t idx;
+    int consumed;
+    enum rules_mlvo mlv;
+    struct sval expanded;
+    char pfx, sfx;
 
-        if (streq(rule->model, "*")) {
-            pending = true;
-        }
-        else if (rule->model[0] == '$') {
-            if (!match_group_member(rules, rule->model, mdefs->model))
-                return 0;
-        }
-        else if (!streq(rule->model, mdefs->model)) {
-            return 0;
-        }
+    /*
+     * Appending  bar to  foo ->  foo (not an error if this happens)
+     * Appending +bar to  foo ->  foo+bar
+     * Appending  bar to +foo ->  bar+foo
+     * Appending +bar to +foo -> +foo+bar
+     */
+    if (!darray_empty(*to) && s[0] != '+' && s[0] != '|') {
+        if (darray_item(*to, 0) == '+' || darray_item(*to, 0) == '|')
+            darray_prepend_items_nullterminate(*to, value.start, value.len);
+        return true;
     }
 
-    if (rule->option) {
-        if (mdefs->options == NULL)
-            return 0;
-
-        if (!match_one_of(mdefs->options, rule->option, ','))
-            return 0;
-    }
+    /*
+     * Some ugly hand-lexing here, but going through the scanner is more
+     * trouble than it's worth, and the format is ugly on its own merit.
+     */
+    for (i = 0; i < value.len; ) {
+        /* Check if that's a start of an expansion. */
+        if (s[i] != '%') {
+            /* Just a normal character. */
+            darray_append_items_nullterminate(*to, &s[i++], 1);
+            continue;
+        }
+        if (++i >= value.len) goto error;
 
-    if (rule->layout) {
-        if (mdefs->layout[rule->layout_num] == NULL)
-            return 0;
+        pfx = sfx = 0;
 
-        if (streq(rule->layout, "*")) {
-            pending = true;
+        /* Check for prefix. */
+        if (s[i] == '(' || s[i] == '+' || s[i] == '|' ||
+            s[i] == '_' || s[i] == '-') {
+            pfx = s[i];
+            if (s[i] == '(') sfx = ')';
+            if (++i >= value.len) goto error;
         }
-        else if (rule->layout[0] == '$') {
-            if (!match_group_member(rules, rule->layout,
-                                    mdefs->layout[rule->layout_num]))
-                return 0;
+
+        /* Mandatory model/layout/variant specifier. */
+        switch (s[i++]) {
+        case 'm': mlv = MLVO_MODEL; break;
+        case 'l': mlv = MLVO_LAYOUT; break;
+        case 'v': mlv = MLVO_VARIANT; break;
+        default: goto error;
         }
-        else if (!streq(rule->layout, mdefs->layout[rule->layout_num])) {
-            return 0;
+
+        /* Check for index. */
+        if (i < value.len) {
+            if (s[i] == '[') {
+                if (mlv != MLVO_LAYOUT && mlv != MLVO_VARIANT) {
+                    matcher_error1(m,
+                                   "invalid index in %%-expansion; "
+                                   "may only index layout or variant");
+                    goto error;
+                }
+
+                consumed = extract_group_index(s + i, value.len - i, &idx);
+                if (consumed == -1) goto error;
+                i += consumed;
+            }
+            else {
+                idx = XKB_GROUP_INVALID;
+            }
         }
-    }
 
-    if (rule->variant) {
-        if (mdefs->variant[rule->variant_num] == NULL)
-            return 0;
+        /* Check for suffix, if there supposed to be one. */
+        if (sfx != 0) {
+            if (i >= value.len) goto error;
+            if (s[i++] != sfx) goto error;
+        }
 
-        if (streq(rule->variant, "*")) {
-            pending = true;
+        /* Get the expanded value. */
+        expanded.len = 0;
+
+        if (mlv == MLVO_LAYOUT) {
+            if (idx != XKB_GROUP_INVALID &&
+                idx < darray_size(m->rmlvo.layouts) &&
+                darray_size(m->rmlvo.layouts) > 1)
+                expanded = darray_item(m->rmlvo.layouts, idx);
+            else if (idx == XKB_GROUP_INVALID &&
+                     darray_size(m->rmlvo.layouts) == 1)
+                expanded = darray_item(m->rmlvo.layouts, 0);
         }
-        else if (rule->variant[0] == '$') {
-            if (!match_group_member(rules, rule->variant,
-                                    mdefs->variant[rule->variant_num]))
-                return 0;
+        else if (mlv == MLVO_VARIANT) {
+            if (idx != XKB_GROUP_INVALID &&
+                idx < darray_size(m->rmlvo.variants) &&
+                darray_size(m->rmlvo.variants) > 1)
+                expanded = darray_item(m->rmlvo.variants, idx);
+            else if (idx == XKB_GROUP_INVALID &&
+                     darray_size(m->rmlvo.variants) == 1)
+                expanded = darray_item(m->rmlvo.variants, 0);
         }
-        else if (!streq(rule->variant, mdefs->variant[rule->variant_num])) {
-            return 0;
+        else if (mlv == MLVO_MODEL) {
+            expanded = m->rmlvo.model;
         }
-    }
 
-    if (pending) {
-        rule->flags |= RULE_FLAG_PENDING_MATCH;
-    }
-    else {
-        /* Exact match, apply it now. */
-        apply_rule(rule, kccgst);
-    }
-
-    return rule->number;
-}
-
-static void
-clear_partial_matches(struct rules *rules)
-{
-    struct rule *rule;
+        /* If we didn't get one, skip silently. */
+        if (expanded.len <= 0)
+            continue;
 
-    darray_foreach(rule, rules->rules)
-        rule->flags &= ~RULE_FLAG_PENDING_MATCH;
-}
+        if (pfx != 0)
+            darray_append_items_nullterminate(*to, &pfx, 1);
+        darray_append_items_nullterminate(*to, expanded.start, expanded.len);
+        if (sfx != 0)
+            darray_append_items_nullterminate(*to, &sfx, 1);
+    }
 
-static void
-apply_partial_matches(struct rules *rules, struct xkb_component_names *kccgst)
-{
-    struct rule *rule;
+    return true;
 
-    darray_foreach(rule, rules->rules)
-        if (rule->flags & RULE_FLAG_PENDING_MATCH)
-            apply_rule(rule, kccgst);
+error:
+    matcher_error1(m, "invalid %%-expansion in value; not used");
+    darray_resize(*to, original_size);
+    return false;
 }
 
 static void
-apply_matching_rules(struct rules *rules, struct multi_defs *mdefs,
-                     struct xkb_component_names *kccgst, unsigned int flags)
+matcher_rule_verify(struct matcher *m)
 {
-    int skip = -1;
-    struct rule *rule;
-
-    darray_foreach(rule, rules->rules) {
-        if ((rule->flags & flags) != flags)
-            continue;
-
-        if ((flags & RULE_FLAG_OPTION) == 0 && rule->number == skip)
-            continue;
-
-        skip = apply_rule_if_matches(rules, rule, mdefs, kccgst);
+    if (m->rule.num_mlvo_values != m->mapping.num_mlvo ||
+        m->rule.num_kccgst_values != m->mapping.num_kccgst) {
+        matcher_error1(m,
+                       "invalid rule: must have same number of values as mapping line;"
+                       "ignoring rule");
+        m->rule.skip = true;
     }
 }
 
-/***====================================================================***/
-
-static char *
-substitute_vars(char *name, struct multi_defs *mdefs)
+static void
+matcher_rule_apply_if_matches(struct matcher *m)
 {
-    char *str, *outstr, *var;
-    char *orig = name;
-    size_t len, extra_len;
-    char pfx, sfx;
-    int ndx;
-
-    if (!name)
-        return NULL;
-
-    str = strchr(name, '%');
-    if (str == NULL)
-        return name;
-
-    len = strlen(name);
-
-    while (str != NULL) {
-        pfx = str[1];
-        extra_len = 0;
-
-        if (pfx == '+' || pfx == '|' || pfx == '_' || pfx == '-') {
-            extra_len = 1;
-            str++;
+    unsigned int i;
+    enum rules_mlvo mlvo;
+    enum rules_kccgst kccgst;
+    struct sval value, *option;
+    enum mlvo_match_type match_type;
+    bool matched = false;
+    xkb_group_index_t idx;
+
+    for (i = 0; i < m->mapping.num_mlvo; i++) {
+        mlvo = m->mapping.mlvo_at_pos[i];
+        value = m->rule.mlvo_value_at_pos[i];
+        match_type = m->rule.match_type_at_pos[i];
+
+        if (mlvo == MLVO_MODEL) {
+            matched = match_value(m, value, m->rmlvo.model, match_type);
         }
-        else if (pfx == '(') {
-            extra_len = 2;
-            str++;
+        else if (mlvo == MLVO_LAYOUT) {
+            idx = m->mapping.layout_idx;
+            idx = (idx == XKB_GROUP_INVALID ? 0 : idx);
+            matched = match_value(m, value,
+                                  darray_item(m->rmlvo.layouts, idx),
+                                  match_type);
         }
-
-        var = str + 1;
-        str = get_index(var + 1, &ndx);
-        if (ndx == -1) {
-            str = strchr(str, '%');
-            continue;
+        else if (mlvo == MLVO_VARIANT) {
+            idx = m->mapping.layout_idx;
+            idx = (idx == XKB_GROUP_INVALID ? 0 : idx);
+            matched = match_value(m, value,
+                                  darray_item(m->rmlvo.variants, idx),
+                                  match_type);
         }
-
-        if (*var == 'l' && mdefs->layout[ndx] && *mdefs->layout[ndx])
-            len += strlen(mdefs->layout[ndx]) + extra_len;
-        else if (*var == 'm' && mdefs->model)
-            len += strlen(mdefs->model) + extra_len;
-        else if (*var == 'v' && mdefs->variant[ndx] && *mdefs->variant[ndx])
-            len += strlen(mdefs->variant[ndx]) + extra_len;
-
-        if (pfx == '(' && *str == ')')
-            str++;
-
-        str = strchr(&str[0], '%');
-    }
-
-    name = malloc(len + 1);
-    str = orig;
-    outstr = name;
-
-    while (*str != '\0') {
-        if (str[0] == '%') {
-            str++;
-            pfx = str[0];
-            sfx = '\0';
-
-            if (pfx == '+' || pfx == '|' || pfx == '_' || pfx == '-') {
-                str++;
-            }
-            else if (pfx == '(') {
-                sfx = ')';
-                str++;
-            }
-            else {
-                pfx = '\0';
-            }
-
-            var = str;
-            str = get_index(var + 1, &ndx);
-            if (ndx == -1)
-                continue;
-
-            if (*var == 'l' && mdefs->layout[ndx] && *mdefs->layout[ndx]) {
-                if (pfx)
-                    *outstr++ = pfx;
-
-                strcpy(outstr, mdefs->layout[ndx]);
-                outstr += strlen(mdefs->layout[ndx]);
-
-                if (sfx)
-                    *outstr++ = sfx;
-            }
-            else if (*var == 'm' && mdefs->model) {
-                if (pfx)
-                    *outstr++ = pfx;
-
-                strcpy(outstr, mdefs->model);
-                outstr += strlen(mdefs->model);
-
-                if (sfx)
-                    *outstr++ = sfx;
-            }
-            else if (*var == 'v' && mdefs->variant[ndx] &&
-                     *mdefs->variant[ndx]) {
-                if (pfx)
-                    *outstr++ = pfx;
-
-                strcpy(outstr, mdefs->variant[ndx]);
-                outstr += strlen(mdefs->variant[ndx]);
-
-                if (sfx)
-                    *outstr++ = sfx;
+        else if (mlvo == MLVO_OPTION) {
+            darray_foreach(option, m->rmlvo.options) {
+                matched = match_value(m, value, *option, match_type);
+                if (matched)
+                    break;
             }
-
-            if (pfx == '(' && *str == ')')
-                str++;
         }
-        else {
-            *outstr++ = *str++;
-        }
-    }
 
-    *outstr++ = '\0';
+        if (!matched)
+            return;
+    }
 
-    if (orig != name)
-        free(orig);
+    for (i = 0; i < m->mapping.num_kccgst; i++) {
+        kccgst = m->mapping.kccgst_at_pos[i];
+        value = m->rule.kccgst_value_at_pos[i];
+        append_expanded_kccgst_value(m, &m->kccgst[kccgst], value);
+    }
 
-    return name;
+    /*
+     * If a rule matches in a rule set, the rest of the set should be
+     * skipped. However, rule sets matching against options may contain
+     * several legitimate rules, so they are processed entirely.
+     */
+    if (!(m->mapping.defined_mlvo_mask & (1 << MLVO_OPTION)))
+        m->mapping.skip = true;
 }
 
-/***====================================================================***/
+enum rules_state {
+    STATE_INITIAL = 0,
+    STATE_BANG,
+    STATE_GROUP_NAME,
+    STATE_GROUP_ELEMENT,
+    STATE_MAPPING_MLVO,
+    STATE_MAPPING_KCCGST,
+    STATE_RULE_MLVO_FIRST,
+    STATE_RULE_MLVO,
+    STATE_RULE_KCCGST,
+};
 
 static bool
-get_components(struct rules *rules, const struct xkb_rule_names *mlvo,
-               struct xkb_component_names *kccgst)
+matcher_match(struct matcher *m, const char *string, size_t len,
+              const char *file_name, struct xkb_component_names *out)
 {
-    struct multi_defs mdefs;
+    enum rules_token tok;
+    enum rules_state state = STATE_INITIAL;
+    union lvalue val;
 
-    memset(kccgst, 0, sizeof(*kccgst));
-
-    make_multi_defs(&mdefs, mlvo);
-
-    clear_partial_matches(rules);
-
-    apply_matching_rules(rules, &mdefs, kccgst, RULE_FLAG_NORMAL);
-    apply_partial_matches(rules, kccgst);
+    if (!m)
+        return false;
 
-    apply_matching_rules(rules, &mdefs, kccgst, RULE_FLAG_APPEND);
-    apply_partial_matches(rules, kccgst);
+    scanner_init(&m->scanner, m->ctx, string, len, file_name);
+    memset(&val, 0, sizeof(val));
 
-    apply_matching_rules(rules, &mdefs, kccgst, RULE_FLAG_OPTION);
-    apply_partial_matches(rules, kccgst);
+    while ((tok = lex(&m->scanner, &val, &m->loc)) != TOK_END_OF_FILE)
+    {
+        if (tok == TOK_ERROR)
+            goto error;
 
-    kccgst->keycodes = substitute_vars(kccgst->keycodes, &mdefs);
-    kccgst->symbols = substitute_vars(kccgst->symbols, &mdefs);
-    kccgst->types = substitute_vars(kccgst->types, &mdefs);
-    kccgst->compat = substitute_vars(kccgst->compat, &mdefs);
+        switch (state) {
+        case STATE_INITIAL:
+            switch (tok) {
+            case TOK_BANG:
+                state = STATE_BANG;
+                break;
+            case TOK_END_OF_LINE:
+                state = STATE_INITIAL;
+                break;
+            default:
+                goto state_error;
+            }
+            break;
 
-    free_multi_defs(&mdefs);
+        case STATE_BANG:
+            switch (tok) {
+            case TOK_GROUP_NAME:
+                matcher_group_start_new(m, val.string);
+                state = STATE_GROUP_NAME;
+                break;
+            case TOK_IDENTIFIER:
+                matcher_mapping_start_new(m);
+                matcher_mapping_set_mlvo(m, val.string);
+                state = STATE_MAPPING_MLVO;
+                break;
+            default:
+                goto state_error;
+            }
+            break;
 
-    if (!kccgst->keycodes || !kccgst->symbols || !kccgst->types ||
-        !kccgst->compat) {
-        free(kccgst->keycodes);
-        free(kccgst->symbols);
-        free(kccgst->types);
-        free(kccgst->compat);
-        return false;
-    }
-    return true;
-}
+        case STATE_GROUP_NAME:
+            switch (tok) {
+            case TOK_EQUALS:
+                state = STATE_GROUP_ELEMENT;
+                break;
+            default:
+                goto state_error;
+            }
+            break;
 
-static struct rules *
-load_rules(struct xkb_context *ctx, FILE *file)
-{
-    darray_char line;
-    struct mapping mapping;
-    struct rule trule;
-    struct group tgroup;
-    struct rules *rules;
-    struct stat stat_buf;
-    const char *buf, *end;
-    char *orig;
-    int fd = fileno(file);
+        case STATE_GROUP_ELEMENT:
+            switch (tok) {
+            case TOK_IDENTIFIER:
+                matcher_group_add_element(m, val.string);
+                state = STATE_GROUP_ELEMENT;
+                break;
+            case TOK_END_OF_LINE:
+                state = STATE_INITIAL;
+                break;
+            default:
+                goto state_error;
+            }
+            break;
 
-    if (fstat(fd, &stat_buf) != 0) {
-        log_err(ctx, "couldn't stat rules file\n");
-        return NULL;
-    }
+        case STATE_MAPPING_MLVO:
+            switch (tok) {
+            case TOK_IDENTIFIER:
+                if (!m->mapping.skip)
+                    matcher_mapping_set_mlvo(m, val.string);
+                state = STATE_MAPPING_MLVO;
+                break;
+            case TOK_EQUALS:
+                state = STATE_MAPPING_KCCGST;
+                break;
+            default:
+                goto state_error;
+            }
+            break;
 
-    orig = mmap(NULL, stat_buf.st_size, PROT_READ, MAP_SHARED, fd, 0);
-    if (!orig) {
-        log_err(ctx, "couldn't mmap rules file (%zu bytes)\n",
-                (size_t) stat_buf.st_size);
-        return NULL;
-    }
+        case STATE_MAPPING_KCCGST:
+            switch (tok) {
+            case TOK_IDENTIFIER:
+                if (!m->mapping.skip)
+                    matcher_mapping_set_kccgst(m, val.string);
+                state = STATE_MAPPING_KCCGST;
+                break;
+            case TOK_END_OF_LINE:
+                if (!m->mapping.skip)
+                    matcher_mapping_verify(m);
+                state = STATE_RULE_MLVO_FIRST;
+                break;
+            default:
+                goto state_error;
+            }
+            break;
 
-    rules = calloc(1, sizeof(*rules));
-    if (!rules)
-        return NULL;
-    darray_init(rules->rules);
-    darray_growalloc(rules->rules, 16);
-
-    memset(&mapping, 0, sizeof(mapping));
-    memset(&tgroup, 0, sizeof(tgroup));
-    darray_init(line);
-    darray_growalloc(line, 128);
-
-    buf = orig;
-    end = orig + stat_buf.st_size;
-    while ((buf = input_line_get(ctx, buf, end, &line))) {
-        if (match_line(ctx, &line, &mapping, &trule, &tgroup)) {
-            if (tgroup.number) {
-                darray_append(rules->groups, tgroup);
-                memset(&tgroup, 0, sizeof(tgroup));
+        case STATE_RULE_MLVO_FIRST:
+            if (tok == TOK_BANG) {
+                state = STATE_BANG;
+                break;
+            } else if (tok == TOK_END_OF_LINE) {
+                state = STATE_INITIAL;
+                break;
             }
-            else {
-                darray_append(rules->rules, trule);
-                memset(&trule, 0, sizeof(trule));
+            matcher_rule_start_new(m);
+            /* fallthrough */
+        case STATE_RULE_MLVO:
+            switch (tok) {
+            case TOK_IDENTIFIER:
+                if (!m->rule.skip)
+                    matcher_rule_set_mlvo(m, val.string);
+                state = STATE_RULE_MLVO;
+                break;
+            case TOK_STAR:
+                if (!m->rule.skip)
+                    matcher_rule_set_mlvo_wildcard(m);
+                state = STATE_RULE_MLVO;
+                break;
+            case TOK_GROUP_NAME:
+                if (!m->rule.skip)
+                    matcher_rule_set_mlvo_group(m, val.string);
+                state = STATE_RULE_MLVO;
+                break;
+            case TOK_EQUALS:
+                state = STATE_RULE_KCCGST;
+                break;
+            default:
+                goto state_error;
             }
-        }
+            break;
 
-        darray_resize(line, 0);
+        case STATE_RULE_KCCGST:
+            switch (tok) {
+            case TOK_IDENTIFIER:
+                if (!m->rule.skip)
+                    matcher_rule_set_kccgst(m, val.string);
+                state = STATE_RULE_KCCGST;
+                break;
+            case TOK_END_OF_LINE:
+                if (!m->rule.skip)
+                    matcher_rule_verify(m);
+                if (!m->rule.skip)
+                    matcher_rule_apply_if_matches(m);
+                state = STATE_RULE_MLVO_FIRST;
+                break;
+            default:
+                goto state_error;
+            }
+            break;
+        }
     }
 
-    munmap(orig, stat_buf.st_size);
+    if (darray_empty(m->kccgst[KCCGST_KEYCODES]) ||
+        darray_empty(m->kccgst[KCCGST_TYPES]) ||
+        darray_empty(m->kccgst[KCCGST_COMPAT]) ||
+        /* darray_empty(m->kccgst[KCCGST_GEOMETRY]) || */
+        darray_empty(m->kccgst[KCCGST_SYMBOLS]))
+        goto error;
 
-    darray_free(line);
-    return rules;
-}
+    out->keycodes = darray_mem(m->kccgst[KCCGST_KEYCODES], 0);
+    out->types = darray_mem(m->kccgst[KCCGST_TYPES], 0);
+    out->compat = darray_mem(m->kccgst[KCCGST_COMPAT], 0);
+    /* out->geometry = darray_mem(m->kccgst[KCCGST_GEOMETRY], 0); */
+    darray_free(m->kccgst[KCCGST_GEOMETRY]);
+    out->symbols = darray_mem(m->kccgst[KCCGST_SYMBOLS], 0);
 
-static void
-free_rules(struct rules *rules)
-{
-    struct rule *rule;
-    struct group *group;
-
-    if (!rules)
-        return;
-
-    darray_foreach(rule, rules->rules) {
-        free(rule->model);
-        free(rule->layout);
-        free(rule->variant);
-        free(rule->option);
-        free(rule->keycodes);
-        free(rule->symbols);
-        free(rule->types);
-        free(rule->compat);
-    }
-    darray_free(rules->rules);
-
-    darray_foreach(group, rules->groups) {
-        free(group->name);
-        free(group->words);
-    }
-    darray_free(rules->groups);
+    return true;
 
-    free(rules);
+state_error:
+    matcher_error1(m, "unexpected token");
+error:
+    return false;
 }
 
 bool
@@ -1085,10 +1200,13 @@ xkb_components_from_rules(struct xkb_context *ctx,
                           struct xkb_component_names *out)
 {
     bool ret = false;
-    struct rules *rules;
     FILE *file;
     char *path;
     char **include;
+    int fd;
+    struct stat stat_buf;
+    char *string;
+    struct matcher *matcher;
 
     file = FindFileInXkbPath(ctx, rmlvo->rules, FILE_TYPE_RULES, &path);
     if (!file) {
@@ -1102,26 +1220,33 @@ xkb_components_from_rules(struct xkb_context *ctx,
                 darray_size(ctx->failed_includes));
         darray_foreach(include, ctx->failed_includes)
             log_err(ctx, "\t%s\n", *include);
-        return false;
+        goto err_out;
     }
 
-    rules = load_rules(ctx, file);
-    if (!rules) {
-        log_err(ctx, "Failed to load XKB rules \"%s\"\n", path);
+    fd = fileno(file);
+
+    if (fstat(fd, &stat_buf) != 0) {
+        log_err(ctx, "Couldn't stat rules file\n");
         goto err_file;
     }
 
-    if (!get_components(rules, rmlvo, out)) {
-        log_err(ctx, "No components returned from XKB rules \"%s\"\n", path);
-        goto err_rules;
+    string = mmap(NULL, stat_buf.st_size, PROT_READ, MAP_SHARED, fd, 0);
+    if (!string) {
+        log_err(ctx, "Couldn't mmap rules file (%zu bytes)\n",
+                (size_t) stat_buf.st_size);
+        goto err_file;
     }
 
-    ret = true;
+    matcher = matcher_new(ctx, rmlvo);
+    ret = matcher_match(matcher, string, stat_buf.st_size, rmlvo->rules, out);
+    if (!ret)
+        log_err(ctx, "No components returned from XKB rules \"%s\"\n", path);
+    matcher_free(matcher);
 
-err_rules:
-    free_rules(rules);
+    munmap(string, stat_buf.st_size);
 err_file:
     free(path);
     fclose(file);
+err_out:
     return ret;
 }