Skip to content

mruby-compiler: report a failed irep round trip in mrb_generate_code() - #7204

Open
takumin wants to merge 1 commit into
mruby:masterfrom
takumin:compat/report-irep-roundtrip
Open

mruby-compiler: report a failed irep round trip in mrb_generate_code()#7204
takumin wants to merge 1 commit into
mruby:masterfrom
takumin:compat/report-irep-roundtrip

Conversation

@takumin

@takumin takumin commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

A program can end at exit 1 with nothing on either stream. On today's master, build_config/default.rb, x86-64:

$ ruby -e "puts 's = \"' + ('a' * 65536) + '\"; p s.size'" > big.rb
$ build/host/bin/mruby big.rb
$ echo $?
1

65535 bytes of a runs and prints 65535. 65536 is the boundary, and it says nothing about it.

Where the report went

Two of the three ways compilation can fail write to stderr themselves, under a gate a caller that captures errors turns off. parse_source():

  /* Unless the caller captures errors (e.g. eval), report parse errors to
     stderr like the bison parser does, so a syntax error is not a silent
     failure. Codegen errors take the same gate inside codegen_error(),
     through `quiet_errors`. */
  if (!c || !c->capture_errors) {

and codegen_error():

#ifndef MRC_NO_STDIO
  if (!s->c->quiet_errors) {

The third is the dump and reload that turns the mrc_irep into an mrb_irep, and it takes neither:

  if (mrc_dump_irep(mc, irep, flags, &bin, &bin_size) != MRC_COMPAT_DUMP_OK) {
    return NULL;
  }
  mir = mrb_read_irep_buf(mrb, bin, bin_size);
  mrc_free(mc, bin);
  if (!mir) {
    return NULL;
  }

mrb_load_exec() does answer that NULL, by raising ScriptError "codegen error" and returning undef. What the front ends do with it is print it only when the value is not undef:

    if (mrb->exc) {
      MRB_EXC_CHECK_EXIT(mrb, mrb->exc);
      if (!mrb_undef_p(v)) {
        mrb_print_error(mrb);
      }
      n = EXIT_FAILURE;
    }

which is 9bd17226d3, "Avoid duplicate message output (SyntaxError, ScriptError etc)": undef means the compiler already said it. mruby, mrb and mrdb all carry that shape. Here the compiler said nothing, so nobody does.

The exception is real, and a caller that reads exceptions rather than stderr has always seen it:

$ build/host/bin/mruby -e 'p eval(%q{s = "} + "a" * 65536 + %q{"; s.size})'
trace (most recent call last):
	[1] -e:1
-e:1:in eval: codegen error (ScriptError)

The change

mrb_generate_code() reports both failures through the same quiet_errors gate as the two beside it, so every front end gets the line and eval keeps getting only the exception:

$ build/host/bin/mruby big.rb
big.rb: irep load error
$ echo $?
1
$ build/host/bin/mruby -e 'begin; eval(%q{s = "} + "a" * 65536 + %q{"}); rescue ScriptError => e; puts "rescued: " + e.message; end'
rescued: codegen error

mrdb prints it too. mirb does not, and this does not change that: it sets capture_errors so it can draw syntax errors itself with a caret, and answers a NULL from mrb_generate_code() with a bare continue. Giving it a line means routing this diagnostic through the parser's error buffer, which is a different change.

What this does not fix

Why a 65536-byte string literal fails at all. write_pool_block() records a pool string's length in a uint16_t, and the only thing standing between a longer literal and a truncated record is an assertion that a release build compiles away:

      len = irep->pool[pool_no].tt>>2;
      mrc_assert_int_fit(mrc_int, len, uint16_t, UINT16_MAX);
      cur += mrc_uint16_to_bin((uint16_t)len, cur); /* data length */

An MRC_DEBUG build stops there instead, which is ci/gcc-clang's full-debug on the same file:

$ build/full-debug/bin/mruby big.rb
mruby: mrbgems/mruby-compiler/src/dump.c:239: write_pool_block: Assertion `(len)>=0 && ((sizeof(len)<=sizeof(uint16_t))||(len<=(mrc_int)((65535))))' failed.

That is a separate defect and wants a separate fix. It is used here because it is the shortest program that reaches this code path on a stock build.

#7201 is the other way in that turned up this week: on an MRB_INT32 build every integer literal from 2**31 to 2**63 - 1 was refused by the loader, and mruby -e 'p 1<<31' was silent for the reason above. With this commit and without #7201, that build answers:

$ build/host-m32/bin/mruby -e 'p 1<<31'
-e: irep load error

Two unrelated defects, both invisible at the same place.

Tests

None. mrb_generate_code() reaches this only when a dump and its reload disagree, which is a defect wherever it happens rather than something a test can ask for and keep. Pinning it to the 65536-byte literal would pin a test to the defect above staying unfixed.

Verification

rake -m test over ci/gcc-clang, every build green, 0 KO, 0 crash, no new warnings, and the counts are master's to the test.

build master with this commit
full-debug 2312 tests, 3 skip 2312 tests, 3 skip
bintest 2312 tests, 11 skip, plus 117 bintests 2312 tests, 11 skip, plus 117 bintests
cxx_abi 2312 tests, 11 skip 2312 tests, 11 skip
byte-string 2243 tests, 48 skip 2243 tests, 48 skip
ascii-case 2309 tests, 13 skip 2309 tests, 13 skip

MRB_NO_STDIO compiles the file with the report gone, warning free, checked on a MRuby::CrossBuild carrying mruby-compiler and mruby-eval (build_config/minimal.rb builds no gem, so it does not compile this file).

Environment

Versions
OS Ubuntu 24.04.4 LTS, Linux 7.0.0-28-generic x86_64
CPU AMD Ryzen 9 5950X, 16 cores
C compiler gcc 13.3.0 and i686-linux-gnu-gcc 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1)
Linker GNU ld 2.47.20260726, GNU ld 2.42 for i686, and g++ for cxx_abi
CRuby 4.0.6 (2026-07-14) +PRISM, running rake and generating the test programs
Compile lines for mruby_compat.c

-MMD -c, -I and -o dropped.

# ci/gcc-clang full-debug, -O0 because enable_debug appends -g3 -O0
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -g3 -O0 -DMRB_GC_STRESS -DMRB_USE_DEBUG_HOOK -DPRISM_XALLOCATOR -DPRISM_DEPTH_MAXIMUM=256 -DMRC_TARGET_MRUBY -DMRC_DEBUG -DMRC_DUMP_PRETTY -DMRBGEM_MRUBY_COMPILER_VERSION=0.0.0 -DMRB_DEBUG -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER mrbgems/mruby-compiler/src/mruby_compat.c

# ci/gcc-clang bintest
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_GC_FIXED_ARENA -DPRISM_XALLOCATOR -DPRISM_DEPTH_MAXIMUM=256 -DMRC_TARGET_MRUBY -DPRISM_BUILD_MINIMAL -DMRBGEM_MRUBY_COMPILER_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -DMRB_USE_DEBUG_HOOK mrbgems/mruby-compiler/src/mruby_compat.c

# ci/gcc-clang cxx_abi, gcc -x c++ rather than g++, which only links
gcc -g -O3 -Wall -Wundef -Wwrite-strings -x c++ -std=gnu++03 -DMRB_GC_FIXED_ARENA -DPRISM_XALLOCATOR -DPRISM_DEPTH_MAXIMUM=256 -DMRC_TARGET_MRUBY -DPRISM_BUILD_MINIMAL -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -DMRBGEM_MRUBY_COMPILER_VERSION=0.0.0 -DMRB_USE_CXX_EXCEPTION -DMRB_USE_CXX_ABI -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER mrbgems/mruby-compiler/src/mruby_compat.c

# ci/gcc-clang byte-string
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DPRISM_XALLOCATOR -DPRISM_DEPTH_MAXIMUM=256 -DMRC_TARGET_MRUBY -DPRISM_BUILD_MINIMAL -DMRBGEM_MRUBY_COMPILER_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER mrbgems/mruby-compiler/src/mruby_compat.c

# ci/gcc-clang ascii-case
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_ASCII_CASE -DPRISM_XALLOCATOR -DPRISM_DEPTH_MAXIMUM=256 -DMRC_TARGET_MRUBY -DPRISM_BUILD_MINIMAL -DMRBGEM_MRUBY_COMPILER_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER mrbgems/mruby-compiler/src/mruby_compat.c

# MRB_NO_STDIO, a CrossBuild with mruby-compiler and mruby-eval
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_NO_STDIO -DPRISM_XALLOCATOR -DPRISM_DEPTH_MAXIMUM=256 -DMRC_TARGET_MRUBY -DPRISM_BUILD_MINIMAL -DMRBGEM_MRUBY_COMPILER_VERSION=0.0.0 mrbgems/mruby-compiler/src/mruby_compat.c

`parse_source()` writes a parse error to stderr and `codegen_error()` writes a
codegen error there, both under the gate a caller that captures errors turns
off. The dump and reload that turns the `mrc_irep` into an `mrb_irep` is the
one compiler-side failure that takes neither: `mrb_generate_code()` returns
`NULL` with nothing said.

`mrb_load_exec()` answers that `NULL` by raising `ScriptError "codegen error"`
and returning `undef`, but `mruby`, `mrb` and `mrdb` only print an exception
when the value is not `undef`, which is how they keep from repeating what the
compiler already printed. Nothing printed here, so the program ends at exit 1
without a word.

Report both failures the way the two beside them are reported.
@takumin
takumin requested a review from matz as a code owner August 16, 2026 16:20
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@takumin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Limit details: You’ve used all 8 included reviews currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 063d88c7-1124-4352-b4e7-b6a04b9d8378

📥 Commits

Reviewing files that changed from the base of the PR and between 9710e46 and a732f94.

📒 Files selected for processing (1)
  • mrbgems/mruby-compiler/src/mruby_compat.c

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant