Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions internal/vm/fft.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,54 +129,66 @@ func checkShape(n int, shape []int) {
}

// registerFFT installs the FFT module and its transform functions.
//
// Every method declares its arity AT the registration site, and def checks it
// before the body runs. That is deliberate rather than a check inside each
// closure: the bodies index args[0..3] directly, so a missing argument used to
// surface as "runtime error: index out of range" -- a recovered Go panic
// relabelled ArgumentError -- where MRI says "wrong number of arguments"
// (#820). Putting the counts in def's signature means a method added later
// cannot be registered without them.
func (vm *VM) registerFFT() {
mod := newClass("FFT", nil)
mod.isModule = true
vm.consts["FFT"] = mod

def := func(name string, fn NativeFn) {
mod.smethods[name] = &Method{name: name, owner: mod, native: fn}
def := func(name string, min, max int, fn NativeFn) {
mod.smethods[name] = &Method{name: name, owner: mod, native: func(vm *VM, self object.Value, args []object.Value, blk *Proc) object.Value {
ndArgc(args, min, max)
return fn(vm, self, args, blk)
}}
}

def("fft", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("fft", 1, 1, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return complexArray(gofft.FFT(complexSlice(args[0])))
})
def("ifft", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("ifft", 1, 1, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return complexArray(gofft.IFFT(complexSlice(args[0])))
})
def("rfft", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("rfft", 1, 1, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return complexArray(gofft.RFFT(floatSlice(args[0])))
})
def("irfft", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("irfft", 2, 2, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
n := int(intArg(args[1]))
return floatArray(gofft.IRFFT(complexSlice(args[0]), n))
})
def("fftfreq", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
// The bin-frequency helpers take numpy's optional sample spacing d.
def("fftfreq", 1, 2, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return floatArray(gofft.FFTFreq(int(intArg(args[0])), sampleSpacing(args, 1)))
})
def("rfftfreq", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("rfftfreq", 1, 2, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return floatArray(gofft.RFFTFreq(int(intArg(args[0])), sampleSpacing(args, 1)))
})

// N-dimensional transforms over flat row-major data plus an explicit shape.
def("fftn", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("fftn", 2, 2, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
data, shape := complexSlice(args[0]), intSlice(args[1])
checkShape(len(data), shape)
return complexArray(gofft.FFTN(data, shape))
})
def("ifftn", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("ifftn", 2, 2, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
data, shape := complexSlice(args[0]), intSlice(args[1])
checkShape(len(data), shape)
return complexArray(gofft.IFFTN(data, shape))
})
// 2-D transforms over flat row-major data of shape rows×cols.
def("fft2", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("fft2", 3, 3, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
data := complexSlice(args[0])
rows, cols := int(intArg(args[1])), int(intArg(args[2]))
checkShape(len(data), []int{rows, cols})
return complexArray(gofft.FFT2(data, [2]int{rows, cols}))
})
def("ifft2", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("ifft2", 3, 3, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
data := complexSlice(args[0])
rows, cols := int(intArg(args[1])), int(intArg(args[2]))
checkShape(len(data), []int{rows, cols})
Expand All @@ -185,7 +197,7 @@ func (vm *VM) registerFFT() {

// Window functions (numpy/scipy conventions): length n → []float64.
window := func(name string, f func(int) []float64) {
def(name, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def(name, 1, 1, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return floatArray(f(int(intArg(args[0]))))
})
}
Expand All @@ -197,10 +209,10 @@ func (vm *VM) registerFFT() {

// Spectral helpers: one-sided power spectral density and a windowed
// spectrogram (an array of per-segment PSD frames).
def("psd", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("psd", 1, 2, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return floatArray(gofft.PSD(floatSlice(args[0]), sampleSpacing(args, 1)))
})
def("spectrogram", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("spectrogram", 4, 5, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
signal := floatSlice(args[0])
segment, overlap := int(intArg(args[1])), int(intArg(args[2]))
win := floatSlice(args[3])
Expand Down
113 changes: 65 additions & 48 deletions internal/vm/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,50 +44,67 @@ func (vm *VM) registerImage() {
vm.cImage = newClass("Image", vm.cObject)
vm.consts["Image"] = vm.cImage

vm.cImage.smethods["new"] = &Method{name: "new", owner: vm.cImage,
native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
w, h := int(intArg(args[0])), int(intArg(args[1]))
if w <= 0 || h <= 0 {
raise("ArgumentError", "image dimensions must be positive")
}
return &Image{img: image.NewRGBA(image.Rect(0, 0, w, h))}
}}
vm.cImage.smethods["load"] = &Method{name: "load", owner: vm.cImage,
native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
img, err := goimg.Load(args[0].ToS())
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return &Image{img: img}
}}
// s registers a module method with its arity, for the same reason as d.
s := func(name string, min, max int, fn NativeFn) {
vm.cImage.smethods[name] = &Method{name: name, owner: vm.cImage,
native: func(vm *VM, self object.Value, args []object.Value, blk *Proc) object.Value {
ndArgc(args, min, max)
return fn(vm, self, args, blk)
}}
}

s("new", 2, 2, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
w, h := int(intArg(args[0])), int(intArg(args[1]))
if w <= 0 || h <= 0 {
raise("ArgumentError", "image dimensions must be positive")
}
return &Image{img: image.NewRGBA(image.Rect(0, 0, w, h))}
})
s("load", 1, 1, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
img, err := goimg.Load(args[0].ToS())
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return &Image{img: img}
})
// Image.decode(bytes) decodes an in-memory PNG/JPEG String — no filesystem,
// so it works in a browser (wasm) where the program is handed image bytes.
vm.cImage.smethods["decode"] = &Method{name: "decode", owner: vm.cImage,
native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
s, ok := args[0].(*object.String)
if !ok {
raise("TypeError", "Image.decode expects a String of image bytes")
}
img, err := goimg.Decode(bytes.NewReader(s.Bytes()))
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return &Image{img: img}
}}

d := func(name string, fn NativeFn) { vm.cImage.define(name, fn) }

d("width", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
s("decode", 1, 1, func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
s, ok := args[0].(*object.String)
if !ok {
raise("TypeError", "Image.decode expects a String of image bytes")
}
img, err := goimg.Decode(bytes.NewReader(s.Bytes()))
if err != nil {
raise("ArgumentError", "%s", err.Error())
}
return &Image{img: img}
})

// d registers an instance method WITH its arity. The counts live in the
// signature so a method added later cannot be registered without them:
// the bodies index args[0..3] directly, and a missing argument used to
// surface as "runtime error: index out of range" -- a recovered Go panic
// relabelled ArgumentError -- where MRI says "wrong number of
// arguments" (#820).
d := func(name string, min, max int, fn NativeFn) {
vm.cImage.define(name, func(vm *VM, self object.Value, args []object.Value, blk *Proc) object.Value {
ndArgc(args, min, max)
return fn(vm, self, args, blk)
})
}

d("width", 0, 0, func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.IntValue(int64(imgOf(v).Bounds().Dx()))
})
d("height", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
d("height", 0, 0, func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.IntValue(int64(imgOf(v).Bounds().Dy()))
})
d("get", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("get", 2, 2, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
c := imgOf(v).RGBAAt(int(intArg(args[0])), int(intArg(args[1])))
return object.NewArray(object.IntValue(int64(c.R)), object.IntValue(int64(c.G)), object.IntValue(int64(c.B)), object.IntValue(int64(c.A)))
})
d("set", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("set", 5, 6, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
a := uint8(255)
if len(args) > 5 {
a = uint8(intArg(args[5]))
Expand All @@ -96,7 +113,7 @@ func (vm *VM) registerImage() {
color.RGBA{uint8(intArg(args[2])), uint8(intArg(args[3])), uint8(intArg(args[4])), a})
return object.NilV
})
d("save", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("save", 1, 1, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
if err := goimg.Save(args[0].ToS(), imgOf(v)); err != nil {
raise("ArgumentError", "%s", err.Error())
}
Expand All @@ -105,7 +122,7 @@ func (vm *VM) registerImage() {
// to_png / to_jpeg encode to an in-memory byte String (browser-friendly: hand
// the bytes to JS for a canvas/Blob). Encoding to a buffer cannot fail.
encode := func(name string, format goimg.Format) {
d(name, func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
d(name, 0, 0, func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
var buf bytes.Buffer
_ = goimg.Encode(&buf, imgOf(v), format)
return object.NewStringBytes(buf.Bytes())
Expand All @@ -116,7 +133,7 @@ func (vm *VM) registerImage() {

// Operations with no error: image → image.
unary := func(name string, f func(image.Image) *image.RGBA) {
d(name, func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
d(name, 0, 0, func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return &Image{img: f(imgOf(v))}
})
}
Expand All @@ -139,7 +156,7 @@ func (vm *VM) registerImage() {

// Operations with a scalar parameter, no error.
scalar := func(name string, f func(image.Image, float64) *image.RGBA) {
d(name, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d(name, 1, 1, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
x, _ := toFloat(args[0])
return &Image{img: f(imgOf(v), x)}
})
Expand All @@ -148,38 +165,38 @@ func (vm *VM) registerImage() {
scalar("adjust_contrast", goimg.AdjustContrast)

// Operations that can fail.
d("gaussian_blur", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("gaussian_blur", 1, 1, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
s, _ := toFloat(args[0])
return mustImg(goimg.GaussianBlur(imgOf(v), s))
})
d("box_blur", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("box_blur", 1, 1, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
return mustImg(goimg.BoxBlur(imgOf(v), int(intArg(args[0]))))
})
d("median", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("median", 1, 1, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
return mustImg(goimg.Median(imgOf(v), int(intArg(args[0]))))
})
d("erode", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("erode", 1, 1, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
return mustImg(goimg.Erode(imgOf(v), int(intArg(args[0]))))
})
d("dilate", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("dilate", 1, 1, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
return mustImg(goimg.Dilate(imgOf(v), int(intArg(args[0]))))
})
d("canny", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("canny", 3, 3, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
sigma, _ := toFloat(args[0])
low, _ := toFloat(args[1])
high, _ := toFloat(args[2])
return mustImg(goimg.Canny(imgOf(v), sigma, low, high))
})
d("resize", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("resize", 2, 2, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
return mustImg(goimg.Resize(imgOf(v), int(intArg(args[0])), int(intArg(args[1])), goimg.Bilinear))
})
d("crop", func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
d("crop", 4, 4, func(_ *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
x, y := int(intArg(args[0])), int(intArg(args[1]))
w, h := int(intArg(args[2])), int(intArg(args[3]))
return mustImg(goimg.Crop(imgOf(v), image.Rect(x, y, x+w, y+h)))
})

d("inspect", func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
d("inspect", 0, 0, func(_ *VM, v object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NewString(v.(*Image).Inspect())
})
vm.cImage.define("to_s", vm.cImage.methods["inspect"].native)
Expand Down
105 changes: 105 additions & 0 deletions internal/vm/wave71_arity_image_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

//go:build !rbgo_core

package vm

import (
"strings"
"testing"
)

// The Image half of #820. Tagged !rbgo_core, as image.go and image_test.go
// are: Image is the first of the gems the core build leaves out, so under
// -tags rbgo_core the constant does not exist and a test naming it would fail
// for the wrong reason. The FFT half is untagged and runs under both.

// TestImageChecksItsArity is the census over both of Image's faces -- the
// module methods and the instance ones. It asserts the number WALKED (3 and
// 34) as well as the empty result, so a census that reads nothing fails
// instead of passing. #save is held out, because it writes a file; it is
// checked by name in TestImageArityMessages.
func TestImageChecksItsArity(t *testing.T) {
src := `def census(recv, names, skip)
bad = []
n = 0
names.sort.each do |m|
next if skip.include?(m.to_s)
n += 1
begin
recv.send(m)
rescue => e
bad << m if e.message.include?("runtime error")
end
end
[n, bad]
end
a, abad = census(Image, Image.methods(false), [])
b, bbad = census(Image.new(2, 2), Image.instance_methods(false), ["save"])
puts [a, b].join(" ")
puts (abad + bbad).join(" ")`
out := strings.SplitN(strings.TrimSpace(eval(t, src)), "\n", 2)
if out[0] != "3 34" {
t.Fatalf("walked %q, want \"3 34\" (Image module, Image instance) -- "+
"a census that does not walk proves nothing", out[0])
}
if len(out) > 1 && strings.TrimSpace(out[1]) != "" {
t.Errorf("these Image methods still leak a Go runtime fault when called with no arguments: %s", out[1])
}
}

// TestImageArityMessages pins MRI's wording for each shape Image has: fixed
// counts, the 5..6 range of #set's optional alpha, and the zero-argument form
// a caller trips by passing something.
func TestImageArityMessages(t *testing.T) {
for _, tc := range []struct{ expr, want string }{
{"Image.new", "wrong number of arguments (given 0, expected 2)"},
{"Image.load", "wrong number of arguments (given 0, expected 1)"},
{"Image.decode", "wrong number of arguments (given 0, expected 1)"},
{"Image.new(2, 2).get", "wrong number of arguments (given 0, expected 2)"},
{"Image.new(2, 2).set", "wrong number of arguments (given 0, expected 5..6)"},
{"Image.new(2, 2).canny", "wrong number of arguments (given 0, expected 3)"},
{"Image.new(2, 2).crop", "wrong number of arguments (given 0, expected 4)"},
{"Image.new(2, 2).resize", "wrong number of arguments (given 0, expected 2)"},
{"Image.new(2, 2).save", "wrong number of arguments (given 0, expected 1)"},
{"Image.new(2, 2).adjust_brightness", "wrong number of arguments (given 0, expected 1)"},
{"Image.new(2, 2).gaussian_blur", "wrong number of arguments (given 0, expected 1)"},
{"Image.new(2, 2).width(1)", "wrong number of arguments (given 1, expected 0)"},
{"Image.new(2, 2).grayscale(1)", "wrong number of arguments (given 1, expected 0)"},
{"Image.new(2, 2).to_png(1)", "wrong number of arguments (given 1, expected 0)"},
} {
t.Run(tc.expr, func(t *testing.T) {
src := "begin\n " + tc.expr + "\n puts \"NO ERROR\"\nrescue ArgumentError => e\n puts e.message\nend"
if got := strings.TrimSpace(eval(t, src)); got != tc.want {
t.Errorf("%s raised %q, want %q", tc.expr, got, tc.want)
}
})
}
}

// TestImageStillWorks is the control for wrapping every method. #to_s earns its
// own row: it is registered from #inspect's native function, so it silently
// inherits whatever wrapper #inspect got, and only comparing the two says the
// alias still works.
func TestImageStillWorks(t *testing.T) {
src := `i = Image.new(4, 4)
i.set(0, 0, 255, 0, 0)
p i.get(0, 0)
i.set(1, 1, 1, 2, 3, 128)
p i.get(1, 1)
p [i.width, i.height, i.grayscale.width, i.resize(2, 2).width, i.crop(0, 0, 2, 2).width]
p [i.adjust_brightness(1.0).width, i.gaussian_blur(0.5).width, i.canny(1.0, 0.1, 0.2).width]
p i.to_png.bytesize > 0
p i.to_s == i.inspect`
const want = "[255, 0, 0, 255]\n" +
"[1, 2, 3, 128]\n" +
"[4, 4, 4, 2, 2]\n" +
"[4, 4, 4]\n" +
"true\n" +
"true\n"
if got := eval(t, src); got != want {
t.Errorf("got\n%s\nwant\n%s", got, want)
}
}
Loading
Loading