From e05bd8f8061350cc16c68aba416e8cb17158ae44 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Wed, 28 Jan 2026 10:15:59 +0900 Subject: [PATCH 01/16] symbol.c: use chunk-based pool for symbol string allocation Replace per-symbol mrb_malloc() with a chunk-based string pool that batches allocations into 4KB chunks. This reduces malloc call count by ~12x (e.g. 909 vs 10,887 for 10k dynamic symbols) and eliminates per-allocation malloc metadata overhead (~16 bytes/symbol). Pool allocations are rounded up to even size to preserve LSB pointer tagging used for literal detection. Co-authored-by: Claude --- include/mruby.h | 1 + src/symbol.c | 49 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/include/mruby.h b/include/mruby.h index b5a66dc184..d65056604c 100644 --- a/include/mruby.h +++ b/include/mruby.h @@ -301,6 +301,7 @@ typedef struct mrb_state { const char **symtbl; size_t symcapa; struct mrb_sym_hash_table *symhash; + void *sym_pool; #ifndef MRB_USE_ALL_SYMBOLS char symbuf[8]; /* buffer for small symbol names */ #endif diff --git a/src/symbol.c b/src/symbol.c index 04d00d2607..c91f64a078 100644 --- a/src/symbol.c +++ b/src/symbol.c @@ -86,6 +86,35 @@ sym_validate_len(mrb_state *mrb, size_t len) } } +/* Chunk-based string pool for heap-allocated symbol names */ +#define MRB_SYM_POOL_CHUNK_SIZE 4096 + +struct sym_pool_chunk { + struct sym_pool_chunk *next; + size_t used; + char buf[]; /* flexible array */ +}; + +static char* +sym_pool_alloc(mrb_state *mrb, size_t size) +{ + /* round up to even size to keep pointers even-aligned (LSB tagging) */ + size_t asize = (size + 1) & ~(size_t)1; + struct sym_pool_chunk *chunk = (struct sym_pool_chunk*)mrb->sym_pool; + if (chunk && chunk->used + asize <= MRB_SYM_POOL_CHUNK_SIZE) { + char *p = chunk->buf + chunk->used; + chunk->used += asize; + return p; + } + size_t csize = asize > MRB_SYM_POOL_CHUNK_SIZE ? asize : MRB_SYM_POOL_CHUNK_SIZE; + chunk = (struct sym_pool_chunk*)mrb_malloc(mrb, + offsetof(struct sym_pool_chunk, buf) + csize); + chunk->next = (struct sym_pool_chunk*)mrb->sym_pool; + chunk->used = asize; + mrb->sym_pool = (void*)chunk; + return chunk->buf; +} + /* Hash table for symbols (allocated on demand when symbols exceed threshold) */ struct mrb_sym_hash_table { uint8_t *symlink; /* collision resolution chains */ @@ -316,7 +345,7 @@ sym_intern_common(mrb_state *mrb, const char *name, size_t len, mrb_bool lit) /* Always heap-allocate when not explicitly literal */ uint32_t ulen = (uint32_t)len; size_t ilen = mrb_packed_int_len(ulen); - char *p = (char*)mrb_malloc(mrb, len+ilen+1); + char *p = sym_pool_alloc(mrb, len+ilen+1); mrb_packed_int_encode(ulen, (uint8_t*)p); memcpy(p+ilen, name, len); p[ilen+len] = 0; @@ -604,16 +633,15 @@ mrb_sym_name_len(mrb_state *mrb, mrb_sym sym, mrb_int *lenp) void mrb_free_symtbl(mrb_state *mrb) { - mrb_sym i, lim; - - for (i=1,lim=mrb->symidx+1; isymtbl[i]; - if (!symtbl_is_literal(tagged_ptr)) { - /* CRITICAL: Untag before mrb_free */ - const char *clean_ptr = symtbl_get_ptr(tagged_ptr); - mrb_free(mrb, (char*)clean_ptr); - } + /* Free symbol string pool chunks */ + struct sym_pool_chunk *chunk = (struct sym_pool_chunk*)mrb->sym_pool; + while (chunk) { + struct sym_pool_chunk *next = chunk->next; + mrb_free(mrb, chunk); + chunk = next; } + mrb->sym_pool = NULL; + mrb_free(mrb, (void*)mrb->symtbl); /* Free hash table if allocated */ @@ -629,6 +657,7 @@ mrb_init_symtbl(mrb_state *mrb) { /* Initialize in linear mode - hash table allocated on demand */ mrb->symhash = NULL; + mrb->sym_pool = NULL; } /********************************************************************** From db4c8d91eaf091ef18809b87e8431bfcab76352c Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Thu, 29 Jan 2026 08:58:08 +0900 Subject: [PATCH 02/16] mruby-bin-mirb: add OSC 11 terminal background color detection Automatically detect terminal background color using OSC 11 escape sequence to select appropriate syntax highlighting theme (dark/light). Detection priority: MIRB_THEME env > OSC 11 > COLORFGBG env > dark default. Co-authored-by: Claude --- mrbgems/mruby-bin-mirb/tools/mirb/mirb.c | 6 + .../tools/mirb/mirb_highlight.c | 45 +++++- .../tools/mirb/mirb_highlight.h | 8 +- mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c | 141 ++++++++++++++++++ mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h | 16 ++ 5 files changed, 209 insertions(+), 7 deletions(-) diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index 8f475122f5..92f625fc68 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -37,6 +37,7 @@ #include "mirb_editor.h" #include "mirb_completion.h" +#include "mirb_highlight.h" /* obsolete configuration */ #ifdef DISABLE_MIRB_UNDERSCORE @@ -520,6 +521,11 @@ main(int argc, char **argv) mrb_define_global_const(mrb, "ARGV", ARGV); mrb_gv_set(mrb, mrb_intern_lit(mrb, "$DEBUG"), mrb_bool_value(args.debug)); + /* Query terminal background color before any output */ + if (isatty(fileno(stdin)) && isatty(fileno(stdout))) { + mirb_highlight_query_terminal(); + } + print_hint(); cxt = mrb_ccontext_new(mrb); diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c index 3d1c4f1f1b..3d6826a8b1 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c @@ -5,6 +5,8 @@ */ #include "mirb_highlight.h" +#include "mirb_term.h" +#include #include #include #include @@ -140,35 +142,66 @@ print_colored(mirb_highlighter *hl, const char *start, size_t len, mirb_token_ty if (*color) printf("%s", reset); } +/* Cached terminal background color from pre-query */ +static mirb_bg_color cached_bg_color = MIRB_BG_UNKNOWN; +static mrb_bool bg_color_queried = FALSE; + +/* + * Pre-query terminal background color + * Must be called before any output to avoid response appearing on screen + */ +void +mirb_highlight_query_terminal(void) +{ + if (!bg_color_queried) { + cached_bg_color = mirb_term_query_bg_color(500); /* 500ms timeout */ + bg_color_queried = TRUE; + } +} + /* - * Detect theme from environment + * Detect theme from terminal background color + * + * Priority: + * 1. MIRB_THEME environment variable (explicit override) + * 2. Cached OSC 11 result (from mirb_highlight_query_terminal) + * 3. COLORFGBG environment variable (rxvt, some xterm) + * 4. Default to dark theme */ mirb_theme mirb_highlight_detect_theme(void) { const char *env; - /* Check explicit MIRB_THEME first */ + /* 1. Check explicit MIRB_THEME first (user override) */ env = getenv("MIRB_THEME"); if (env) { if (strcmp(env, "light") == 0) return MIRB_THEME_LIGHT; if (strcmp(env, "dark") == 0) return MIRB_THEME_DARK; } - /* Check COLORFGBG (format: "fg;bg" where bg > 6 usually means light) */ + /* 2. Use cached OSC 11 result (must call mirb_highlight_query_terminal first) */ + if (cached_bg_color == MIRB_BG_LIGHT) return MIRB_THEME_LIGHT; + if (cached_bg_color == MIRB_BG_DARK) return MIRB_THEME_DARK; + + /* 3. Check COLORFGBG (format: "fg;bg" where bg > 6 usually means light) */ env = getenv("COLORFGBG"); if (env) { const char *semi = strchr(env, ';'); if (semi) { - int bg = atoi(semi + 1); + int bg_color = atoi(semi + 1); /* Background colors 7, 15, or high values typically mean light theme */ - if (bg == 7 || bg == 15 || (bg >= 230 && bg <= 255)) { + if (bg_color == 7 || bg_color == 15 || (bg_color >= 230 && bg_color <= 255)) { return MIRB_THEME_LIGHT; } + /* Low values (0-6, 8) typically mean dark theme */ + if (bg_color <= 8) { + return MIRB_THEME_DARK; + } } } - /* Default to dark theme (more common in terminals) */ + /* 4. Default to dark theme (more common in terminals) */ return MIRB_THEME_DARK; } diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h index b81a238450..505d211e68 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h @@ -59,11 +59,17 @@ void mirb_highlight_init(mirb_highlighter *hl, mrb_bool enabled); void mirb_highlight_set_theme(mirb_highlighter *hl, mirb_theme theme); /* - * Detect theme from environment variables + * Detect theme from environment variables and terminal query * Returns MIRB_THEME_DARK if cannot detect */ mirb_theme mirb_highlight_detect_theme(void); +/* + * Pre-query terminal background color (call before any output) + * This caches the result for later use by mirb_highlight_detect_theme() + */ +void mirb_highlight_query_terminal(void); + /* * Print a line with syntax highlighting * Handles multi-line strings/comments by tracking state diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c index ced2b4c2fc..68759e2622 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c @@ -348,3 +348,144 @@ mirb_term_clear_below(void) { printf("\033[J"); } + +#if !defined(_WIN32) && !defined(_WIN64) +/* + * Query terminal background color using OSC 11 escape sequence + * + * Protocol: + * Send: ESC ] 11 ; ? ESC \ (or BEL instead of ESC \) + * Recv: ESC ] 11 ; rgb:RRRR/GGGG/BBBB ESC \ + * + * The response uses 16-bit color values (0000-FFFF per component). + * Some terminals use 8-bit (00-FF) format instead. + */ +mirb_bg_color +mirb_term_query_bg_color(int timeout_ms) +{ + struct termios old_term, new_term; + char buf[64]; + size_t total = 0; + ssize_t n; + int r, g, b; + mirb_bg_color result = MIRB_BG_UNKNOWN; + fd_set fds; + struct timeval tv; + const char *p; + + /* Must be a terminal */ + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) { + return MIRB_BG_UNKNOWN; + } + + /* Save original terminal settings */ + if (tcgetattr(STDIN_FILENO, &old_term) < 0) { + return MIRB_BG_UNKNOWN; + } + + /* Flush any pending input first */ + tcflush(STDIN_FILENO, TCIFLUSH); + + /* Disable echo and canonical mode for raw read */ + new_term = old_term; + new_term.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL); + new_term.c_iflag &= ~(IXON | IXOFF | ICRNL); + new_term.c_cc[VMIN] = 0; + new_term.c_cc[VTIME] = 0; + + /* TCSAFLUSH: flush I/O and apply settings */ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_term) < 0) { + return MIRB_BG_UNKNOWN; + } + + /* Wait for terminal settings to take effect */ + tcdrain(STDOUT_FILENO); + + /* Send OSC 11 query: ESC ] 11 ; ? ESC \ */ + if (write(STDOUT_FILENO, "\033]11;?\033\\", 8) != 8) { + goto restore; + } + /* Ensure query is sent to terminal */ + tcdrain(STDOUT_FILENO); + /* Give terminal time to process and respond */ + usleep(50000); /* 50ms */ + + /* Read response with timeout - loop to get complete response */ + while (total < sizeof(buf) - 1) { + int sel; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + + sel = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv); + if (sel < 0) { + if (errno == EINTR) continue; /* Retry on signal interrupt */ + break; /* Other error */ + } + if (sel == 0) break; /* Timeout */ + + n = read(STDIN_FILENO, buf + total, sizeof(buf) - 1 - total); + if (n <= 0) { + if (n < 0 && errno == EINTR) continue; /* Retry on signal interrupt */ + break; + } + total += (size_t)n; + + /* Check for terminator: ESC \ (0x1b 0x5c) or BEL (0x07) */ + if (total >= 2 && buf[total-2] == '\033' && buf[total-1] == '\\') break; + if (total >= 1 && buf[total-1] == '\007') break; + + /* Reduce timeout for subsequent reads */ + timeout_ms = 10; + } + + if (total == 0) { + goto restore; + } + buf[total] = '\0'; + + /* Parse response: look for "rgb:" followed by hex values */ + p = strstr(buf, "rgb:"); + if (p) { + p += 4; + /* Try 16-bit format: rgb:RRRR/GGGG/BBBB */ + if (sscanf(p, "%4x/%4x/%4x", &r, &g, &b) == 3) { + /* Normalize to 8-bit range */ + r >>= 8; g >>= 8; b >>= 8; + } + /* Try 8-bit format: rgb:RR/GG/BB */ + else if (sscanf(p, "%2x/%2x/%2x", &r, &g, &b) == 3) { + /* Already 8-bit */ + } + else { + goto restore; + } + + /* Calculate relative luminance (ITU-R BT.709 simplified) */ + /* Y = 0.2126*R + 0.7152*G + 0.0722*B */ + /* Scale: 0-255 input, threshold at ~127.5 */ + int luminance = (2126 * r + 7152 * g + 722 * b) / 10000; + result = (luminance < 128) ? MIRB_BG_DARK : MIRB_BG_LIGHT; + } + +restore: + /* Flush any remaining input before restoring terminal */ + tcflush(STDIN_FILENO, TCIFLUSH); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term); + return result; +} + +#else /* Windows */ + +mirb_bg_color +mirb_term_query_bg_color(int timeout_ms) +{ + (void)timeout_ms; + /* Windows Terminal supports OSC 11, but implementation requires + * different I/O handling. For now, return unknown and rely on + * fallback detection methods. */ + return MIRB_BG_UNKNOWN; +} + +#endif /* _WIN32 */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h index 6995bb15fd..8c9c5d1f26 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h @@ -118,4 +118,20 @@ void mirb_term_get_size(mirb_term *term); */ void mirb_term_flush(void); +/* + * Background color detection result + */ +typedef enum mirb_bg_color { + MIRB_BG_UNKNOWN, /* could not detect */ + MIRB_BG_DARK, /* dark background (luminance < 0.5) */ + MIRB_BG_LIGHT /* light background (luminance >= 0.5) */ +} mirb_bg_color; + +/* + * Query terminal background color using OSC 11 + * Returns MIRB_BG_UNKNOWN if terminal doesn't respond within timeout + * timeout_ms: timeout in milliseconds (recommended: 100) + */ +mirb_bg_color mirb_term_query_bg_color(int timeout_ms); + #endif /* MIRB_TERM_H */ From e8e2e76fd66a7ab518a6fd1c80d50812fa5608ab Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Fri, 30 Jan 2026 22:55:30 +0900 Subject: [PATCH 03/16] mruby-bin-mirb: add colored output for results and errors Result values are shown in cyan, errors in bold red. The arrow " => " uses gray for subtle appearance. Co-authored-by: Claude --- mrbgems/mruby-bin-mirb/tools/mirb/mirb.c | 19 +++---- .../tools/mirb/mirb_highlight.c | 53 +++++++++++++++++++ .../tools/mirb/mirb_highlight.h | 11 ++++ 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index 92f625fc68..7622aacd19 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -45,26 +45,22 @@ #endif static void -p(mrb_state *mrb, mrb_value obj) +p(mrb_state *mrb, mrb_value obj, mirb_highlighter *hl) { mrb_value val = mrb_funcall_argv(mrb, obj, MRB_SYM(inspect), 0, NULL); - if (!mrb->exc) { - fputs(" => ", stdout); - } - else { + if (mrb->exc) { val = mrb_exc_get_output(mrb, mrb->exc); } if (!mrb_string_p(val)) { val = mrb_obj_as_string(mrb, obj); } char* msg = mrb_locale_from_utf8(RSTRING_PTR(val), (int)RSTRING_LEN(val)); - fwrite(msg, strlen(msg), 1, stdout); + mirb_highlight_print_result(hl, msg); mrb_locale_free(msg); - putc('\n', stdout); } static void -p_error(mrb_state *mrb, struct RObject* exc, mrb_ccontext *cxt) +p_error(mrb_state *mrb, struct RObject* exc, mrb_ccontext *cxt, mirb_highlighter *hl) { mrb_value val = mrb_exc_get_output(mrb, exc); if (!mrb_string_p(val)) { @@ -104,9 +100,8 @@ p_error(mrb_state *mrb, struct RObject* exc, mrb_ccontext *cxt) } char* msg = mrb_locale_from_utf8(RSTRING_PTR(val), (int)RSTRING_LEN(val)); - fwrite(msg, strlen(msg), 1, stdout); + mirb_highlight_print_error(hl, msg); mrb_locale_free(msg); - putc('\n', stdout); } /* Guess if the user might want to enter more @@ -762,7 +757,7 @@ main(int argc, char **argv) /* did an exception occur? */ if (mrb->exc) { MRB_EXC_CHECK_EXIT(mrb, mrb->exc); - p_error(mrb, mrb->exc, cxt); + p_error(mrb, mrb->exc, cxt, &editor.highlight); mrb->exc = 0; } else { @@ -770,7 +765,7 @@ main(int argc, char **argv) if (!mrb_respond_to(mrb, result, MRB_SYM(inspect))){ result = mrb_any_to_s(mrb, result); } - p(mrb, result); + p(mrb, result, &editor.highlight); #ifndef MRB_NO_MIRB_UNDERSCORE *(mrb->c->ci->stack + 1) = result; #endif diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c index 3d6826a8b1..b196ba9716 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c @@ -23,6 +23,9 @@ #define DARK_IVAR "\033[34m" /* blue */ #define DARK_GVAR "\033[1;34m" /* bold blue */ #define DARK_REGEXP "\033[31m" /* red */ +#define DARK_RESULT "\033[36m" /* cyan (same as number) */ +#define DARK_ERROR "\033[1;31m" /* bold red */ +#define DARK_ARROW "\033[90m" /* gray */ /* Light theme colors (dark colors on light background) */ #define LIGHT_KEYWORD "\033[35m" /* magenta */ @@ -34,6 +37,9 @@ #define LIGHT_IVAR "\033[34m" /* blue */ #define LIGHT_GVAR "\033[34m" /* blue */ #define LIGHT_REGEXP "\033[31m" /* red */ +#define LIGHT_RESULT "\033[36m" /* cyan */ +#define LIGHT_ERROR "\033[31m" /* red */ +#define LIGHT_ARROW "\033[90m" /* gray */ #define COLOR_RESET "\033[0m" @@ -456,3 +462,50 @@ mirb_highlight_print_line(mirb_highlighter *hl, const char *line) putchar(*p++); } } + +/* + * Print result value with highlighting + */ +void +mirb_highlight_print_result(mirb_highlighter *hl, const char *result) +{ + if (!hl->enabled) { + fputs(" => ", stdout); + fputs(result, stdout); + putchar('\n'); + return; + } + + if (hl->theme == MIRB_THEME_DARK) { + fputs(DARK_ARROW " => " COLOR_RESET, stdout); + fputs(DARK_RESULT, stdout); + } + else { + fputs(LIGHT_ARROW " => " COLOR_RESET, stdout); + fputs(LIGHT_RESULT, stdout); + } + fputs(result, stdout); + fputs(COLOR_RESET "\n", stdout); +} + +/* + * Print error message with highlighting + */ +void +mirb_highlight_print_error(mirb_highlighter *hl, const char *error) +{ + if (!hl->enabled) { + fputs(error, stdout); + putchar('\n'); + return; + } + + if (hl->theme == MIRB_THEME_DARK) { + fputs(DARK_ERROR, stdout); + } + else { + fputs(LIGHT_ERROR, stdout); + } + fputs(error, stdout); + fputs(COLOR_RESET "\n", stdout); +} diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h index 505d211e68..a6383f9fa4 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h @@ -81,4 +81,15 @@ void mirb_highlight_print_line(mirb_highlighter *hl, const char *line); */ void mirb_highlight_reset(mirb_highlighter *hl); +/* + * Print result value with highlighting + * Prints " => " prefix and the result string with appropriate colors + */ +void mirb_highlight_print_result(mirb_highlighter *hl, const char *result); + +/* + * Print error message with highlighting + */ +void mirb_highlight_print_error(mirb_highlighter *hl, const char *error); + #endif /* MIRB_HIGHLIGHT_H */ From 1713d4a2e76b350770f70b49d1f324a2e1ee7754 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sun, 1 Feb 2026 15:11:30 +0900 Subject: [PATCH 04/16] mruby-bin-mirb: syntax highlight result values and hash key symbols Use syntax highlighter for result values instead of single color. Add support for hash key symbol syntax (e.g., `a:` in `{a: 1}`). Co-authored-by: Claude --- .../mruby-bin-mirb/tools/mirb/mirb_highlight.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c index b196ba9716..16e371fc73 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c @@ -443,7 +443,12 @@ mirb_highlight_print_line(mirb_highlighter *hl, const char *line) size_t len = (size_t)(p - token_start); - if (is_const) { + /* Check for hash key symbol syntax: identifier followed by ': ' */ + if (*p == ':' && (p[1] == ' ' || p[1] == '\0' || p[1] == ',' || p[1] == '}')) { + p++; /* include the colon */ + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_SYMBOL); + } + else if (is_const) { print_colored(hl, token_start, len, MIRB_TOK_CONSTANT); } else if (!after_dot && is_keyword(token_start, len)) { @@ -476,16 +481,16 @@ mirb_highlight_print_result(mirb_highlighter *hl, const char *result) return; } + /* Print arrow in gray */ if (hl->theme == MIRB_THEME_DARK) { fputs(DARK_ARROW " => " COLOR_RESET, stdout); - fputs(DARK_RESULT, stdout); } else { fputs(LIGHT_ARROW " => " COLOR_RESET, stdout); - fputs(LIGHT_RESULT, stdout); } - fputs(result, stdout); - fputs(COLOR_RESET "\n", stdout); + /* Syntax highlight the result value */ + mirb_highlight_print_line(hl, result); + putchar('\n'); } /* From cafbf8ca6b4663ccd911a8a18b880d3e0d60a887 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Mon, 2 Feb 2026 22:32:37 +0900 Subject: [PATCH 05/16] bigint.c: fix memory leak in mpz_mul_sparse and bint_mul mpz_mul_sparse allocated temporary mpz_t variables (shifted, temp) that were leaked when an exception was raised (e.g., RangeError from shift width too large). bint_mul had the same issue with its output mpz_t z. Wrap both functions with MRB_ENSURE to guarantee cleanup runs regardless of exceptions, following the existing pattern used by mpz_mul_all_ones. Co-authored-by: Claude --- mrbgems/mruby-bigint/core/bigint.c | 73 ++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/mrbgems/mruby-bigint/core/bigint.c b/mrbgems/mruby-bigint/core/bigint.c index af85a5ddc5..7d5b60a8a8 100644 --- a/mrbgems/mruby-bigint/core/bigint.c +++ b/mrbgems/mruby-bigint/core/bigint.c @@ -2016,13 +2016,25 @@ mpz_sparse_p(mpz_t *x) * * O(k * n) where k = popcount, much faster than Karatsuba when k is small. */ -static void -mpz_mul_sparse(mpz_ctx_t *ctx, mpz_t *w, mpz_t *sparse, mpz_t *dense) +struct mpz_mul_sparse_data { + mpz_ctx_t *ctx; + mpz_t *w; + mpz_t *sparse; + mpz_t *dense; + mpz_t shifted, temp; /* cleanup targets */ +}; + +static mrb_value +mpz_mul_sparse_body(mrb_state *mrb, void *userdata) { - mpz_t shifted, temp; + struct mpz_mul_sparse_data *d = (struct mpz_mul_sparse_data *)userdata; + mpz_ctx_t *ctx = d->ctx; + mpz_t *w = d->w; + mpz_t *sparse = d->sparse; + mpz_t *dense = d->dense; - mpz_init(ctx, &shifted); - mpz_init(ctx, &temp); + mpz_init(ctx, &d->shifted); + mpz_init(ctx, &d->temp); zero(w); for (size_t i = 0; i < sparse->sz; i++) { @@ -2042,9 +2054,9 @@ mpz_mul_sparse(mpz_ctx_t *ctx, mpz_t *w, mpz_t *sparse, mpz_t *dense) #endif /* Add dense << (base_bit + bit) to result */ - mpz_mul_2exp(ctx, &shifted, dense, base_bit + bit); - mpz_add(ctx, &temp, w, &shifted); - mpz_set(ctx, w, &temp); + mpz_mul_2exp(ctx, &d->shifted, dense, base_bit + bit); + mpz_add(ctx, &d->temp, w, &d->shifted); + mpz_set(ctx, w, &d->temp); /* Clear this bit */ limb &= limb - 1; @@ -2054,8 +2066,19 @@ mpz_mul_sparse(mpz_ctx_t *ctx, mpz_t *w, mpz_t *sparse, mpz_t *dense) /* Handle sign */ if (sparse->sn < 0) w->sn = -w->sn; - mpz_clear(ctx, &shifted); - mpz_clear(ctx, &temp); + return mrb_nil_value(); +} + +static void +mpz_mul_sparse(mpz_ctx_t *ctx, mpz_t *w, mpz_t *sparse, mpz_t *dense) +{ + struct mpz_mul_sparse_data d = {ctx, w, sparse, dense, {0,0,0}, {0,0,0}}; + mrb_value exc; + MRB_ENSURE(MPZ_MRB(ctx), exc, mpz_mul_sparse_body, &d) { + /* Cleanup always runs (mpz_clear is safe on zero-initialized mpz_t) */ + mpz_clear(ctx, &d.shifted); + mpz_clear(ctx, &d.temp); + } } /* @@ -5576,19 +5599,41 @@ mrb_bint_sub(mrb_state *mrb, mrb_value x, mrb_value y) return bint_norm(mrb, RBIGINT(x)); } +struct bint_mul_data { + mpz_ctx_t *ctx; + mpz_t *a; + mpz_t *b; + mpz_t z; /* cleanup target */ +}; + +static mrb_value +bint_mul_body(mrb_state *mrb, void *userdata) +{ + struct bint_mul_data *d = (struct bint_mul_data *)userdata; + mpz_init(d->ctx, &d->z); + mpz_mul(d->ctx, &d->z, d->a, d->b); + return mrb_nil_value(); +} + static struct RBigint* bint_mul(mrb_state *mrb, mrb_value x, mrb_value y) { - mpz_t a, b, z; + mpz_t a, b; y = mrb_as_bint(mrb, y); bint_as_mpz(RBIGINT(x), &a); bint_as_mpz(RBIGINT(y), &b); MPZ_CTX_INIT(mrb, ctx, pool); - mpz_init(ctx, &z); - mpz_mul(ctx, &z, &a, &b); - return bint_new(ctx, &z); + struct bint_mul_data d = {ctx, &a, &b, {0,0,0}}; + mrb_value exc; + MRB_ENSURE(mrb, exc, bint_mul_body, &d) { + /* On exception, cleanup z (mpz_clear is safe on zero-initialized mpz_t) */ + if (mrb->exc) { + mpz_clear(ctx, &d.z); + } + } + return bint_new(ctx, &d.z); } mrb_value From edce0a338f549dd2681cdfb9bfef0329ece688d4 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Tue, 3 Feb 2026 21:22:13 +0900 Subject: [PATCH 06/16] bigint.c: fix stack buffer overflow in Montgomery reduction The work buffer size in mpz_montgomery_reduce() was calculated as x_len + k + 2, which assumed x_len >= k. However, when R^2 mod n produces a small result, x_len can be much smaller than k. The Montgomery reduction loop writes k limbs at work[i] for each iteration i=0..k-1, so the maximum index accessed is work[2k-1]. This requires at least 2k limbs in the work buffer. Fixed by ensuring work_size is at least 2*k+2 limbs when x_len < k. Also initialize b->as.heap before mpz_move in bint_set() to ensure the destination mpz_t has valid initial state. Co-authored-by: Claude --- mrbgems/mruby-bigint/core/bigint.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mrbgems/mruby-bigint/core/bigint.c b/mrbgems/mruby-bigint/core/bigint.c index 7d5b60a8a8..57b7cd7a44 100644 --- a/mrbgems/mruby-bigint/core/bigint.c +++ b/mrbgems/mruby-bigint/core/bigint.c @@ -5098,8 +5098,10 @@ mpz_montgomery_reduce(mpz_ctx_t *ctx, mpz_t *result, size_t k = n->sz; /* Number of limbs in modulus */ size_t x_len = x->sz; - /* Allocate workspace: need k+1 extra limbs for the product accumulation */ - size_t work_size = x_len + k + 2; + /* Allocate workspace: Montgomery reduction writes k limbs at work[i] for i=0..k-1, + * so the maximum index accessed is work[2k-1] (from carry propagation). + * We need at least 2k limbs, plus extra if x_len > k. */ + size_t work_size = (x_len > k) ? (x_len + k + 2) : (2 * k + 2); size_t pool_state = pool_save(ctx); mp_limb *work = NULL; @@ -5263,6 +5265,7 @@ bint_set(mpz_ctx_t *ctx, struct RBigint *b, mpz_t *x) } else { RBIGINT_SET_HEAP(b); + mpz_init(ctx, &b->as.heap); /* Initialize before mpz_move */ mpz_move(ctx, &b->as.heap, x); } } From 06d6d0b0a5cbfa4ef6e1ec9665fcdb033c271a48 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Wed, 4 Feb 2026 23:38:36 +0900 Subject: [PATCH 07/16] bigint.c: fix memory leak in powm with oversized modulus Barrett and Montgomery reduction compute 2^(2k) internally where k is the modulus bit length. When this exceeds MRB_BIGINT_BIT_LIMIT, mrb_raise() via longjmp skips cleanup of allocated temporaries. Add early modulus size check before any heap allocation. Co-authored-by: Claude --- mrbgems/mruby-bigint/core/bigint.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mrbgems/mruby-bigint/core/bigint.c b/mrbgems/mruby-bigint/core/bigint.c index 57b7cd7a44..4ef5319f6f 100644 --- a/mrbgems/mruby-bigint/core/bigint.c +++ b/mrbgems/mruby-bigint/core/bigint.c @@ -4514,6 +4514,15 @@ mpz_powm(mpz_ctx_t *ctx, mpz_t *zz, mpz_t *x, mpz_t *ex, mpz_t *n) return; } + /* Check modulus size before allocating large temporaries. */ + { + size_t mod_bits = (size_t)n->sz * DIG_SIZE; + if (mod_bits > MRB_BIGINT_BIT_LIMIT / 2) { + mrb_state *mrb = MPZ_MRB(ctx); + mrb_raise(mrb, E_RANGE_ERROR, "modulus too large"); + } + } + /* * Use Montgomery reduction for odd moduli >= 4 limbs. * Montgomery is faster because it replaces division with multiplication. @@ -4592,6 +4601,17 @@ mpz_powm_i(mpz_ctx_t *ctx, mpz_t *zz, mpz_t *x, mrb_int ex, mpz_t *n) return; } + /* Check modulus size before allocating large temporaries. + * Both Barrett and Montgomery need 2^(2k) internally, + * which would exceed MRB_BIGINT_BIT_LIMIT for large moduli. */ + { + size_t mod_bits = (size_t)n->sz * DIG_SIZE; + if (mod_bits > MRB_BIGINT_BIT_LIMIT / 2) { + mrb_state *mrb = MPZ_MRB(ctx); + mrb_raise(mrb, E_RANGE_ERROR, "modulus too large"); + } + } + /* * Use Montgomery reduction for odd moduli (common in cryptography). * Convert integer exponent to mpz_t and use the main Montgomery implementation. From eea9e30979a9dc603349ca211d434e33992eb0d1 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Thu, 5 Feb 2026 07:52:25 +0900 Subject: [PATCH 08/16] mruby-compiler: fix heap-buffer-overflow in pattern alternation codegen The JMPNOT-to-JMPIF optimization in NODE_PAT_ALT assumed the fail chain always ends with OP_JMPNOT (format BS), but NODE_PAT_PIN generates OP_JMP (format S) when the pinned variable is undefined. Writing OP_JMPIF at left_fail-2 then corrupts the preceding instruction's operand, causing out-of-bounds pool access at runtime. Co-authored-by: Claude --- mrbgems/mruby-compiler/core/codegen.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mrbgems/mruby-compiler/core/codegen.c b/mrbgems/mruby-compiler/core/codegen.c index b9f2781b76..63e66136e2 100644 --- a/mrbgems/mruby-compiler/core/codegen.c +++ b/mrbgems/mruby-compiler/core/codegen.c @@ -4498,9 +4498,12 @@ codegen_pattern(codegen_scope *s, node *pattern, int target, uint32_t *fail_pos, * 1. Left pattern is not another NODE_PAT_ALT (avoid recursion issues) * 2. Left pattern generated at least one JMPNOT * 3. The last JMPNOT is immediately before current position + * 4. The instruction is actually OP_JMPNOT (not OP_JMP which has + * different format S vs BS - converting OP_JMP would corrupt bytecode) * In this case, convert JMPNOT to JMPIF and skip generating JMP */ if (node_type(pat_alt->left) != NODE_PAT_ALT && - left_fail != JMPLINK_START && left_fail + 2 == s->pc) { + left_fail != JMPLINK_START && left_fail + 2 == s->pc && + s->iseq[left_fail - 2] == OP_JMPNOT) { /* Extract the previous link from the JMPNOT chain. * The chain uses relative offsets where the end is marked by * an offset that points to address 0 (i.e., (pos+2)+offset == 0) */ From b287c12e4853969c8d21a38b317f7ae50a032c1c Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Thu, 5 Feb 2026 22:38:42 +0900 Subject: [PATCH 09/16] mruby-compiler: raise error for pin operator with undefined variable CRuby raises SyntaxError for `^a` in pattern matching when `a` is not a local variable. Previously mruby silently generated an unconditional fail jump, which also led to bytecode corruption when combined with alternation patterns. Co-authored-by: Claude --- mrbgems/mruby-compiler/core/codegen.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mrbgems/mruby-compiler/core/codegen.c b/mrbgems/mruby-compiler/core/codegen.c index 63e66136e2..a77f9f17a1 100644 --- a/mrbgems/mruby-compiler/core/codegen.c +++ b/mrbgems/mruby-compiler/core/codegen.c @@ -4567,9 +4567,11 @@ codegen_pattern(codegen_scope *s, node *pattern, int target, uint32_t *fail_pos, *fail_pos = tmp; } else { - /* Variable not found - this is an error case, but for robustness fail the match */ - tmp = genjmp(s, OP_JMP, *fail_pos); - *fail_pos = tmp; + /* Variable not found - raise compile error like CRuby */ + const char *name = mrb_sym_name_len(s->mrb, pat_pin->name, NULL); + char buf[256]; + snprintf(buf, sizeof(buf), "%.200s: no such local variable", name); + codegen_error(s, buf); } } break; From 6afff1c3ebeffeb355353d50d613f0db3b9e36d7 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Fri, 6 Feb 2026 16:31:34 +0900 Subject: [PATCH 10/16] string.c: fix integer overflow in str_check_length() Reject MRB_INT_MAX length strings to prevent signed integer overflow when adding 1 for the null terminator in str_init_normal_capa() and resize_capa(). Co-authored-by: Claude --- src/string.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/string.c b/src/string.c index a0dee0f858..0cab3d80e6 100644 --- a/src/string.c +++ b/src/string.c @@ -40,7 +40,7 @@ const char mrb_digitmap[] = "0123456789abcdefghijklmnopqrstuvwxyz"; static void str_check_length(mrb_state *mrb, mrb_int len) { - if (len < 0) { + if (len < 0 || len == MRB_INT_MAX) { mrb_raise(mrb, E_ARGUMENT_ERROR, "negative (or overflowed) string size"); } #if MRB_STR_LENGTH_MAX != 0 From 4617263030d8f9acb558829f4da6134509755fa3 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sat, 7 Feb 2026 19:10:15 +0900 Subject: [PATCH 11/16] mruby-compiler: fix JMPNOT-to-MATCHERR rewriting in pattern match codegen The MATCHERR optimization replaced JMPNOT (BS, 4 bytes) with MATCHERR (B, 2 bytes) and rewound s->pc by 2. When pattern alternation (e.g. a|B) dispatched a success jump to s->pc before the optimization, the rewind shifted subsequent instructions and the jump landed in the middle of the next instruction, causing out-of-bounds access at runtime. Replace JMPNOT in-place with MATCHERR+NOP+NOP to keep the same 4-byte size, so s->pc does not change and jump targets stay valid. Co-authored-by: Claude --- mrbgems/mruby-compiler/core/codegen.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mrbgems/mruby-compiler/core/codegen.c b/mrbgems/mruby-compiler/core/codegen.c index a77f9f17a1..db7b3af020 100644 --- a/mrbgems/mruby-compiler/core/codegen.c +++ b/mrbgems/mruby-compiler/core/codegen.c @@ -6701,11 +6701,12 @@ codegen(codegen_scope *s, node *tree, int val) fail_pos + 2 == s->pc && s->iseq[fail_pos - 2] == OP_JMPNOT) { if (mp->raise_on_fail) { - /* Single failure point with raise - replace JMPNOT with MATCHERR */ - int reg = s->iseq[fail_pos - 1]; /* Register from JMPNOT */ - s->pc = fail_pos - 2; /* Rewind past JMPNOT */ - s->lastpc = s->pc; - genop_1(s, OP_MATCHERR, reg); /* Emit MATCHERR with the register */ + /* Replace JMPNOT(BS,4bytes) with MATCHERR(B,2bytes)+NOP+NOP; + * keep the same size so that any jump targeting s->pc stays valid */ + s->iseq[fail_pos - 2] = OP_MATCHERR; + /* fail_pos-1 already holds the register operand */ + s->iseq[fail_pos] = OP_NOP; + s->iseq[fail_pos + 1] = OP_NOP; s->sp = saved_sp - 1; if (val) push(); break; /* Pattern matching complete */ From 638a18dfe785b7916808bc8107509a32145195b2 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sun, 8 Feb 2026 21:14:50 +0900 Subject: [PATCH 12/16] replace pre-commit with prek prek is a faster, Rust-based drop-in replacement for pre-commit. It reads the same .pre-commit-config.yaml with no changes needed. Co-authored-by: Claude --- .github/workflows/pre-commit.yml | 17 ++++------------- Makefile | 2 +- Rakefile | 8 ++++---- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 78617629c1..4cae372d73 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,4 +1,4 @@ -# https://pre-commit.com/ +# https://github.com/j178/prek name: pre-commit on: [pull_request] @@ -13,17 +13,8 @@ jobs: steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" uses: actions/checkout@v6 - - name: Install - run: | - python -m pip install --upgrade pip - pip install pre-commit - - name: Set PY - run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> "$GITHUB_ENV" - - uses: actions/cache@v5 + - uses: j178/prek-action@v1 with: - path: ~/.cache/pre-commit - key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: Run pre-commit - run: pre-commit run --color=always --all-files + extra-args: --color=always --all-files - name: Run manual pre-commit hooks - run: pre-commit run --color=always --all-files --hook-stage manual + run: prek run --color=always --all-files --hook-stage manual diff --git a/Makefile b/Makefile index 20dbf3874a..f095f05840 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ RAKE = rake DOCKER_COMPOSE = docker-compose -PRE_COMMIT = pre-commit +PRE_COMMIT = prek define check_command @command -v $(1) >/dev/null 2>&1 || { \ diff --git a/Rakefile b/Rakefile index ec73b2c587..3bab7ad7cb 100644 --- a/Rakefile +++ b/Rakefile @@ -78,22 +78,22 @@ end desc "run all pre-commit hooks against all files" task :check do - sh "pre-commit run --all-files" + sh "prek run --all-files" end desc "install the pre-commit hooks" task :checkinstall do - sh "pre-commit install" + sh "prek install" end desc "check the pre-commit hooks for updates" task :checkupdate do - sh "pre-commit autoupdate" + sh "prek autoupdate" end desc "run all pre-commit hooks against all files with docker-compose" task :composecheck do - sh "docker-compose -p mruby run test pre-commit run --all-files" + sh "docker-compose -p mruby run test prek run --all-files" end desc "build and run all mruby tests with docker-compose" From 564995a91a47e35333fd8927f5c12bbae5f8bf76 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Mon, 9 Feb 2026 22:45:19 +0900 Subject: [PATCH 13/16] update documentation and Makefile for prek migration Co-authored-by: Claude --- .pre-commit-config.yaml | 2 +- CONTRIBUTING.md | 49 ++++++++++++++++++++--------------------- Makefile | 10 ++++----- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f85fa50a8e..37b6566284 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ --- -# https://pre-commit.com/ +# https://github.com/j178/prek default_stages: [pre-commit, pre-push] default_language_version: python: python3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52d1235290..fac854ab98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,41 +27,41 @@ If you discover a security vulnerability: For detailed guidance on what qualifies as a security issue and what doesn't, see [SECURITY.md](SECURITY.md). -## pre-commit +## prek -A framework for managing and maintaining multi-language `pre-commit` hooks. -`pre-commit` can be [installed](https://pre-commit.com/#installation) with `pip`, `curl`, `brew` or `conda`. +We use [prek](https://github.com/j178/prek), a fast Rust-based pre-commit hook manager. +It reads the standard `.pre-commit-config.yaml` format. -You need to first install `pre-commit` and then install the `pre-commit` hooks with `pre-commit install`. -Now `pre-commit` will run automatically on git commit! +Install `prek` following the [installation guide](https://github.com/j178/prek#installation), +then install the hooks with `prek install`. +Now `prek` will run automatically on git commit! -It's usually a good idea to run the hooks against all the files when adding new hooks (usually `pre-commit` -will only run on the changed files during git hooks). Use `pre-commit run --all-files` to check all files. +It's usually a good idea to run the hooks against all the files when adding new hooks (usually `prek` +will only run on the changed files during git hooks). Use `prek run --all-files` to check all files. -To run a single hook use `pre-commit run --all-files ` +To run a single hook use `prek run --all-files ` -To update use `pre-commit autoupdate` +To update use `prek autoupdate` Sometimes you might need to skip one or more hooks which can be done with the `SKIP` environment variable. `$ SKIP=yamllint git commit -m "foo"` -For convenience, we have added `pre-commit run --all-files`, `pre-commit install` and `pre-commit autoupdate` +For convenience, we have added `prek run --all-files`, `prek install` and `prek autoupdate` to both the Makefile and the Rakefile. Run them with: - `make check` or `rake check` - `make checkinstall` or `rake checkinstall` - `make checkupdate` or `rake checkupdate` -To configure `pre-commit` you can modify the config file [.pre-commit-config.yaml](.pre-commit-config.yaml). -We use [GitHub Actions](.github/workflows/pre-commit.yml) to run `pre-commit` on every pull request. +To configure hooks you can modify the config file [.pre-commit-config.yaml](.pre-commit-config.yaml). +We use [GitHub Actions](.github/workflows/pre-commit.yml) to run `prek` on every pull request. -### pre-commit quick links +### prek quick links -- [Quick start](https://pre-commit.com/#quick-start) -- [Usage](https://pre-commit.com/#usage) -- [pre-commit autoupdate](https://pre-commit.com/#pre-commit-autoupdate) -- [Temporarily disabling hooks](https://pre-commit.com/#temporarily-disabling-hooks) +- [prek GitHub](https://github.com/j178/prek) +- [Installation](https://github.com/j178/prek#installation) +- [Usage](https://github.com/j178/prek#usage) ## Docker @@ -97,20 +97,19 @@ mruby-test latest ec60f9536948 29 seconds ago 1.29GB ``` You can also run any custom `docker-compose` command which will override -the default. For example to run `pre-commit run --all-files` type: +the default. For example to run `prek run --all-files` type: -`$ docker-compose -p mruby run test pre-commit run --all-files` +`$ docker-compose -p mruby run test prek run --all-files` -For convenience, you can also run `pre-commit` with: +For convenience, you can also run `prek` with: - `make composecheck` - `rake composecheck` -The bonus of running `pre-commit` with `docker-compose` is that you won't need -to install `pre-commit` and the hooks on your local machine. And that also -means you won't need to install `brew`, `conda` or `pip`. +The bonus of running `prek` with `docker-compose` is that you won't need +to install `prek` and the hooks on your local machine. -Note limitation: currently running `pre-commit` with `docker-compose` we +Note limitation: currently running `prek` with `docker-compose` we skip the `check-executables-have-shebangs` hook. Two more examples of custom `docker-compose` commands are: @@ -128,7 +127,7 @@ can use the `-f` flag: ## Spell Checking -We are using `pre-commit` to run [codespell](https://github.com/codespell-project/codespell) +We are using `prek` to run [codespell](https://github.com/codespell-project/codespell) to check code for common misspellings. We have a small custom dictionary file [codespell.txt](.github/linters/codespell.txt). ## Coding conventions diff --git a/Makefile b/Makefile index f095f05840..65a5231dcb 100644 --- a/Makefile +++ b/Makefile @@ -32,16 +32,16 @@ test: check_rake all ## build and run all mruby tests clean: check_rake ## clean all built and in-repo installed artifacts $(RAKE) clean -check: check_pre_commit ## run all pre-commit hooks against all files +check: check_pre_commit ## run all prek hooks against all files $(PRE_COMMIT) run --all-files -checkinstall: check_pre_commit ## install the pre-commit hooks +checkinstall: check_pre_commit ## install the prek hooks $(PRE_COMMIT) install -checkupdate: check_pre_commit ## check the pre-commit hooks for updates +checkupdate: check_pre_commit ## check the prek hooks for updates $(PRE_COMMIT) autoupdate -composecheck: check_docker_compose check_pre_commit ## run all pre-commit hooks against all files with docker-compose +composecheck: check_docker_compose check_pre_commit ## run all prek hooks against all files with docker-compose $(DOCKER_COMPOSE) -p mruby run test $(PRE_COMMIT) run --all-files composetest: check_docker_compose ## build and run all mruby tests with docker-compose @@ -53,7 +53,7 @@ check_rake: ## check if Rake is installed check_docker_compose: ## check if docker-compose is installed $(call check_command, $(DOCKER_COMPOSE)) -check_pre_commit: ## check if pre-commit is installed +check_pre_commit: ## check if prek is installed $(call check_command, $(PRE_COMMIT)) help: ## display this help message From f80f1cd27d60f964df5ee577ad3bf6e96d3b634e Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Tue, 10 Feb 2026 14:56:12 +0900 Subject: [PATCH 14/16] load.c: fix off-by-one in bounds check for pool strings The bounds check for IREP_TT_STR pool data only validated pool_data_len bytes, but the binary format includes a null terminator after the string content. Both memcpy and the source pointer advance by pool_data_len+1, so the check must account for the extra byte. Co-authored-by: Claude --- src/load.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/load.c b/src/load.c index ac850373d2..49f6580b40 100644 --- a/src/load.c +++ b/src/load.c @@ -189,7 +189,7 @@ read_irep_record_1(mrb_state *mrb, const uint8_t *bin, const uint8_t *end, size_ case IREP_TT_STR: pool_data_len = bin_to_uint16(src); /* pool data length */ src += sizeof(uint16_t); - if (src + pool_data_len > end) return FALSE; + if (src + pool_data_len + 1 > end) return FALSE; if (st) { pool[i].tt = (pool_data_len<<2) | IREP_TT_SSTR; pool[i].u.str = (const char*)src; From b3b8c0176fbd4cf463f91967012c446e5b9707d3 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Tue, 10 Feb 2026 14:56:25 +0900 Subject: [PATCH 15/16] load.c: fix off-by-one in bounds check for symbol names Same issue as the pool string fix: the bounds check for symbol names only validated snl bytes, but the binary format includes a null terminator. The source pointer advances by snl+1, so the check must account for it. Co-authored-by: Claude --- src/load.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/load.c b/src/load.c index 49f6580b40..6f9954b829 100644 --- a/src/load.c +++ b/src/load.c @@ -231,7 +231,7 @@ read_irep_record_1(mrb_state *mrb, const uint8_t *bin, const uint8_t *end, size_ continue; } - if (src + snl > end) return FALSE; + if (src + snl + 1 > end) return FALSE; if (flags & FLAG_SRC_MALLOC) { syms[i] = mrb_intern(mrb, (char*)src, snl); } From c06a11912c3ee82dc69b166dda457987d64bcae6 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Tue, 10 Feb 2026 15:14:53 +0900 Subject: [PATCH 16/16] mruby-bin-mirb: fix uninitialized editor struct causing bintest failures Zero-initialize the mirb_editor struct to prevent highlight.enabled from containing garbage values when stdin is not a tty (e.g. in bintest). Without this, ANSI color codes could be emitted in non-interactive mode, breaking output string matching in tests. Co-authored-by: Claude --- mrbgems/mruby-bin-mirb/tools/mirb/mirb.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index 7622aacd19..590bc13b58 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -477,6 +477,8 @@ main(int argc, char **argv) mirb_editor editor; mirb_check_data check_data; mrb_bool use_editor = FALSE; + + memset(&editor, 0, sizeof(editor)); mrb_ccontext *cxt; struct mrb_parser_state *parser; mrb_state *mrb;