vessel/scripts/temple.c
Arija A. d9b70ac06e
feat(web): Implement basic Temple
Signed-off-by: Arija A. <ari@ari.lt>
2025-07-01 22:20:30 +03:00

3149 lines
103 KiB
C

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
typedef enum {
ASTNodeType_template = 0, /* Template: entire template */
ASTNodeType_text, /* Raw text outside of tags */
ASTNodeType_comment, /* Comment: {# ... #} */
ASTNodeType_output_expr, /* Output: {{ expression }} */
ASTNodeType_set_stmt, /* Set statement: {% set member_or_var.here = expr %} */
ASTNodeType_unset_stmt, /* Unset statement (only nonmembers): {% unset var %} */
ASTNodeType_unmacro_stmt, /* Unmacro statement (only macros): {% unmacro name %} */
ASTNodeType_conditional, /* If: {% if ... %} ... {% elif ... %} ... {% end %} or ternary: cond ?
a : b */
ASTNodeType_macro_stmt, /* Block: {% macro ... %} ... {% end %} */
ASTNodeType_namespace_stmt, /* Namespace: {% namespace ... %} ... {% end %} */
ASTNodeType_import_stmt, /* Import: {% import "filename.tpl" %} */
ASTNodeType_for_loop, /* For loop: {% for x : y %} */
ASTNodeType_while_loop, /* While loop: {% while expr %} */
ASTNodeType_binary_expr, /* Binary expression: a + b */
ASTNodeType_unary_expr, /* Unary expression: !x, -x */
ASTNodeType_function_call, /* Function call: lower(x) */
ASTNodeType_variable, /* Variable: x */
ASTNodeType_literal_string, /* Literal string: "..." or r"..." */
ASTNodeType_fmt_string, /* Literal format string: f"... {expr} ..." */
ASTNodeType_literal_number, /* Literal number: 123 (base 10) (or 0d123), 0x123 (base 16), 0o123
(base 8), 0b110 (base 2) */
ASTNodeType_literal_array, /* Literal array: [..., ...] */
ASTNodeType_literal_boolean, /* Literal boolean: true or false */
ASTNodeType_literal_null, /* Literal null: null */
ASTNodeType_literal_float, /* Literal float: 3.14159 or 12e-3 (0.012) */
ASTNodeType_literal_inf, /* Literal infinity: inf */
ASTNodeType_parent_stmt, /* Parent stmt: parent */
ASTNodeType_lookup, /* Lookup: [...] */
ASTNodeType_member_lookup, /* Member lookup: a.b */
ASTNodeType_finish_stmt, /* Finish render: {% finish %} */
ASTNodeType_break_stmt, /* Break statement: {% break %} */
ASTNodeType_continue_stmt, /* Continue statement: {% continue %} */
ASTNodeType_block_stmt, /* Block statement: {% block name %} {% end %} */
ASTNodeType_range, /* Range: a:b/step */
ASTNodeType_literal_hashmap, /* Literal hashmao: {a: b} */
} ASTNodeType;
typedef enum {
BinaryOperator_add = 0, /* + */
BinaryOperator_sub, /* - */
BinaryOperator_mul, /* * */
BinaryOperator_div, /* / */
BinaryOperator_mod, /* % */
BinaryOperator_keyword, /* = (for keyword arguments and default values) */
BinaryOperator_eq, /* == */
BinaryOperator_neq, /* != */
BinaryOperator_lt, /* < */
BinaryOperator_gt, /* > */
BinaryOperator_leq, /* <= */
BinaryOperator_geq, /* >= */
BinaryOperator_and, /* && */
BinaryOperator_or, /* || */
BinaryOperator_band, /* & */
BinaryOperator_bor, /* | */
BinaryOperator_bxor, /* ^ */
BinaryOperator_shl, /* << */
BinaryOperator_shr, /* >> */
BinaryOperator_in, /* @ */
} BinaryOperator;
typedef enum {
AssignmentOperator_simple = 0, /* = */
AssignmentOperator_add, /* += */
AssignmentOperator_sub, /* -= */
AssignmentOperator_mul, /* *= */
AssignmentOperator_div, /* /= */
AssignmentOperator_mod, /* %= */
AssignmentOperator_band, /* &= */
AssignmentOperator_bor, /* |= */
AssignmentOperator_bxor, /* ^= */
AssignmentOperator_shl, /* <<= */
AssignmentOperator_shr, /* >>= */
} AssignmentOperator;
typedef enum {
UnaryOperator_neg = 0, /* - */
UnaryOperator_pos, /* + */
UnaryOperator_not, /* ! */
UnaryOperator_bnot, /* ~ */
UnaryOperator_spread, /* * */
UnaryOperator_kspread, /* ** */
} UnaryOperator;
typedef struct ASTNode {
ASTNodeType type;
union {
struct {
char *text;
size_t len;
} text;
struct {
char *text;
size_t len;
} comment;
struct {
struct ASTNode *expr;
} output;
struct {
struct ASTNode *name;
AssignmentOperator eq;
struct ASTNode *expr;
bool trim; /* trim value */
} set_stmt;
struct {
char *name;
size_t name_len;
} unset_stmt;
struct {
char *name;
size_t name_len;
} unmacro_stmt;
struct {
struct ASTNode *condition;
struct ASTNode *body;
struct ASTNode *else_clause;
bool trim_body, trim_clause;
} conditional;
struct {
char *name;
size_t name_len;
struct ASTNode **args;
size_t args_count;
struct ASTNode *body;
bool trim; /* trim macro body */
} macro_stmt;
struct {
char *name;
size_t name_len;
struct ASTNode *body;
bool trim; /* trim macro body */
} namespace_stmt;
struct {
struct ASTNode *filename;
char *namespace;
size_t namespace_size;
bool trim; /* trim template body */
} import_stmt;
struct {
char *varname;
struct ASTNode *iterable;
struct ASTNode *body;
bool trim; /* trim loop body */
} for_loop;
struct {
struct ASTNode *condition;
struct ASTNode *body;
bool trim; /* trim loop body */
} while_loop;
struct {
BinaryOperator op;
struct ASTNode *left;
struct ASTNode *right;
} binary;
struct {
UnaryOperator op;
struct ASTNode *operand;
} unary;
struct {
struct ASTNode *name;
struct ASTNode **args;
size_t args_count;
} function_call;
struct {
char *name;
size_t name_len;
} variable;
struct {
int64_t value;
} number;
struct {
bool raw;
char *value;
size_t value_len;
} string;
struct {
struct ASTNode *string;
} fmt_string;
struct {
struct ASTNode *operand;
struct ASTNode *index;
} lookup;
struct {
struct ASTNode *operand;
char *member;
size_t member_len;
} member_lookup;
struct {
struct ASTNode **items;
size_t items_count;
} array;
struct {
bool value;
} boolean;
struct {
double value;
} floatnum;
struct {
char *name;
size_t name_len;
struct ASTNode *body;
bool trim; /* trim block body */
} block;
struct {
struct ASTNode *from;
struct ASTNode *to;
struct ASTNode *step;
} range;
struct {
struct ASTNode **keys;
struct ASTNode **values;
size_t count;
} hashmap;
} data;
struct ASTNode *next;
} ASTNode;
/* BEGIN FORWARD DECLARATIONS */
static void free_ast(ASTNode *node);
static const char *binary_op_str(BinaryOperator op);
static char *unary_op_str(UnaryOperator op);
static const char *eq_op_str(AssignmentOperator op);
static ASTNode *new_text_node(const char *start, size_t len);
static ASTNode *new_comment_node(const char *start, size_t len);
static ASTNode *new_output_node(ASTNode *expr);
static ASTNode *new_literal_number_node(int64_t value);
static ASTNode *new_fmt_string_node(ASTNode *expr);
static ASTNode *new_literal_string_node(const char *start, size_t len, bool raw);
static ASTNode *new_variable_node(const char *start, size_t len);
static ASTNode *new_function_call_node(ASTNode *name, ASTNode **args, size_t args_count);
static ASTNode *new_lookup_node(ASTNode *operand, ASTNode *index);
static ASTNode *new_member_lookup_node(ASTNode *operand, const char *member, size_t len);
static ASTNode *new_binary_node(BinaryOperator op, ASTNode *left, ASTNode *right);
static ASTNode *new_import_node(ASTNode *filename, const char *namespace, size_t len, bool trim);
static ASTNode *new_set_node(ASTNode *name, AssignmentOperator eq, ASTNode *expr, bool trim);
static ASTNode *new_unset_node(const char *start, size_t len);
static ASTNode *new_unmacro_node(const char *start, size_t len);
static ASTNode *new_macro_node(
const char *start, size_t len, ASTNode **args, size_t args_count, ASTNode *body, bool trim);
static ASTNode *new_namespace_node(const char *start, size_t len, ASTNode *body, bool trim);
static ASTNode *new_while_node(ASTNode *condition, ASTNode *body, bool trim);
static ASTNode *
new_for_node(const char *start, size_t len, ASTNode *iterable, ASTNode *body, bool trim);
static ASTNode *new_conditional_node(
ASTNode *condition, ASTNode *body, ASTNode *else_clause, bool trim_body, bool trim_clause);
static ASTNode *new_array_node(ASTNode **items, size_t items_count);
static ASTNode *new_hashmap_node(ASTNode **keys, ASTNode **values, size_t count);
static ASTNode *new_finish_stmt_node(void);
static ASTNode *new_unary_node(UnaryOperator op, ASTNode *operand);
static ASTNode *new_boolean_node(bool value);
static ASTNode *new_null_node(void);
static ASTNode *new_inf_node(void);
static ASTNode *new_break_node(void);
static ASTNode *new_continue_node(void);
static ASTNode *new_literal_float_node(double value);
static ASTNode *new_block_node(const char *start, size_t len, ASTNode *body, bool trim);
static ASTNode *new_range_node(ASTNode *from, ASTNode *to, ASTNode *step);
static void skip_whitespace(const char **p, const char *end);
static ASTNode *parse_number(const char **p, const char *end);
static ASTNode *parse_fmt_string(const char **p, const char *end);
static ASTNode *parse_string(const char **p, const char *end);
static ASTNode *parse_array(const char **p, const char *end);
static ASTNode *parse_hashmap(const char **p, const char *end);
static ASTNode *parse_identifier(const char **p, const char *end);
static ASTNode *parse_primary_expr(const char **p, const char *end);
static ASTNode *parse_postfix_expr(ASTNode *base, const char **p, const char *end);
static bool match_binary_operator(const char **p, const char *end, BinaryOperator *op);
static bool match_unary_operator(const char **p, const char *end, UnaryOperator *op);
static uint8_t get_operator_precedence(BinaryOperator op);
static ASTNode *
parse_binary_expr_rhs(uint8_t min_prec, ASTNode *lhs, const char **p, const char *end);
static ASTNode *parse_unary_expr_lhs(const char **p, const char *end);
static ASTNode *parse_conditional_expr(ASTNode *condition, const char **p, const char *end);
static ASTNode *parse_expression(const char **p, const char *end);
static bool match_assignment_operator(const char **p, const char *end, AssignmentOperator *op);
static ASTNode *parse_set_stmt(const char *args_begin, const char *args_end, bool trim);
static ASTNode *parse_unset_stmt(const char *args_begin, const char *args_end);
static ASTNode *parse_unmacro_stmt(const char *args_begin, const char *args_end);
static ASTNode *parse_import_stmt(const char *args_begin, const char *args_end, bool trim);
static ASTNode *
parse_while_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim);
static ASTNode *
parse_macro_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim);
static ASTNode *
parse_namespace_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim);
static ASTNode *
parse_for_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim);
static ASTNode **parse_list(const char **p, const char *end, char close_char, size_t *out_argc);
static ASTNode *
parse_block_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim);
static ASTNode *parse_range(ASTNode *from, const char **p, const char *end);
static void print_indent(size_t indent);
static void print_ast(const ASTNode *node, size_t indent);
static bool parse_template(ASTNode *root, const char *input, size_t len);
/* END FORWARD DECLARATIONS */
static void free_ast(ASTNode *node) {
while (node) {
ASTNode *next = node->next;
if (node->type == ASTNodeType_text) {
free(node->data.text.text);
} else if (node->type == ASTNodeType_comment) {
free(node->data.comment.text);
}
free(node);
node = next;
}
}
static const char *binary_op_str(BinaryOperator op) {
switch (op) {
case BinaryOperator_add:
return "+";
case BinaryOperator_sub:
return "-";
case BinaryOperator_mul:
return "*";
case BinaryOperator_div:
return "/";
case BinaryOperator_mod:
return "%";
case BinaryOperator_keyword:
return "=";
case BinaryOperator_eq:
return "==";
case BinaryOperator_neq:
return "!=";
case BinaryOperator_lt:
return "<";
case BinaryOperator_gt:
return ">";
case BinaryOperator_leq:
return "<=";
case BinaryOperator_geq:
return ">=";
case BinaryOperator_and:
return "&&";
case BinaryOperator_or:
return "||";
case BinaryOperator_band:
return "&";
case BinaryOperator_bor:
return "|";
case BinaryOperator_bxor:
return "^";
case BinaryOperator_shl:
return "<<";
case BinaryOperator_shr:
return ">>";
case BinaryOperator_in:
return "@";
default:
return "?";
}
}
static char *unary_op_str(UnaryOperator op) {
switch (op) {
case UnaryOperator_neg:
return "-";
case UnaryOperator_pos:
return "+";
case UnaryOperator_not:
return "!";
case UnaryOperator_bnot:
return "~";
case UnaryOperator_spread:
return "*";
case UnaryOperator_kspread:
return "**";
default:
return "?";
}
}
static const char *eq_op_str(AssignmentOperator op) {
switch (op) {
case AssignmentOperator_simple:
return "=";
case AssignmentOperator_add:
return "+=";
case AssignmentOperator_sub:
return "-=";
case AssignmentOperator_mul:
return "*=";
case AssignmentOperator_div:
return "/=";
case AssignmentOperator_mod:
return "%=";
case AssignmentOperator_band:
return "&=";
case AssignmentOperator_bor:
return "|=";
case AssignmentOperator_bxor:
return "^=";
case AssignmentOperator_shl:
return "<<=";
case AssignmentOperator_shr:
return ">>=";
default:
return "?";
}
}
/* BEGIN HELPERS */
static ASTNode *new_text_node(const char *start, size_t len) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_text;
node->data.text.text = malloc(len + 1);
memcpy(node->data.text.text, start, len);
node->data.text.text[len] = '\0';
node->data.text.len = len;
node->next = NULL;
return node;
}
static ASTNode *new_comment_node(const char *start, size_t len) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_comment;
node->data.comment.text = malloc(len + 1);
memcpy(node->data.comment.text, start, len);
node->data.comment.text[len] = '\0';
node->data.comment.len = len;
node->next = NULL;
return node;
}
static ASTNode *new_output_node(ASTNode *expr) {
ASTNode *node = malloc(sizeof(ASTNode));
if (!node) {
return NULL;
}
node->type = ASTNodeType_output_expr;
node->data.output.expr = expr;
node->next = NULL;
return node;
}
static ASTNode *new_literal_number_node(int64_t value) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_number;
node->data.number.value = value;
node->next = NULL;
return node;
}
static ASTNode *new_literal_string_node(const char *start, size_t len, bool raw) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_string;
node->data.string.value = malloc(len + 1);
memcpy(node->data.string.value, start, len);
node->data.string.value[len] = '\0';
node->data.string.raw = raw;
node->data.string.value_len = len;
node->next = NULL;
return node;
}
static ASTNode *new_fmt_string_node(ASTNode *expr) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_fmt_string;
node->data.fmt_string.string = expr;
node->next = NULL;
return node;
}
static ASTNode *new_variable_node(const char *start, size_t len) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_variable;
node->data.variable.name = malloc(len + 1);
memcpy(node->data.variable.name, start, len);
node->data.variable.name[len] = '\0';
node->data.variable.name_len = len;
node->next = NULL;
return node;
}
static ASTNode *new_function_call_node(ASTNode *name, ASTNode **args, size_t args_count) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_function_call;
node->data.function_call.name = name;
node->data.function_call.args = args;
node->data.function_call.args_count = args_count;
node->next = NULL;
return node;
}
static ASTNode *new_lookup_node(ASTNode *operand, ASTNode *index) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_lookup;
node->data.lookup.operand = operand;
node->data.lookup.index = index;
node->next = NULL;
return node;
}
static ASTNode *new_member_lookup_node(ASTNode *operand, const char *member, size_t len) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_member_lookup;
node->data.member_lookup.operand = operand;
node->data.member_lookup.member = malloc(len + 1);
memcpy(node->data.member_lookup.member, member, len);
node->data.member_lookup.member[len] = '\0';
node->data.member_lookup.member_len = len;
node->next = NULL;
return node;
}
static ASTNode *new_binary_node(BinaryOperator op, ASTNode *left, ASTNode *right) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_binary_expr;
node->data.binary.op = op;
node->data.binary.left = left;
node->data.binary.right = right;
node->next = NULL;
return node;
}
static ASTNode *new_import_node(ASTNode *filename, const char *namespace, size_t len, bool trim) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_import_stmt;
node->data.import_stmt.filename = filename;
node->data.import_stmt.namespace = malloc(len + 1);
memcpy(node->data.import_stmt.namespace, namespace, len);
node->data.import_stmt.namespace[len] = '\0';
node->data.import_stmt.namespace_size = len;
node->data.import_stmt.trim = trim;
node->next = NULL;
return node;
}
static ASTNode *new_set_node(ASTNode *name, AssignmentOperator eq, ASTNode *expr, bool trim) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_set_stmt;
node->data.set_stmt.name = name;
node->data.set_stmt.eq = eq;
node->data.set_stmt.expr = expr;
node->data.set_stmt.trim = trim;
node->next = NULL;
return node;
}
static ASTNode *new_unset_node(const char *start, size_t len) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_unset_stmt;
node->data.unset_stmt.name = malloc(len + 1);
memcpy(node->data.unset_stmt.name, start, len);
node->data.unset_stmt.name[len] = '\0';
node->data.unset_stmt.name_len = len;
node->next = NULL;
return node;
}
static ASTNode *new_unmacro_node(const char *start, size_t len) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_unmacro_stmt;
node->data.unmacro_stmt.name = malloc(len + 1);
memcpy(node->data.unmacro_stmt.name, start, len);
node->data.unmacro_stmt.name[len] = '\0';
node->data.unmacro_stmt.name_len = len;
node->next = NULL;
return node;
}
static ASTNode *new_macro_node(
const char *start, size_t len, ASTNode **args, size_t args_count, ASTNode *body, bool trim) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_macro_stmt;
node->data.macro_stmt.name = malloc(len + 1);
memcpy(node->data.macro_stmt.name, start, len);
node->data.macro_stmt.name[len] = '\0';
node->data.macro_stmt.body = body;
node->data.macro_stmt.name_len = len;
node->data.macro_stmt.args = args;
node->data.macro_stmt.args_count = args_count;
node->data.macro_stmt.trim = trim;
node->next = NULL;
return node;
}
static ASTNode *new_namespace_node(const char *start, size_t len, ASTNode *body, bool trim) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_namespace_stmt;
node->data.namespace_stmt.name = malloc(len + 1);
memcpy(node->data.namespace_stmt.name, start, len);
node->data.namespace_stmt.name[len] = '\0';
node->data.namespace_stmt.body = body;
node->data.namespace_stmt.name_len = len;
node->data.namespace_stmt.trim = trim;
node->next = NULL;
return node;
}
static ASTNode *new_while_node(ASTNode *condition, ASTNode *body, bool trim) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_while_loop;
node->data.while_loop.condition = condition;
node->data.while_loop.body = body;
node->data.while_loop.trim = trim;
node->next = NULL;
return node;
}
static ASTNode *
new_for_node(const char *start, size_t len, ASTNode *iterable, ASTNode *body, bool trim) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_for_loop;
node->data.for_loop.varname = malloc(len + 1);
memcpy(node->data.for_loop.varname, start, len);
node->data.for_loop.varname[len] = '\0';
node->data.for_loop.iterable = iterable;
node->data.for_loop.body = body;
node->data.for_loop.trim = trim;
node->next = NULL;
return node;
}
static ASTNode *new_conditional_node(
ASTNode *condition, ASTNode *body, ASTNode *else_clause, bool trim_body, bool trim_clause) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_conditional;
node->data.conditional.condition = condition;
node->data.conditional.body = body;
node->data.conditional.else_clause = else_clause;
node->data.conditional.trim_body = trim_body;
node->data.conditional.trim_clause = trim_clause;
node->next = NULL;
return node;
}
static ASTNode *new_array_node(ASTNode **items, size_t items_count) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_array;
node->data.array.items = items;
node->data.array.items_count = items_count;
node->next = NULL;
return node;
}
static ASTNode *new_hashmap_node(ASTNode **keys, ASTNode **values, size_t count) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_hashmap;
node->data.hashmap.keys = keys;
node->data.hashmap.values = values;
node->data.hashmap.count = count;
node->next = NULL;
return node;
}
static ASTNode *new_finish_stmt_node(void) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_finish_stmt;
node->next = NULL;
return node;
}
static ASTNode *new_unary_node(UnaryOperator op, ASTNode *operand) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_unary_expr;
node->data.unary.op = op;
node->data.unary.operand = operand;
node->next = NULL;
return node;
}
static ASTNode *new_boolean_node(bool value) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_boolean;
node->data.boolean.value = value;
node->next = NULL;
return node;
}
static ASTNode *new_null_node(void) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_null;
node->next = NULL;
return node;
}
static ASTNode *new_inf_node(void) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_inf;
node->next = NULL;
return node;
}
static ASTNode *new_parent_node(void) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_parent_stmt;
node->next = NULL;
return node;
}
static ASTNode *new_break_node(void) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_break_stmt;
node->next = NULL;
return node;
}
static ASTNode *new_continue_node(void) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_continue_stmt;
node->next = NULL;
return node;
}
static ASTNode *new_literal_float_node(double value) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_literal_float;
node->data.floatnum.value = value;
node->next = NULL;
return node;
}
static ASTNode *new_block_node(const char *start, size_t len, ASTNode *body, bool trim) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_block_stmt;
if (len == 0) {
node->data.block.name = NULL;
} else {
node->data.block.name = malloc(len + 1);
memcpy(node->data.block.name, start, len);
node->data.block.name[len] = '\0';
}
node->data.block.name_len = len;
node->data.block.body = body;
node->data.block.trim = trim;
node->next = NULL;
return node;
}
static ASTNode *new_range_node(ASTNode *from, ASTNode *to, ASTNode *step) {
ASTNode *node = malloc(sizeof(ASTNode));
node->type = ASTNodeType_range;
node->data.range.from = from;
node->data.range.to = to;
node->data.range.step = step;
node->next = NULL;
return node;
}
/* END HELPERS */
/* BEGIN PARSERS */
static void skip_whitespace(const char **p, const char *end) {
while (*p < end && isspace(**p)) {
++(*p);
}
}
static inline bool is_valid_digit(char c, uint8_t base) {
if (base <= 10) {
return (c >= '0' && c < '0' + base);
} else {
/* base > 10, allow digits and letters up to base */
if (c >= '0' && c <= '9') {
return (c - '0') < base;
}
if (c >= 'a' && c < 'a' + base - 10) {
return true;
}
if (c >= 'A' && c < 'A' + base - 10) {
return true;
}
return false;
}
}
static inline int64_t str_to_int64(const char *start, const char *end, uint8_t base) {
int64_t result = 0;
int64_t sign = 1;
if (start < end && *start == '-') {
sign = -1;
++start;
}
while (start < end) {
char c = *start++;
int digit = 0;
if (c >= '0' && c <= '9') {
digit = c - '0';
} else if (c >= 'a' && c <= 'f') {
digit = 10 + (c - 'a');
} else if (c >= 'A' && c <= 'F') {
digit = 10 + (c - 'A');
} else {
break;
}
if (digit >= base) {
break;
}
result = result * base + digit;
}
return sign * result;
}
static inline double power10(int64_t exponent) {
if (exponent == 0) {
return 1.0;
}
int64_t abs_exp = exponent < 0 ? -exponent : exponent;
double result = 1.0;
double base = 10.0;
while (abs_exp) {
if (abs_exp & 1) {
result *= base;
}
base *= base;
abs_exp /= 2;
}
return exponent < 0 ? 1.0 / result : result;
}
static inline double str_to_double(const char *start, const char *end) {
const char *p = start;
double sign = 1.0;
if (p < end && (*p == '+' || *p == '-')) {
sign = (*p == '-') ? -1.0 : 1.0;
++p;
}
double integer_part = 0.0;
double fractional_part = 0.0;
bool has_value = false;
while (p < end && isdigit(*p)) {
integer_part = integer_part * 10.0 + (*p - '0');
has_value = true;
++p;
}
if (p < end && *p == '.') {
double divisor = 10.0;
++p;
while (p < end && isdigit(*p)) {
fractional_part += (*p - '0') / divisor;
divisor *= 10.0;
has_value = 1;
p++;
}
}
if (!has_value) {
return 0.0;
}
double combined = integer_part + fractional_part;
int64_t exponent_val = 0;
if (p < end && (*p == 'e' || *p == 'E')) {
p++;
int64_t exp_sign = 1;
if (p < end && (*p == '+' || *p == '-')) {
exp_sign = (*p == '-') ? -1 : 1;
++p;
}
while (p < end && isdigit(*p)) {
exponent_val = exponent_val * 10 + (*p - '0');
++p;
}
exponent_val *= exp_sign;
}
return sign * combined * power10(exponent_val);
}
static ASTNode *parse_number(const char **p, const char *end) {
skip_whitespace(p, end);
uint8_t base = 10;
bool has_exp = false;
/* Check for base prefix only if it is NOT a float */
if (**p == '0' && ((*p) + 1) < end && isalpha(*(*p + 1))) {
char next_char = *(*p + 1);
if (next_char == 'b' || next_char == 'B' || next_char == 'o' || next_char == 'O' ||
next_char == 'd' || next_char == 'D' || next_char == 'x' || next_char == 'X' ||
next_char == 'e' || next_char == 'E') {
if (next_char == 'e' || next_char == 'E') {
base = 10;
has_exp = true;
} else {
switch (next_char) {
case 'b':
case 'B':
base = 2;
break;
case 'o':
case 'O':
base = 8;
break;
case 'd':
case 'D':
base = 10;
break;
case 'x':
case 'X':
base = 16;
break;
}
*p += 2;
}
} else {
fprintf(stderr, "Error: Invalid number literal base identifier: %c\n", next_char);
return NULL;
}
}
/* Now parse digits and optionally decimal point and exponent */
const char *start = *p;
const char *scan = *p;
bool has_dot = false;
while (scan < end) {
char c = *scan;
if (c == '.') {
if (has_dot || has_exp) {
break; /* Only one dot allowed, no dot after exponent */
}
if (base != 10) {
break; /* Float only allowed in base 10 */
}
has_dot = true;
++scan;
} else if (c == 'e' || c == 'E') {
if (has_exp || base != 10) {
break; /* Only one exponent allowed, only base 10 */
}
has_exp = true;
++scan;
/* Optional sign after exponent */
if (scan < end && (*scan == '+' || *scan == '-')) {
++scan;
}
} else if (is_valid_digit(c, base)) {
++scan;
} else {
break;
}
}
if (start == scan) {
fprintf(stderr, "Error: Invalid number literal at '%s'\n", *p);
return NULL;
}
/* If float detected, base must be 10 (ignore any prefix) */
if (has_dot || has_exp) {
base = 10;
*p = scan;
return new_literal_float_node(str_to_double(start, scan));
} else {
*p = scan;
return new_literal_number_node(str_to_int64(start, scan, base));
}
}
static ASTNode *parse_fmt_string(const char **p, const char *end) {
const char *s = *p;
const char *lit_start = s; /* Start of literal text */
ASTNode *head = NULL;
ASTNode **current = &head;
while (s < end) {
if (*s == '{') {
if (s + 1 < end && s[1] == '{') {
/* Escaped {{ */
/* Add any literal text before {{ */
if (lit_start < s) {
ASTNode *text =
new_literal_string_node(lit_start, (size_t)(s - lit_start), false);
*current = text;
current = &text->next;
}
/* Add literal '{' */
ASTNode *text = new_literal_string_node("{", 1, true);
*current = text;
current = &text->next;
s += 2;
lit_start = s;
continue;
}
/* Single '{' - parse expression */
if (lit_start < s) {
ASTNode *text = new_literal_string_node(lit_start, (size_t)(s - lit_start), false);
*current = text;
current = &text->next;
}
const char *expr_start = s + 1;
const char *expr_end = expr_start;
int brace_count = 1;
while (expr_end < end && brace_count > 0) {
if (*expr_end == '{') {
brace_count++;
} else if (*expr_end == '}') {
brace_count--;
}
if (brace_count > 0) {
expr_end++;
}
}
if (brace_count != 0 || expr_end >= end) {
fprintf(stderr, "Error: Unterminated expression in format string\n");
free_ast(head);
return NULL;
}
/* Parse expression inside {} */
const char *tmp = expr_start;
ASTNode *expr = parse_expression(&tmp, expr_end);
if (!expr) {
fprintf(stderr, "Error: Invalid expression in format string\n");
free_ast(head);
return NULL;
}
*current = expr;
while (*current) {
current = &(*current)->next;
}
s = expr_end + 1;
lit_start = s;
*p = s;
} else if (*s == '}') {
if (s + 1 < end && s[1] == '}') {
/* Escaped }} */
if (lit_start < s) {
ASTNode *text =
new_literal_string_node(lit_start, (size_t)(s - lit_start), false);
*current = text;
current = &text->next;
}
ASTNode *text = new_literal_string_node("}", 1, true);
*current = text;
current = &text->next;
s += 2;
lit_start = s;
*p = s;
continue;
} else {
/* Unescaped } without matching { - treat as literal */
s++;
}
} else {
s++;
}
}
/* Add any remaining literal text */
if (lit_start < s) {
ASTNode *text = new_literal_string_node(lit_start, (size_t)(s - lit_start), false);
*current = text;
current = &text->next;
}
*p = s;
return new_fmt_string_node(head);
}
static ASTNode *parse_string(const char **p, const char *end) {
skip_whitespace(p, end);
if (*p >= end) {
return NULL;
}
char fmt = '"';
bool is_not_raw = true;
if (**p == 'r' || **p == 'f' || **p == 'R' || **p == 'F') {
fmt = **p;
is_not_raw = (fmt != 'r') && (fmt != 'R');
++(*p);
}
if (**p != '"') {
fprintf(stderr, "Error: Expected 'r' ('R') or 'f' ('F') for string type, got '%c'\n", **p);
return NULL;
}
++(*p); /* skip opening quote */
const char *start = *p;
while (*p < end && **p != '"') {
if (is_not_raw && **p == '\\') {
++(*p); /* Skip past backslash */
if (*p >= end) {
/* No next character (invalid) */
fprintf(stderr, "Error: Unterminated backslash\n");
return NULL;
}
}
++(*p);
}
if (*p >= end || **p != '"') {
fprintf(stderr, "Error: Unterminated string\n");
return NULL;
}
const char *s_end = *p;
size_t len = (size_t)(s_end - start);
++(*p); /* skip closing quote */
switch (fmt) {
case 'r':
case 'R':
return new_literal_string_node(start, len, true);
case 'f':
case 'F':
return parse_fmt_string(&start, s_end);
default:
return new_literal_string_node(start, len, false);
}
}
static ASTNode *parse_array(const char **p, const char *end) {
size_t argc = 0;
++(*p); /* skip [ */
ASTNode **items = parse_list(p, end, ']', &argc);
return new_array_node(items, argc);
}
static ASTNode *parse_identifier(const char **p, const char *end) {
skip_whitespace(p, end);
if (*p >= end || !(isalpha(**p) || **p == '_' || **p == '$')) {
fprintf(stderr, "Error: Invalid identifier start\n");
return NULL;
}
const char *start = *p;
while (*p < end && (isalnum(**p) || **p == '_')) {
++(*p);
}
const size_t len = (size_t)(*p - start);
if (len == 4 && strncmp(start, "true", 4) == 0) {
return new_boolean_node(true);
} else if (len == 5 && strncmp(start, "false", 5) == 0) {
return new_boolean_node(false);
} else if (len == 4 && strncmp(start, "null", 4) == 0) {
return new_null_node();
} else if (len == 3 && strncmp(start, "inf", 3) == 0) {
return new_inf_node();
}
return new_variable_node(start, len);
}
static ASTNode *parse_primary_expr(const char **p, const char *end) {
skip_whitespace(p, end);
if (*p >= end) {
return NULL;
}
if (**p == ':') {
return parse_range(NULL, p, end);
}
if (isdigit(**p)) {
return parse_number(p, end);
}
if (**p == '[') {
return parse_array(p, end);
}
if (**p == '{') {
return parse_hashmap(p, end);
}
if (**p == '"' || ((**p == 'r' || **p == 'f' || **p == 'R' || **p == 'F') && (*p)[1] == '"')) {
return parse_string(p, end);
}
if (isalpha(**p) || **p == '_' || **p == '$') {
return parse_identifier(p, end);
}
/* Parenthesised expression */
if (**p == '(') {
++(*p); /* Skip initial '(' */
const char *start = *p;
int paren_depth = 1;
while (*p < end && paren_depth > 0) {
if (**p == '(') {
++paren_depth;
} else if (**p == ')') {
--paren_depth;
}
++(*p);
}
if (paren_depth != 0) {
return NULL; /* Unbalanced parentheses */
}
const char *subexpr_end = *p - 1; /* Points to the matching ')' */
/* Parse the subexpression */
const char *subexpr_ptr = start;
ASTNode *expr = parse_expression(&subexpr_ptr, subexpr_end);
if (!expr) {
return NULL;
}
skip_whitespace(p, end);
return expr;
}
return NULL;
}
static ASTNode *parse_postfix_expr(ASTNode *base, const char **p, const char *end) {
while (*p < end) {
skip_whitespace(p, end);
if (*p >= end)
break;
if (**p == '[') {
++(*p); /* skip '[' */
/* Find matching ']' to respect nested brackets */
const char *bracket_start = *p;
int bracket_depth = 1;
const char *bracket_end = bracket_start;
while (bracket_end < end && bracket_depth > 0) {
if (*bracket_end == '[') {
++bracket_depth;
} else if (*bracket_end == ']') {
--bracket_depth;
if (bracket_depth == 0) {
break;
}
}
++bracket_end;
}
if (bracket_depth != 0 || bracket_end >= end) {
fprintf(stderr, "Error: Missing closing ']' in postfix expression\n");
free_ast(base);
return NULL;
}
ASTNode *index = parse_expression(p, bracket_end);
if (!index) {
free_ast(base);
return NULL;
}
*p = bracket_end + 1; /* skip ']' */
base = new_lookup_node(base, index);
continue;
}
if (**p == '.') {
++(*p); /* skip '.' */
skip_whitespace(p, end);
if (*p >= end || !(isalpha(**p) || **p == '_')) {
fprintf(stderr, "Error: Invalid member name after '.'\n");
free_ast(base);
return NULL;
}
const char *member_start = *p;
while (*p < end && (isalnum(**p) || **p == '_')) {
++(*p);
}
base = new_member_lookup_node(base, member_start, (size_t)(*p - member_start));
continue;
}
if (**p == '(') {
++(*p); /* skip '(' */
size_t argc = 0;
ASTNode **args = parse_list(p, end, ')', &argc);
if (!args && argc > 0) {
free_ast(base);
return NULL;
}
base = new_function_call_node(base, args, argc);
continue;
}
/* No more postfix expressions */
break;
}
return base;
}
static bool match_binary_operator(const char **p, const char *end, BinaryOperator *op) {
if (*p >= end) {
return false;
}
#define TRY_MATCH(op_str, op_enum) \
do { \
size_t len = strlen(op_str); \
if ((size_t)(end - *p) >= len && strncmp(*p, op_str, len) == 0) { \
*op = op_enum; \
*p += len; \
return true; \
} \
} while (0)
TRY_MATCH("==", BinaryOperator_eq);
TRY_MATCH("<=", BinaryOperator_leq);
TRY_MATCH(">=", BinaryOperator_geq);
TRY_MATCH("&&", BinaryOperator_and);
TRY_MATCH("||", BinaryOperator_or);
TRY_MATCH("<<", BinaryOperator_shl);
TRY_MATCH(">>", BinaryOperator_shr);
TRY_MATCH("!=", BinaryOperator_neq);
TRY_MATCH("=", BinaryOperator_keyword);
TRY_MATCH("+", BinaryOperator_add);
TRY_MATCH("-", BinaryOperator_sub);
TRY_MATCH("*", BinaryOperator_mul);
TRY_MATCH("/", BinaryOperator_div);
TRY_MATCH("%", BinaryOperator_mod);
TRY_MATCH("<", BinaryOperator_lt);
TRY_MATCH(">", BinaryOperator_gt);
TRY_MATCH("&", BinaryOperator_band);
TRY_MATCH("|", BinaryOperator_bor);
TRY_MATCH("^", BinaryOperator_bxor);
TRY_MATCH("@", BinaryOperator_in);
#undef TRY_MATCH
return false;
}
static bool match_unary_operator(const char **p, const char *end, UnaryOperator *op) {
if (*p >= end)
return false;
switch (**p) {
case '-':
*op = UnaryOperator_neg;
++(*p);
return true;
case '+':
*op = UnaryOperator_pos;
++(*p);
return true;
case '!':
*op = UnaryOperator_not;
++(*p);
return true;
case '~':
*op = UnaryOperator_bnot;
++(*p);
return true;
case '*':
*op = UnaryOperator_spread;
++(*p);
if (*p < end && **p == '*') {
*op = UnaryOperator_kspread;
++(*p);
}
return true;
default:
return false;
}
}
static uint8_t get_operator_precedence(BinaryOperator op) {
switch (op) {
/* Multiplicative */
case BinaryOperator_mul:
case BinaryOperator_div:
case BinaryOperator_mod:
return 11;
/* Additive */
case BinaryOperator_add:
case BinaryOperator_sub:
return 10;
/* Shift */
case BinaryOperator_shl:
case BinaryOperator_shr:
return 9;
/* Relational */
case BinaryOperator_lt:
case BinaryOperator_gt:
case BinaryOperator_leq:
case BinaryOperator_geq:
case BinaryOperator_in:
return 8;
/* Equality */
case BinaryOperator_eq:
case BinaryOperator_neq:
return 7;
/* Bitwise AND */
case BinaryOperator_band:
return 6;
/* Bitwise XOR */
case BinaryOperator_bxor:
return 5;
/* Bitwise OR */
case BinaryOperator_bor:
return 4;
/* Logical AND */
case BinaryOperator_and:
return 3;
/* Logical OR */
case BinaryOperator_or:
return 2;
/* Keyword */
case BinaryOperator_keyword:
return 1;
default:
return 0;
}
}
static ASTNode *
parse_binary_expr_rhs(uint8_t min_prec, ASTNode *lhs, const char **p, const char *end) {
while (true) {
skip_whitespace(p, end);
BinaryOperator op;
const char *op_start = *p;
if (!match_binary_operator(p, end, &op)) {
break;
}
uint8_t prec = get_operator_precedence(op);
if (prec < min_prec) {
*p = op_start;
break;
}
ASTNode *rhs = parse_unary_expr_lhs(p, end);
if (!rhs) {
return NULL;
}
rhs = parse_postfix_expr(rhs, p, end);
rhs = parse_binary_expr_rhs(prec + 1, rhs, p, end);
lhs = new_binary_node(op, lhs, rhs);
}
return lhs;
}
static ASTNode *parse_unary_expr_lhs(const char **p, const char *end) {
skip_whitespace(p, end);
UnaryOperator op;
if (match_unary_operator(p, end, &op)) {
ASTNode *operand = parse_unary_expr_lhs(p, end);
if (!operand) {
return NULL;
}
return new_unary_node(op, operand);
}
/* No unary operator, parse primary + postfix */
ASTNode *primary = parse_primary_expr(p, end);
if (!primary) {
return NULL;
}
return parse_postfix_expr(primary, p, end);
}
static ASTNode *parse_conditional_expr(ASTNode *condition, const char **p, const char *end) {
skip_whitespace(p, end);
if (*p >= end || **p != '?') {
return condition;
}
++(*p);
const char *colon_pos = NULL;
int paren = 0, brack = 0;
const char *cur = *p;
while (cur < end) {
char c = *cur;
if (c == '(')
++paren;
else if (c == ')') {
if (paren > 0)
--paren;
} else if (c == '[')
++brack;
else if (c == ']') {
if (brack > 0)
--brack;
} else if (c == ':' && paren == 0 && brack == 0) {
colon_pos = cur;
break;
}
cur++;
}
if (!colon_pos) {
fprintf(stderr, "Error: Expected ':' in conditional expression\n");
free_ast(condition);
return NULL;
}
/* Parse the true branch expression (from *p to colon) */
const char *true_expr_end = colon_pos;
ASTNode *true_expr = parse_expression(p, true_expr_end);
if (!true_expr) {
free_ast(condition);
return NULL;
}
*p = colon_pos + 1; // skip past ':'
skip_whitespace(p, end);
// Parse the false branch
ASTNode *false_expr = parse_expression(p, end);
if (!false_expr) {
free_ast(condition);
free_ast(true_expr);
return NULL;
}
return new_conditional_node(condition, true_expr, false_expr, false, false);
}
static ASTNode *parse_range(ASTNode *from, const char **p, const char *end) {
ASTNode *start = from;
ASTNode *stop = NULL;
ASTNode *step = NULL;
if (*p >= end || **p != ':') {
return from;
}
++(*p); /* consume ':' */
skip_whitespace(p, end);
const char *stop_start = *p;
int paren_depth = 0;
/* scan to next ':' or '/' or end */
while (*p < end) {
if (**p == '(') {
++paren_depth;
} else if (**p == ')') {
--paren_depth;
if (paren_depth < 0) {
return NULL;
}
} else if (**p == '/' && paren_depth == 0) {
break;
}
++(*p);
}
const char *stop_end = *p;
if (stop_start < *p) {
stop = parse_expression(&stop_start, *p);
if (!stop || stop_start != stop_end) {
return NULL;
}
}
if (*p < end && **p == '/') {
/* support a:b/c form */
++(*p); /* consume '/' */
skip_whitespace(p, end);
paren_depth = 0;
const char *scan = *p;
while (scan < end) {
if (*scan == '(') {
++paren_depth;
} else if (*scan == ')') {
--paren_depth;
if (paren_depth < 0) {
return NULL;
}
}
++scan;
}
if (paren_depth != 0) {
return NULL; /* unbalanced parentheses in step */
}
step = parse_expression(p, end);
if (!step) {
return NULL; /* failed to parse step expression */
}
}
return new_range_node(start, stop, step);
}
static ASTNode *parse_expression(const char **p, const char *end) {
ASTNode *lhs = parse_unary_expr_lhs(p, end);
if (!lhs)
return NULL;
ASTNode *binary_expr = parse_binary_expr_rhs(1, lhs, p, end);
if (!binary_expr) {
free_ast(lhs);
return NULL;
}
ASTNode *cond = parse_conditional_expr(binary_expr, p, end);
return parse_range(cond, p, end);
}
static bool match_assignment_operator(const char **p, const char *end, AssignmentOperator *op) {
if (*p >= end)
return false;
#define TRY_MATCH(op_str, op_enum) \
do { \
size_t len = strlen(op_str); \
if ((size_t)(end - *p) >= len && strncmp(*p, op_str, len) == 0) { \
*op = op_enum; \
*p += len; \
return true; \
} \
} while (0)
TRY_MATCH("<<=", AssignmentOperator_shl);
TRY_MATCH(">>=", AssignmentOperator_shr);
TRY_MATCH("+=", AssignmentOperator_add);
TRY_MATCH("-=", AssignmentOperator_sub);
TRY_MATCH("*=", AssignmentOperator_mul);
TRY_MATCH("/=", AssignmentOperator_div);
TRY_MATCH("%=", AssignmentOperator_mod);
TRY_MATCH("&=", AssignmentOperator_band);
TRY_MATCH("|=", AssignmentOperator_bor);
TRY_MATCH("^=", AssignmentOperator_bxor);
TRY_MATCH("=", AssignmentOperator_simple);
#undef TRY_MATCH
return false;
}
static ASTNode *parse_set_stmt(const char *args_begin, const char *end, bool trim) {
skip_whitespace(&args_begin, end);
const char *varname_start = args_begin;
const char *op_pos = NULL;
AssignmentOperator op;
/* Scan for assignment operator at the top level only */
int paren_depth = 0;
const char *p = args_begin;
while (p < end) {
if (*p == '(') {
++paren_depth;
++p;
} else if (*p == ')') {
--paren_depth;
++p;
} else if (paren_depth == 0) {
const char *q = p;
if (match_assignment_operator(&q, end, &op)) {
op_pos = p;
p = q; /* advance past operator */
break;
}
++p;
} else {
++p;
}
}
if (paren_depth > 0) {
fprintf(stderr, "Error: Unmatched `(` in assignment expression.\n");
return NULL;
}
if (!op_pos) {
fprintf(stderr, "Error: No assignment operator found for set.\n");
return NULL;
}
/* Parse the variable name (left-hand side) */
ASTNode *name = parse_expression(&varname_start, op_pos);
/* p is after the assignment operator */
skip_whitespace(&p, end);
if (p >= end) {
free_ast(name);
fprintf(stderr, "Error: No value for set.\n");
return NULL;
}
ASTNode *expr = parse_expression(&p, end);
if (!expr) {
free_ast(name);
return NULL;
}
return new_set_node(name, op, expr, trim);
}
static ASTNode *parse_unset_stmt(const char *args_begin, const char *args_end) {
skip_whitespace(&args_begin, args_end);
const char *name_start = args_begin;
while (args_begin < args_end && (isalnum(*args_begin) || *args_begin == '_')) {
++args_begin;
}
const size_t len = (size_t)(args_begin - name_start);
skip_whitespace(&args_begin, args_end);
return new_unset_node(name_start, len);
}
static ASTNode *parse_unmacro_stmt(const char *args_begin, const char *args_end) {
skip_whitespace(&args_begin, args_end);
const char *name_start = args_begin;
while (args_begin < args_end && (isalnum(*args_begin) || *args_begin == '_')) {
++args_begin;
}
const size_t len = (size_t)(args_begin - name_start);
skip_whitespace(&args_begin, args_end);
return new_unmacro_node(name_start, len);
}
static ASTNode *parse_import_stmt(const char *args_begin, const char *args_end, bool trim) {
skip_whitespace(&args_begin, args_end);
if (args_begin >= args_end) {
fprintf(stderr, "Error: No import expression supplied\n");
return NULL;
}
const char *p = args_begin;
int paren = 0, brack = 0;
const char *split = NULL;
while (p < args_end) {
char c = *p;
if (c == '(')
++paren;
else if (c == ')') {
if (paren > 0)
--paren;
} else if (c == '[')
++brack;
else if (c == ']') {
if (brack > 0)
--brack;
} else if (c == ':' && paren == 0 && brack == 0) {
split = p;
break;
}
p++;
}
const char *expr_end = split ? split : args_end;
const char *expr_ptr = args_begin;
ASTNode *template_expr = parse_expression(&expr_ptr, expr_end);
if (!template_expr) {
fprintf(stderr, "Error: Invalid import expression\n");
return NULL;
}
if (!split) {
return new_import_node(template_expr, NULL, 0, trim);
}
const char *ns = split + 1;
skip_whitespace(&ns, args_end);
if (ns >= args_end || !(isalpha(*ns) || *ns == '_')) {
fprintf(stderr, "Error: Missing or invalid namespace for import\n");
return NULL;
}
const char *ns_end = ns;
while (ns_end < args_end && (isalnum(*ns_end) || *ns_end == '_')) {
ns_end++;
}
return new_import_node(template_expr, ns, (size_t)(ns_end - ns), trim);
}
static ASTNode *
parse_while_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim) {
skip_whitespace(&args_begin, args_end);
if (args_begin >= args_end) {
fprintf(stderr, "Error: No condition for while loop supplied\n");
return NULL;
}
ASTNode *condition = parse_expression(&args_begin, args_end);
if (!condition) {
fprintf(stderr, "Error: No condition in while loop.\n");
return NULL;
}
return new_while_node(condition, body, trim);
}
static ASTNode *
parse_macro_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim) {
skip_whitespace(&args_begin, args_end);
if (args_begin >= args_end) {
fprintf(stderr, "Error: No name for macro supplied\n");
return NULL;
}
const char *name_start = args_begin;
while (args_begin < args_end && (isalnum(*args_begin) || *args_begin == '_')) {
++args_begin;
}
const size_t len = (size_t)(args_begin - name_start);
skip_whitespace(&args_begin, args_end);
if (*args_begin == '(') {
++args_begin;
skip_whitespace(&args_begin, args_end);
size_t argc = 0;
ASTNode **args = parse_list(&args_begin, args_end, ')', &argc);
return new_macro_node(name_start, len, args, argc, body, trim);
} else {
return new_macro_node(name_start, len, NULL, 0, body, trim);
}
}
static ASTNode *
parse_namespace_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim) {
skip_whitespace(&args_begin, args_end);
if (args_begin >= args_end) {
fprintf(stderr, "Error: No name for namespace supplied\n");
return NULL;
}
const char *name_start = args_begin;
while (args_begin < args_end && (isalnum(*args_begin) || *args_begin == '_')) {
++args_begin;
}
const size_t len = (size_t)(args_begin - name_start);
skip_whitespace(&args_begin, args_end);
return new_namespace_node(name_start, len, body, trim);
}
static ASTNode *
parse_for_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim) {
skip_whitespace(&args_begin, args_end);
if (args_begin >= args_end) {
fprintf(stderr, "Error: No for loop expression supplied\n");
return NULL;
}
const char *varname_start = args_begin;
while (args_begin < args_end && (isalnum(*args_begin) || *args_begin == '_')) {
++args_begin;
}
const size_t len = (size_t)(args_begin - varname_start);
skip_whitespace(&args_begin, args_end);
if (args_begin >= args_end || *args_begin != ':') {
fprintf(stderr, "Error: No for loop iterable supplied\n");
return NULL;
}
++args_begin; /* skip past : */
skip_whitespace(&args_begin, args_end);
ASTNode *expr = parse_expression(&args_begin, args_end);
if (!expr) {
fprintf(stderr, "Error: Invalid iterable\n");
return NULL;
}
return new_for_node(varname_start, len, expr, body, trim);
}
static ASTNode **parse_list(const char **p, const char *end, char close_char, size_t *out_argc) {
ASTNode **args = NULL;
size_t capacity = 0, count = 0;
*out_argc = 0;
skip_whitespace(p, end);
/* Handle empty list case */
if (*p < end && **p == close_char) {
++(*p);
return NULL;
}
while (*p < end) {
/* Parse argument expression */
ASTNode *arg = parse_expression(p, end);
if (!arg) {
/* Cleanup on parse error */
for (size_t idx = 0; idx < count; ++idx) {
free_ast(args[idx]);
}
free(args);
return NULL;
}
/* Dynamically grow arguments array */
if (count >= capacity) {
capacity = capacity ? capacity * 2 : 4;
ASTNode **new_args = realloc(args, capacity * sizeof(ASTNode *));
if (!new_args) {
free_ast(arg);
for (size_t idx = 0; idx < count; ++idx) {
free_ast(args[idx]);
}
free(args);
return NULL;
}
args = new_args;
}
args[count++] = arg;
skip_whitespace(p, end);
/* Check for terminator or separator */
if (*p < end && **p == close_char) {
++(*p);
break;
}
if (*p < end && **p == ',') {
++(*p);
skip_whitespace(p, end);
} else if (*p >= end) {
fprintf(stderr, "Error: Unexpected end of input in list\n");
break;
} else {
fprintf(stderr, "Error: Expected ',' or '%c'\n", close_char);
for (size_t idx = 0; idx < count; ++idx) {
free_ast(args[idx]);
}
free(args);
return NULL;
}
}
*out_argc = count;
return args;
}
static inline const char *find_hashmap_key_value_colon(const char *start, const char *end) {
int paren = 0, bracket = 0, brace = 0;
bool in_single_quote = false, in_double_quote = false;
for (const char *c = start; c < end; ++c) {
char ch = *c;
if (ch == '\'' && !in_double_quote) {
in_single_quote = !in_single_quote;
continue;
}
if (ch == '"' && !in_single_quote) {
in_double_quote = !in_double_quote;
continue;
}
if (in_single_quote || in_double_quote) {
continue;
}
if (ch == '(') {
paren++;
} else if (ch == ')') {
paren--;
} else if (ch == '[') {
bracket++;
} else if (ch == ']') {
bracket--;
} else if (ch == '{') {
brace++;
} else if (ch == '}') {
brace--;
}
if (ch == ':' && paren == 0 && bracket == 0 && brace == 0) {
return c;
}
}
return NULL;
}
static ASTNode *parse_hashmap(const char **p, const char *end) {
ASTNode **keys = NULL, **values = NULL;
size_t capacity = 0, count = 0;
int error = 0;
++(*p); /* Skip opening '{' */
skip_whitespace(p, end);
/* Handle empty hashmap */
if (*p < end && **p == '}') {
++(*p);
return new_hashmap_node(NULL, NULL, 0);
}
while (*p < end && !error) {
const char *colon_pos = find_hashmap_key_value_colon(*p, end);
if (!colon_pos) {
fprintf(stderr, "Error: Expected ':' after key in hashmap\n");
error = 1;
break;
}
/* Parse key expression within [*p, colon_pos) */
const char *key_end = colon_pos;
const char *key_start = *p;
ASTNode *key = parse_expression(&key_start, key_end);
if (!key) {
error = 1;
break;
}
/* Advance *p to colon_pos + 1 (skip colon) */
*p = colon_pos + 1;
skip_whitespace(p, end);
/* Now parse value normally */
ASTNode *value = parse_expression(p, end);
if (!value) {
free_ast(key);
error = 1;
break;
}
if (count >= capacity) {
capacity = capacity ? capacity * 2 : 4;
ASTNode **new_keys = realloc(keys, capacity * sizeof(ASTNode *));
ASTNode **new_values = realloc(values, capacity * sizeof(ASTNode *));
if (!new_keys || !new_values) {
free_ast(key);
free_ast(value);
error = 1;
break;
}
keys = new_keys;
values = new_values;
}
keys[count] = key;
values[count] = value;
count++;
skip_whitespace(p, end);
/* Check terminator or separator */
if (*p < end && **p == '}') {
++(*p); /* Skip closing character */
break;
}
if (*p < end && **p == ',') {
++(*p);
skip_whitespace(p, end);
} else {
fprintf(stderr, "Error: Expected ',' or '}'\n");
error = 1;
}
}
if (error) {
for (size_t idx = 0; idx < count; ++idx) {
free_ast(keys[idx]);
free_ast(values[idx]);
}
free(keys);
free(values);
return NULL;
}
return new_hashmap_node(keys, values, count);
}
static ASTNode *
parse_block_stmt(const char *args_begin, const char *args_end, ASTNode *body, bool trim) {
skip_whitespace(&args_begin, args_end);
const char *name_start = args_begin;
while (args_begin < args_end && (isalnum(*args_begin) || *args_begin == '_')) {
++args_begin;
}
const size_t len = (size_t)(args_begin - name_start);
return new_block_node(name_start, len, body, trim);
}
/* END PARSERS */
static void print_indent(size_t indent) {
for (size_t idx = 0; idx < indent; ++idx) {
printf(" ");
}
}
static void print_ast(const ASTNode *node, size_t indent) {
if (!node) {
print_indent(indent);
puts("(null)");
return;
}
while (node) {
print_indent(indent);
switch (node->type) {
case ASTNodeType_template:
printf("Template:\n");
print_ast(node->next, indent + 1);
return;
case ASTNodeType_text:
printf("Text [%zu]: \"%s\"\n", node->data.text.len, node->data.text.text);
break;
case ASTNodeType_comment:
printf("Comment [%zu]: {#%s#}\n", node->data.comment.len, node->data.comment.text);
break;
case ASTNodeType_output_expr:
printf("OutputExpr:\n");
print_ast(node->data.output.expr, indent + 1);
break;
case ASTNodeType_set_stmt:
printf("SetStmt%s: '%s'\n",
node->data.set_stmt.trim ? " (trimmed)" : "",
eq_op_str(node->data.set_stmt.eq));
print_indent(indent + 1);
printf("Name:\n");
print_ast(node->data.set_stmt.name, indent + 2);
print_indent(indent + 1);
printf("Expr:\n");
print_ast(node->data.set_stmt.expr, indent + 2);
break;
case ASTNodeType_unset_stmt:
printf("UnsetStmt [%zu]: %s\n",
node->data.unset_stmt.name_len,
node->data.unset_stmt.name);
break;
case ASTNodeType_unmacro_stmt:
printf("UnmacroStmt [%zu]: %s\n",
node->data.unmacro_stmt.name_len,
node->data.unmacro_stmt.name);
break;
case ASTNodeType_conditional:
printf("Conditional:\n");
print_indent(indent + 1);
printf("Condition:\n");
print_ast(node->data.conditional.condition, indent + 2);
print_indent(indent + 1);
printf("Body%s:\n", node->data.conditional.trim_body ? " (trimmed)" : "");
print_ast(node->data.conditional.body, indent + 2);
print_indent(indent + 1);
printf("ElseClause%s:\n", node->data.conditional.trim_clause ? " (trimmed)" : "");
print_ast(node->data.conditional.else_clause, indent + 2);
break;
case ASTNodeType_for_loop:
printf("ForLoop%s: varname='%s'\n",
node->data.for_loop.trim ? " (trimmed)" : "",
node->data.for_loop.varname);
print_indent(indent + 1);
printf("Iterable:\n");
print_ast(node->data.for_loop.iterable, indent + 2);
print_indent(indent + 1);
printf("Body:\n");
print_ast(node->data.for_loop.body, indent + 2);
break;
case ASTNodeType_while_loop:
printf("WhileLoop%s:\n", node->data.while_loop.trim ? " (trimmed)" : "");
print_indent(indent + 1);
printf("Condition:\n");
print_ast(node->data.while_loop.condition, indent + 2);
print_indent(indent + 1);
printf("Body:\n");
print_ast(node->data.while_loop.body, indent + 2);
break;
case ASTNodeType_binary_expr:
printf("BinaryExpr: op='%s'\n", binary_op_str(node->data.binary.op));
print_indent(indent + 1);
printf("Left:\n");
print_ast(node->data.binary.left, indent + 2);
print_indent(indent + 1);
printf("Right:\n");
print_ast(node->data.binary.right, indent + 2);
break;
case ASTNodeType_unary_expr:
printf("UnaryExpr: op='%s'\n", unary_op_str(node->data.unary.op));
print_indent(indent + 1);
printf("Operand:\n");
print_ast(node->data.unary.operand, indent + 2);
break;
case ASTNodeType_function_call:
printf("FunctionCall:\n");
print_indent(indent + 1);
printf("Name:\n");
print_ast(node->data.function_call.name, indent + 2);
if (node->data.function_call.args_count == 0) {
print_indent(indent + 1);
printf("Args: (null)\n");
} else {
for (size_t arg = 0; arg < node->data.function_call.args_count; ++arg) {
print_indent(indent + 1);
printf("Args [%zu]:\n", arg);
print_ast(node->data.function_call.args[arg], indent + 2);
}
}
break;
case ASTNodeType_variable:
printf("Variable [%zu]: name='%s'\n",
node->data.variable.name_len,
node->data.variable.name);
break;
case ASTNodeType_literal_string:
printf("LiteralString [%zu]: %s\"%s\"\n",
node->data.string.value_len,
node->data.string.raw ? "r" : "",
node->data.string.value);
break;
case ASTNodeType_fmt_string:
printf("FmtString:\n");
print_ast(node->data.fmt_string.string, indent + 1);
break;
case ASTNodeType_literal_number:
printf("LiteralNumber: %ju\n", node->data.number.value);
break;
case ASTNodeType_literal_float:
printf("LiteralFloat: %.15lf\n", node->data.floatnum.value);
break;
case ASTNodeType_literal_boolean:
printf("LiteralBoolean: %s\n", node->data.boolean.value ? "true" : "false");
break;
case ASTNodeType_literal_null:
printf("LiteralNull\n");
break;
case ASTNodeType_literal_inf:
printf("LiteralInf\n");
break;
case ASTNodeType_parent_stmt:
printf("ParentStmt\n");
break;
case ASTNodeType_import_stmt:
printf("ImportStmt%s:\n", node->data.import_stmt.trim ? " (trimmed)" : "");
print_indent(indent + 1);
printf("Name:\n");
print_ast(node->data.import_stmt.filename, indent + 1);
print_indent(indent + 1);
if (node->data.import_stmt.namespace_size == 0) {
puts("Namespace: <global>");
} else {
printf("Namespace [%zu]: %s\n",
node->data.import_stmt.namespace_size,
node->data.import_stmt.namespace);
}
break;
case ASTNodeType_lookup:
printf("Lookup:\n");
print_indent(indent + 1);
printf("Operand:\n");
print_ast(node->data.lookup.operand, indent + 2);
print_indent(indent + 1);
printf("Index:\n");
print_ast(node->data.lookup.index, indent + 2);
break;
case ASTNodeType_member_lookup:
printf("MemberLookup:\n");
print_indent(indent + 1);
printf("Operand:\n");
print_ast(node->data.member_lookup.operand, indent + 2);
print_indent(indent + 1);
printf("Member [%zu]: membername='%s'\n",
node->data.member_lookup.member_len,
node->data.member_lookup.member);
break;
case ASTNodeType_macro_stmt:
printf("MacroStmt%s:\n", node->data.macro_stmt.trim ? " (trimmed)" : "");
print_indent(indent + 1);
printf(
"Name [%zu]: %s\n", node->data.macro_stmt.name_len, node->data.macro_stmt.name);
if (node->data.macro_stmt.args_count == 0) {
print_indent(indent + 1);
printf("Args: (null)\n");
} else {
for (size_t idx = 0; idx < node->data.macro_stmt.args_count; ++idx) {
print_indent(indent + 1);
printf("Args [%zu]:\n", idx);
print_ast(node->data.macro_stmt.args[idx], indent + 2);
}
}
print_indent(indent + 1);
printf("Body:\n");
print_ast(node->data.macro_stmt.body, indent + 2);
break;
case ASTNodeType_namespace_stmt:
printf("NamespaceStmt%s:\n", node->data.namespace_stmt.trim ? " (trimmed)" : "");
print_indent(indent + 1);
printf("Name [%zu]: %s\n",
node->data.namespace_stmt.name_len,
node->data.namespace_stmt.name);
print_indent(indent + 1);
printf("Body:\n");
print_ast(node->data.namespace_stmt.body, indent + 2);
break;
case ASTNodeType_literal_array:
printf("LiteralArray:\n");
if (node->data.array.items_count == 0) {
print_indent(indent + 1);
printf("Items: (null)\n");
} else {
for (size_t idx = 0; idx < node->data.array.items_count; ++idx) {
print_indent(indent + 1);
printf("Items [%zu]:\n", idx);
print_ast(node->data.array.items[idx], indent + 2);
}
}
break;
case ASTNodeType_literal_hashmap:
printf("LiteralHashmap:\n");
if (node->data.array.items_count == 0) {
print_indent(indent + 1);
printf("Pairs: (null)\n");
} else {
for (size_t idx = 0; idx < node->data.hashmap.count; ++idx) {
print_indent(indent + 1);
printf("Pairs [%zu]:\n", idx);
print_indent(indent + 2);
printf("Key:\n");
print_ast(node->data.hashmap.keys[idx], indent + 3);
print_indent(indent + 2);
printf("Value:\n");
print_ast(node->data.hashmap.values[idx], indent + 3);
}
}
break;
case ASTNodeType_finish_stmt:
printf("FinishStmt\n");
break;
case ASTNodeType_break_stmt:
printf("BreakStmt\n");
break;
case ASTNodeType_continue_stmt:
printf("ContinueStmt\n");
break;
case ASTNodeType_block_stmt:
printf("BlockStmt%s [%zu]: %s\n",
node->data.block.trim ? " (trimmed)" : "",
node->data.block.name_len,
node->data.block.name ? node->data.block.name : "<whitespace control>");
print_ast(node->data.block.body, indent + 2);
break;
case ASTNodeType_range:
printf("Range:\n");
print_indent(indent + 1);
printf("From:\n");
print_ast(node->data.range.from, indent + 2);
print_indent(indent + 1);
printf("To:\n");
print_ast(node->data.range.to, indent + 2);
print_indent(indent + 1);
printf("Step:\n");
print_ast(node->data.range.step, indent + 2);
break;
default:
printf("Unknown ASTNodeType: %d\n", node->type);
}
node = node->next;
}
}
static bool parse_template(ASTNode *root, const char *input, size_t len) {
ASTNode **current = &root->next; /* pointer to next field to append nodes */
const char *p = input;
const char *end = input + len;
const char *text_start = p;
while (p <= end && *p) {
if (p[0] == '{' && p[1] == '#') {
/* Comment node */
if (p > text_start) {
*current = new_text_node(text_start, (size_t)(p - text_start));
current = &(*current)->next;
}
/* Find comment end #} */
p += 2; /* skip {# */
const char *comment_start = p;
while (*p && !(p[0] == '#' && p[1] == '}')) {
++p;
}
if (*p == '\0') {
fprintf(stderr, "Error: unterminated comment. Expected #}\n");
free_ast(root);
return false;
}
const size_t comment_len = (size_t)(p - comment_start);
*current = new_comment_node(comment_start, comment_len);
current = &(*current)->next;
p += 2; /* skip #} */
text_start = p;
} else if (p[0] == '{' && p[1] == '{') {
/* Output node */
if (p > text_start) {
*current = new_text_node(text_start, (size_t)(p - text_start));
current = &(*current)->next;
}
p += 2; /* skip {{ */
const char *expr_start = p;
/* Find closing "}}" */
while (*p && !(p[0] == '}' && p[1] == '}')) {
if (*p == '"') {
++p;
while (*p) {
if (*p == '\\') {
p += 2;
} else if (*p == '"') {
++p;
break;
} else {
++p;
}
}
if (*(p - 1) != '"') {
fprintf(stderr, "Error: Unterminated string literal\n");
return false;
}
continue;
}
++p;
}
if (*p == '\0') {
fprintf(stderr, "Error: unterminated output expression. Expected }}\n");
free_ast(root);
return false;
}
ASTNode *expr_node = parse_expression(&expr_start, p);
if (!expr_node) {
fprintf(stderr, "Error: invalid expression inside {{ }}\n");
free_ast(root);
return false;
}
*current = new_output_node(expr_node);
current = &(*current)->next;
p += 2; /* skip }} */
text_start = p;
} else if (p[0] == '{' && p[1] == '%') {
/* Statement node */
if (p > text_start) {
*current = new_text_node(text_start, (size_t)(p - text_start));
current = &(*current)->next;
}
p += 2; /* skip {% */
int trim = 0;
if (*p == '-') {
trim = 1;
++p;
}
skip_whitespace(&p, end); /* skip any whitespace after {% */
/* const char *stmt_start = p; */
/* Parse the keyword */
const char *kw_start = p;
while (p < end && isalpha(*p)) {
++p;
}
const char *kw_end = p;
if (kw_start == kw_end) {
fprintf(stderr, "Error: expected statement keyword after `{%%`\n");
free_ast(root);
return false;
}
size_t kw_len = (size_t)(kw_end - kw_start);
skip_whitespace(&p, end);
/* Find end of the {% ... %} tag */
const char *tag_close = strstr(p, trim ? "-%}" : "%}");
if (!tag_close) {
fprintf(stderr, "Error: unterminated `{%% ... %%}` statement\n");
free_ast(root);
return false;
}
const char *stmt_keyword = kw_start;
if (kw_len == 3 && strncmp(stmt_keyword, "set", 3) == 0) {
ASTNode *stmt = parse_set_stmt(kw_end, tag_close, trim);
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 5 && strncmp(stmt_keyword, "unset", 5) == 0) {
if (trim) {
fprintf(stderr, "Error: `unset` cannot be trimmed.\n");
free_ast(root);
return false;
}
ASTNode *stmt = parse_unset_stmt(kw_end, tag_close);
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 7 && strncmp(stmt_keyword, "unmacro", 7) == 0) {
if (trim) {
fprintf(stderr, "Error: `unmacro` cannot be trimmed.\n");
free_ast(root);
return false;
}
ASTNode *stmt = parse_unmacro_stmt(kw_end, tag_close);
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 6 && strncmp(stmt_keyword, "import", 6) == 0) {
ASTNode *stmt = parse_import_stmt(kw_end, tag_close, trim);
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 6 && strncmp(stmt_keyword, "finish", 6) == 0) {
if (trim) {
fprintf(stderr, "Error: `finish` cannot be trimmed.\n");
free_ast(root);
return false;
}
ASTNode *stmt = new_finish_stmt_node();
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 5 && strncmp(stmt_keyword, "break", 5) == 0) {
if (trim) {
fprintf(stderr, "Error: `break` cannot be trimmed.\n");
free_ast(root);
return false;
}
ASTNode *stmt = new_break_node();
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 8 && strncmp(stmt_keyword, "continue", 8) == 0) {
if (trim) {
fprintf(stderr, "Error: `continue` cannot be trimmed.\n");
free_ast(root);
return false;
}
ASTNode *stmt = new_continue_node();
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 6 && strncmp(stmt_keyword, "parent", 6) == 0) {
ASTNode *stmt = new_parent_node();
if (!stmt) {
free_ast(root);
return false;
}
*current = stmt;
current = &(*current)->next;
p = tag_close + 2 + trim; /* skip %} */
} else if (kw_len == 2 && strncmp(stmt_keyword, "if", 2) == 0) {
ASTNode *condition = parse_expression(&kw_end, tag_close);
if (!condition) {
fprintf(stderr, "Error: invalid expression in if statement\n");
free_ast(root);
return false;
}
const char *block_start = tag_close + 2 + trim;
const char *block_end = block_start;
int nest_level = 1;
ASTNode *root_if = NULL;
ASTNode *current_if = NULL; /* Current if/elif node being processed */
ASTNode **current_else_ptr = NULL; /* Pointer to next else_clause slot */
/* Initialize first if node */
current_if = new_conditional_node(condition, NULL, NULL, trim, trim);
root_if = current_if;
current_else_ptr = &current_if->data.conditional.else_clause;
while (block_end < end) {
if (block_end[0] == '{' && block_end[1] == '%') {
const char *tmp = block_end + 2;
int local_trim = 0;
if (*tmp == '-') {
local_trim = 1;
++tmp;
}
skip_whitespace(&tmp, end);
const char *inner_kw = tmp;
while (tmp < end && isalpha(*tmp))
++tmp;
size_t inner_kw_len = (size_t)(tmp - inner_kw);
if (inner_kw_len == 2 && strncmp(inner_kw, "if", 2) == 0) {
++nest_level;
} else if (inner_kw_len == 5 && strncmp(inner_kw, "while", 5) == 0) {
++nest_level;
} else if (inner_kw_len == 3 && strncmp(inner_kw, "for", 3) == 0) {
++nest_level;
} else if (inner_kw_len == 3 && strncmp(inner_kw, "end", 3) == 0) {
skip_whitespace(&tmp, end);
if (nest_level == 1) {
if (trim) {
++tmp;
}
} else {
if (local_trim) {
++tmp;
}
}
if (tmp + 1 < end && tmp[0] == '%' && tmp[1] == '}') {
--nest_level;
if (nest_level == 0) {
/* Parse final body segment */
ASTNode *final_body = malloc(sizeof(ASTNode));
final_body->next = NULL;
if (!parse_template(final_body,
block_start,
(size_t)(block_end - block_start))) {
fprintf(stderr, "Error: Invalid final block\n");
free(final_body);
free_ast(root);
return false;
}
if (condition != NULL) {
/* If/elif branch: set body of current node */
if (current_if) {
current_if->data.conditional.body = final_body->next;
} else {
fprintf(stderr, "Error: no current if node\n");
free(final_body);
free_ast(root);
return false;
}
} else {
/* Else branch: attach to else_clause */
*current_else_ptr = final_body->next;
}
free(final_body);
p = tmp + 2;
break;
}
}
} else if (nest_level == 1 && inner_kw_len == 4 &&
strncmp(inner_kw, "elif", 4) == 0) {
/* Parse elif condition */
skip_whitespace(&tmp, end);
const char *elif_tag_close = strstr(tmp, local_trim ? "-%}" : "%}");
if (!elif_tag_close) {
fprintf(stderr, "Error: unterminated elif tag\n");
free_ast(root);
return false;
}
ASTNode *elif_condition = parse_expression(&tmp, elif_tag_close);
if (!elif_condition) {
fprintf(stderr, "Error: invalid expression in elif\n");
free_ast(root);
return false;
}
/* Parse body for current if/elif */
ASTNode *body = malloc(sizeof(ASTNode));
body->next = NULL;
if (!parse_template(
body, block_start, (size_t)(block_end - block_start))) {
fprintf(stderr, "Error: Invalid if/elif block\n");
free(body);
free_ast(root);
return false;
}
/* Assign body to current if node */
current_if->data.conditional.body = body->next;
current_if->data.conditional.trim_clause = local_trim;
free(body);
/* Create new node for elif */
ASTNode *elif_node =
new_conditional_node(elif_condition, NULL, NULL, local_trim, trim);
*current_else_ptr = elif_node; /* Attach to previous else_clause */
current_else_ptr =
&elif_node->data.conditional.else_clause; /* Update pointer */
current_if = elif_node; /* Set as current node */
condition = elif_condition; /* Update condition state */
block_start =
elif_tag_close + 2 + local_trim; /* Start after {% elif %} */
} else if (nest_level == 1 && inner_kw_len == 4 &&
strncmp(inner_kw, "else", 4) == 0) {
/* Parse body for current if/elif */
ASTNode *body = malloc(sizeof(ASTNode));
body->next = NULL;
if (!parse_template(
body, block_start, (size_t)(block_end - block_start))) {
fprintf(stderr, "Error: Invalid if/else block\n");
free(body);
free_ast(root);
return false;
}
/* Assign body to current node */
current_if->data.conditional.body = body->next;
current_if->data.conditional.trim_clause = local_trim;
free(body);
/* Prepare for else clause */
current_else_ptr = &current_if->data.conditional.else_clause;
condition = NULL; /* Mark as else branch */
current_if = NULL; /* Clear current node */
/* Locate else tag end */
const char *else_tag_close = strstr(tmp, local_trim ? "-%}" : "%}");
if (!else_tag_close) {
fprintf(stderr, "Error: unterminated else tag\n");
free_ast(root);
return false;
}
block_start = else_tag_close + 2 + local_trim;
}
/* Advance to next tag */
const char *inner_tag_close = strstr(tmp, local_trim ? "-%}" : "%}");
if (!inner_tag_close) {
fprintf(stderr, "Error: unterminated tag inside if-block\n");
free_ast(root);
return false;
}
block_end = inner_tag_close + 2 + local_trim;
continue;
}
++block_end;
}
if (nest_level > 0) {
fprintf(stderr, "Error: unmatched if statement\n");
free_ast(root);
return false;
}
if (!root_if) {
fprintf(stderr, "Error: no if statement created\n");
free_ast(root);
return false;
}
*current = root_if;
current = &(*current)->next;
} else {
/* Simple blocks */
const char *block_start = tag_close + 2 + trim;
const char *block_end = block_start;
int nest_level = 1;
while (block_end < end) {
if (block_end[0] == '{' && block_end[1] == '%') {
const char *tmp = block_end + 2; /* After `{%` */
int local_trim = 0;
if (*tmp == '-') {
local_trim = 1;
++tmp;
}
skip_whitespace(&tmp, end); /* Skip whitespace before keyword */
if (strncmp(tmp, "end", 3) == 0) {
tmp += 3; /* Move past "end" */
skip_whitespace(&tmp, end); /* Skip whitespace after "end" */
if (local_trim) {
++tmp;
}
/* Now next chars should be `%}` to close the tag */
if (tmp[0] == '%' && tmp[1] == '}') {
--nest_level;
if (nest_level == 0) {
break;
}
}
} else {
/* Check if this is a nested start block */
const char *inner_kw = tmp;
while (*tmp && isalpha(*tmp)) {
++tmp;
}
size_t inner_kw_len = (size_t)(tmp - inner_kw);
/* block keywords that can nest (macros and namespaces cannot nest) */
if ((inner_kw_len == 5 && strncmp(inner_kw, "while", 5) == 0) ||
(inner_kw_len == 3 && strncmp(inner_kw, "for", 3) == 0) ||
(inner_kw_len == 2 && strncmp(inner_kw, "if", 2) == 0) ||
(inner_kw_len == 5 && strncmp(inner_kw, "block", 5) == 0)) {
++nest_level;
}
}
}
++block_end;
}
if (nest_level > 0) {
fprintf(stderr,
"Error: unmatched `{%% %.*s %%}` block\n",
(int)kw_len,
stmt_keyword);
free_ast(root);
return false;
}
ASTNode *body = malloc(sizeof(ASTNode));
if (!body) {
return NULL;
}
body->type = ASTNodeType_template;
body->next = NULL;
if (!parse_template(body, block_start, (size_t)(block_end - block_start))) {
free_ast(root);
return NULL;
}
/* Pass body (excluding {% end %}) to corresponding parser */
ASTNode *stmt = NULL;
if (kw_len == 5 && strncmp(stmt_keyword, "while", 5) == 0) {
stmt = parse_while_stmt(kw_end, tag_close, body->next, trim);
} else if (kw_len == 5 && strncmp(stmt_keyword, "macro", 5) == 0) {
stmt = parse_macro_stmt(kw_end, tag_close, body->next, trim);
} else if (kw_len == 9 && strncmp(stmt_keyword, "namespace", 9) == 0) {
stmt = parse_namespace_stmt(kw_end, tag_close, body->next, trim);
} else if (kw_len == 3 && strncmp(stmt_keyword, "for", 3) == 0) {
stmt = parse_for_stmt(kw_end, tag_close, body->next, trim);
} else if (kw_len == 5 && strncmp(stmt_keyword, "block", 5) == 0) {
stmt = parse_block_stmt(kw_end, tag_close, body->next, trim);
} else {
fprintf(stderr,
"Error: unknown statement `{%% %.*s %%}`\n",
(int)kw_len,
stmt_keyword);
free_ast(root);
free_ast(body);
return false;
}
if (!stmt) {
free_ast(root);
free_ast(body);
return false;
}
*current = stmt;
current = &(*current)->next;
/* Skip to after {% end %} */
const char *end_tag = strstr(block_end, trim ? "-%}" : "%}");
if (!end_tag) {
fprintf(stderr, "Error: unterminated `{%% end %%}`\n");
free_ast(root);
free_ast(body);
return false;
}
p = end_tag + 2 + trim; /* skip final %} */
free(body);
}
text_start = p;
} else {
++p;
}
}
/* Add trailing text */
if (p > text_start) {
*current = new_text_node(text_start, (size_t)(p - text_start));
}
return true;
}
#if 1
int main(void) {
const char *template_text = "{{ 1.2e3 @ hi }}";
puts(template_text);
ASTNode *root = malloc(sizeof(ASTNode));
root->type = ASTNodeType_template;
root->next = NULL;
if (parse_template(root, template_text, strlen(template_text))) {
print_ast(root, 0);
free_ast(root);
} else {
puts("NOOOOOOOO");
}
return 0;
}
#else
int main(void) {
const char *filename = "../doc/temple-example/templates/dashboard.lpt";
FILE *file = fopen(filename, "rb");
if (!file) {
perror("Failed to open file");
return 1;
}
if (fseek(file, 0, SEEK_END) != 0) {
perror("Failed to seek file");
fclose(file);
return 1;
}
long filesize = ftell(file);
if (filesize < 0) {
perror("Failed to get file size");
fclose(file);
return 1;
}
rewind(file);
char *template_text = malloc(filesize + 1);
if (!template_text) {
perror("Failed to allocate memory");
fclose(file);
return 1;
}
size_t read_size = fread(template_text, 1, filesize, file);
if (read_size != (size_t)filesize) {
perror("Failed to read file");
free(template_text);
fclose(file);
return 1;
}
template_text[filesize] = '\0';
fclose(file);
ASTNode *root = malloc(sizeof(ASTNode));
if (!root) {
perror("Failed to allocate ASTNode");
free(template_text);
return 1;
}
root->type = ASTNodeType_template;
root->next = NULL;
if (parse_template(root, template_text, filesize)) {
print_ast(root, 0);
free_ast(root);
} else {
puts("NOOOOOOOO");
}
free(template_text);
return 0;
}
#endif