mruby-bin-mirb: restrict evaluation to simple receivers

fixed a bug where tab completion on complex expressions like "d.new(1).a"
would corrupt local variables, causing them to become nil.

the root cause was that evaluating complex receiver expressions during tab
completion ran mrb_vm_run() without proper stack management (stack_keep)
and environment adjustment that mirb's main REPL loop performs. this
corrupted the local variable storage.

the fix restricts tab completion to only evaluate simple receiver
expressions (variable/constant names without operators or method calls).
complex expressions are skipped for completion. this means:
- works: d.<tab> completes methods of variable d
- works: String.<tab> completes methods of constant String
- skipped: d.new(1).<tab> provides no completion

this is a reasonable trade-off that prevents the corruption bug while
still supporting the most common completion scenarios.

also updated mirb_eval_receiver() to use the compiler context for proper
local variable resolution, with argument order matching mrb_parse_string.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-12-04 16:09:35 +09:00
parent 2f152823f3
commit 4cf75fb035
2 changed files with 32 additions and 7 deletions
@@ -199,16 +199,36 @@ mirb_extract_receiver(const char *line, int cursor_pos, int *recv_end)
* Receiver Evaluation
* ============================================================ */
/* Check if receiver expression is simple (just a name, no method calls) */
static mrb_bool
is_simple_receiver(const char *expr)
{
int i;
/* Empty is not simple */
if (!expr || expr[0] == '\0') return FALSE;
/* Check if it contains only alphanumeric, underscore, or scope resolution */
for (i = 0; expr[i]; i++) {
char c = expr[i];
if (!(ISALNUM(c) || c == '_' || c == ':')) {
return FALSE; /* Contains operators, parentheses, etc. */
}
}
return TRUE;
}
mrb_value
mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr)
mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr, mrb_ccontext *cxt)
{
struct mrb_parser_state *parser;
struct RProc *proc;
mrb_value result;
int ai = mrb_gc_arena_save(mrb);
/* Parse the receiver expression */
parser = mrb_parse_string(mrb, receiver_expr, NULL);
/* Parse the receiver expression WITH compiler context to access local variables */
parser = mrb_parse_string(mrb, receiver_expr, cxt);
if (!parser || parser->nerr > 0) {
if (parser) mrb_parser_free(parser);
return mrb_nil_value();
@@ -454,9 +474,14 @@ mirb_generate_completions(mirb_completion_ctx *ctx, const char *line, int cursor
case COMPLETION_METHOD:
receiver_expr = mirb_extract_receiver(line, cursor_pos, &recv_end);
if (receiver_expr) {
receiver = mirb_eval_receiver(ctx->mrb, receiver_expr);
if (!mrb_nil_p(receiver)) {
mirb_complete_methods(ctx, receiver);
/* Only evaluate simple receivers to avoid corrupting VM state.
* Complex expressions like "obj.method()" are skipped for now.
* This prevents local variables from being cleared during tab completion. */
if (is_simple_receiver(receiver_expr)) {
receiver = mirb_eval_receiver(ctx->mrb, receiver_expr, ctx->cxt);
if (!mrb_nil_p(receiver)) {
mirb_complete_methods(ctx, receiver);
}
}
free(receiver_expr);
}
@@ -95,7 +95,7 @@ void mirb_add_completion(mirb_completion_ctx *ctx, const char *text);
char *mirb_extract_receiver(const char *line, int cursor_pos, int *recv_end);
/* Evaluate receiver expression to get object */
mrb_value mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr);
mrb_value mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr, mrb_ccontext *cxt);;
/* Check if in file completion context */
mrb_bool mirb_in_file_context(const char *line, int quote_pos);