読者です 読者をやめる 読者になる 読者になる

mrubyだと32個以上の必須引数をとれない

技術系日記 Ruby

www.amazon.co.jp

別に普段mruby使ってるわけでもないんですが『言語のしくみ』を読んでたら気になったので検証。

本の記載

タイプ3 Ax(25ビット) op(7ビット)

命令タイプ3はオペランド雨を全部1つにして25ビットのオペランド(Ax)として取り扱います。タイプ3の命令は、OP_ENTERの一つだけです。
(中略)
OP_ENTERはAxに指定されたビットパターンを見て、メソッドの引数チェックをします。OP_ENTERは25ビットのうち23ビットを5/5/1/5/5/1/1に分割して引数指定と解釈します。各ビットの意味を表2に示します。

ビット | 内容  
 5  | 必須引数の数
(以下略)
表2 OP_ENTERの引数指定 より一部抜粋

全体は1-3 バーチャルマシン、mrubyの命令を見てみる より。

思ったこと

必須引数の数が5ビットで管理されてるってことは 25 = 32 で 0〜31 までしかないはず。
32個の引数をとったら困るんじゃね?

検証

環境はこんな感じ。

$ ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]
$ bin/mruby --version
mruby 1.2.0 (2015-11-17)

さて、引数32個も書くの嫌なのでさくっと生成します*1

n = 32
args = (1..n).map { |e| "a#{e}" }
str = <<-EOS
def foo(#{args.join(', ')})
  puts true
end
foo(*Array.new(#{n}))
EOS

File.write('main.rb', str)

じゃあ検証してみましょう。rubyとmruby両方で実行します。

$ ruby generate.rb
$ cat main.rb
def foo(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32)
  puts true
end
foo(*Array.new(32))
$ ruby main.rb
true
$ bin/mruby main.rb
trace:
        [1] main.rb:1:in Object.foo
        [0] main.rb:4
ArgumentError: 'foo': wrong number of arguments (32 for 0)

たしかに引数チェックは5ビットで行われてるみたい。 32 for 0 だから0個の引数を期待してることがわかります。

31個だと両方共trueを出力します。 33個だと wrong number of arguments(33 for 1) になってることも確認。

じゃあ0個を期待してるらしいので実際0個にしたらどうなるの?というのが以下。

$ cat main.rb
def foo(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32)
  p a1 # 中身みたかったので変更
end
foo # 引数ゼロ個
$ ruby main.rb
main.rb:1:in `foo': wrong number of arguments (given 0, expected 32) (ArgumentError)
        from main.rb:4:in `<main>'
$ bin/mruby main.rb
nil

当然rubyでは引数の数が違ってエラー。mrubyだと通りました。やったね
ただし引数は全部nilなので特に意味はありません。

じゃあ引数の最大値は31個なのかというと違います。
表2を全部写すのがめんどかったので書いていませんが、オプション引数の数とかキーワード引数の数も管理されています。
キーワード引数はmruby 1.2.0では未対応なので使えません*2。しかしオプション引数ならば5ビットまで使えるのでもう少し頑張れそうです。

n = 31 # これで31 * 2 の62個まで
args = (1..n).map { |e| "a#{e}" }
args += (1..n).map { |e| "b#{e} = #{e}" }
str = <<-EOS
def foo(#{args.join(', ')})
  puts true
end
foo(*Array.new(#{n}))
EOS

File.write('main.rb', str)

これで実行するとちゃんと通るのでたぶん62個ですかね。

$ cat main.rb
def foo(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, b1 = 1, b2 = 2, b3 = 3, b4 = 4, b5 = 5, b6 = 6, b7 = 7, b8 = 8, b9 = 9, b10 = 10, b11 = 11, b12 = 12, b13 = 13, b14 = 14, b15 = 15, b16 = 16, b17 = 17, b18 = 18, b19 = 19, b20 = 20, b21 = 21, b22 = 22, b23 = 23, b24 = 24, b25 = 25, b26 = 26, b27 = 27, b28 = 28, b29 = 29, b30 = 30, b31 = 31)
  puts true
end
foo(*Array.new(31))
$ ruby main.rb
true
$ bin/mruby main.rb
true

ビット的には必須引数の数とオプション引数の数が隣あってるからオーバーフローしたりしないかなって思ったんですがしなかったです。まあそりゃそうだ

# generate.rbは上記のをn = 32に変更
$ bin/mruby main.rb
trace:
        [1] main.rb:1:in Object.foo
        [0] main.rb:4
ArgumentError: 'foo': wrong number of arguments (32 for 0)

rubyだと引数の最大値いくつなんだろ?というのも気になりますが、n = 2_000_000_000_000 でも大丈夫だったのでまあいいかと思って最大値は見てません ( ˘ω˘)

*1:時間的には変わんないというか遅い気もするけど気持ちの問題ですよ

*2:たぶん。表2中に未対応とあり、mirbでやってもSyntax Errorになったので