Skip to content

FFT and Image convert a Go index-out-of-range into ArgumentError instead of checking arity (31 methods measured) #820

Closed

Description

@tannevaled

What happens

Calling a method of FFT or Image with too few arguments converts a Go runtime panic into a Ruby error, instead of checking the count:

FFT.fft()
# ArgumentError: fft: runtime error: index out of range [0] with length 0

Image.new
# ArgumentError: new: runtime error: index out of range [0] with length 0

MRI says wrong number of arguments (given 0, expected 1). What rbgo answers names an internal fault, and its class is accidental — it is ArgumentError only because the recovery path labels every panic that way.

Extent, measured

A census over rbgo itself (Special.methods(false).each { |m| … }, every method called with zero arguments, classified by its message) on wave70-special, 2026-10-12:

receiver unchecked checked takes zero arguments
FFT. 17 0 0
Image. 2 0 0
Image# 12 0 22
NDArray. 0 10 2
NDArray# 0 6 30
Special. 0 66 0
Stats. 0 23 9
Stats::Continuous# / Stats::Discrete# 0 10 / 10 9 / 9

31 methods in all: every one of FFT's 17, and 14 of Image's.

  • FFT: bartlett blackman blackman_harris fft fft2 fftfreq fftn hamming hann ifft ifft2 ifftn irfft psd rfft rfftfreq spectrogram
  • Image.: decode new
  • Image#: adjust_brightness adjust_contrast box_blur canny crop dilate erode gaussian_blur get median resize set

Why NDArray is clean, and the fix

NDArray already has the answer in the same package: ndArgc(args, min, max) (internal/vm/ndarray.go:70) raises MRI's ArgumentError with MRI's message, including the min..max and min+ forms. Special and Stats (#819) use it, which is why they report 0.

So the fix is one line per method, and internal/vm/fft.go is the smaller half: most of its methods are registered through two helpers (def and window), so a check in the helper covers the window functions at once.

A test that does not need the list

The census is cheap enough to BE the test, and better than a list because a method added later is covered without being named. TestSpecialArityIsAlwaysChecked and TestStatsArityIsAlwaysChecked (#819) do this for the two new modules:

Special.methods(false).each do |m|
  begin; Special.send(m)
  rescue => e; bad << m if e.message.include?("runtime error")
  end
end

They assert the number of methods walked as well as the empty result, so a census that reads nothing fails rather than passing. Dropping one arity check from special.go makes it name the exact 33 methods affected — verified.

The same shape would fit FFT and Image.

Severity

Not a crash: the panic is recovered and the VM survives. It is wrong behaviour — a Ruby program rescuing ArgumentError and reading e.message sees Go's internals, and a program relying on the message to tell a user what it passed wrongly cannot. It is also the kind of defect that hides the next one, since a genuine index fault inside a binding would read identically.

Measured 2026-10-12 with go1.27.2, CGO_ENABLED=0.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions