This articles was published on 2012-05-23

mirb - Embeddable Interactive Ruby Shell

My first patch which was non-trivial to mruby was a reimplementation of the IRB. The Interactive Ruby Shell is a quite useful tool all over the Ruby community. It is available for all other implementations and there is even a web-version available. Enough reason to also create an embeddable version.

The first attempt was actually not done by me but by Frank Celler. He was in need for a playground to experiment with mruby in his no-sql database AvocadoDB. The downside of his solution was that he didn’t yet evaluated the return value of each evaluation and the complete thing was implemented in C++ (mruby is written in C99).

As time went by matz implemented this missing return value and I took another look at the API of mruby. The result was a quite naive implementation of an IRB based on mruby:

/*
** mirb - Embeddable Interactive Ruby Shell
**
** This program takes code from the user in
** an interactive way and executes it
** immediately. It's a REPL...
*/

#include <string.h>

#include <mruby.h>
#include <mruby/proc.h>
#include <mruby/data.h>
#include <mruby/compile.h>

/* Guess if the user might want to enter more
 * or if he wants an evaluation of his code now */
int
is_code_block_open(struct mrb_parser_state *parser)
{
  int code_block_open = FALSE;

  /* check for unterminated string */
  if (parser->sterm) return TRUE;

  /* check if parser error are available */
  if (0 < parser->nerr) {
    const char *unexpected_end = "syntax error, unexpected $end";
    const char *message = parser->error_buffer[0].message;

    /* a parser error occur, we have to check if */
    /* we need to read one more line or if there is */
    /* a different issue which we have to show to */
    /* the user */

    if (strncmp(message, unexpected_end, strlen(unexpected_end)) == 0) {
      code_block_open = TRUE;
    }
    else if (strcmp(message, "syntax error, unexpected keyword_end") == 0) {
      code_block_open = TRUE;
    }
    else if (strcmp(message, "syntax error, unexpected tREGEXP_BEG") == 0) {
      code_block_open = TRUE;
    }
    return code_block_open;
  }

  switch (parser->lstate) {

  /* all states which need more code */

  case EXPR_BEG:
    /* an expression was just started, */
    /* we can't end it like this */
    code_block_open = TRUE;
    break;
  case EXPR_DOT:
    /* a message dot was the last token, */
    /* there has to come more */
    code_block_open = TRUE;
    break;
  case EXPR_CLASS:
    /* a class keyword is not enough! */
    /* we need also a name of the class */
    code_block_open = TRUE;
    break;
  case EXPR_FNAME:
    /* a method name is necessary */
    code_block_open = TRUE;
    break;
  case EXPR_VALUE:
    /* if, elsif, etc. without condition */
    code_block_open = TRUE;
    break;

  /* now all the states which are closed */

  case EXPR_ARG:
    /* an argument is the last token */
    code_block_open = FALSE;
    break;

  /* all states which are unsure */

  case EXPR_CMDARG:
    break;
  case EXPR_END:
    /* an expression was ended */
    break;
  case EXPR_ENDARG:
    /* closing parenthese */
    break;
  case EXPR_ENDFN:
    /* definition end */
    break;
  case EXPR_MID:
    /* jump keyword like break, return, ... */
    break;
  case EXPR_MAX_STATE:
    /* don't know what to do with this token */
    break;
  default:
    /* this state is unexpected! */
    break;
  }

  return code_block_open;
}

/* Print a short remark for the user */
void print_hint(void)
{
  printf("mirb - Embeddable Interactive Ruby Shelln");
  printf("nThis is a very early version, please test and report errors.n");
  printf("Thanks :)nn");
}

/* Print the command line prompt of the REPL */
void
print_cmdline(int code_block_open)
{
  if (code_block_open) {
    printf("* ");
  }
  else {
    printf("> ");
  }
}

int
main(void)
{
  char last_char, ruby_code[1024], last_code_line[1024];
  int char_index;
  struct mrb_parser_state *parser;
  mrb_state *mrb_interpreter;
  mrb_value mrb_return_value;
  int byte_code;
  int code_block_open = FALSE;

  print_hint();

  /* new interpreter instance */
  mrb_interpreter = mrb_open();
  /* new parser instance */
  parser = mrb_parser_new(mrb_interpreter);
  memset(ruby_code, 0, sizeof(*ruby_code));
  memset(last_code_line, 0, sizeof(*last_code_line));

  while (TRUE) {
    print_cmdline(code_block_open);

    char_index = 0;
    while ((last_char = getchar()) != 'n') {
      if (last_char == EOF) break;
      last_code_line[char_index++] = last_char;
    }
    if (last_char == EOF) {
      printf("n");
      break;
    }

    last_code_line[char_index] = '�';

    if ((strcmp(last_code_line, "quit") == 0) ||
        (strcmp(last_code_line, "exit") == 0)) {
      if (code_block_open) {
        /* cancel the current block and reset */
        code_block_open = FALSE;
        memset(ruby_code, 0, sizeof(*ruby_code));
        memset(last_code_line, 0, sizeof(*last_code_line));
        continue;
      }
      else {
        /* quit the program */
        break;
      }
    }
    else {
      if (code_block_open) {
        strcat(ruby_code, "n");
        strcat(ruby_code, last_code_line);
      }
      else {
        memset(ruby_code, 0, sizeof(*ruby_code));
        strcat(ruby_code, last_code_line);
      }

      /* parse code */
      parser->s = ruby_code;
      parser->send = ruby_code + strlen(ruby_code);
      parser->capture_errors = 1;
      parser->lineno = 1;
      mrb_parser_parse(parser);
      code_block_open = is_code_block_open(parser); 

      if (code_block_open) {
        /* no evaluation of code */
      }
      else {
        if (0 < parser->nerr) {
          /* syntax error */
          printf("line %d: %sn", parser->error_buffer[0].lineno, parser->error_buffer[0].message);
        }
        else {
          /* generate bytecode */
          byte_code = mrb_generate_code(mrb_interpreter, parser->tree);

          /* evaluate the bytecode */
          mrb_return_value = mrb_run(mrb_interpreter,
            /* pass a proc for evaulation */
            mrb_proc_new(mrb_interpreter, mrb_interpreter->irep[byte_code]),
            mrb_top_self(mrb_interpreter));
          /* did an exception occur? */
          if (mrb_interpreter->exc) {
            mrb_p(mrb_interpreter, mrb_obj_value(mrb_interpreter->exc));
            mrb_interpreter->exc = 0;
          }
          else {
            /* no */
            printf(" => ");
            mrb_p(mrb_interpreter, mrb_return_value);
          }
        }

        memset(ruby_code, 0, sizeof(*ruby_code));
        memset(ruby_code, 0, sizeof(*last_code_line));
      }
    }
  }
  mrb_close(mrb_interpreter);

  return 0;
}

But for some reason matz accepted it and since two weeks (2012-05-12) mruby has now a Read Eval Print Loop which provides an easy way to explore mruby:

boviAir:mruby daniel$ ./bin/mirb
mirb - Embeddable Interactive Ruby Shell

This is a very early version, please test and report errors.
Thanks :)

> class Testii
*   def testoo
*     puts 1
*   end
*
*   def testuu arg
*     puts arg
*   end
* end
=> nil
> Testii.testoo
#<NoMethodError: no method named testoo>
> Testii.new.testoo
1
=> false
> Testii.new.testuu 'hui'
hui
=> false
> 1+1
=> 2

But having such a tool doesn’t only makes it easy to play with Ruby. It also makes it possible to learn how to interact with mruby.

We start by getting a mruby instance:

mrb_interpreter = mrb_open();

With this instance we are getting a parser instance which we will use to parse our input code:

parser = mrb_parser_new(mrb_interpreter);

We now tell the parser where our ruby code is ->s, then we have to give him the same again with the length ->send. For our special use case of a REPL we tell the parser to capture all errors instead of screaming them out to stdio. This is done by ->capture_errors. With ->lineno we set the starting point and then we start the parsing with mrb_parser_parse:

/* parse code */
parser->s = ruby_code;
parser->send = ruby_code + strlen(ruby_code);
parser->capture_errors = 1;
parser->lineno = 1;
mrb_parser_parse(parser);

Next up, we want to compile down to byte code for the VM:

/* generate bytecode */
byte_code = mrb_generate_code(mrb_interpreter, parser->tree);

Last but not least we run the byte code inside of the VM and capturing the return value:

/* evaluate the bytecode */
mrb_return_value = mrb_run(mrb_interpreter,
    /* pass a proc for evaulation */
    mrb_proc_new(mrb_interpreter, mrb_interpreter->irep[byte_code]),
    mrb_top_self(mrb_interpreter))

And that’s it. In this way you can use mruby in any of your application too.