diff --git a/internal/vm/builtins.go b/internal/vm/builtins.go index c13243bd..7013d4e3 100644 --- a/internal/vm/builtins.go +++ b/internal/vm/builtins.go @@ -429,6 +429,18 @@ func (vm *VM) bootstrap() { if len(args) > 0 && !args[0].Truthy() { return vm.filterVisibility(self, vm.singletonMethodNames(self, false), keep) } + if _, isClass := self.(*RClass); isClass { + // MRI's rb_obj_methods lists the instance methods of CLASS_OF(obj) + // (object.c). For a class or module that is its SINGLETON class, whose + // ancestry runs through the singleton classes of its superclasses and only + // then reaches Class/Module/Object — so `C.methods` carries C's class + // methods and the inherited ones as well as Class's instance methods. + // classOf answers Class/Module for an RClass and cannot see the singleton + // chain, so the two halves are gathered separately and merged. + return vm.filterVisibility(self, mergeMethodNames( + vm.singletonMethodNames(self, true), + vm.methodNames(vm.classOf(self), true)), keep) + } c := vm.classOf(self) if o, ok := self.(*RObject); ok && o.singleton != nil { c = o.singleton // its super is the real class, so the walk picks up both @@ -6209,6 +6221,10 @@ func (vm *VM) bootstrap() { // Range edge methods (#reverse_each, #entries); runs after the prelude so the // range-specific definitions win over any inherited Enumerable ones. vm.registerRangeEdges() + // The Kernel global functions whose body forwards to ARGF / IO / Process: + // those objects must already exist, so this runs after them and before the + // module-function split that reflects their visibility. + vm.registerKernelDelegates() // Split the Kernel module functions into a private instance method + a public // Kernel-module method, as MRI does — runs last, once every listed method is // defined above. @@ -6275,12 +6291,14 @@ func (vm *VM) registerKernelPublicMethods() { func (vm *VM) registerKernelModuleFunctions() { names := []string{ "Array", "Complex", "Float", "Hash", "Integer", "Rational", "String", - "__dir__", "abort", "at_exit", "autoload", "autoload?", "caller", + "__dir__", "`", "abort", "at_exit", "autoload", "autoload?", "caller", "caller_locations", - "catch", "eval", "exec", "exit", "exit!", "fail", "fork", "format", "lambda", + "catch", "eval", "exec", "exit", "exit!", "fail", "fork", "format", "gets", + "global_variables", "lambda", "load", "loop", "open", "p", "print", "printf", "proc", "putc", "puts", - "raise", "rand", "require", "require_relative", "sleep", "sprintf", - "srand", "system", "throw", "trap", "warn", + "raise", "rand", "readline", "readlines", "require", "require_relative", + "select", "sleep", "spawn", "sprintf", + "srand", "syscall", "system", "test", "throw", "trap", "warn", } // Two names that share ONE underlying Object record (a genuine built-in alias // such as format/sprintf) must keep sharing after the mirror, or their mirrored @@ -9124,6 +9142,31 @@ func (vm *VM) methodNames(c *RClass, all bool) []object.Value { return out } +// mergeMethodNames concatenates two already-sorted Symbol name lists into one +// sorted, duplicate-free list. Both inputs come from methodNames / +// singletonMethodNames, which sort and de-duplicate within themselves; a name can +// still appear in both (a class method shadowing a Class instance method), and it +// must be listed once. +func mergeMethodNames(a, b []object.Value) []object.Value { + seen := make(map[object.Symbol]bool, len(a)+len(b)) + names := make([]string, 0, len(a)+len(b)) + for _, list := range [][]object.Value{a, b} { + for _, n := range list { + sym := n.(object.Symbol) + if !seen[sym] { + seen[sym] = true + names = append(names, string(sym)) + } + } + } + sort.Strings(names) + out := make([]object.Value, len(names)) + for i, n := range names { + out[i] = object.Symbol(n) + } + return out +} + // reflectMethodNames backs #public_methods / #private_methods / #protected_methods: // it lists the receiver's applicable method names filtered to the target // visibility want. The candidate set is the receiver's singleton (class) methods @@ -9727,6 +9770,13 @@ func nativeRaise(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Va []object.Value{object.NewString("")}, nil)))) default: exc := vm.raiseExceptionObject(args) + // A third argument is the backtrace to raise with: MRI's make_exception + // ends `if (argc == 3) set_backtrace(mesg, argv[2])` (eval.c), applying it + // before the raise — so captureBacktrace below finds one already stamped and + // leaves it alone, exactly as it does for a re-raise. + if len(args) >= 3 { + vm.applyRaiseBacktrace(exc, args[2]) + } // A cause: that reaches the exception being raised through its own cause // chain is a circular reference — MRI rejects it before linking. cause == exc // itself is not circular (it is simply not set); the walk starts one link in. @@ -9742,6 +9792,31 @@ func nativeRaise(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Va } } +// applyRaiseBacktrace stamps Kernel#raise's optional third argument onto the +// exception as its backtrace. MRI routes it through set_backtrace (eval.c), +// which calls the exception's own #set_backtrace — so a subclass override runs — +// and which also accepts Thread::Backtrace::Location values, converting them with +// rb_backtrace_to_str_ary when it cannot store the locations themselves. rbgo +// never stores locations: a backtrace is an Array of String and +// Exception#backtrace_locations re-derives Locations by parsing those strings +// back, so a Location element is converted here the same way. Because a +// Location's #to_s is the very line it was parsed from, the round trip is exact. +// Elements of any other non-String type are passed through untouched, so +// #set_backtrace raises MRI's TypeError for them. +func (vm *VM) applyRaiseBacktrace(exc, bt object.Value) { + if a, ok := bt.(*object.Array); ok { + conv := make([]object.Value, len(a.Elems)) + for i, e := range a.Elems { + conv[i] = e + if o, isObj := e.(*RObject); isObj && o.class == vm.backtraceLocationClass { + conv[i] = vm.send(e, "to_s", nil, nil) + } + } + bt = object.NewArrayFromSlice(conv) + } + vm.send(exc, "set_backtrace", []object.Value{bt}, nil) +} + // raiseExceptionObject builds the exception object for a raise from a non-empty // argument list, matching MRI's make_exception (eval.c). A lone String is // shorthand for a RuntimeError with that message. Otherwise the first argument's diff --git a/internal/vm/kernel_delegates_wave23_internal_test.go b/internal/vm/kernel_delegates_wave23_internal_test.go new file mode 100644 index 00000000..f383a0e3 --- /dev/null +++ b/internal/vm/kernel_delegates_wave23_internal_test.go @@ -0,0 +1,319 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package vm + +import ( + "runtime" + "testing" +) + +// TestKernelModuleFunctionSplitCoversTheDelegates covers the visibility half of +// registerKernelDelegates: every global function it installs must be listed by +// registerKernelModuleFunctions, so it is a PRIVATE instance method of Kernel +// and a PUBLIC method on the Kernel module — the split MRI gives anything +// declared with rb_define_global_function. Each expectation was read off ruby +// 4.0.5 (Kernel.private_instance_methods(false) / Kernel.public_methods(false)). +func TestKernelModuleFunctionSplitCoversTheDelegates(t *testing.T) { + for _, name := range []string{ + "`", "gets", "global_variables", "readline", "readlines", + "select", "spawn", "syscall", "test", + } { + src := `p [Kernel.private_instance_methods(false).include?(:"` + name + `"),` + + ` Kernel.public_methods(false).include?(:"` + name + `"),` + + ` Kernel.public_instance_methods(false).include?(:"` + name + `")]` + if got := eval(t, src); got != "[true, true, false]\n" { + t.Errorf("Kernel#%s visibility: got %q want %q", name, got, "[true, true, false]\n") + } + } +} + +// TestKernelGetsReadlineReadlinesForwardToARGF covers the ARGF-forwarding arm of +// registerKernelDelegates. MRI's rb_f_gets / rb_f_readline / rb_f_readlines +// (io.c) forward to ARGF by DISPATCH — `forward(argf, idGets, argc, argv)` — so +// a redefined ARGF.gets is what runs and the arguments reach it unchanged. The +// stubs keep the test off any real input stream. +func TestKernelGetsReadlineReadlinesForwardToARGF(t *testing.T) { + values := []struct{ src, want string }{ + {"def ARGF.gets(*a); \"g#{a.inspect}\"; end\np gets", `"g[]"`}, + {"def ARGF.gets(*a); \"g#{a.inspect}\"; end\np gets(\"x\", 3)", `"g[\"x\", 3]"`}, + {"def ARGF.readline(*a); \"r#{a.inspect}\"; end\np readline", `"r[]"`}, + {"def ARGF.readlines(*a); [a.size]; end\np readlines(\"s\")", "[1]"}, + } + for _, c := range values { + if got := eval(t, c.src); got != c.want+"\n" { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want+"\n") + } + } +} + +// TestKernelSelectForwardsToIOSelect covers the Kernel#select arm. MRI's +// rb_f_select (io.c) is IO.select under another name: nil when nothing is ready +// within the timeout, and the [readable, writable, exceptional] triple otherwise. +// rbgo reaches its single implementation by dispatch, so the two names cannot +// drift apart — the redefinition case below pins that property. +func TestKernelSelectForwardsToIOSelect(t *testing.T) { + values := []struct{ src, want string }{ + {`p select([], [], [], 0)`, "nil"}, + {`IO.pipe { |r, w| p select([r], [], [], 0) }`, "nil"}, + {"IO.pipe do |r, w|\n w.write \"data\"\n res = select([r], [], [], 0)\n p [res[0] == [r], res[1], res[2]]\nend", "[true, [], []]"}, + {"def IO.select(*a); [:stub, a.size]; end\np select(1, 2)", "[:stub, 2]"}, + } + for _, c := range values { + if got := eval(t, c.src); got != c.want+"\n" { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want+"\n") + } + } +} + +// TestKernelSpawnForwardsToProcessSpawn covers the Kernel#spawn arm. MRI declares +// spawn twice over one implementation (rb_f_spawn, process.c); rbgo reaches its +// single Process.spawn by dispatch, which is what the stub observes — no +// subprocess is started here. +func TestKernelSpawnForwardsToProcessSpawn(t *testing.T) { + src := "def Process.spawn(*a); [:spawned, a]; end\np spawn(\"cmd\", 7)" + if got := eval(t, src); got != "[:spawned, [\"cmd\", 7]]\n" { + t.Errorf("got %q", got) + } +} + +// TestKernelSyscallIsNotImplemented covers the Kernel#syscall arm. A CGO-free +// runtime has no portable syscall(2), which is the situation CRuby is in without +// HAVE_SYSCALL: io.c ends `#define rb_f_syscall rb_f_notimplement`. MRI 4.0.5 on +// darwin answers with exactly this class and message. +func TestKernelSyscallIsNotImplemented(t *testing.T) { + class, msg := evalErr(t, `syscall(1)`) + if class != "NotImplementedError" || msg != "syscall() function is unimplemented on this machine" { + t.Errorf("got %s: %q", class, msg) + } +} + +// TestKernelBackquoteMethod covers the Kernel#` arm. MRI registers it with arity +// 1 and StringValue (io.c rb_f_backquote), so a non-String converts with #to_str +// and anything else is a TypeError. It is reached here through #send because +// rbgo's parser does not accept a backquote as a method name after a dot; the +// backquote LITERAL compiles to OpXStr, which runs the same runShellCommand, so +// both spellings share one implementation. +func TestKernelBackquoteMethod(t *testing.T) { + if runtime.GOOS == "windows" || runtime.GOOS == "wasip1" || runtime.GOOS == "js" { + t.Skip("needs a POSIX shell to run `echo`") + } + if got := eval(t, "p send(:`, \"echo hi\")"); got != "\"hi\\n\"\n" { + t.Errorf("literal command: got %q", got) + } + toStr := "o = Object.new\ndef o.to_str\n \"echo two\"\nend\np Kernel.send(:`, o)" + if got := eval(t, toStr); got != "\"two\\n\"\n" { + t.Errorf("#to_str command: got %q", got) + } + if class, msg := evalErr(t, "send(:`, \"a\", \"b\")"); class != "ArgumentError" || + msg != "wrong number of arguments (given 2, expected 1)" { + t.Errorf("arity: got %s: %q", class, msg) + } + if class, msg := evalErr(t, "send(:`, 5)"); class != "TypeError" || + msg != "no implicit conversion of Integer into String" { + t.Errorf("non-String: got %s: %q", class, msg) + } +} + +// TestKernelGlobalVariables covers the Kernel#global_variables arm. +// rb_f_global_variables (variable.c) walks the global table and answers Symbols; +// the process/exception specials specialGvar serves out of VM state rather than +// the table ($!, $0, $$) are always defined, so they are listed even though the +// table holds no slot for them — that is the alwaysDefinedGvars branch. The list +// is sorted, so a freshly assigned global lands in order and the answer does not +// depend on Go's randomised map iteration. +func TestKernelGlobalVariables(t *testing.T) { + values := []struct{ src, want string }{ + {`p global_variables.class`, "Array"}, + {`p global_variables.first.class`, "Symbol"}, + {`p global_variables.include?(:$stdout)`, "true"}, + // The specials that live outside the table. + {`p [:$!, :$0, :"$$"].all? { |g| global_variables.include?(g) }`, "true"}, + // A newly assigned global appears; the count rises by exactly one. + {"before = global_variables\n$k23_fresh = 1\nafter = global_variables\np [before.include?(:$k23_fresh), after.include?(:$k23_fresh), after.size - before.size]", + "[false, true, 1]"}, + {`p global_variables == global_variables.sort`, "true"}, + } + for _, c := range values { + if got := eval(t, c.src); got != c.want+"\n" { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want+"\n") + } + } +} + +// TestKernelExitStatus covers exitStatusArg and raiseSystemExit. MRI's rb_f_exit +// (process.c) maps the status through exit_status_code — true is EXIT_SUCCESS, +// false is EXIT_FAILURE, anything else NUM2INT — and raises +// SystemExit.new(status, "exit"); exit! differs only in defaulting to +// EXIT_FAILURE. Every expectation below was read off ruby 4.0.5. +func TestKernelExitStatus(t *testing.T) { + probe := func(call string) string { + t.Helper() + return eval(t, "begin\n "+call+"\nrescue SystemExit => e\n p [e.status, e.message]\nend") + } + values := []struct{ call, want string }{ + {`exit`, `[0, "exit"]`}, + {`exit 0`, `[0, "exit"]`}, + {`exit 8`, `[8, "exit"]`}, + {`exit(-65536)`, `[-65536, "exit"]`}, + {`exit 65536`, `[65536, "exit"]`}, + {`exit true`, `[0, "exit"]`}, + {`exit false`, `[1, "exit"]`}, + // NUM2INT truncates a Float towards zero (through #to_int). + {`exit 5.5`, `[5, "exit"]`}, + {`exit(-2.2)`, `[-2, "exit"]`}, + {`exit 827.999`, `[827, "exit"]`}, + // exit! defaults to EXIT_FAILURE but takes the same status mapping. + {`exit!`, `[1, "exit"]`}, + {`exit! 7`, `[7, "exit"]`}, + {`exit! true`, `[0, "exit"]`}, + // Reachable as a public Kernel-module method too. + {`Kernel.exit(3)`, `[3, "exit"]`}, + {`Kernel.exit!`, `[1, "exit"]`}, + } + for _, c := range values { + if got := probe(c.call); got != c.want+"\n" { + t.Errorf("%s: got %q want %q", c.call, got, c.want+"\n") + } + } + // A status with no #to_int is a TypeError, and 2+ arguments an ArgumentError. + errs := []struct{ src, class, msg string }{ + {`exit Object.new`, "TypeError", "no implicit conversion of Object into Integer"}, + {`exit "0"`, "TypeError", "no implicit conversion of String into Integer"}, + {`exit [0]`, "TypeError", "no implicit conversion of Array into Integer"}, + {`exit 1, 2`, "ArgumentError", "wrong number of arguments (given 2, expected 0..1)"}, + {`exit! 1, 2`, "ArgumentError", "wrong number of arguments (given 2, expected 0..1)"}, + } + for _, c := range errs { + if class, msg := evalErr(t, c.src); class != c.class || msg != c.msg { + t.Errorf("%s: got %s: %q want %s: %q", c.src, class, msg, c.class, c.msg) + } + } + // A custom #to_int is honoured, as NUM2INT does. + got := eval(t, "o = Object.new\ndef o.to_int\n 5\nend\nbegin\n exit o\nrescue SystemExit => e\n p e.status\nend") + if got != "5\n" { + t.Errorf("#to_int status: got %q", got) + } +} + +// TestKernelAbortStatusAndMessage covers the abort arm. MRI's rb_f_abort +// (process.c) puts the message — through StringValue, so #to_str converts — on +// stderr and raises SystemExit.new(EXIT_FAILURE, message); with no argument the +// message is the plain "exit". Unlike exit, the status never comes from the +// argument. +func TestKernelAbortStatusAndMessage(t *testing.T) { + // eval's VM sends $stderr to the same buffer as $stdout (errOut defaults to + // out), so the message abort puts on stderr precedes the probe's own line — + // which is also how the message reaching stderr at all is asserted. + values := []struct{ src, want string }{ + {"begin\n abort\nrescue SystemExit => e\n p [e.status, e.message]\nend", `[1, "exit"]`}, + {"begin\n abort \"boom\"\nrescue SystemExit => e\n p [e.status, e.message]\nend", "boom\n" + `[1, "boom"]`}, + {"o = Object.new\ndef o.to_str\n \"strish\"\nend\nbegin\n abort o\nrescue SystemExit => e\n p [e.status, e.message]\nend", "strish\n" + `[1, "strish"]`}, + } + for _, c := range values { + if got := eval(t, c.src); got != c.want+"\n" { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want+"\n") + } + } + errs := []struct{ src, class, msg string }{ + {`abort 1`, "TypeError", "no implicit conversion of Integer into String"}, + {`abort "a", "b"`, "ArgumentError", "wrong number of arguments (given 2, expected 0..1)"}, + } + for _, c := range errs { + if class, msg := evalErr(t, c.src); class != c.class || msg != c.msg { + t.Errorf("%s: got %s: %q want %s: %q", c.src, class, msg, c.class, c.msg) + } + } +} + +// TestRaiseAppliesTheBacktraceArgument covers applyRaiseBacktrace and the +// len(args) >= 3 arm of nativeRaise. MRI's make_exception ends +// `if (argc == 3) set_backtrace(mesg, argv[2])` (eval.c): the value goes through +// the exception's own #set_backtrace, so a String becomes a one-element array, an +// Array of String is taken as-is, nil clears it, and a non-String, non-Location +// element is a TypeError. Thread::Backtrace::Location values are accepted too and +// converted to their #to_s lines, which is exactly what +// Exception#backtrace_locations parses back. +func TestRaiseAppliesTheBacktraceArgument(t *testing.T) { + values := []struct{ src, want string }{ + {"begin\n raise ArgumentError, \"m\", [\"line1\", \"line2\"]\nrescue => e\n p e.backtrace\nend", + `["line1", "line2"]`}, + {"begin\n raise ArgumentError, \"m\", \"single\"\nrescue => e\n p e.backtrace\nend", + `["single"]`}, + {"begin\n raise ArgumentError, \"m\", []\nrescue => e\n p e.backtrace\nend", `[]`}, + // A Location array round-trips through #to_s and back through + // #backtrace_locations. + {"locs = caller_locations(0, 2)\nbegin\n raise ArgumentError, \"m\", locs\nrescue => e\n p e.backtrace == locs.map(&:to_s)\n p e.backtrace_locations.map(&:to_s) == locs.map(&:to_s)\nend", + "true\ntrue"}, + // The backtrace argument coexists with the cause: keyword. + {"c = StandardError.new(\"cm\")\nbegin\n raise ArgumentError, \"m\", [\"l1\"], cause: c\nrescue => e\n p [e.backtrace, e.cause.message]\nend", + `[["l1"], "cm"]`}, + // nil clears the backtrace rather than stamping the current stack. + {"begin\n raise ArgumentError, \"m\", nil\nrescue => e\n p e.backtrace.nil?\nend", "false"}, + } + for _, c := range values { + if got := eval(t, c.src); got != c.want+"\n" { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want+"\n") + } + } + // A non-String, non-Location element is rejected by #set_backtrace, as MRI. + if class, msg := evalErr(t, `raise ArgumentError, "m", [1, 2]`); class != "TypeError" || + msg != "backtrace must be an Array of String or an Array of Thread::Backtrace::Location" { + t.Errorf("got %s: %q", class, msg) + } +} + +// TestClassMethodsListsTheSingletonChain covers the class/module arm of +// Kernel#methods and mergeMethodNames. rb_obj_methods lists the instance methods +// of CLASS_OF(obj) (object.c); for a class that is its SINGLETON class, whose +// ancestry runs through the singleton classes of its superclasses before reaching +// Class/Module/Object — so a class method, an inherited class method and Class's +// own instance methods are all listed, each exactly once. Verified against +// ruby 4.0.5. +func TestClassMethodsListsTheSingletonChain(t *testing.T) { + const defs = "class K23Base\n" + + " def self.inherited_cm; end\n" + + "end\n" + + "class K23Sub < K23Base\n" + + " def self.own_cm; end\n" + + " class << self\n" + + " def meta_cm; end\n" + + " private\n" + + " def priv_cm; end\n" + + " end\n" + + " def inst_m; end\n" + + "end\n" + values := []struct{ src, want string }{ + {defs + `p K23Sub.methods.include?(:own_cm)`, "true"}, + {defs + `p K23Sub.methods.include?(:meta_cm)`, "true"}, + {defs + `p K23Sub.methods.include?(:inherited_cm)`, "true"}, + // Class's own instance methods survive the merge. + {defs + `p K23Sub.methods.include?(:new)`, "true"}, + {defs + `p K23Sub.methods.include?(:instance_methods)`, "true"}, + // A private class method is excluded, and an instance method is not a + // method OF the class object. + {defs + `p K23Sub.methods.include?(:priv_cm)`, "false"}, + {defs + `p K23Sub.methods.include?(:inst_m)`, "false"}, + // Listed exactly once each, and sorted (the merge de-duplicates). + {defs + `p K23Sub.methods.count(:own_cm)`, "1"}, + {defs + `p K23Sub.methods.count(:new)`, "1"}, + {defs + `p K23Sub.methods == K23Sub.methods.uniq`, "true"}, + {defs + `p K23Sub.methods == K23Sub.methods.sort`, "true"}, + // methods(false) still reports only the class's OWN class methods. + {defs + `p K23Sub.methods(false).sort`, "[:meta_cm, :own_cm]"}, + // The intersection the ruby/spec example takes is no longer empty. + {defs + `p (K23Sub.methods(false) & K23Sub.methods).sort`, "[:meta_cm, :own_cm]"}, + // A module receiver takes the same path: its own class method and Module's + // public instance methods are listed, while Module#module_function — a + // PRIVATE instance method — is not. + {"module K23Mod\n def self.mod_cm; end\nend\np [K23Mod.methods.include?(:mod_cm), K23Mod.methods.include?(:instance_methods), K23Mod.methods.include?(:module_function)]", "[true, true, false]"}, + // A plain object is unaffected: its per-object singleton method is listed. + {"o = Object.new\ndef o.solo; end\np [o.methods.include?(:solo), o.methods.include?(:frozen?)]", "[true, true]"}, + } + for _, c := range values { + if got := eval(t, c.src); got != c.want+"\n" { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want+"\n") + } + } +} diff --git a/internal/vm/kernel_introspect.go b/internal/vm/kernel_introspect.go index fb3be226..df754ddf 100644 --- a/internal/vm/kernel_introspect.go +++ b/internal/vm/kernel_introspect.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "runtime" + "sort" "github.com/go-embedded-ruby/ruby/internal/object" ) @@ -189,33 +190,161 @@ func (vm *VM) registerKernelIntrospection() { return blk }) - // Kernel#exit raises SystemExit, which unwinds to the top (running at_exit - // handlers there). A bare `exit` and any status argument both unwind the same - // way here — the embedded host has no real process, so the status is not a - // process code, only the signal that the program asked to stop. exit! is the - // same minus at_exit semantics (modelled identically). abort prints an optional - // message to $stderr first. Puppet's exit_on_fail calls exit(code) after - // logging, so these let the real CLI terminate via SystemExit rather than a - // NoMethodError. - exitFn := func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { - return raise("SystemExit", "exit") - } - vm.cObject.define("exit", exitFn) - vm.cObject.define("exit!", exitFn) + // Kernel#exit(status = true) raises SystemExit, which unwinds to the top + // (running at_exit handlers there). MRI's rb_f_exit (process.c) accepts 0..1 + // arguments, maps the status through exit_status_code and raises + // SystemExit.new(status, "exit") — so the status the program asked to stop + // with survives to the rescuer. Puppet's exit_on_fail calls exit(code) after + // logging and then reads the code back off the SystemExit, so discarding it + // was a real loss, not just a spec gap. + // + // exit!(status = false) differs only in its default (EXIT_FAILURE) and in + // skipping at_exit handlers; the embedded host has no process to _exit() from, + // so it unwinds the same way. Both are also reachable as Kernel.exit / + // Kernel.exit! — registerKernelModuleFunctions mirrors the records. + vm.cObject.define("exit", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + return vm.raiseSystemExit(vm.exitStatusArg(args, 0), "exit") + }) + vm.cObject.define("exit!", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + return vm.raiseSystemExit(vm.exitStatusArg(args, 1), "exit") + }) + // abort(message = nil): MRI's rb_f_abort (process.c) puts the message — + // through StringValue, so a non-String converts with #to_str — on $stderr and + // then raises SystemExit.new(EXIT_FAILURE, message); with no argument it + // exits EXIT_FAILURE with the plain "exit" message. Unlike exit, the status is + // never taken from the argument. vm.cObject.define("abort", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { - if len(args) > 0 { - if s, ok := args[0].(*object.String); ok { - fmt.Fprintln(vm.errOut, s.Str()) - } + if len(args) > 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 0..1)", len(args)) } - return raise("SystemExit", "exit") + if len(args) == 1 { + msg := vm.coerceToString(args[0]) + fmt.Fprintln(vm.errOut, msg) + return vm.raiseSystemExit(1, msg) + } + return vm.raiseSystemExit(1, "exit") }) - if kernel, ok := vm.consts["Kernel"].(*RClass); ok { - kernel.smethods["exit"] = &Method{name: "exit", owner: kernel, native: exitFn} - kernel.smethods["exit!"] = &Method{name: "exit!", owner: kernel, native: exitFn} +} + +// exitStatusArg reads Kernel#exit / #exit!'s optional status argument, applying +// MRI's arity check (0..1) and exit_status_code mapping (process.c): true is +// EXIT_SUCCESS (0), false is EXIT_FAILURE (1), and anything else goes through +// NUM2INT — an Integer passes, a Float truncates through #to_int, and a String, +// nil or Array (none of which define #to_int) is a TypeError. dflt is the code +// for a bare call: 0 for exit, 1 for exit!. +func (vm *VM) exitStatusArg(args []object.Value, dflt int64) int64 { + if len(args) > 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 0..1)", len(args)) + } + if len(args) == 0 { + return dflt } + switch args[0] { + case object.Value(object.True): + return 0 + case object.Value(object.False): + return 1 + } + return vm.toIntCoerce(args[0]) +} + +// raiseSystemExit raises the SystemExit that Kernel#exit / #exit! / #abort stop +// the program with, built as MRI's rb_exit does it — +// SystemExit.new(status, message) — so #status reports the requested code and +// #message the requested text. It never returns; the object.Value result type +// only lets it be the tail expression of a native method body. +func (vm *VM) raiseSystemExit(status int64, message string) object.Value { + exc := vm.send(vm.consts["SystemExit"].(*RClass), "new", + []object.Value{object.IntValue(status), object.NewString(message)}, nil) + panic(vm.excError(vm.captureBacktrace(exc))) +} + +// registerKernelDelegates installs the Kernel global functions whose behaviour +// CRuby defines by forwarding to another object. MRI declares each with +// rb_define_global_function (io.c:15615-15628, process.c), which makes it a +// private instance method reachable without a receiver AND — through the +// Kernel module_function split registerKernelModuleFunctions applies — a public +// method on the Kernel module; the names are listed there so the split covers +// them. +// +// Every body here forwards through vm.send to the object CRuby forwards to, so +// a spec that stubs ARGF.gets (core/kernel/gets_spec) or replaces IO.select +// sees its replacement run, exactly as MRI's `forward(argf, idGets, …)` does. +func (vm *VM) registerKernelDelegates() { + // Kernel#gets / #readline / #readlines read the ARGV-concatenated input + // stream: rb_f_gets, rb_f_readline and rb_f_readlines (io.c) all + // `forward(argf, …)` when the receiver is not ARGF itself. + for _, name := range []string{"gets", "readline", "readlines"} { + meth := name + vm.cObject.define(meth, func(vm *VM, _ object.Value, args []object.Value, blk *Proc) object.Value { + return vm.send(vm.consts["ARGF"], meth, args, blk) + }) + } + + // Kernel#select is IO.select under another name (rb_f_select, io.c, shares + // select_call with rb_io_s_select). + vm.cObject.define("select", func(vm *VM, _ object.Value, args []object.Value, blk *Proc) object.Value { + return vm.send(vm.consts["IO"], "select", args, blk) + }) + + // Kernel#spawn is Process.spawn (rb_f_spawn, process.c). + vm.cObject.define("spawn", func(vm *VM, _ object.Value, args []object.Value, blk *Proc) object.Value { + return vm.send(vm.consts["Process"], "spawn", args, blk) + }) + + // Kernel#syscall invokes a system call by number. A pure-Go, CGO-free runtime + // has no portable way to do that, which is the situation CRuby itself is in + // on a platform without syscall(2): io.c ends with + // `#define rb_f_syscall rb_f_notimplement`, and rb_f_notimplement raises + // NotImplementedError with this message shape (error.c). MRI 4.0.5 on darwin + // answers exactly this. + vm.cObject.define("syscall", func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { + return raise("NotImplementedError", "syscall() function is unimplemented on this machine") + }) + + // Kernel#` (backquote): rb_f_backquote (io.c) takes exactly one argument, + // puts it through StringValue (so a non-String is converted with #to_str, and + // anything else is a TypeError) and returns the command's output. The command + // runs through the same runShellCommand the `%x{…}`/backtick *literal* uses + // (OpXStr), so the two spellings cannot drift apart. + vm.cObject.define("`", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) != 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 1)", len(args)) + } + return object.NewString(vm.runShellCommand(vm.coerceToString(args[0]))) + }) + + // Kernel#global_variables lists every defined global as a Symbol + // (rb_f_global_variables, variable.c, walks rb_global_tbl). The stored + // globals are this VM's table; the process/exception specials specialGvar + // answers out of VM state rather than the table ($!, $0, $$) are always + // defined, so they are listed too when the table has no slot for them. MRI's + // order is the global table's internal one and unspecified; sorting keeps the + // answer stable across calls (Go map iteration is randomised). + vm.cObject.define("global_variables", func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { + names := make([]string, 0, len(vm.globals)+len(alwaysDefinedGvars)) + for n := range vm.globals { + names = append(names, n) + } + for _, n := range alwaysDefinedGvars { + if _, ok := vm.globals[n]; !ok { + names = append(names, n) + } + } + sort.Strings(names) + out := make([]object.Value, len(names)) + for i, n := range names { + out[i] = object.Symbol(n) + } + return object.NewArrayFromSlice(out) + }) } +// alwaysDefinedGvars are the globals specialGvar answers from VM state instead +// of the vm.globals table, so they are defined even with no slot in it. Kernel# +// global_variables adds them to the table's names. +var alwaysDefinedGvars = []string{"$!", "$0", "$$"} + // currentFile returns the path of the file currently executing: the innermost // file being required, or the top-level script path ($0) when none is on the // require stack. It backs Kernel#__FILE__ / #__dir__.