#include #include #include #include #include "tinytemplate.h" /* Configurations */ // When enabled, the compiler dumps the location of // the calls to [next_token] and [peek_token] to // stderr. This is very useful when debugging. // // #define TINYTEMPLATE_TRACE_TOKENS /* Some definitions to help with readability */ #define DONE TINYTEMPLATE_STATUS_DONE #define ESYMBOL TINYTEMPLATE_STATUS_ESYMBOL #define ESCOPE TINYTEMPLATE_STATUS_ESCOPE #define EDEPTH TINYTEMPLATE_STATUS_EDEPTH #define ETYPE TINYTEMPLATE_STATUS_ETYPE #define EITER TINYTEMPLATE_STATUS_EITER #define EMEMORY TINYTEMPLATE_STATUS_EMEMORY #define ESYNTAX TINYTEMPLATE_STATUS_ESYNTAX #define ESEMANT TINYTEMPLATE_STATUS_ESEMANT #define instr_t tinytemplate_instr_t #define status_t tinytemplate_status_t /* Utilities */ #define NOT_IMPLEMENTED_YET assert(0) /* NOPE - No effect DONE - Conclude execution PUSHI - Push integer on the evaluation stack PUSHF - Push float on the evaluation stack PUSHS - Push string on the evaluation stack PUSHV - Push value of a variable on the evaluation stack JUMP - Jump to a given instruction of the program JCND - Jump to a given instruction of the program if a condition is verified WRITE - Write to output a string WRTOP - Write to output the top of the evaluation stack POP - Pop a value from the evaluation stack ADD - Pop the top 2 value of the evaluation stack and push their sum SUB - Same as ADD, but with subtraction MUL - Same as ADD, but with multiplication DIV - Same as ADD, but with division MOD - Same as ADD, but with the remainder of division */ typedef enum { OPCODE_NOPE, OPCODE_DONE, OPCODE_PUSHI, OPCODE_PUSHF, OPCODE_PUSHS, OPCODE_PUSHV, OPCODE_JUMP, OPCODE_JCND, OPCODE_WRITE, OPCODE_WRTOP, OPCODE_POP, OPCODE_ADD, OPCODE_SUB, OPCODE_MUL, OPCODE_DIV, OPCODE_MOD, OPCODE_NEG, OPCODE_GETS, OPCODE_ITER, OPCODE_NEXT, OPCODE_CHLD, OPCODE_IDX, } opcode_t; // Represents a substring of the template typedef struct { size_t offset; size_t length; } slice_t; typedef enum { SCOPE_IF, SCOPE_IF_ELSE, SCOPE_FOR, } scope_type_t; typedef struct { scope_type_t type; size_t if_jcnd; size_t if_jump; size_t for_next; slice_t for_child_label; slice_t for_index_label; } scope_t; typedef struct { scope_t scope_stack[TINYTEMPLATE_MAX_SCOPE_DEPTH]; size_t scope_depth; instr_t *program; size_t num_instr; size_t max_instr; bool failed; } compile_state_t; typedef struct { const char *src; size_t cur, len; } scanner_t; typedef union { int64_t as_int; double as_float; } token_payload_t; typedef enum { TOKEN_OPER_ADD = '+', // These are defined to avoid TOKEN_OPER_SUB = '-', // compiler warnings. All ASCII TOKEN_OPER_MUL = '*', // values are assumed to be TOKEN_OPER_DIV = '/', // valid "token_t"s. TOKEN_END = 128, TOKEN_IDENT, TOKEN_NONASCII, TOKEN_NONPRINT, TOKEN_VALUE_INT, TOKEN_VALUE_FLOAT, TOKEN_KWORD_IF, TOKEN_KWORD_IN, TOKEN_KWORD_FOR, TOKEN_KWORD_ELSE, TOKEN_KWORD_END, TOKEN_OPER_MOD, } token_t; typedef struct { size_t max; char *dst; } error_t; static void report(error_t *error, const char *fmt, ...) { if (error->dst) { va_list args; va_start(args, fmt); vsnprintf(error->dst, error->max, fmt, args); va_end(args); } } static void append_instr(compile_state_t *state, opcode_t opcode, ...) { if (state->failed) return; if (state->num_instr == state->max_instr) { state->failed = true; return; } va_list operands; va_start(operands, opcode); tinytemplate_instr_t *instr = &state->program[state->num_instr++]; instr->opcode = opcode; switch (opcode) { case OPCODE_ITER: case OPCODE_CHLD: case OPCODE_IDX: break; case OPCODE_NEXT: instr->operands[0].as_size = va_arg(operands, size_t); break; case OPCODE_NOPE: case OPCODE_DONE: break; case OPCODE_PUSHI: instr->operands[0].as_int = va_arg(operands, int64_t); break; case OPCODE_PUSHF: instr->operands[0].as_float = va_arg(operands, double); break; case OPCODE_GETS: case OPCODE_PUSHV: case OPCODE_PUSHS: instr->operands[0].as_size = va_arg(operands, size_t); instr->operands[1].as_size = va_arg(operands, size_t); break; case OPCODE_JUMP: case OPCODE_JCND: instr->operands[0].as_size = va_arg(operands, size_t); break; case OPCODE_WRITE: instr->operands[0].as_size = va_arg(operands, size_t); instr->operands[1].as_size = va_arg(operands, size_t); break; case OPCODE_WRTOP: case OPCODE_POP: case OPCODE_ADD: case OPCODE_SUB: case OPCODE_MUL: case OPCODE_DIV: case OPCODE_MOD: case OPCODE_NEG: break; } va_end(operands); } static bool is_alpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } static bool is_digit(char c) { return c >= '0' && c <= '9'; } static bool is_space(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } static bool is_ascii(char c) { return !((unsigned char) c & (1 << 7)); } static bool is_printable(char c) { return (unsigned char) c >= 32 && (unsigned char) c <= 126; } static token_t next_token_int(scanner_t *scanner, slice_t *slice, token_payload_t *payload) { assert(scanner->cur < scanner->len && is_digit(scanner->src[scanner->cur])); size_t offset = scanner->cur; int64_t buf = 0; do { int d = scanner->src[scanner->cur++] - '0'; if (buf > (INT64_MAX - d) / 10) { // Overflow! buf = INT64_MAX; break; } buf = buf * 10 + d; } while (scanner->cur < scanner->len && is_digit(scanner->src[scanner->cur])); if (slice) { slice->offset = offset; slice->length = scanner->cur - offset; } if (payload) payload->as_int = buf; return TOKEN_VALUE_INT; } static token_t next_token_float(scanner_t *scanner, slice_t *slice, token_payload_t *payload) { // The caller made sure that follows a float: // a string of digits followed by a dot and // another string of digits. assert(scanner->cur < scanner->len && is_digit(scanner->src[scanner->cur])); size_t offset = scanner->cur; double buf = 0; do { int d = scanner->src[scanner->cur++] - '0'; buf = buf * 10 + d; } while (scanner->src[scanner->cur] != '.'); scanner->cur++; // Skip the "." double q = 1; do { q /= 10; int d = scanner->src[scanner->cur++] - '0'; buf += d * q; } while (scanner->cur < scanner->len && is_digit(scanner->src[scanner->cur])); if (slice) { slice->offset = offset; slice->length = scanner->cur - offset; } if (payload) payload->as_float = buf; return TOKEN_VALUE_FLOAT; } static token_t next_token_numeric(scanner_t *scanner, slice_t *slice, token_payload_t *payload) { // Scanner points to a digit assert(scanner->cur < scanner->len && is_digit(scanner->src[scanner->cur])); // Is it a float or an int? Check if at the end // of the first sequence of digit there's a dot // followed by a digit. size_t cur = scanner->cur; while (cur < scanner->len && is_digit(scanner->src[cur])) cur++; if (cur+1 < scanner->len && scanner->src[cur] == '.' && is_digit(scanner->src[cur+1])) return next_token_float(scanner, slice, payload); else return next_token_int(scanner, slice, payload); } static token_t next_token_kword_or_ident(scanner_t *scanner, slice_t *slice, token_payload_t *payload) { (void) payload; assert(scanner->cur < scanner->len && is_alpha(scanner->src[scanner->cur])); size_t offset = scanner->cur; do scanner->cur++; while (scanner->cur < scanner->len && is_alpha(scanner->src[scanner->cur])); size_t length = scanner->cur - offset; if (slice) { slice->offset = offset; slice->length = length; } switch (length) { case 2: // if, in if (!strncmp(scanner->src + offset, "if", length)) return TOKEN_KWORD_IF; if (!strncmp(scanner->src + offset, "in", length)) return TOKEN_KWORD_IN; break; case 3: // end, mod, for if (!strncmp(scanner->src + offset, "end", length)) return TOKEN_KWORD_END; if (!strncmp(scanner->src + offset, "mod", length)) return TOKEN_OPER_MOD; if (!strncmp(scanner->src + offset, "for", length)) return TOKEN_KWORD_FOR; break; case 4: // else if (!strncmp(scanner->src + offset, "else", length)) return TOKEN_KWORD_ELSE; break; } return TOKEN_IDENT; } static token_t next_token(scanner_t *scanner, slice_t *slice, token_payload_t *payload) { while (scanner->cur < scanner->len && is_space(scanner->src[scanner->cur])) scanner->cur++; if (scanner->cur == scanner->len) { if (slice) { slice->offset = scanner->cur; slice->length = 0; } return TOKEN_END; } char c = scanner->src[scanner->cur]; if (is_digit(c)) return next_token_numeric(scanner, slice, payload); if (is_alpha(c)) return next_token_kword_or_ident(scanner, slice, payload); if (!is_ascii(c)) { size_t offset = scanner->cur; do scanner->cur++; while (scanner->cur < scanner->len && !is_ascii(scanner->src[scanner->cur])); if (slice) { slice->offset = offset; slice->length = scanner->cur - offset; } return TOKEN_NONASCII; } if (!is_printable(c)) { size_t offset = scanner->cur; do scanner->cur++; while (scanner->cur < scanner->len && !is_printable(scanner->src[scanner->cur])); if (slice) { slice->offset = offset; slice->length = scanner->cur - offset; } return TOKEN_NONPRINT; } size_t offset = scanner->cur; scanner->cur++; if (slice) { slice->offset = offset; slice->length = scanner->cur - offset; } return (token_t) c; } static token_t peek_token(scanner_t *scanner, slice_t *slice, token_payload_t *payload) { size_t cur = scanner->cur; token_t token = next_token(scanner, slice, payload); scanner->cur = cur; return token; } #ifdef TINYTEMPLATE_TRACE_TOKENS static token_t trace_next_token(const char *c_func, const char *c_file, int c_line, scanner_t *scanner, slice_t *slice, token_payload_t *payload) { slice_t maybe; if (slice == NULL) slice = &maybe; token_t token = next_token(scanner, slice, payload); fprintf(stderr, "NEXT TOKEN [%.*s] @ %s in %s:%d\n", (int) slice->length, scanner->src + slice->offset, c_func, c_file, c_line); return token; } static token_t trace_peek_token(const char *c_func, const char *c_file, int c_line, scanner_t *scanner, slice_t *slice, token_payload_t *payload) { slice_t maybe; if (slice == NULL) slice = &maybe; token_t token = peek_token(scanner, slice, payload); fprintf(stderr, "PEEK TOKEN [%.*s] @ %s in %s:%d\n", (int) slice->length, scanner->src + slice->offset, c_func, c_file, c_line); return token; } #define next_token(scanner, slice, payload) trace_next_token(__func__, __FILE__, __LINE__, scanner, slice, payload) #define peek_token(scanner, slice, payload) trace_peek_token(__func__, __FILE__, __LINE__, scanner, slice, payload) #endif static status_t parse_primary(scanner_t *scanner, compile_state_t *state, error_t *error) { slice_t slice; token_payload_t payload; token_t token = next_token(scanner, &slice, &payload); switch (token) { case TOKEN_VALUE_INT: append_instr(state, OPCODE_PUSHI, payload.as_int); break; case TOKEN_VALUE_FLOAT: append_instr(state, OPCODE_PUSHF, payload.as_float); break; case TOKEN_IDENT: { // If the identifier refers to an iteration label, // than push it. Otherwise assume is a template // parameter provided by the caller program during // evaluation. bool found = false; for (size_t i = 0, j = 0; i < state->scope_depth; i++) { scope_t *scope = &state->scope_stack[state->scope_depth-i-1]; if (scope->type == SCOPE_FOR) { // Check if the label matches the iteration label if (slice.length == scope->for_child_label.length && !memcmp(scanner->src + slice.offset, scanner->src + scope->for_child_label.offset, slice.length)) { append_instr(state, OPCODE_CHLD, slice.offset, slice.length); found = true; break; } // Check if the label matches the index label. // If no iteration label was specified, then // its length will be 0 and the expression's // label won't match. if (slice.length == scope->for_index_label.length && !memcmp(scanner->src + slice.offset, scanner->src + scope->for_index_label.offset, slice.length)) { append_instr(state, OPCODE_IDX, slice.offset, slice.length); found = true; break; } j++; } } if (!found) // Label doesn't refer to an iteration append_instr(state, OPCODE_PUSHV, slice.offset, slice.length); break; } default: report(error, "Bad token [%.*s] in primary expression", (int) slice.length, scanner->src + slice.offset); return ESYNTAX; } return DONE; } static status_t parse_suffix(scanner_t *scanner, compile_state_t *state, error_t *error) { status_t status; status = parse_primary(scanner, state, error); if (status != DONE) return status; token_t suffix = peek_token(scanner, NULL, NULL); if (suffix == '.') { // Offset of the dot token size_t checkpoint = scanner->cur; // Consume the dot next_token(scanner, NULL, NULL); slice_t slice; token_t key = next_token(scanner, &slice, NULL); if (key == TOKEN_IDENT) append_instr(state, OPCODE_GETS, slice.offset, slice.length); else scanner->cur = checkpoint; } return DONE; } static bool parse_prefix(scanner_t *scanner, compile_state_t *state, error_t *error) { status_t status; token_t prefix = peek_token(scanner, NULL, NULL); if (prefix == '+' || prefix == '-') { next_token(scanner, NULL, NULL); status = parse_prefix(scanner, state, error); if (status != DONE) return status; if (prefix == '-') append_instr(state, OPCODE_NEG); return DONE; } return parse_suffix(scanner, state, error); } static int precedence_of(token_t token) { switch (token) { case '+': return 1; case '-': return 1; case '*': return 2; case '/': return 2; case TOKEN_OPER_MOD: return 2; default: break; } return 0; } static bool is_binary_operator(token_t token) { return precedence_of(token) > 0; } static bool is_right_associative(token_t token) { (void) token; return false; } static bool can_continue_climbing(scanner_t *scanner, int min_precedence) { token_t peek = peek_token(scanner, NULL, NULL); return is_binary_operator(peek) && precedence_of(peek) >= min_precedence; } static bool should_associate_right(scanner_t *scanner, token_t oper, token_t *peek) { *peek = peek_token(scanner, NULL, NULL); return is_binary_operator(*peek) && (precedence_of(*peek) > precedence_of(oper) || (precedence_of(*peek) == precedence_of(oper) && is_right_associative(*peek))); } static opcode_t operator_opcode(token_t token) { switch (token) { case '+': return OPCODE_ADD; case '-': return OPCODE_SUB; case '*': return OPCODE_MUL; case '/': return OPCODE_DIV; case TOKEN_OPER_MOD: return OPCODE_MOD; default: break; } return OPCODE_ADD; } static status_t parse_expr_2(scanner_t *scanner, compile_state_t *state, int min_precedence, error_t *error) { status_t status; while (can_continue_climbing(scanner, min_precedence)) { token_t oper = next_token(scanner, NULL, NULL); if ((status = parse_prefix(scanner, state, error)) != DONE) return status; token_t peek; while (should_associate_right(scanner, oper, &peek)) { int precedence = precedence_of(oper) + (precedence_of(peek) > precedence_of(oper)); if ((status = parse_expr_2(scanner, state, precedence, error)) != DONE) return status; } append_instr(state, operator_opcode(oper)); } return DONE; } static bool parse_expr(scanner_t *scanner, compile_state_t *state, error_t *error) { status_t status; if ((status = parse_prefix(scanner, state, error)) != DONE) return status; return parse_expr_2(scanner, state, 0, error); } static status_t expr_block(scanner_t *scanner, compile_state_t *state, error_t *error) { scanner->cur += 2; // Skip the "{{" status_t status = parse_expr(scanner, state, error); if (status != DONE) return status; append_instr(state, OPCODE_WRTOP); append_instr(state, OPCODE_POP, 1); if (scanner->cur+1 < scanner->len && scanner->src[scanner->cur+0] == '}' && scanner->src[scanner->cur+1] == '}') scanner->cur += 2; else { report(error, "No closing [}}] after expression block"); return ESYNTAX; } return DONE; } static status_t close_construct(scanner_t *scanner, error_t *error, const char *block_name) { // Consume the following "%}" while (scanner->cur < scanner->len && is_space(scanner->src[scanner->cur])) scanner->cur++; if (scanner->cur+1 < scanner->len && scanner->src[scanner->cur] == '%' && scanner->src[scanner->cur+1] == '}') scanner->cur += 2; else { report(error, "Missing closing %%} in {%% %s %%} block", block_name); return ESYNTAX; } return DONE; } static status_t selection_construct_start(scanner_t *scanner, compile_state_t *state, error_t *error) { status_t status; // This function is called after "{% if" is parsed assert(scanner->src[scanner->cur-2] == 'i' && scanner->src[scanner->cur-1] == 'f'); if (state->scope_depth == TINYTEMPLATE_MAX_SCOPE_DEPTH) { report(error, "Scope depth limit reached"); return ESCOPE; } if ((status = parse_expr(scanner, state, error)) != DONE) return status; if ((status = close_construct(scanner, error, "if")) != DONE) return status; size_t if_jcnd = state->num_instr; append_instr(state, OPCODE_JCND, 0); state->scope_stack[state->scope_depth].type = SCOPE_IF; state->scope_stack[state->scope_depth].if_jcnd = if_jcnd; state->scope_depth++; return DONE; } static status_t iteration_construct_start(scanner_t *scanner, compile_state_t *state, error_t *error) { // This function is called after "{% for" is parsed assert(scanner->src[scanner->cur-3] == 'f' && scanner->src[scanner->cur-2] == 'o' && scanner->src[scanner->cur-1] == 'r'); if (state->scope_depth == TINYTEMPLATE_MAX_SCOPE_DEPTH) { report(error, "Scope depth limit reached"); return ESCOPE; } slice_t child_label; if (next_token(scanner, &child_label, NULL) != TOKEN_IDENT) { report(error, "Missing iteration label"); return ESYNTAX; } slice_t index_label; if (peek_token(scanner, NULL, NULL) == ',') { next_token(scanner, NULL, NULL); // Consume the comma if (next_token(scanner, &index_label, NULL) != TOKEN_IDENT) { report(error, "Missing iteration index label after [,]"); return ESYNTAX; } } else index_label = (slice_t) {0, 0}; if (next_token(scanner, NULL, NULL) != TOKEN_KWORD_IN) { report(error, "Missing [in] keyword after iteration label"); return ESYNTAX; } status_t status; if ((status = parse_expr(scanner, state, error)) != DONE) return status; append_instr(state, OPCODE_ITER); size_t for_next = state->num_instr; append_instr(state, OPCODE_NEXT, 0); if ((status = close_construct(scanner, error, "for")) != DONE) return status; state->scope_stack[state->scope_depth].type = SCOPE_FOR; state->scope_stack[state->scope_depth].for_child_label = child_label; state->scope_stack[state->scope_depth].for_index_label = index_label; state->scope_stack[state->scope_depth].for_next = for_next; state->scope_depth++; return DONE; } static void resolve_scope(compile_state_t *state) { assert(state->scope_depth > 0); scope_t *scope = state->scope_stack + state->scope_depth - 1; switch (scope->type) { // Useful for all cases but it changes // meaning for each one. tinytemplate_instr_t *instr; case SCOPE_FOR: append_instr(state, OPCODE_JUMP, scope->for_next); instr = state->program + scope->for_next; instr->operands[0].as_size = state->num_instr; break; case SCOPE_IF: instr = state->program + scope->if_jcnd; instr->operands[0].as_size = state->num_instr; break; case SCOPE_IF_ELSE: instr = state->program + scope->if_jump; instr->operands[0].as_size = state->num_instr; break; } state->scope_depth--; } static status_t construct_end(scanner_t *scanner, compile_state_t *state, error_t *error) { if (state->scope_depth == 0) { report(error, "Orphan {%% end %%} block"); return ESEMANT; } status_t status = close_construct(scanner, error, "end"); if (status != DONE) return status; resolve_scope(state); return DONE; } static status_t construct_else(scanner_t *scanner, compile_state_t *state, error_t *error) { if (state->scope_depth == 0) { report(error, "Orphan {%% else %%} block"); return ESEMANT; } status_t status = close_construct(scanner, error, "else"); if (status != DONE) return status; scope_t *scope = state->scope_stack + state->scope_depth - 1; switch (scope->type) { tinytemplate_instr_t *instr; case SCOPE_IF: scope->if_jump = state->num_instr; append_instr(state, OPCODE_JUMP, 0); instr = state->program + scope->if_jcnd; instr->operands[0].as_size = state->num_instr; scope->type = SCOPE_IF_ELSE; break; case SCOPE_IF_ELSE: report(error, "Duplicate {%% else %%} case"); return ESEMANT; case SCOPE_FOR: report(error, "Bad {%% else %%} coupled with {%% for .. %%}"); return ESEMANT; } return DONE; } static status_t control_flow_block(scanner_t *scanner, compile_state_t *state, error_t *error) { scanner->cur += 2; // Skip the "{%" slice_t slice; switch (next_token(scanner, &slice, NULL)) { case TOKEN_KWORD_IF: return selection_construct_start(scanner, state, error); case TOKEN_KWORD_FOR: return iteration_construct_start(scanner, state, error); case TOKEN_KWORD_END: return construct_end(scanner, state, error); case TOKEN_KWORD_ELSE: return construct_else(scanner, state, error); default: report(error, "Bad token [%.*s] after [{%%]", (int) slice.length, scanner->src + slice.offset); return ESYNTAX; } return DONE; } status_t tinytemplate_compile(const char *src, size_t len, instr_t *program, size_t max_instr, size_t *num_instr, char *errmsg, size_t errmax) { error_t error = { .dst=errmsg, .max=errmax, }; scanner_t scanner = { .src=src, .len=len, .cur=0 }; compile_state_t state = { .program=program, .max_instr=max_instr, .num_instr=0, .failed=false }; status_t status; while (scanner.cur < scanner.len) { // A program is a sequence of alternating raw text blocks and // blocks enclosed in "{%" and "%}" or "{{" and "}}". So for // each iteration we scan through a block of raw text and then // a "{{ .. }}"/"{% .. %}" block. size_t raw_off = scanner.cur; // Start offset of the raw block while (scanner.cur < scanner.len) { // Look for a "{" or the end while (scanner.cur < scanner.len && scanner.src[scanner.cur] != '{') scanner.cur++; // If the end wasn't reached (a "{" was found) // then exit if the following character is a // "{" or "%". If it isn't, then skip the first // "{" and continue scanning for the next "{" // by starting a new iteration. if (scanner.cur < scanner.len) { assert(scanner.src[scanner.cur] == '{'); if (scanner.cur+1 < scanner.len && (scanner.src[scanner.cur+1] == '{' || scanner.src[scanner.cur+1] == '%')) break; scanner.cur++; // Consume the "{" } } size_t raw_len = scanner.cur - raw_off; // Length of the raw block if (raw_len > 0) // The raw block isn't empty append_instr(&state, OPCODE_WRITE, raw_off, raw_len); if (scanner.cur < scanner.len) { assert(scanner.cur+1 < scanner.len && scanner.src[scanner.cur] == '{' && (scanner.src[scanner.cur+1] == '{' || scanner.src[scanner.cur+1] == '%')); if (scanner.src[scanner.cur+1] == '{') status = expr_block(&scanner, &state, &error); else { assert(scanner.src[scanner.cur+1] == '%'); status = control_flow_block(&scanner, &state, &error); } if (status != DONE) return status; } } // Close all pending scoped that were // waiting for a {% end %} block. while (state.scope_depth > 0) resolve_scope(&state); append_instr(&state, OPCODE_DONE); if (state.failed) { report(&error, "Out of template program memory"); return EMEMORY; } if (num_instr) *num_instr = state.num_instr; return DONE; } static bool value_can_be_considered_true(tinytemplate_type_t type, tinytemplate_value_t value) { switch (type) { case TINYTEMPLATE_TYPE_INT: return value.as_int != 0; case TINYTEMPLATE_TYPE_FLOAT: return value.as_float != 0; default: return false; } } typedef struct { tinytemplate_array_t iter; tinytemplate_value_t child; tinytemplate_type_t ctype; size_t next_index; } iter_state_t; status_t tinytemplate_eval(const char *src, const instr_t *program, void *userp, tinytemplate_getter_t params, tinytemplate_callback_t callback, char *errmsg, size_t errmax) { error_t error = {.dst=errmsg, .max=errmax}; iter_state_t iter_stack[TINYTEMPLATE_MAX_ITER_DEPTH]; size_t iter_depth = 0; tinytemplate_type_t types[TINYTEMPLATE_MAX_EXPR_DEPTH]; tinytemplate_value_t stack[TINYTEMPLATE_MAX_EXPR_DEPTH]; size_t stack_depth = 0; bool done = false; int index = 0; while (!done) { const tinytemplate_instr_t *instr = &program[index++]; switch (instr->opcode) { case OPCODE_NOPE: /* Do nothing */ break; case OPCODE_DONE: done = true; break; case OPCODE_ITER: assert(stack_depth > 0); // ITER excepts a value on the stack // as an iteration target if (types[stack_depth-1] != TINYTEMPLATE_TYPE_ARRAY) { report(&error, "Iteration on something other than an array"); return ETYPE; } if (iter_depth == TINYTEMPLATE_MAX_ITER_DEPTH) { report(&error, "Maximum nested iteration limit reached"); return EITER; } iter_stack[iter_depth].iter = stack[--stack_depth].as_array; iter_stack[iter_depth].next_index = 0; iter_depth++; break; case OPCODE_NEXT: { assert(iter_depth > 0); // The NEXT instruction can't // be run outside of an iteration. iter_state_t *top_iter = &iter_stack[iter_depth-1]; tinytemplate_type_t type; tinytemplate_value_t value; if (top_iter->iter.next(top_iter->iter.data, &type, &value)) { top_iter->child = value; top_iter->ctype = type; top_iter->next_index++; } else { index = instr->operands[0].as_size; iter_depth--; } break; } case OPCODE_CHLD: { if (stack_depth == TINYTEMPLATE_MAX_EXPR_DEPTH) { report(&error, "Evaluation stack limit reached"); return EDEPTH; } size_t iter_idx = instr->operands[0].as_size; assert(iter_depth > 0); // The CHLD instruction can't // be run outside of an iteration. assert(iter_idx < iter_depth); // The index of the iteration must // refer to one of the active ones. iter_state_t *iter = &iter_stack[iter_depth-iter_idx-1]; assert(iter->next_index > 0); // CHLD can only be executed after // the iteration started. stack[stack_depth] = iter->child; types[stack_depth] = iter->ctype; stack_depth++; break; } case OPCODE_IDX: { if (stack_depth == TINYTEMPLATE_MAX_EXPR_DEPTH) { report(&error, "Evaluation stack limit reached"); return EDEPTH; } size_t iter_idx = instr->operands[0].as_size; assert(iter_depth > 0); // The IDX instruction can't // be run outside of an iteration. assert(iter_idx < iter_depth); // The index of the iteration must // refer to one of the active ones. iter_state_t *iter = &iter_stack[iter_depth-iter_idx-1]; assert(iter->next_index > 0); // IDX can only be executed after // the iteration started. stack[stack_depth].as_int = iter->next_index-1; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack_depth++; break; } case OPCODE_GETS: { assert(stack_depth > 0); // GETS takes the top of the stack // and transforms it in one of its // properties, so the stack can't // be empty. if (types[stack_depth-1] != TINYTEMPLATE_TYPE_DICT) { report(&error, "Access by string on non-dict value"); return ETYPE; } size_t offset = instr->operands[0].as_size; size_t length = instr->operands[1].as_size; tinytemplate_type_t type; tinytemplate_value_t value; if (!stack[stack_depth-1].as_dict.get(stack[stack_depth-1].as_dict.data, src + offset, length, &type, &value)) { report(&error, "Key %.*s not in dict", (int) length, src + offset); return ESYMBOL; } types[stack_depth-1] = type; stack[stack_depth-1] = value; break; } case OPCODE_PUSHI: if (stack_depth == TINYTEMPLATE_MAX_EXPR_DEPTH) { report(&error, "Evaluation stack limit reached"); return EDEPTH; } types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = instr->operands[0].as_int; stack_depth++; break; case OPCODE_PUSHF: if (stack_depth == TINYTEMPLATE_MAX_EXPR_DEPTH) { report(&error, "Evaluation stack limit reached"); return EDEPTH; } types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = instr->operands[0].as_float; stack_depth++; break; case OPCODE_PUSHS: NOT_IMPLEMENTED_YET; break; case OPCODE_PUSHV: { if (stack_depth == TINYTEMPLATE_MAX_EXPR_DEPTH) { report(&error, "Evaluation stack limit reached"); return EDEPTH; } size_t varname_offset = instr->operands[0].as_size; size_t varname_length = instr->operands[1].as_size; tinytemplate_type_t type; tinytemplate_value_t value; if (params(userp, src + varname_offset, varname_length, &type, &value)) { types[stack_depth] = type; stack[stack_depth] = value; stack_depth++; } else { report(&error, "Undefined variable [%.*s]", (int) varname_length, src + varname_offset); return ESYMBOL; } break; } case OPCODE_JUMP: index = instr->operands[0].as_size; break; case OPCODE_JCND: assert(stack_depth > 0); // JCND jumps if the top of the // stack is true, so the stack // can't be empty. if (!value_can_be_considered_true(types[stack_depth-1], stack[stack_depth-1])) index = instr->operands[0].as_size; stack_depth--; break; case OPCODE_WRITE: { size_t offset = instr->operands[0].as_size; size_t length = instr->operands[1].as_size; callback(userp, src + offset, length); break; } case OPCODE_WRTOP: { assert(stack_depth > 0); // WRTOP output the top of the stack, // so the stack can't be empty. switch (types[stack_depth-1]) { case TINYTEMPLATE_TYPE_INT: { char text[128]; int num = snprintf(text, sizeof(text), "%lld", stack[stack_depth-1].as_int); assert(num > 0); callback(userp, text, (size_t) num); break; } case TINYTEMPLATE_TYPE_FLOAT: { char text[128]; int num = snprintf(text, sizeof(text), "%lf", stack[stack_depth-1].as_float); assert(num > 0); callback(userp, text, (size_t) num); break; } case TINYTEMPLATE_TYPE_STRING: callback(userp, stack[stack_depth-1].as_string.str, stack[stack_depth-1].as_string.len); break; default: report(&error, "Can't write non-primitive value"); return ETYPE; } break; } case OPCODE_POP: assert(stack_depth > 0); stack_depth--; break; // This will be useful from here on #define PAIR(x, y) ((unsigned char) (x) | ((unsigned char) (y) << 8)) case OPCODE_NEG: { assert(stack_depth > 0); switch (types[stack_depth-1]) { case TINYTEMPLATE_TYPE_INT: stack[stack_depth-1].as_int = -stack[stack_depth-1].as_int; break; case TINYTEMPLATE_TYPE_FLOAT: stack[stack_depth-1].as_float = -stack[stack_depth-1].as_float; break; default: report(&error, "Negation on non-numeric operands"); return ETYPE; } break; } case OPCODE_ADD: { assert(stack_depth >= 2); switch PAIR(types[stack_depth-2], types[stack_depth-1]) { case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_INT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = stack[stack_depth-1].as_int; int64_t res = op1 + op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_FLOAT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = (int64_t) stack[stack_depth-1].as_float; int64_t res = op1 + op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_INT): { double op1 = stack[stack_depth-2].as_float; double op2 = (double) stack[stack_depth-1].as_int; double res = op1 + op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_FLOAT): { double op1 = stack[stack_depth-2].as_float; double op2 = stack[stack_depth-1].as_float; double res = op1 + op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } default: report(&error, "Addition on non-numeric operands"); return ETYPE; } break; } case OPCODE_SUB: { assert(stack_depth >= 2); switch PAIR(types[stack_depth-2], types[stack_depth-1]) { case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_INT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = stack[stack_depth-1].as_int; int64_t res = op1 - op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_FLOAT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = (int64_t) stack[stack_depth-1].as_float; int64_t res = op1 - op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_INT): { double op1 = stack[stack_depth-2].as_float; double op2 = (double) stack[stack_depth-1].as_int; double res = op1 - op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_FLOAT): { double op1 = stack[stack_depth-2].as_float; double op2 = stack[stack_depth-1].as_float; double res = op1 - op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } default: report(&error, "Subtraction on non-numeric operands"); return ETYPE; } break; } case OPCODE_MUL: { assert(stack_depth >= 2); switch PAIR(types[stack_depth-2], types[stack_depth-1]) { case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_INT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = stack[stack_depth-1].as_int; int64_t res = op1 * op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_FLOAT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = (int64_t) stack[stack_depth-1].as_float; int64_t res = op1 * op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_INT): { double op1 = stack[stack_depth-2].as_float; double op2 = (double) stack[stack_depth-1].as_int; double res = op1 * op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_FLOAT): { double op1 = stack[stack_depth-2].as_float; double op2 = stack[stack_depth-1].as_float; double res = op1 * op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } default: report(&error, "Multiplication on non-numeric operands"); return ETYPE; } break; } case OPCODE_DIV: { assert(stack_depth >= 2); switch PAIR(types[stack_depth-2], types[stack_depth-1]) { case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_INT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = stack[stack_depth-1].as_int; int64_t res = op1 / op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_FLOAT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = (int64_t) stack[stack_depth-1].as_float; int64_t res = op1 / op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_INT): { double op1 = stack[stack_depth-2].as_float; double op2 = (double) stack[stack_depth-1].as_int; double res = op1 / op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } case PAIR(TINYTEMPLATE_TYPE_FLOAT, TINYTEMPLATE_TYPE_FLOAT): { double op1 = stack[stack_depth-2].as_float; double op2 = stack[stack_depth-1].as_float; double res = op1 / op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_FLOAT; stack[stack_depth].as_float = res; stack_depth++; break; } default: report(&error, "Division on non-numeric operands"); return ETYPE; } break; } case OPCODE_MOD: { assert(stack_depth >= 2); switch PAIR(types[stack_depth-2], types[stack_depth-1]) { case PAIR(TINYTEMPLATE_TYPE_INT, TINYTEMPLATE_TYPE_INT): { int64_t op1 = stack[stack_depth-2].as_int; int64_t op2 = stack[stack_depth-1].as_int; int64_t res = op1 % op2; stack_depth -= 2; types[stack_depth] = TINYTEMPLATE_TYPE_INT; stack[stack_depth].as_int = res; stack_depth++; break; } default: report(&error, "Modulo operator [mod] only works on integer operands"); return ETYPE; } break; } } } return DONE; }