e [foo.bar]
    size_t index = key.find(".");
    if (index == std::string::npos)
    {
        if (!has(key))
        {
            if (!create)
            {
                printf("invalid map key: %s\n", key.c_str());
                throw key_error("invalid map key");
            }
            data[key] = make_data("");
        }
        return operator[](key);
    }

    std::string sub_key = key.substr(0, index);
    if (!has(sub_key))
    {
        printf("invalid map key: %s\n", sub_key.c_str());
        throw key_error("invalid map key");
    }

    return operator[](sub_key)->getmap().parse_path(key.substr(index + 1), create);
}

void dump_data(data_ptr data)
{
    data->dump();
}

namespace impl
{
std::string indent(int level)
{
    std::string result;
    while (level--)
    {
        result += "    ";
    }
    return result;
}

Token TokenIterator::s_endToken(END_TOKEN);

const Token *TokenIterator::get() const
{
    if (is_valid())
    {
        return &m_tokens[m_index];
    }
    else
    {
        return &s_endToken;
    }
}

const Token *TokenIterator::next()
{
    if (m_index < size())
    {
        ++m_index;
    }
    return get();
}

const Token *TokenIterator::match(TokenType tokenType, const char *failure_msg)
{
    const Token *result = get();
    if (result->get_type()!= tokenType)
    {
        throw TemplateException(failure_msg? failure_msg : "unexpected token");
    }
    next();
    return result;
}

TokenIterator &TokenIterator::operator++()
{
    next();
    return *this;
}

inline bool is_key_path_char(char c)
{
    return (isalnum(c) || c == '.' || c == '_');
}

const KeywordDef k_keywords[] = { { TRUE_TOKEN, "true" },     { FALSE_TOKEN, "false" }, { FOR_TOKEN, "for" },
                                  { IN_TOKEN, "in" },         { IF_TOKEN, "if" },       { ELIF_TOKEN, "elif" },
                                  { ELSE_TOKEN, "else" },     { DEF_TOKEN, "def" },     { SET_TOKEN, "set" },
                                  { ENDFOR_TOKEN, "endfor" }, { ENDIF_TOKEN, "endif" }, { ENDDEF_TOKEN, "enddef" },
                                  { AND_TOKEN, "and" },       { OR_TOKEN, "or" },       { NOT_TOKEN, "not" },
                                  { INVALID_TOKEN } };

TokenType get_keyword_token(const std::string &s)
{
    const KeywordDef *k = k_keywords;
    for (; k->tok!= INVALID_TOKEN; ++k)
    {
        if (s == k->keyword)
        {
            return k->tok;
        }
    }
    return INVALID_TOKEN;
}

void create_id_token(token_vector &tokens, const std::string &s)
{
    TokenType t = get_keyword_token(s);
    if (t == INVALID_TOKEN)
    {
        // Create key path token.
        tokens.emplace_back(KEY_PATH_TOKEN, s);
    }
    else
    {
        tokens.emplace_back(t);
    }
}

int append_string_escape(std::string &str, std::function<char(unsigned)> peek)
{
    char esc = peek(1);
    std::string hex;
    int n = 2;
    switch (esc)
    {
        // standard C escape sequences
        case 'a':
            esc = '\a';
            break;
        case 'b':
            esc = '\b';
            break;
        case 'f':
            esc = '\f';
            break;
        case 'n':
            esc = '\n';
            break;
        case 'r':
            esc = '\r';
            break;
        case 't':
            esc = '\t';
            break;
        case 'v':
            esc = '\v';
            break;
        case '0':
            esc = '\0';
            break;
        // handle hex escapes like \x2a
        case 'x':
            for (; std::isxdigit(peek(n)); ++n)
            {
                hex += peek(n);
            }
            esc = static_cast<char>(hex.empty()? 0 : std::strtoul(hex.c_str(), nullptr, 16));
            break;
    }
    str += esc;
    return n - 1;
}

token_vector tokenize_statement(const std::string &text)
{
    token_vector tokens;
    lexer_state_t state = INIT_STATE;
    unsigned i = 0;
    uint32_t pos = 0;
    char str_open_quote = 0;
    std::string literal;
    bool is_hex_literal = false;

    // closure to get the next char without advancing
    auto peek = [&](unsigned n)
    {
        return i + n < text.size()? text[i + n] : 0;
    };

    for (; i < text.size(); ++i)
    {
        char c = text[i];

        switch (state)
        {
            case INIT_STATE:
                // Skip any whitespace
                if (isspace(c))
                {
                    continue;
                }
                else if (isdigit(c))
                {
                    literal = c;
                    if (c == '0' && peek(1) == 'x')
                    {
                        literal += "x";
                        is_hex_literal = true;
                        ++i;
                    }
                    else
                    {
                        is_hex_literal = false;
                    }
                    state = INT_LITERAL_STATE;
                }
                else if (is_key_path_char(c))
                {
                    pos = i;
                    state = KEY_PATH_STATE;
                }
                else if (c == '(' || c == ')' || c == ',' || c == '+' || c == '*' || c == '/' || c == '%')
                {
                    tokens.emplace_back(static_cast<TokenType>(c));
                }
                else if (c == '\"' || c == '\'')
                {
                    str_open_quote = c;
                    literal.clear();
                    state = STRING_LITERAL_STATE;
                }
                else if (c == '=')
                {
                    if (peek(1) == '=')
                    {
                        tokens.emplace_back(EQ_TOKEN);
                        ++i;
                    }
                    else
                    {
                        tokens.emplace_back(ASSIGN_TOKEN);
                    }
                }
                else if (c == '>')
                {
                    if (peek(1) == '=')
                    {
                        tokens.emplace_back(GE_TOKEN);
                        ++i;
                    }
                    else
                    {
                        tokens.emplace_back(GT_TOKEN);
                    }
                }
                else if (c == '<')
                {
                    if (peek(1) == '=')
                    {
                        tokens.emplace_back(LE_TOKEN);
                        ++i;
                    }
                    else
                    {
                        tokens.emplace_back(LT_TOKEN);
                    }
                }
                else if (c == '!')
                {
                    if (peek(1) == '=')
                    {
                        tokens.emplace_back(NEQ_TOKEN);
                        ++i;
                    }
                    else
                    {
                        tokens.emplace_back(NOT_TOKEN);
                    }
                }
                else if (c == '&')
                {
                    if (peek(1) == '&')
                    {
                        tokens.emplace_back(AND_TOKEN);
