/* * crprintf - printf with inline color tags and variables, powered by a register-based VM * * Copyright (c) 2026 theMackabu (me@themackabu.dev) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include #include #include #include "crprintf.h" static bool crprintf_no_color = false; static bool crprintf_debug = false; static bool crprintf_debug_hex = false; void crprintf_set_color(bool enable) { crprintf_no_color = !enable; } bool crprintf_get_color(void) { return !crprintf_no_color; } void crprintf_set_debug(bool enable) { crprintf_debug = enable; } bool crprintf_get_debug(void) { return crprintf_debug; } void crprintf_set_debug_hex(bool enable) { crprintf_debug_hex = enable; } bool crprintf_get_debug_hex(void) { return crprintf_debug_hex; } static inline int hex_digit(char c) { static const int8_t lookup[256] = { ['0']=0, ['1']=1, ['2']=2, ['3']=3, ['4']=4, ['5']=5, ['6']=6, ['7']=7, ['8']=8, ['9']=9, ['a']=10, ['b']=11, ['c']=12, ['d']=13, ['e']=14, ['f']=15, ['A']=10, ['B']=11, ['C']=12, ['D']=13, ['E']=14, ['F']=15, }; int8_t val = lookup[(unsigned char)c]; return val ? val : (c == '0' ? 0 : -1); } typedef enum { OP_NOP = 0, OP_EMIT_LIT, OP_EMIT_FMT, OP_SET_FG, OP_SET_BG, OP_SET_FG_RGB, OP_SET_BG_RGB, OP_SET_BOLD, OP_SET_DIM, OP_SET_UL, OP_SET_ITALIC, OP_SET_STRIKE, OP_SET_INVERT, OP_STYLE_PUSH, OP_STYLE_FLUSH, OP_STYLE_RESET, OP_STYLE_RESET_ALL, OP_PAD_BEGIN, OP_RPAD_BEGIN, OP_PAD_END, OP_EMIT_SPACES, OP_EMIT_NEWLINES, OP_SNAPSHOT, OP_HALT, OP_MAX } opcode_t; typedef struct { uint32_t op; uint32_t operand; } instruction_t; typedef enum { COL_NONE = 0, COL_BLACK = 30, COL_RED, COL_GREEN, COL_YELLOW, COL_BLUE, COL_MAGENTA, COL_CYAN, COL_WHITE, COL_GRAY = 90, COL_BRIGHT_RED, COL_BRIGHT_GREEN, COL_BRIGHT_YELLOW, COL_BRIGHT_BLUE, COL_BRIGHT_MAGENTA, COL_BRIGHT_CYAN, COL_BRIGHT_WHITE, COL_RGB = 0xFF } color_t; #define UNPACK_R(c) (((c)>>16)&0xFF) #define UNPACK_G(c) (((c)>>8)&0xFF) #define UNPACK_B(c) ((c)&0xFF) #define PACK_RGB(r,g,b) \ (((uint32_t)(r)<<16)|((uint32_t)(g)<<8)|(uint32_t)(b)) #define STYLE_BOLD 0x01 #define STYLE_DIM 0x02 #define STYLE_UL 0x04 #define STYLE_ITALIC 0x08 #define STYLE_STRIKE 0x10 #define STYLE_INVERT 0x20 typedef struct { size_t mark; int width; int right_align; } pad_entry_t; typedef struct { uint32_t fg; uint32_t bg; uint32_t fg_rgb; uint32_t bg_rgb; uint8_t flags; } style_entry_t; typedef struct { style_entry_t current; style_entry_t style_stack[8]; pad_entry_t pad_stack[8]; int style_depth; int pad_depth; } vm_regs_t; #define MAX_VARS 64 #define MAX_VAR_NAME 32 #define MAX_VAR_VALUE 128 typedef struct { char name[MAX_VAR_NAME]; char value[MAX_VAR_VALUE]; int nlen; int vlen; int is_fmt; } crprintf_var_t; typedef struct { crprintf_var_t vars[MAX_VARS]; int count; } var_table_t; typedef struct { const var_table_t *base; var_table_t local; bool has_local; } var_scope_t; typedef struct { vm_regs_t regs; char *out_buf; size_t out_pos; size_t out_cap; size_t resume_ip; bool valid; } vm_checkpoint_t; struct crprintf_compiled { instruction_t *code; size_t code_len; size_t code_cap; char *literals; size_t lit_len; size_t lit_cap; char *source; size_t source_len; uint32_t *src_map; uint32_t *lit_marks; size_t map_cap; const char *compile_base; uint32_t _cur_src_off; vm_checkpoint_t checkpoint; }; typedef struct crprintf_state { style_entry_t current; style_entry_t style_stack[8]; int style_depth; } crprintf_state; static var_table_t global_vars = {0}; static inline const var_table_t *scope_read(const var_scope_t *s) { return s->has_local ? &s->local : s->base; } static inline var_table_t *scope_write(var_scope_t *s) { if (!s->has_local) { s->local = *s->base; s->has_local = true; } return &s->local; } static const char *scan_var_brace( crprintf_compiled *p, const char *ptr, const char **lit, var_scope_t *vars ); static crprintf_compiled *program_new(void) { crprintf_compiled *p = calloc(1, sizeof(*p)); *p = (crprintf_compiled){ .code_cap = 32, .code = malloc(32 * sizeof(instruction_t)), .lit_cap = 256, .literals = malloc(256), }; return p; } static inline void emit_op(crprintf_compiled *p, uint32_t op, uint32_t operand) { if (__builtin_expect(p->code_len >= p->code_cap, 0)) { size_t new_cap = p->code_cap * 2; instruction_t *new_code = realloc(p->code, new_cap * sizeof(instruction_t)); if (!new_code) return; p->code = new_code; p->code_cap = new_cap; } if (p->src_map) { if (__builtin_expect(p->code_len >= p->map_cap, 0)) { size_t new_cap = p->map_cap * 2; p->src_map = realloc(p->src_map, new_cap * sizeof(uint32_t)); p->lit_marks = realloc(p->lit_marks, new_cap * sizeof(uint32_t)); p->map_cap = new_cap; } p->src_map[p->code_len] = p->_cur_src_off; p->lit_marks[p->code_len] = (uint32_t)p->lit_len; } p->code[p->code_len++] = (instruction_t){ op, operand }; } static uint32_t add_literal(crprintf_compiled *p, const char *s, size_t len) { size_t required = p->lit_len + len + 1; if (__builtin_expect(required > p->lit_cap, 0)) { size_t new_cap = p->lit_cap; while (new_cap < required) new_cap *= 2; char *new_literals = realloc(p->literals, new_cap); if (!new_literals) return 0; p->literals = new_literals; p->lit_cap = new_cap; } uint32_t off = (uint32_t)p->lit_len; memcpy(p->literals + p->lit_len, s, len); p->literals[p->lit_len + len] = '\0'; p->lit_len += len + 1; return off; } typedef struct { const char *name; int nlen; color_t col; } color_entry_t; static const color_entry_t fg_colors[] = { {"black",5,COL_BLACK}, {"red",3,COL_RED}, {"green",5,COL_GREEN}, {"yellow",6,COL_YELLOW}, {"blue",4,COL_BLUE}, {"magenta",7,COL_MAGENTA}, {"cyan",4,COL_CYAN}, {"white",5,COL_WHITE}, {"gray",4,COL_GRAY}, {"grey",4,COL_GRAY}, {"bright_red",10,COL_BRIGHT_RED}, {"bright_green",12,COL_BRIGHT_GREEN}, {"bright_yellow",13,COL_BRIGHT_YELLOW}, {"bright_blue",11,COL_BRIGHT_BLUE}, {"bright_magenta",14,COL_BRIGHT_MAGENTA}, {"bright_cyan",11,COL_BRIGHT_CYAN}, {"bright_white",12,COL_BRIGHT_WHITE}, }; static const color_entry_t bg_colors[] = { {"bg_black",8,COL_BLACK}, {"bg_red",6,COL_RED}, {"bg_green",8,COL_GREEN}, {"bg_yellow",9,COL_YELLOW}, {"bg_blue",7,COL_BLUE}, {"bg_magenta",10,COL_MAGENTA}, {"bg_cyan",7,COL_CYAN}, {"bg_white",8,COL_WHITE}, }; static const color_entry_t seg_bg_colors[] = { {"black",5,COL_BLACK}, {"red",3,COL_RED}, {"green",5,COL_GREEN}, {"yellow",6,COL_YELLOW}, {"blue",4,COL_BLUE}, {"magenta",7,COL_MAGENTA}, {"cyan",4,COL_CYAN}, {"white",5,COL_WHITE}, }; #define FG_COUNT (sizeof(fg_colors)/sizeof(fg_colors[0])) #define BG_COUNT (sizeof(bg_colors)/sizeof(bg_colors[0])) #define SEG_BG_COUNT (sizeof(seg_bg_colors)/sizeof(seg_bg_colors[0])) static inline int parse_hex_rgb(const char *hex, int len, uint32_t *rgb) { int r, g, b; if (len == 4) { int r1=hex_digit(hex[1]), g1=hex_digit(hex[2]), b1=hex_digit(hex[3]); if ((r1|g1|b1) < 0) return 0; r=r1*17; g=g1*17; b=b1*17; } else if (len == 7) { int r1=hex_digit(hex[1]),r2=hex_digit(hex[2]); int g1=hex_digit(hex[3]),g2=hex_digit(hex[4]); int b1=hex_digit(hex[5]),b2=hex_digit(hex[6]); if (((r1|r2|g1|g2|b1|b2) < 0)) return 0; r=(r1<<4)+r2; g=(g1<<4)+g2; b=(b1<<4)+b2; } else return 0; *rgb = PACK_RGB(r, g, b); return 1; } static int compile_hex_fg(crprintf_compiled *p, const char *tag, int len) { uint32_t rgb; if (!parse_hex_rgb(tag, len, &rgb)) return 0; emit_op(p, OP_SET_FG_RGB, rgb); return 1; } static int compile_hex_bg(crprintf_compiled *p, const char *hex, int hlen) { uint32_t rgb; if (!parse_hex_rgb(hex, hlen, &rgb)) return 0; emit_op(p, OP_SET_BG_RGB, rgb); return 1; } static int tag_eq(const char *tag, int len, const char *lit) { int ll = (int)strlen(lit); return len == ll && memcmp(tag, lit, ll) == 0; } static int tag_prefix(const char *tag, int len, const char *pfx) { int pl = (int)strlen(pfx); return len > pl && memcmp(tag, pfx, pl) == 0; } static int match_fg(crprintf_compiled *p, const char *s, int len) { for (int i = 0; i < (int)FG_COUNT; i++) if ( len == fg_colors[i].nlen && memcmp(s, fg_colors[i].name, len) == 0) { emit_op(p, OP_SET_FG, fg_colors[i].col); return 1; } return 0; } static int match_bg(crprintf_compiled *p, const char *s, int len) { for (int i = 0; i < (int)BG_COUNT; i++) if ( len == bg_colors[i].nlen && memcmp(s, bg_colors[i].name, len) == 0) { emit_op(p, OP_SET_BG, bg_colors[i].col); return 1; } return 0; } static int match_seg_bg(crprintf_compiled *p, const char *s, int len) { for (int i = 0; i < (int)SEG_BG_COUNT; i++) if ( len == seg_bg_colors[i].nlen && memcmp(s, seg_bg_colors[i].name, len) == 0) { emit_op(p, OP_SET_BG, seg_bg_colors[i].col); return 1; } return 0; } typedef struct { const char *name; int nlen; int op; } attr_entry_t; static const attr_entry_t attrs[] = { { "bold", 4, OP_SET_BOLD }, { "dim", 3, OP_SET_DIM }, { "ul", 2, OP_SET_UL }, { "i", 1, OP_SET_ITALIC }, { "italic", 6, OP_SET_ITALIC }, { "strike", 6, OP_SET_STRIKE }, { "invert", 6, OP_SET_INVERT }, }; #define ATTR_COUNT (sizeof(attrs) / sizeof(attrs[0])) static int match_attr(crprintf_compiled *p, const char *s, int len) { for (int i = 0; i < (int)ATTR_COUNT; i++) if ( len == attrs[i].nlen && memcmp(s, attrs[i].name, len) == 0) { emit_op(p, attrs[i].op, 1); return 1; } return 0; } static const char *next_seg(const char *cur, const char *end, int *out_len) { const char *sep = cur; while (sep < end && *sep != '_') sep++; *out_len = (int)(sep - cur); return sep; } static int match_plus_seg(crprintf_compiled *p, const char *seg, int slen) { if (match_attr(p, seg, slen)) return 1; if (match_fg(p, seg, slen)) return 1; if (match_bg(p, seg, slen)) return 1; if (slen > 0 && seg[0] == '#') return compile_hex_fg(p, seg, slen); if (tag_prefix(seg, slen, "bg_#")) return compile_hex_bg(p, seg + 3, slen - 3); if (tag_prefix(seg, slen, "bg_")) return match_seg_bg(p, seg + 3, slen - 3); return 0; } static int compile_plus_segs(crprintf_compiled *p, const char *s, int len) { const char *seg = s; const char *end = s + len; int emitted = 0; while (seg < end) { const char *next = seg; while (next < end && *next != '+') next++; int slen = (int)(next - seg); if (!match_plus_seg(p, seg, slen)) return 0; emitted++; seg = (next < end) ? next + 1 : end; } return emitted; } static int compile_let(var_scope_t *scope, const char *body, int len) { if (len > 0 && body[len - 1] == '/') len--; const char *p = body; const char *end = body + len; while (p < end) { while (p < end && (*p == ' ' || *p == ',')) p++; if (p >= end) break; const char *eq = memchr(p, '=', end - p); if (!eq) return 0; int nlen = (int)(eq - p); const char *vstart = eq + 1; const char *vend = vstart; if (vstart < end && (*vstart == '\'' || *vstart == '"')) { char quote = *vstart; vstart++; vend = vstart; while (vend < end && *vend != quote) vend++; if (vend >= end) return 0; const char *after = vend + 1; while (after < end && (*after == ' ' || *after == ',')) after++; int vlen = (int)(vend - vstart); if (nlen <= 0 || nlen >= MAX_VAR_NAME || vlen < 0 || vlen >= MAX_VAR_VALUE) return 0; var_table_t *vars = scope_write(scope); if (vars->count >= MAX_VARS) return 0; crprintf_var_t *v = &vars->vars[vars->count++]; memcpy(v->name, p, nlen); v->name[nlen] = '\0'; v->nlen = nlen; memcpy(v->value, vstart, vlen); v->value[vlen] = '\0'; v->vlen = vlen; v->is_fmt = 0; for (int j = 0; j < vlen; j++) { if (vstart[j] == '%' && j + 1 < vlen && vstart[j + 1] != '%') { v->is_fmt = 1; break; } } p = after; continue; } while (vend < end && *vend != ',') vend++; int vlen = (int)(vend - vstart); if (nlen <= 0 || nlen >= MAX_VAR_NAME || vlen <= 0 || vlen >= MAX_VAR_VALUE) return 0; var_table_t *vars = scope_write(scope); if (vars->count >= MAX_VARS) return 0; crprintf_var_t *v = &vars->vars[vars->count++]; memcpy(v->name, p, nlen); v->name[nlen] = '\0'; v->nlen = nlen; memcpy(v->value, vstart, vlen); v->value[vlen] = '\0'; v->vlen = vlen; p = vend; } return 1; } void crprintf_var(const char *name, const char *value) { if (global_vars.count >= MAX_VARS) return; int nlen = (int)strlen(name); int vlen = (int)strlen(value); if (nlen <= 0 || nlen >= MAX_VAR_NAME || vlen <= 0 || vlen >= MAX_VAR_VALUE) return; for (int i = 0; i < global_vars.count; i++) { crprintf_var_t *v = &global_vars.vars[i]; if (v->nlen == nlen && memcmp(v->name, name, nlen) == 0) { memcpy(v->value, value, vlen); v->value[vlen] = '\0'; v->vlen = vlen; return; } } crprintf_var_t *v = &global_vars.vars[global_vars.count++]; memcpy(v->name, name, nlen); v->name[nlen] = '\0'; v->nlen = nlen; memcpy(v->value, value, vlen); v->value[vlen] = '\0'; v->vlen = vlen; } crprintf_state *crprintf_state_new(void) { return calloc(1, sizeof(crprintf_state)); } void crprintf_state_free(crprintf_state *state) { free(state); } crprintf_state *crprintf_state_clone(const crprintf_state *state) { if (!state) return crprintf_state_new(); crprintf_state *clone = malloc(sizeof(crprintf_state)); if (clone) memcpy(clone, state, sizeof(crprintf_state)); return clone; } bool crprintf_state_eq(const crprintf_state *a, const crprintf_state *b) { if (!a && !b) return true; if (!a || !b) return false; if (a->style_depth != b->style_depth) return false; if (memcmp(&a->current, &b->current, sizeof(style_entry_t)) != 0) return false; for (int i = 0; i < a->style_depth; i++) { if (memcmp(&a->style_stack[i], &b->style_stack[i], sizeof(style_entry_t)) != 0) return false; } return true; } static int emit_style_esc(char *esc, size_t esc_size, const style_entry_t *s) { int n = snprintf(esc, esc_size, "\x1b[0m"); if (s->flags & STYLE_BOLD) n += snprintf(esc+n, esc_size-n, "\x1b[1m"); if (s->flags & STYLE_DIM) n += snprintf(esc+n, esc_size-n, "\x1b[2m"); if (s->flags & STYLE_UL) n += snprintf(esc+n, esc_size-n, "\x1b[4m"); if (s->flags & STYLE_ITALIC) n += snprintf(esc+n, esc_size-n, "\x1b[3m"); if (s->flags & STYLE_STRIKE) n += snprintf(esc+n, esc_size-n, "\x1b[9m"); if (s->flags & STYLE_INVERT) n += snprintf(esc+n, esc_size-n, "\x1b[7m"); if (s->fg == COL_RGB) n += snprintf(esc+n, esc_size-n, "\x1b[38;2;%d;%d;%dm", UNPACK_R(s->fg_rgb), UNPACK_G(s->fg_rgb), UNPACK_B(s->fg_rgb)); else if (s->fg) n += snprintf(esc+n, esc_size-n, "\x1b[%dm", s->fg); if (s->bg == COL_RGB) n += snprintf(esc+n, esc_size-n, "\x1b[48;2;%d;%d;%dm", UNPACK_R(s->bg_rgb), UNPACK_G(s->bg_rgb), UNPACK_B(s->bg_rgb)); else if (s->bg) n += snprintf(esc+n, esc_size-n, "\x1b[%dm", s->bg + 10); return n; } static int compile_var_ref(crprintf_compiled *p, var_scope_t *scope, const char *tag, int len) { const char *name = tag + 1; int nlen = len - 1; const char *plus = memchr(name, '+', nlen); int var_nlen = plus ? (int)(plus - name) : nlen; const var_table_t *vars = scope_read(scope); for (int i = 0; i < vars->count; i++) { const crprintf_var_t *v = &vars->vars[i]; if (var_nlen != v->nlen || memcmp(name, v->name, var_nlen) != 0) continue; emit_op(p, OP_STYLE_PUSH, 0); if (!compile_plus_segs(p, v->value, v->vlen)) return 0; if (plus) { const char *rest = plus + 1; int rlen = nlen - var_nlen - 1; if (rlen > 0 && !compile_plus_segs(p, rest, rlen)) return 0; } emit_op(p, OP_STYLE_FLUSH, 0); return 1; } return 0; } static int match_attr_off(crprintf_compiled *p, const char *s, int len) { for (int i = 0; i < (int)ATTR_COUNT; i++) if ( len == attrs[i].nlen && memcmp(s, attrs[i].name, len) == 0) { emit_op(p, attrs[i].op, 0); return 1; } return 0; } static int match_fg_off(crprintf_compiled *p, const char *s, int len) { for (int i = 0; i < (int)FG_COUNT; i++) if ( len == fg_colors[i].nlen && memcmp(s, fg_colors[i].name, len) == 0) { emit_op(p, OP_SET_FG, COL_NONE); return 1; } if (len > 0 && s[0] == '#') { emit_op(p, OP_SET_FG, COL_NONE); return 1; } return 0; } static int match_bg_off(crprintf_compiled *p, const char *s, int len) { for (int i = 0; i < (int)BG_COUNT; i++) if ( len == bg_colors[i].nlen && memcmp(s, bg_colors[i].name, len) == 0) { emit_op(p, OP_SET_BG, COL_NONE); return 1; } if (tag_prefix(s, len, "bg_#")) { emit_op(p, OP_SET_BG, COL_NONE); return 1; } return 0; } static int compile_tag(crprintf_compiled *p, const char *tag, int len, int closing, var_scope_t *vars) { if (closing) { if (tag_eq(tag, len, "pad") || tag_eq(tag, len, "rpad")) { emit_op(p, OP_PAD_END, 0); return 1; } if (match_attr_off(p, tag, len) || match_fg_off(p, tag, len) || match_bg_off(p, tag, len)) { emit_op(p, OP_STYLE_FLUSH, 0); return 1; } emit_op(p, OP_STYLE_RESET, 0); return 1; } if (tag_prefix(tag, len, "let ")) return compile_let(vars, tag + 4, len - 4); if (tag[0] == '$' && len > 1) return compile_var_ref(p, vars, tag, len); if (tag_prefix(tag, len, "pad=")) { emit_op(p, OP_PAD_BEGIN, (uint32_t)atoi(tag + 4)); return 1; } if (tag_prefix(tag, len, "rpad=")) { emit_op(p, OP_RPAD_BEGIN, (uint32_t)atoi(tag + 5)); return 1; } if (tag_prefix(tag, len, "space=") && tag[len-1] == '/') { emit_op(p, OP_EMIT_SPACES, (uint32_t)atoi(tag + 6)); return 1; } if (tag_prefix(tag, len, "gap=") && tag[len-1] == '/') { emit_op(p, OP_EMIT_SPACES, (uint32_t)atoi(tag + 4)); return 1; } if (tag_eq(tag, len, "reset/")) { emit_op(p, OP_STYLE_RESET_ALL, 0); return 1; } if (tag_eq(tag, len, "br/")) { emit_op(p, OP_EMIT_NEWLINES, 1); return 1; } if (tag_prefix(tag, len, "br=") && tag[len-1] == '/') { emit_op(p, OP_EMIT_NEWLINES, (uint32_t)atoi(tag + 3)); return 1; } emit_op(p, OP_STYLE_PUSH, 0); if (match_attr(p, tag, len)) { emit_op(p, OP_STYLE_FLUSH, 0); return 1; } if (match_fg(p, tag, len)) { emit_op(p, OP_STYLE_FLUSH, 0); return 1; } if (match_bg(p, tag, len)) { emit_op(p, OP_STYLE_FLUSH, 0); return 1; } if (tag[0] == '#') { if (!compile_hex_fg(p, tag, len)) return 0; emit_op(p, OP_STYLE_FLUSH, 0); return 1; } if (tag_prefix(tag, len, "bg_#")) { if (!compile_hex_bg(p, tag + 3, len - 3)) return 0; emit_op(p, OP_STYLE_FLUSH, 0); return 1; } if (memchr(tag, '+', len)) { if (compile_plus_segs(p, tag, len)) { emit_op(p, OP_STYLE_FLUSH, 0); return 1; } } const char *seg = tag; const char *end = tag + len; int emitted = 0; while (seg < end) { int slen; const char *sep = next_seg(seg, end, &slen); if (match_attr(p, seg, slen)) { } else if (tag_eq(seg, slen, "bg") && sep < end) { seg = sep + 1; sep = next_seg(seg, end, &slen); if (!match_seg_bg(p, seg, slen)) return 0; } else if (!match_fg(p, seg, slen)) return 0; emitted++; seg = (sep < end) ? sep + 1 : end; } if (emitted) { emit_op(p, OP_STYLE_FLUSH, 0); return 1; } return 0; } static void flush_lit(crprintf_compiled *p, const char *lit, const char *end) { if (end <= lit) return; uint32_t saved_off = p->_cur_src_off; if (p->compile_base && lit >= p->compile_base) p->_cur_src_off = (uint32_t)(lit - p->compile_base); uint32_t off = add_literal(p, lit, end - lit); emit_op(p, OP_EMIT_LIT, off); p->_cur_src_off = saved_off; } static const char *scan_tag(crprintf_compiled *p, const char *ptr, const char **lit, var_scope_t *vars) { flush_lit(p, *lit, ptr); const char *start = ptr + 1; int closing = 0; if (*start == '/') { closing = 1; start++; } if (closing && *start == '>') { emit_op(p, OP_STYLE_RESET, 0); *lit = start + 1; return *lit; } const char *end = start; while (*end && *end != '>') end++; if (*end == '>' && compile_tag(p, start, (int)(end - start), closing, vars)) { *lit = end + 1; return *lit; } uint32_t off = add_literal(p, "<", 1); emit_op(p, OP_EMIT_LIT, off); *lit = ptr + 1; return *lit; } typedef enum { ARG_NONE = 0, ARG_INT, ARG_LONG, ARG_LLONG, ARG_SIZE, ARG_DOUBLE, ARG_CSTR, ARG_PTR, ARG_WINT, ARG_WSTR, } arg_class_t; static arg_class_t classify_arg(const char *spec, int len) { char conv = spec[len - 1]; if (conv == '%') return ARG_NONE; if (conv == 'n') return ARG_PTR; if (conv == 's') return ARG_CSTR; if (conv == 'p') return ARG_PTR; if ( conv == 'f' || conv == 'F' || conv == 'e' || conv == 'E' || conv == 'g' || conv == 'G' || conv == 'a' || conv == 'A' ) return ARG_DOUBLE; const char *p = spec + 1; while (*p == '-' || *p == '+' || *p == ' ' || *p == '#' || *p == '0') p++; if (*p == '*') p++; else while (*p >= '0' && *p <= '9') p++; if (*p == '.') { p++; if (*p == '*') p++; else while (*p >= '0' && *p <= '9') p++; } if (p[0] == 'z') return ARG_SIZE; if (p[0] == 'l' && p[1] == 'l') return ARG_LLONG; if (p[0] == 'l' && conv == 'c') return ARG_WINT; if (p[0] == 'l' && conv == 's') return ARG_WSTR; if (p[0] == 'l') return ARG_LONG; if (p[0] == 'j') return ARG_LLONG; return ARG_INT; } #if defined(__x86_64__) && !defined(_WIN32) #define CRP_VA_REF_T va_list #define CRP_VA_PASS(ap) (ap) #define CRP_VA_DEREF(ap) (ap) #else #define CRP_VA_REF_T va_list * #define CRP_VA_PASS(ap) (&(ap)) #define CRP_VA_DEREF(ap) (*(ap)) #endif static inline void advance_format_args(const char *spec, CRP_VA_REF_T ap) { const char *p = spec + 1; while ( *p == '-' || *p == '+' || *p == ' ' || *p == '#' || *p == '0' ) p++; if (*p == '*') { (void)va_arg(CRP_VA_DEREF(ap), int); p++; } else while (*p >= '0' && *p <= '9') p++; if (*p == '.') { p++; if (*p == '*') { (void)va_arg(CRP_VA_DEREF(ap), int); p++; } else while (*p >= '0' && *p <= '9') p++; } switch (classify_arg(spec, (int)strlen(spec))) { case ARG_INT: (void)va_arg(CRP_VA_DEREF(ap), int); break; case ARG_LONG: (void)va_arg(CRP_VA_DEREF(ap), long); break; case ARG_LLONG: (void)va_arg(CRP_VA_DEREF(ap), long long); break; case ARG_SIZE: (void)va_arg(CRP_VA_DEREF(ap), size_t); break; case ARG_DOUBLE: (void)va_arg(CRP_VA_DEREF(ap), double); break; case ARG_CSTR: (void)va_arg(CRP_VA_DEREF(ap), const char *); break; case ARG_PTR: (void)va_arg(CRP_VA_DEREF(ap), void *); break; case ARG_WINT: (void)va_arg(CRP_VA_DEREF(ap), wint_t); break; case ARG_WSTR: (void)va_arg(CRP_VA_DEREF(ap), wchar_t *); break; case ARG_NONE: break; } } static const char *scan_fmt(crprintf_compiled *p, const char *ptr, const char **lit) { flush_lit(p, *lit, ptr); const char *fs = ptr + 1; while (*fs=='-'||*fs=='+'||*fs==' '||*fs=='#'||*fs=='0') fs++; if (*fs == '*') fs++; else while (*fs >= '0' && *fs <= '9') fs++; if (*fs == '.') { fs++; if (*fs == '*') fs++; else while (*fs >= '0' && *fs <= '9') fs++; } while (*fs=='h'||*fs=='l'||*fs=='L'||*fs=='z'||*fs=='j'||*fs=='t') fs++; if (*fs) fs++; uint32_t off = add_literal(p, ptr, fs - ptr); arg_class_t cls = classify_arg(ptr, (int)(fs - ptr)); emit_op(p, OP_EMIT_FMT, off | ((uint32_t)cls << 28)); *lit = fs; return fs; } static const char *scan_escape( crprintf_compiled *p, const char *ptr, const char **lit, const char *emit, size_t elen ) { flush_lit(p, *lit, ptr); uint32_t off = add_literal(p, emit, elen); emit_op(p, OP_EMIT_LIT, off); *lit = ptr + 2; return *lit; } static void transform_case(char *dst, const char *src, int len, int lower) { for (int i = 0; i < len; i++) dst[i] = (char)(lower ? tolower((unsigned char)src[i]) : toupper((unsigned char)src[i]) ); } static const char *scan_let_brace(crprintf_compiled *p, const char *ptr, const char **lit, var_scope_t *vars) { flush_lit(p, *lit, ptr); const char *body = ptr + 5; const char *end = body; while (*end && *end != '}') end++; if (*end == '}' && compile_let(vars, body, (int)(end - body))) { *lit = end + 1; return *lit; } uint32_t off = add_literal(p, "{", 1); emit_op(p, OP_EMIT_LIT, off); *lit = ptr + 1; return *lit; } static void compile_fragment(crprintf_compiled *p, const char *fmt, var_scope_t *vars) { const char *ptr = fmt; const char *lit = ptr; while (*ptr) { if (p->compile_base && fmt >= p->compile_base && fmt <= p->compile_base + p->source_len) p->_cur_src_off = (uint32_t)(ptr - p->compile_base); if (*ptr == '<' && ptr[1] == '<') ptr = scan_escape(p, ptr, &lit, "<", 1); else if (*ptr == '>' && ptr[1] == '>') ptr = scan_escape(p, ptr, &lit, ">", 1); else if (*ptr == '%' && ptr[1] == '%') ptr = scan_escape(p, ptr, &lit, "%", 1); else if (*ptr == '{' && memcmp(ptr, "{let ", 5) == 0) ptr = scan_let_brace(p, ptr, &lit, vars); else if (*ptr == '{') ptr = scan_var_brace(p, ptr, &lit, vars); else if (*ptr == '<') ptr = scan_tag(p, ptr, &lit, vars); else if (*ptr == '%' && ptr[1] && ptr[1] != '%') ptr = scan_fmt(p, ptr, &lit); else ptr++; } if (p->compile_base && fmt >= p->compile_base && fmt <= p->compile_base + p->source_len) p->_cur_src_off = (uint32_t)(ptr - p->compile_base); flush_lit(p, lit, ptr); } static const char *scan_var_brace(crprintf_compiled *p, const char *ptr, const char **lit, var_scope_t *vars) { flush_lit(p, *lit, ptr); const char *name = ptr + 1; const char *end = name; while (*end && *end != '}') end++; if (*end != '}') goto emit_brace; int lower = 0, upper = 0; if (*name == '~') { lower = 1; name++; } else if (*name == '^') { upper = 1; name++; } int nlen = (int)(end - name); if (*name == '\'' || *name == '"') { const char *s = name + 1; const char *e = memchr(s, *name, end - s); if (!e) goto emit_brace; int slen = (int)(e - s); if (slen > 0 && slen < MAX_VAR_VALUE) { char buf[MAX_VAR_VALUE]; if (lower || upper) { transform_case(buf, s, slen, lower); s = buf; } uint32_t off = add_literal(p, s, slen); emit_op(p, OP_EMIT_LIT, off); } *lit = end + 1; return *lit; } const var_table_t *vtbl = scope_read(vars); for (int i = 0; i < vtbl->count; i++) { const crprintf_var_t *v = &vtbl->vars[i]; if (nlen != v->nlen || memcmp(name, v->name, nlen) != 0) continue; const char *val = v->value; int vlen = v->vlen; char buf[MAX_VAR_VALUE]; if (lower || upper) { transform_case(buf, val, vlen, lower); val = buf; } if (memchr(val, '<', vlen)) { char tmp[MAX_VAR_VALUE + 1]; memcpy(tmp, val, vlen); tmp[vlen] = '\0'; const char *saved_base = p->compile_base; p->compile_base = NULL; compile_fragment(p, tmp, vars); p->compile_base = saved_base; } else if (v->is_fmt) { arg_class_t cls = classify_arg(val, vlen); uint32_t off = add_literal(p, val, vlen); emit_op(p, OP_EMIT_FMT, off | ((uint32_t)cls << 28)); } else { uint32_t off = add_literal(p, val, vlen); emit_op(p, OP_EMIT_LIT, off); } *lit = end + 1; return *lit; } emit_brace:; uint32_t off = add_literal(p, "{", 1); emit_op(p, OP_EMIT_LIT, off); *lit = ptr + 1; return *lit; } crprintf_compiled *crprintf_compile(const char *fmt) { crprintf_compiled *p = program_new(); var_scope_t vars = { .base = &global_vars }; compile_fragment(p, fmt, &vars); emit_op(p, OP_HALT, 0); return p; } static size_t visible_len(const char *s, size_t n) { size_t vis = 0; for (size_t i = 0; i < n; i++) { if (s[i] == '\x1b') while (++i < n && !isalpha(s[i])); else vis++; } return vis; } typedef struct { char *data; size_t len; } vm_output_t; static vm_output_t crprintf_vm_run_ex(crprintf_compiled *prog, va_list ap, crprintf_state *state, const vm_checkpoint_t *ckpt); static vm_output_t crprintf_vm_run(crprintf_compiled *prog, va_list ap, crprintf_state *state) { return crprintf_vm_run_ex(prog, ap, state, NULL); } static vm_output_t crprintf_vm_run_ex(crprintf_compiled *prog, va_list ap, crprintf_state *state, const vm_checkpoint_t *ckpt) { vm_regs_t regs = {0}; size_t cap, pos; char *out; if (ckpt && ckpt->valid) { regs = ckpt->regs; cap = ckpt->out_cap; pos = ckpt->out_pos; out = malloc(cap); if (!out) return (vm_output_t){ NULL, 0 }; memcpy(out, ckpt->out_buf, pos); } else { if (state) { regs.current = state->current; memcpy(regs.style_stack, state->style_stack, sizeof(state->style_stack)); regs.style_depth = state->style_depth; } cap = 512; pos = 0; out = malloc(cap); if (!out) return (vm_output_t){ NULL, 0 }; } #define ENSURE(n) ({ \ while (pos + (n) + 1 > cap) { \ cap *= 2; out = realloc(out, cap); \ if (!out) return (vm_output_t){ NULL, 0 }; \ }}) #define OUT_STR(s, l) ({ ENSURE(l); memcpy(out+pos, s, l); pos += (l); }) #define OUT_CSTR(s) ({ size_t _l = strlen(s); OUT_STR(s, _l); }) if (!(ckpt && ckpt->valid) && state && !crprintf_no_color) { style_entry_t *cur = ®s.current; if (cur->fg || cur->bg || cur->flags) { char esc[128]; int n = emit_style_esc(esc, sizeof(esc), cur); OUT_STR(esc, (size_t)n); } } const instruction_t *ip = (ckpt && ckpt->valid) ? prog->code + ckpt->resume_ip : prog->code; static const void *dispatch[OP_MAX] = { [OP_NOP] = &&op_nop, [OP_EMIT_LIT] = &&op_emit_lit, [OP_EMIT_FMT] = &&op_emit_fmt, [OP_SET_FG] = &&op_set_fg, [OP_SET_BG] = &&op_set_bg, [OP_SET_FG_RGB] = &&op_set_fg_rgb, [OP_SET_BG_RGB] = &&op_set_bg_rgb, [OP_SET_BOLD] = &&op_set_bold, [OP_SET_DIM] = &&op_set_dim, [OP_SET_UL] = &&op_set_ul, [OP_SET_ITALIC] = &&op_set_italic, [OP_SET_STRIKE] = &&op_set_strike, [OP_SET_INVERT] = &&op_set_invert, [OP_STYLE_PUSH] = &&op_style_push, [OP_STYLE_FLUSH] = &&op_style_flush, [OP_STYLE_RESET] = &&op_style_reset, [OP_STYLE_RESET_ALL] = &&op_style_reset_all, [OP_PAD_BEGIN] = &&op_pad_begin, [OP_RPAD_BEGIN] = &&op_rpad_begin, [OP_PAD_END] = &&op_pad_end, [OP_EMIT_SPACES] = &&op_emit_spaces, [OP_EMIT_NEWLINES] = &&op_emit_newlines, [OP_SNAPSHOT] = &&op_snapshot, [OP_HALT] = &&op_halt, }; #define DISPATCH() goto *dispatch[ip->op] #define NEXT() do { ip++; DISPATCH(); } while(0) DISPATCH(); op_nop: NEXT(); op_emit_lit: { OUT_CSTR(prog->literals + ip->operand); NEXT(); } op_emit_fmt: { uint32_t lit_off = ip->operand & 0x0FFFFFFF; const char *spec = prog->literals + lit_off; char tmp[256]; va_list ap_copy; va_copy(ap_copy, ap); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" int n = vsnprintf(tmp, sizeof(tmp), spec, ap_copy); va_end(ap_copy); if (n > 0 && (size_t)n < sizeof(tmp)) { OUT_STR(tmp, (size_t)n); } else if (n > 0) { ENSURE((size_t)n); va_copy(ap_copy, ap); vsnprintf(out + pos, (size_t)n + 1, spec, ap_copy); va_end(ap_copy); pos += (size_t)n; } #pragma GCC diagnostic pop advance_format_args(spec, CRP_VA_PASS(ap)); NEXT(); } op_set_fg: { regs.current.fg = ip->operand; NEXT(); } op_set_bg: { regs.current.bg = ip->operand; NEXT(); } op_set_bold: { if (ip->operand) regs.current.flags |= STYLE_BOLD; else regs.current.flags &= ~STYLE_BOLD; NEXT(); } op_set_dim: { if (ip->operand) regs.current.flags |= STYLE_DIM; else regs.current.flags &= ~STYLE_DIM; NEXT(); } op_set_ul: { if (ip->operand) regs.current.flags |= STYLE_UL; else regs.current.flags &= ~STYLE_UL; NEXT(); } op_set_italic: { if (ip->operand) regs.current.flags |= STYLE_ITALIC; else regs.current.flags &= ~STYLE_ITALIC; NEXT(); } op_set_strike: { if (ip->operand) regs.current.flags |= STYLE_STRIKE; else regs.current.flags &= ~STYLE_STRIKE; NEXT(); } op_set_invert: { if (ip->operand) regs.current.flags |= STYLE_INVERT; else regs.current.flags &= ~STYLE_INVERT; NEXT(); } op_set_fg_rgb: { regs.current.fg = COL_RGB; regs.current.fg_rgb = ip->operand; NEXT(); } op_set_bg_rgb: { regs.current.bg = COL_RGB; regs.current.bg_rgb = ip->operand; NEXT(); } op_style_push: { if (regs.style_depth < 8) regs.style_stack[regs.style_depth++] = regs.current; NEXT(); } op_pad_begin: { if (regs.pad_depth < 8) regs.pad_stack[regs.pad_depth++] = (pad_entry_t){ pos, (int)ip->operand, 0 }; NEXT(); } op_rpad_begin: { if (regs.pad_depth < 8) regs.pad_stack[regs.pad_depth++] = (pad_entry_t){ pos, (int)ip->operand, 1 }; NEXT(); } op_pad_end: { if (regs.pad_depth <= 0) NEXT(); regs.pad_depth--; pad_entry_t pe = regs.pad_stack[regs.pad_depth]; size_t vis = visible_len(out + pe.mark, pos - pe.mark); if ((size_t)pe.width <= vis) NEXT(); size_t pad_n = pe.width - vis; ENSURE(pad_n); if (pe.right_align) { memmove(out + pe.mark + pad_n, out + pe.mark, pos - pe.mark); memset(out + pe.mark, ' ', pad_n); } else memset(out + pos, ' ', pad_n); pos += pad_n; NEXT(); } op_emit_spaces: { size_t n = (size_t)ip->operand; ENSURE(n); memset(out + pos, ' ', n); pos += n; NEXT(); } op_emit_newlines: { size_t n = (size_t)ip->operand; ENSURE(n); memset(out + pos, '\n', n); pos += n; NEXT(); } op_style_reset: { if (regs.style_depth > 0) regs.current = regs.style_stack[--regs.style_depth]; else regs.current = (style_entry_t){.fg = COL_NONE, .bg = COL_NONE}; if (!crprintf_no_color) { char esc[128]; int n = snprintf(esc, sizeof(esc), "\x1b[0m"); if (regs.current.flags & STYLE_BOLD) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[1m"); if (regs.current.flags & STYLE_DIM) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[2m"); if (regs.current.flags & STYLE_UL) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[4m"); if (regs.current.flags & STYLE_ITALIC) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[3m"); if (regs.current.flags & STYLE_STRIKE) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[9m"); if (regs.current.flags & STYLE_INVERT) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[7m"); if (regs.current.fg == COL_RGB) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[38;2;%d;%d;%dm", UNPACK_R(regs.current.fg_rgb), UNPACK_G(regs.current.fg_rgb), UNPACK_B(regs.current.fg_rgb)); else if (regs.current.fg) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[%dm", regs.current.fg); if (regs.current.bg == COL_RGB) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[48;2;%d;%d;%dm", UNPACK_R(regs.current.bg_rgb), UNPACK_G(regs.current.bg_rgb), UNPACK_B(regs.current.bg_rgb)); else if (regs.current.bg) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[%dm", regs.current.bg + 10); OUT_STR(esc, (size_t)n); } NEXT(); } op_style_reset_all: { regs.current = (style_entry_t){.fg = COL_NONE, .bg = COL_NONE}; regs.style_depth = 0; if (!crprintf_no_color) { OUT_CSTR("\x1b[0m"); } NEXT(); } op_style_flush: { if (!crprintf_no_color) { char esc[128]; int n = snprintf(esc, sizeof(esc), "\x1b[0m"); if (regs.current.flags & STYLE_BOLD) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[1m"); if (regs.current.flags & STYLE_DIM) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[2m"); if (regs.current.flags & STYLE_UL) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[4m"); if (regs.current.flags & STYLE_ITALIC) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[3m"); if (regs.current.flags & STYLE_STRIKE) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[9m"); if (regs.current.flags & STYLE_INVERT) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[7m"); if (regs.current.fg == COL_RGB) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[38;2;%d;%d;%dm", UNPACK_R(regs.current.fg_rgb), UNPACK_G(regs.current.fg_rgb), UNPACK_B(regs.current.fg_rgb)); else if (regs.current.fg) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[%dm", regs.current.fg); if (regs.current.bg == COL_RGB) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[48;2;%d;%d;%dm", UNPACK_R(regs.current.bg_rgb), UNPACK_G(regs.current.bg_rgb), UNPACK_B(regs.current.bg_rgb)); else if (regs.current.bg) n += snprintf(esc+n, sizeof(esc)-n, "\x1b[%dm", regs.current.bg + 10); OUT_STR(esc, (size_t)n); } NEXT(); } op_snapshot: { vm_checkpoint_t *save = &prog->checkpoint; free(save->out_buf); save->regs = regs; save->out_buf = malloc(pos); if (save->out_buf) { memcpy(save->out_buf, out, pos); save->out_pos = pos; save->out_cap = pos; save->resume_ip = (size_t)(ip + 1 - prog->code); save->valid = true; } NEXT(); } op_halt: { if (state) { state->current = regs.current; memcpy(state->style_stack, regs.style_stack, sizeof(state->style_stack)); state->style_depth = regs.style_depth; } out[pos] = '\0'; return (vm_output_t){ out, pos }; } #undef ENSURE #undef OUT_STR #undef OUT_CSTR } int crprintf_exec(crprintf_compiled *prog, FILE *stream, ...) { va_list ap; va_start(ap, stream); vm_output_t o = crprintf_vm_run(prog, ap, NULL); va_end(ap); if (!o.data) return -1; int ret = (int)fwrite(o.data, 1, o.len, stream); free(o.data); return ret; } int crsprintf_inner(crprintf_compiled *prog, char *buf, size_t size, ...) { va_list ap; va_start(ap, size); vm_output_t o = crprintf_vm_run(prog, ap, NULL); va_end(ap); if (!o.data) return -1; size_t copy = (o.len < size) ? o.len : size - 1; memcpy(buf, o.data, copy); buf[copy] = '\0'; free(o.data); return (int)o.len; } int crsprintf_stateful(char *buf, size_t size, crprintf_state *state, const char *fmt, ...) { crprintf_compiled *prog = crprintf_compile(fmt); if (__builtin_expect(crprintf_get_debug(), 0)) crprintf_disasm(prog, stderr); if (__builtin_expect(crprintf_get_debug_hex(), 0)) crprintf_hexdump(prog, stderr); va_list ap; va_start(ap, fmt); vm_output_t o = crprintf_vm_run(prog, ap, state); va_end(ap); free(prog->code); free(prog->literals); free(prog->source); free(prog->src_map); free(prog->lit_marks); free(prog); if (!o.data) return -1; size_t copy = (o.len < size) ? o.len : size - 1; memcpy(buf, o.data, copy); buf[copy] = '\0'; free(o.data); return (int)o.len; } int crfprintf_stateful(FILE *stream, crprintf_state *state, const char *fmt, ...) { crprintf_compiled *prog = crprintf_compile(fmt); if (__builtin_expect(crprintf_get_debug(), 0)) crprintf_disasm(prog, stderr); if (__builtin_expect(crprintf_get_debug_hex(), 0)) crprintf_hexdump(prog, stderr); va_list ap; va_start(ap, fmt); vm_output_t o = crprintf_vm_run(prog, ap, state); va_end(ap); free(prog->code); free(prog->literals); free(prog->source); free(prog->src_map); free(prog->lit_marks); free(prog); if (!o.data) return -1; int ret = (int)fwrite(o.data, 1, o.len, stream); free(o.data); return ret; } int crsprintf_compiled(char *buf, size_t size, crprintf_state *state, crprintf_compiled *prog, ...) { va_list ap; va_start(ap, prog); const vm_checkpoint_t *ckpt = prog->checkpoint.valid ? &prog->checkpoint : NULL; vm_output_t o = crprintf_vm_run_ex(prog, ap, state, ckpt); va_end(ap); if (!o.data) return -1; size_t copy = (o.len < size) ? o.len : size - 1; memcpy(buf, o.data, copy); buf[copy] = '\0'; free(o.data); return (int)o.len; } void crprintf_compiled_free(crprintf_compiled *prog) { if (!prog) return; free(prog->code); free(prog->literals); free(prog->source); free(prog->src_map); free(prog->lit_marks); free(prog->checkpoint.out_buf); free(prog); } static crprintf_compiled *compile_tracked(const char *fmt) { crprintf_compiled *p = program_new(); p->source_len = strlen(fmt); p->source = malloc(p->source_len + 1); memcpy(p->source, fmt, p->source_len + 1); p->map_cap = p->code_cap; p->src_map = malloc(p->map_cap * sizeof(uint32_t)); p->lit_marks = malloc(p->map_cap * sizeof(uint32_t)); p->compile_base = p->source; var_scope_t vars = { .base = &global_vars }; compile_fragment(p, p->source, &vars); emit_op(p, OP_HALT, 0); p->compile_base = NULL; if (__builtin_expect(crprintf_get_debug(), 0)) crprintf_disasm(p, stderr); if (__builtin_expect(crprintf_get_debug_hex(), 0)) crprintf_hexdump(p, stderr); return p; } crprintf_compiled *crprintf_recompile(crprintf_compiled *prev, const char *fmt) { if (!prev) return compile_tracked(fmt); size_t new_len = strlen(fmt); if (prev->source && prev->source_len == new_len && memcmp(prev->source, fmt, new_len) == 0) { return prev; } if (!prev->src_map) { crprintf_compiled_free(prev); return compile_tracked(fmt); } size_t min_len = prev->source_len < new_len ? prev->source_len : new_len; size_t diverge = 0; while (diverge < min_len && prev->source[diverge] == fmt[diverge]) diverge++; size_t trunc_idx = prev->code_len; for (size_t i = 0; i < prev->code_len; i++) { if (prev->src_map[i] >= (uint32_t)diverge) { trunc_idx = i; break; } } uint32_t resume_off = 0; if (trunc_idx > 0) { uint32_t boundary_src = prev->src_map[trunc_idx - 1]; while (trunc_idx > 0 && prev->src_map[trunc_idx - 1] == boundary_src) trunc_idx--; resume_off = boundary_src; for (uint32_t s = resume_off; s <= resume_off; s--) { char c = fmt[s]; if (c == '<' || c == '{') { resume_off = s; break; } if (c == '>' || c == '}') break; } while (trunc_idx > 0 && prev->src_map[trunc_idx - 1] >= resume_off) trunc_idx--; } prev->code_len = trunc_idx; prev->lit_len = (trunc_idx > 0) ? prev->lit_marks[trunc_idx - 1] : 0; if (prev->checkpoint.valid && prev->checkpoint.resume_ip != trunc_idx + 1) { free(prev->checkpoint.out_buf); prev->checkpoint = (vm_checkpoint_t){0}; } bool prefix_has_fmt = false; for (size_t i = 0; i < trunc_idx; i++) { if (prev->code[i].op == OP_EMIT_FMT) { prefix_has_fmt = true; break; } } free(prev->source); prev->source_len = new_len; prev->source = malloc(new_len + 1); memcpy(prev->source, fmt, new_len + 1); var_scope_t vars = { .base = &global_vars }; for (size_t i = 0; i + 5 < diverge; i++) { if (fmt[i] == '<' && memcmp(fmt + i + 1, "let ", 4) == 0) { const char *body = fmt + i + 5; const char *end = memchr(body, '>', diverge - (i + 5)); if (end) { compile_let(&vars, body, (int)(end - body)); i = (size_t)(end - fmt); } } else if (fmt[i] == '{' && memcmp(fmt + i + 1, "let ", 4) == 0) { const char *body = fmt + i + 5; const char *end = memchr(body, '}', diverge - (i + 5)); if (end) { compile_let(&vars, body, (int)(end - body)); i = (size_t)(end - fmt); } } } if (trunc_idx > 0 && !prefix_has_fmt) emit_op(prev, OP_SNAPSHOT, 0); prev->compile_base = prev->source; compile_fragment(prev, prev->source + resume_off, &vars); emit_op(prev, OP_HALT, 0); prev->compile_base = NULL; if (__builtin_expect(crprintf_get_debug(), 0)) crprintf_disasm(prev, stderr); if (__builtin_expect(crprintf_get_debug_hex(), 0)) crprintf_hexdump(prev, stderr); return prev; } static const char *op_names[OP_MAX] = { [OP_NOP] = "NOP", [OP_EMIT_LIT] = "EMIT_LIT", [OP_EMIT_FMT] = "EMIT_FMT", [OP_SET_FG] = "SET_FG", [OP_SET_BG] = "SET_BG", [OP_SET_FG_RGB] = "SET_FG_RGB", [OP_SET_BG_RGB] = "SET_BG_RGB", [OP_SET_BOLD] = "SET_BOLD", [OP_SET_DIM] = "SET_DIM", [OP_SET_UL] = "SET_UL", [OP_SET_ITALIC] = "SET_ITALIC", [OP_SET_STRIKE] = "SET_STRIKE", [OP_SET_INVERT] = "SET_INVERT", [OP_STYLE_PUSH] = "STYLE_PUSH", [OP_STYLE_FLUSH] = "STYLE_FLUSH", [OP_STYLE_RESET] = "STYLE_RESET", [OP_STYLE_RESET_ALL] = "STYLE_RESET_ALL", [OP_PAD_BEGIN] = "PAD_BEGIN", [OP_RPAD_BEGIN] = "RPAD_BEGIN", [OP_PAD_END] = "PAD_END", [OP_EMIT_SPACES] = "EMIT_SPACES", [OP_EMIT_NEWLINES] = "EMIT_NEWLINES", [OP_SNAPSHOT] = "SNAPSHOT", [OP_HALT] = "HALT", }; static const char *color_name(uint32_t col) { switch (col) { case COL_NONE: return "none"; case COL_BLACK: return "black"; case COL_RED: return "red"; case COL_GREEN: return "green"; case COL_YELLOW: return "yellow"; case COL_BLUE: return "blue"; case COL_MAGENTA: return "magenta"; case COL_CYAN: return "cyan"; case COL_WHITE: return "white"; case COL_GRAY: return "gray"; case COL_BRIGHT_RED: return "bright_red"; case COL_BRIGHT_GREEN: return "bright_green"; case COL_BRIGHT_YELLOW: return "bright_yellow"; case COL_BRIGHT_BLUE: return "bright_blue"; case COL_BRIGHT_MAGENTA: return "bright_magenta"; case COL_BRIGHT_CYAN: return "bright_cyan"; case COL_BRIGHT_WHITE: return "bright_white"; default: return "?"; }} static const char *arg_class_name(arg_class_t cls) { switch (cls) { case ARG_NONE: return "none"; case ARG_INT: return "int"; case ARG_LONG: return "long"; case ARG_LLONG: return "llong"; case ARG_SIZE: return "size_t"; case ARG_DOUBLE: return "double"; case ARG_CSTR: return "char*"; case ARG_PTR: return "void*"; case ARG_WINT: return "wint_t"; case ARG_WSTR: return "wchar_t*"; default: return "?"; }} static void fprint_escaped(FILE *out, const char *s, int max_chars) { for (int c = 0; *s && (max_chars < 0 || c < max_chars); s++, c++) { if (*s == '\n') fprintf(out, "\\n"); else if (*s == '\t') fprintf(out, "\\t"); else if (*s == '"') fprintf(out, "\\\""); else if (*s < 0x20) fprintf(out, "\\x%02x", (unsigned char)*s); else fputc(*s, out); } if (max_chars >= 0 && *s) fprintf(out, "..."); } static void fprint_quoted(FILE *out, const char *s, int max_chars) { fputc('"', out); fprint_escaped(out, s, max_chars); fputc('"', out); } static void fprint_operand(FILE *out, crprintf_compiled *prog, instruction_t *ins, bool compact) { switch (ins->op) { case OP_EMIT_LIT: { const char *s = prog->literals + ins->operand; fprint_quoted(out, s, compact ? 24 : -1); break; } case OP_EMIT_FMT: { uint32_t lit_off = ins->operand & 0x0FFFFFFF; arg_class_t cls = (arg_class_t)(ins->operand >> 28); const char *s = prog->literals + lit_off; fprint_quoted(out, s, compact ? 24 : -1); fprintf(out, " (%s)", arg_class_name(cls)); break; } case OP_SET_FG: case OP_SET_BG: if (compact) fprintf(out, "%s", color_name(ins->operand)); else fprintf(out, "%s (ANSI %u)", color_name(ins->operand), ins->operand); break; case OP_SET_FG_RGB: case OP_SET_BG_RGB: fprintf(out, "#%02x%02x%02x", UNPACK_R(ins->operand), UNPACK_G(ins->operand), UNPACK_B(ins->operand)); break; case OP_SET_BOLD: case OP_SET_DIM: case OP_SET_UL: case OP_SET_ITALIC: case OP_SET_STRIKE: case OP_SET_INVERT: fprintf(out, "%s", ins->operand ? "ON" : "OFF"); break; case OP_PAD_BEGIN: case OP_RPAD_BEGIN: fprintf(out, "width=%u", ins->operand); break; case OP_EMIT_SPACES: fprintf(out, "%u", ins->operand); break; case OP_EMIT_NEWLINES: fprintf(out, "%u", ins->operand); break; case OP_NOP: case OP_STYLE_PUSH: case OP_STYLE_FLUSH: case OP_STYLE_RESET: case OP_STYLE_RESET_ALL: case OP_PAD_END: case OP_SNAPSHOT: case OP_HALT: break; default: if (compact) { if (ins->operand) fprintf(out, "0x%x", ins->operand); } else fprintf(out, "0x%08x", ins->operand); break; } } void crprintf_disasm(crprintf_compiled *prog, FILE *out) { fprintf(out, "; crprintf bytecode — %zu instructions, %zu bytes literal pool\n", prog->code_len, prog->lit_len); fprintf(out, "; %-4s %-16s %s\n", "addr", "opcode", "operand"); fprintf(out, "; ---- ---------------- -------\n"); for (size_t i = 0; i < prog->code_len; i++) { instruction_t *ins = &prog->code[i]; const char *name = (ins->op < OP_MAX) ? op_names[ins->op] : "???"; fprintf(out, " %04zu %-16s ", i, name); fprint_operand(out, prog, ins, false); fputc('\n', out); } } void crprintf_hexdump(crprintf_compiled *prog, FILE *out) { fprintf(out, "; crprintf hex dump — %zu instructions, %zu bytes literal pool\n", prog->code_len, prog->lit_len); fprintf(out, "; %-4s %-26s %s\n", "addr", "bytes", "decoded"); fprintf(out, "; ---- ------------------------- --------\n"); for (size_t i = 0; i < prog->code_len; i++) { instruction_t *ins = &prog->code[i]; const uint8_t *raw = (const uint8_t *)ins; const char *name = (ins->op < OP_MAX) ? op_names[ins->op] : "???"; fprintf(out, " %04zu ", i); for (size_t b = 0; b < sizeof(instruction_t); b++) fprintf(out, "%02x ", raw[b]); fprintf(out, " ; %s ", name); fprint_operand(out, prog, ins, true); fputc('\n', out); } if (prog->lit_len > 0) { fprintf(out, "\n; literal pool (%zu bytes):\n", prog->lit_len); const uint8_t *lit = (const uint8_t *)prog->literals; for (size_t off = 0; off < prog->lit_len; off += 16) { fprintf(out, " %04zx ", off); size_t end = off + 16; if (end > prog->lit_len) end = prog->lit_len; for (size_t b = off; b < off + 16; b++) { if (b < end) fprintf(out, "%02x ", lit[b]); else fprintf(out, " "); if (b == off + 7) fputc(' ', out); } fprintf(out, " |"); for (size_t b = off; b < end; b++) fputc((lit[b] >= 0x20 && lit[b] < 0x7f) ? lit[b] : '.', out); fprintf(out, "|\n"); } } }