diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index a85ddd4a..9e4e13a1 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -851,6 +851,21 @@ func (c *Compiler) compileCall(v *ast.Call) { patchSafe() return } + // A syntactic setter call (recv.foo = v, recv[i] = v) evaluates to the + // assigned value v, NOT the setter method's own return value — this is a Ruby + // language guarantee independent of what `foo=`/`[]=` returns. The assigned + // value is the last argument and sits on top of the stack here; stash a copy, + // run the send, drop its result, and leave the stashed value as the + // expression's value. (Index/attr setters never carry a block.) + if isSetterCall(v) && len(args) > 0 { + tmp := b.localSlot("") + b.emit(bytecode.OpSetLocal, tmp, 0) // stash the assigned value (SetLocal peeks, leaving it as the send arg) + sendFlags(b.emit(bytecode.OpSend, b.addName(v.Name), len(args))) + b.emit(bytecode.OpPop, 0, 0) // discard the setter's return value + b.emit(bytecode.OpGetLocal, tmp, 0) // leave the assigned value as the expression's value + patchSafe() + return + } at := sendFlags(b.emit(bytecode.OpSend, b.addName(v.Name), len(args))) if v.Block != nil { b.insns[at].C = c.compileBlock(v.Block) + 1 // C-1 indexes Children @@ -858,6 +873,23 @@ func (c *Compiler) compileCall(v *ast.Call) { patchSafe() } +// isSetterCall reports whether v is a syntactic attribute/index setter — +// `recv.name = value` (name ends in "=") or `recv[...] = value` (name "[]=") — +// as opposed to a comparison/operator method (==, !=, <=, >=, ===) or a +// receiver-less call. Such calls must evaluate to the assigned value, per Ruby. +func isSetterCall(v *ast.Call) bool { + // A setter has an explicit receiver and no block. + if v.Recv == nil || v.Block != nil { + return false + } + n := v.Name + switch n { + case "==", "!=", "<=", ">=", "===", "=~", "=": + return false + } + return n != "" && n[len(n)-1] == '=' +} + // extractBlockPass removes a `&block-pass` argument from args (wherever it sits — // the parser may place it before a trailing keyword hash, or after a bare `**` // anonymous kwsplat) and returns the remaining args plus its value node (nil when diff --git a/internal/vm/bag_test.go b/internal/vm/bag_test.go index c8f06b52..474e154b 100644 --- a/internal/vm/bag_test.go +++ b/internal/vm/bag_test.go @@ -26,6 +26,13 @@ func TestBag(t *testing.T) { // Heterogeneous comparable members. {`p Bag.new(["a", :a, 1, 1.5, true, nil]).distinct_size`, "6\n"}, {`p Bag.new([10 ** 30, 10 ** 30]).count(10 ** 30)`, "2\n"}, // Bignum keying + // Arbitrary objects are valid members, keyed by identity (an Array member + // is allowed; an absent object reads as 0/false). + {`p Bag.new([[1, 2]]).size`, "1\n"}, + {`b = Bag.new([1]); b.add([2]); p b.size`, "2\n"}, + {`p Bag.new([1]).count([2])`, "0\n"}, + {`p Bag.new([1]).include?([2])`, "false\n"}, + {`o = Object.new; b = Bag.new; b.add(o); b.add(o); p b.count(o)`, "2\n"}, // add / << (with multiplicity). {`b = Bag.new([1]); b.add(1); p b.count(1)`, "2\n"}, {`b = Bag.new; b << 1 << 1 << 2; p b`, "#2, 2=>1}>\n"}, @@ -114,12 +121,7 @@ func TestBagMostCommon(t *testing.T) { // block. func TestBagErrors(t *testing.T) { for _, c := range []struct{ src, want string }{ - {`Bag.new([[1, 2]])`, "TypeError"}, // Array member (non-comparable) - {`Bag.new({})`, "TypeError"}, // Hash is not enumerable here - {`Bag.new([1]).add([2])`, "TypeError"}, // non-comparable add - {`Bag.new([1]).remove([2])`, "TypeError"}, - {`Bag.new([1]).count([2])`, "TypeError"}, - {`Bag.new([1]).include?([2])`, "TypeError"}, + {`Bag.new({})`, "TypeError"}, // Hash is not enumerable here {`Bag.new([1]) | [2]`, "TypeError"}, // non-Bag operand {`Bag.new([1]) & 3`, "TypeError"}, // non-Bag operand {`Bag.new([1]).union(5)`, "TypeError"}, diff --git a/internal/vm/builtins.go b/internal/vm/builtins.go index fa3c8124..d2c5f348 100644 --- a/internal/vm/builtins.go +++ b/internal/vm/builtins.go @@ -112,6 +112,19 @@ func (vm *VM) bootstrap() { vm.cProc.define("lambda?", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { return object.Bool(self.(*Proc).isLambda) }) + // Proc#source_location returns [file, line] for where the block was written, + // or nil when no source is known (a synthesized/native Proc, or compiled-in + // code such as the prelude whose ISeq carries no File). The VM does not track + // per-instruction line numbers, so the line is reported as 0 — the array + // shape ([String, Integer]) is what callers (e.g. Puppet building a type URI) + // depend on. + vm.cProc.define("source_location", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + p := self.(*Proc) + if p.iseq == nil || p.iseq.File == "" { + return object.NilV + } + return &object.Array{Elems: []object.Value{object.NewString(p.iseq.File), object.Integer(0)}} + }) vm.cProc.define("curry", func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value { p := self.(*Proc) need := p.arityVal() @@ -387,26 +400,27 @@ func (vm *VM) bootstrap() { return object.Bool(!ok || s == 0) }) - vm.registerFile() // needs the exception hierarchy (Errno::ENOENT < StandardError) - vm.registerIO() // IO/StringIO + $stdout/$stderr/$stdin (needs IOError/EOFError) - vm.registerDir() // Dir (reuses the Errno module set up by registerFile) - vm.registerTmpdir() // Dir.tmpdir / Dir.mktmpdir (layers onto Dir; require "tmpdir") - vm.registerProcess() // Process module — identity + clock_gettime - vm.registerOpenSSL() // OpenSSL (real digest/HMAC/random + PKI/TLS shell); needs StandardError - vm.registerNetHTTP() // net/http + net/https loadable shell; needs StandardError - vm.registerResolv() // Resolv (real IPv4/IPv6 parse; DNS sockets stubbed); needs StandardError - vm.registerTimeout() // Timeout module (loadable shell); needs RuntimeError - vm.registerYAML() // YAML/Psych loadable shell; needs StandardError - vm.registerFileUtils() // FileUtils (real fs ops over os); needs Errno (registerFile) - vm.registerGetoptLong() // GetoptLong loadable shell; needs StandardError - vm.registerOptParse() // optparse loadable shell (declares; parse raises); needs StandardError - vm.registerRipper() // ripper loadable shell (Ripper.sexp etc. raise); needs StandardError - vm.registerSyslog() // Syslog loadable shell (feature probe) - vm.registerCGI() // CGI.escape/unescape (real over net/url) + HTML helpers - vm.registerMonitor() // Monitor/MonitorMixin (single-thread synchronize); needs StandardError - vm.registerZlib() // needs the exception hierarchy (Zlib::Error < StandardError) - vm.registerFiber() // needs the exception hierarchy (FiberError < StandardError) - vm.registerThread() // needs StandardError/StopIteration (ThreadError, ClosedQueueError) + vm.registerFile() // needs the exception hierarchy (Errno::ENOENT < StandardError) + vm.registerIO() // IO/StringIO + $stdout/$stderr/$stdin (needs IOError/EOFError) + vm.registerDir() // Dir (reuses the Errno module set up by registerFile) + vm.registerTmpdir() // Dir.tmpdir / Dir.mktmpdir (layers onto Dir; require "tmpdir") + vm.registerProcess() // Process module — identity + clock_gettime + vm.registerObjectSpace() // ObjectSpace module — finalizer API + reflective no-ops + vm.registerOpenSSL() // OpenSSL (real digest/HMAC/random + PKI/TLS shell); needs StandardError + vm.registerNetHTTP() // net/http + net/https loadable shell; needs StandardError + vm.registerResolv() // Resolv (real IPv4/IPv6 parse; DNS sockets stubbed); needs StandardError + vm.registerTimeout() // Timeout module (loadable shell); needs RuntimeError + vm.registerYAML() // YAML/Psych loadable shell; needs StandardError + vm.registerFileUtils() // FileUtils (real fs ops over os); needs Errno (registerFile) + vm.registerGetoptLong() // GetoptLong loadable shell; needs StandardError + vm.registerOptParse() // optparse loadable shell (declares; parse raises); needs StandardError + vm.registerRipper() // ripper loadable shell (Ripper.sexp etc. raise); needs StandardError + vm.registerSyslog() // Syslog loadable shell (feature probe) + vm.registerCGI() // CGI.escape/unescape (real over net/url) + HTML helpers + vm.registerMonitor() // Monitor/MonitorMixin (single-thread synchronize); needs StandardError + vm.registerZlib() // needs the exception hierarchy (Zlib::Error < StandardError) + vm.registerFiber() // needs the exception hierarchy (FiberError < StandardError) + vm.registerThread() // needs StandardError/StopIteration (ThreadError, ClosedQueueError) // Exception instance protocol: initialize stores @message; message/to_s // return it (or the class name when unset). @@ -1601,6 +1615,38 @@ func (vm *VM) bootstrap() { } vm.cArray.define("unshift", unshift) vm.cArray.define("prepend", unshift) + // Array#insert(index, *objects): insert the objects before the element at + // index (or, for a negative index, after the element index counts back to — + // so -1 appends). Inserting past the end pads the gap with nil, as in MRI. + // With no objects the array is returned unchanged. + vm.cArray.define("insert", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value { + a := self.(*object.Array) + if len(args) == 0 { + raise("ArgumentError", "wrong number of arguments (given 0, expected 1+)") + } + idx := int(intArg(args[0])) + ins := args[1:] + if len(ins) == 0 { + return a + } + // A negative index inserts after the element it points at: pos = len+idx+1. + if idx < 0 { + idx += len(a.Elems) + 1 + if idx < 0 { + raise("IndexError", "index %d too small for array; minimum: %d", int(intArg(args[0])), -len(a.Elems)-1) + } + } + if idx > len(a.Elems) { + pad := make([]object.Value, idx-len(a.Elems)) + for i := range pad { + pad[i] = object.NilV + } + a.Elems = append(a.Elems, pad...) + } + tail := append([]object.Value{}, a.Elems[idx:]...) + a.Elems = append(append(a.Elems[:idx:idx], ins...), tail...) + return a + }) vm.cArray.define("delete", func(vm *VM, self object.Value, args []object.Value, blk *Proc) object.Value { // Remove every element == the argument; return it, or (a block's result, // else nil) when nothing matched. @@ -3375,6 +3421,22 @@ func (vm *VM) bootstrap() { } return c }} + // Module.new { body } builds an anonymous module (no superclass, like every + // module), running the block as its body — so `def self.foo` adds a singleton + // method and `def foo` an instance method that mixes into includers. Without + // this dedicated handler, `Module.new` would fall through to Class#new and + // allocate a plain instance (dropping the block), so the result could not be + // included or have methods defined into it. + vm.cModule.smethods["new"] = &Method{name: "new", owner: vm.cModule, + native: func(vm *VM, _ object.Value, _ []object.Value, blk *Proc) object.Value { + m := newClass("", nil) + m.isModule = true + m.defaultVis, m.funcMode = visPublic, false + if blk != nil { + vm.classEval(m, blk, nil) + } + return m + }} vm.cClass.define("superclass", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { if c := self.(*RClass); c.super != nil { return c.super @@ -4341,15 +4403,45 @@ func (vm *VM) singletonMethodNames(self object.Value, all bool) []object.Value { } } } + // collectMeta gathers the class methods a class/module receives from modules + // mixed into its singleton class via `extend` (and those modules' own + // prepends/includes) — the enumeration counterpart of lookupSMethod's walk of + // c.meta.{prepends,includes}. + var collectTree func(m *RClass) + collectTree = func(m *RClass) { + for i := len(m.prepends) - 1; i >= 0; i-- { + collectTree(m.prepends[i]) + } + collect(m.methods) + for i := len(m.includes) - 1; i >= 0; i-- { + collectTree(m.includes[i]) + } + } + collectMeta := func(c *RClass) { + if c.meta == nil { + return + } + for i := len(c.meta.prepends) - 1; i >= 0; i-- { + collectTree(c.meta.prepends[i]) + } + for i := len(c.meta.includes) - 1; i >= 0; i-- { + collectTree(c.meta.includes[i]) + } + } if c, ok := self.(*RClass); ok { collect(c.smethods) + collectMeta(c) if all { for s := c.super; s != nil; s = s.super { collect(s.smethods) + collectMeta(s) } } } else if sc := vm.objSingleton(self); sc != nil { collect(sc.methods) + for i := len(sc.includes) - 1; i >= 0; i-- { + collectTree(sc.includes[i]) + } } sort.Strings(names) out := make([]object.Value, len(names)) diff --git a/internal/vm/concurrent.go b/internal/vm/concurrent.go index b4b208df..f2d46aa8 100644 --- a/internal/vm/concurrent.go +++ b/internal/vm/concurrent.go @@ -36,19 +36,39 @@ func (vm *VM) registerConcurrent() { vm.send(o, "initialize", args, blk) return o }} - tlv.define("initialize", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value { - var def object.Value = object.NilV + // initialize(default = nil) { default_block }: store either an eager default + // value or a lazy default block (called on first read per the gem's API). + // "@set" tracks whether a value has been written yet, so a nil default still + // triggers the block exactly once. + tlv.define("initialize", func(_ *VM, self object.Value, args []object.Value, blk *Proc) object.Value { if len(args) > 0 { - def = args[0] + setIvar(self, "@value", args[0]) + setIvar(self, "@set", object.True) + } else if blk != nil { + setIvar(self, "@default_block", blk) + setIvar(self, "@set", object.False) + } else { + setIvar(self, "@value", object.NilV) + setIvar(self, "@set", object.True) } - setIvar(self, "@value", def) return object.NilV }) - tlv.define("value", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + tlv.define("value", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + if !getIvar(self, "@set").Truthy() { + // First read with an unset value and a default block: evaluate the block, + // memoise the result, and clear the lazy flag (the block runs once). + if blk, ok := getIvar(self, "@default_block").(*Proc); ok { + v := vm.callBlock(blk, nil) + setIvar(self, "@value", v) + setIvar(self, "@set", object.True) + return v + } + } return getIvar(self, "@value") }) tlv.define("value=", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value { setIvar(self, "@value", args[0]) + setIvar(self, "@set", object.True) return args[0] }) } diff --git a/internal/vm/file.go b/internal/vm/file.go index 4b989d27..366622c3 100644 --- a/internal/vm/file.go +++ b/internal/vm/file.go @@ -2,7 +2,8 @@ package vm import ( "os" - "path" // always '/'-separated, as Ruby's File is — not path/filepath + "path" // always '/'-separated, as Ruby's File is — not path/filepath + "path/filepath" // OS-native, only for symlink resolution (File.realpath) "strings" gotime "github.com/go-composites/time/src" @@ -107,6 +108,25 @@ func (vm *VM) registerFile() { fi, err := os.Stat(pathArg(vm, args[0])) return object.Bool(err == nil && fi.IsDir()) }) + // File.symlink? reports whether the path is a symbolic link. Like MRI it uses + // lstat (does not follow the link) and returns false for a missing path or a + // non-symlink, rather than raising. + def("symlink?", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + fi, err := os.Lstat(pathArg(vm, args[0])) + return object.Bool(err == nil && fi.Mode()&os.ModeSymlink != 0) + }) + // File.realpath returns the canonical absolute path with every symlink + // resolved; the path (and each component) must exist, otherwise — as in MRI — + // Errno::ENOENT is raised. An optional second argument is the base directory + // a relative path is resolved against. + def("realpath", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + p := fileExpand(pathArg(vm, args[0]), args[1:]) + resolved, err := filepath.EvalSymlinks(p) + if err != nil { + raise("Errno::ENOENT", "No such file or directory @ realpath_rec - %s", p) + } + return object.NewString(toSlash(resolved)) + }) def("read", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { p := pathArg(vm, args[0]) b, err := os.ReadFile(p) @@ -185,6 +205,14 @@ func fileJoin(parts []string) string { return b.String() } +// isAbsPath reports whether p is absolute, recognising both the forward-slash +// rooted form ("/x") and — on Windows — a drive-letter root ("C:/x"). rbgo keeps +// paths forward-slashed internally, so path.IsAbs alone would treat a Windows +// absolute path as relative and wrongly prepend the working directory. +func isAbsPath(p string) bool { + return path.IsAbs(p) || filepath.IsAbs(filepath.FromSlash(p)) +} + // fileExpand implements File.expand_path: ~ expands to the home directory, a // relative path is resolved against the optional base (default: the working // directory), and the result is cleaned (so .. and . collapse). @@ -194,7 +222,7 @@ func fileExpand(p string, rest []object.Value) string { p = toSlash(home) + strings.TrimPrefix(p, "~") } } - if path.IsAbs(p) { + if isAbsPath(p) { return path.Clean(p) } base := "" diff --git a/internal/vm/file_test.go b/internal/vm/file_test.go index ce9800d3..dbd5e1d1 100644 --- a/internal/vm/file_test.go +++ b/internal/vm/file_test.go @@ -2,6 +2,7 @@ package vm_test import ( "fmt" + "os" "path/filepath" "strings" "testing" @@ -13,16 +14,16 @@ func TestFilePath(t *testing.T) { {`p File.basename("/a/b/c.txt")`, "\"c.txt\"\n"}, {`p File.basename("/a/b/c.txt", ".txt")`, "\"c\"\n"}, {`p File.basename("/a/b/c.txt", ".*")`, "\"c\"\n"}, - {`p File.basename("foo", ".*")`, "\"foo\"\n"}, // no extension to strip - {`p File.basename(".txt", ".txt")`, "\".txt\"\n"}, // would empty -> kept + {`p File.basename("foo", ".*")`, "\"foo\"\n"}, // no extension to strip + {`p File.basename(".txt", ".txt")`, "\".txt\"\n"}, // would empty -> kept {`p File.basename("foo")`, "\"foo\"\n"}, {`p File.dirname("/a/b/c.txt")`, "\"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/a/b\"\n"}, {`p File.dirname("foo")`, "\".\"\n"}, {`p File.dirname("/")`, "\"/\"\n"}, {`p File.extname("/a/b/c.txt")`, "\".txt\"\n"}, {`p File.extname("foo")`, "\"\"\n"}, - {`p File.extname(".bashrc")`, "\"\"\n"}, // leading-dot dotfile - {`p File.extname("a.")`, "\".\"\n"}, // trailing dot + {`p File.extname(".bashrc")`, "\"\"\n"}, // leading-dot dotfile + {`p File.extname("a.")`, "\".\"\n"}, // trailing dot {`p File.extname("a.b.c")`, "\".c\"\n"}, {`p File.join("a", "b", "c")`, "\"a/b/c\"\n"}, {`p File.join("a/", "/b")`, "\"a/b\"\n"}, // both slashes -> collapse @@ -87,4 +88,27 @@ func TestFileIO(t *testing.T) { t.Errorf("src=%q got %v want Errno::ENOENT", src, err) } } + // realpath resolves an existing path to its canonical absolute form; a missing + // path raises Errno::ENOENT. (EvalSymlinks canonicalises, e.g. /var -> /private/var + // on darwin, so assert the basename rather than a fixed prefix.) + if got := eval(t, fmt.Sprintf(`File.write(%q, "x"); p File.basename(File.realpath(%q))`, a, a)); got != "\"a.txt\"\n" { + t.Errorf("realpath basename got=%q", got) + } + if err := runErr(t, fmt.Sprintf(`File.realpath(%q)`, dir+"/missing_rp.txt")); err == nil || !strings.Contains(err.Error(), "Errno::ENOENT") { + t.Errorf("realpath missing got %v want Errno::ENOENT", err) + } + // symlink? distinguishes a symlink from a regular file and a missing path. The + // link is created via Go; on platforms where that is not permitted, skip. + link := dir + "/link.txt" + if err := os.Symlink(a, link); err == nil { + if got := eval(t, fmt.Sprintf(`p File.symlink?(%q)`, link)); got != "true\n" { + t.Errorf("symlink? on link got=%q want true", got) + } + if got := eval(t, fmt.Sprintf(`p File.symlink?(%q)`, a)); got != "false\n" { + t.Errorf("symlink? on regular file got=%q want false", got) + } + } + if got := eval(t, fmt.Sprintf(`p File.symlink?(%q)`, dir+"/missing_sl.txt")); got != "false\n" { + t.Errorf("symlink? on missing got=%q want false", got) + } } diff --git a/internal/vm/object_model.go b/internal/vm/object_model.go index e8fd2338..da325442 100644 --- a/internal/vm/object_model.go +++ b/internal/vm/object_model.go @@ -554,6 +554,11 @@ func (vm *VM) classOf(v object.Value) *RClass { case *RObject: return x.class case *RClass: + // A module's class is Module; a class's class is Class. (Matches MRI: + // `String.class == Class`, `Comparable.class == Module`.) + if x.isModule { + return vm.cModule + } return vm.cClass case object.Integer: return vm.cInteger diff --git a/internal/vm/objectspace.go b/internal/vm/objectspace.go new file mode 100644 index 00000000..f72e2a1e --- /dev/null +++ b/internal/vm/objectspace.go @@ -0,0 +1,59 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package vm + +import "github.com/go-embedded-ruby/ruby/internal/object" + +// registerObjectSpace installs a minimal ObjectSpace module. The VM relies on +// Go's garbage collector and does not expose the live object set, so the +// reflective walkers (each_object / count_objects) report empty and the +// collection trigger is a no-op. The finalizer API is provided because library +// code (e.g. concurrent-ruby's ThreadLocalVar) registers finalizers +// unconditionally at construction; finalizers never actually run (Go reclaims +// memory itself), but define_finalizer must accept the call and return MRI's +// [0, callable] shape so construction proceeds. +func (vm *VM) registerObjectSpace() { + mod := newClass("ObjectSpace", nil) + mod.isModule = true + vm.consts["ObjectSpace"] = mod + def := func(name string, fn NativeFn) { mod.smethods[name] = &Method{name: name, owner: mod, native: fn} } + + // define_finalizer(obj, callable = block): MRI returns [object_id_flag, callable]. + // We return [0, callable]; the finalizer is recorded nowhere since the Go GC + // owns reclamation. + def("define_finalizer", func(vm *VM, _ object.Value, args []object.Value, blk *Proc) object.Value { + if len(args) == 0 { + raise("ArgumentError", "wrong number of arguments (given 0, expected 1..2)") + } + var callable object.Value = object.NilV + if len(args) >= 2 { + callable = args[1] + } else if blk != nil { + callable = blk + } + return &object.Array{Elems: []object.Value{object.Integer(0), callable}} + }) + // undefine_finalizer(obj): a no-op that returns the object, as in MRI. + def("undefine_finalizer", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) == 0 { + raise("ArgumentError", "wrong number of arguments (given 0, expected 1)") + } + return args[0] + }) + // each_object([class]) yields nothing (the live set is not exposed) and + // returns 0 — the count of objects iterated. Without a block it would return + // an Enumerator in MRI; here it simply reports 0. + def("each_object", func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { + return object.Integer(0) + }) + // garbage_collect / start: trigger a collection — a no-op, returning nil. + gc := func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { return object.NilV } + def("garbage_collect", gc) + def("start", gc) + // count_objects returns an empty Hash (the live set is not tracked). + def("count_objects", func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { + return object.NewHash() + }) +} diff --git a/internal/vm/prelude_frozen_gen.go b/internal/vm/prelude_frozen_gen.go index ec11c0d0..028fc173 100644 --- a/internal/vm/prelude_frozen_gen.go +++ b/internal/vm/prelude_frozen_gen.go @@ -219,7 +219,7 @@ func embeddedPrelude() *bytecode.ISeq { Children: []*bytecode.ISeq{ &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 52}, {Op: 28, A: 5}, {Op: 11}, {Op: 48, A: 1}, {Op: 26, A: 6}, {Op: 11}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 33}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 25}, {Op: 28, A: 28}, {Op: 5}, {Op: 33, A: 2}, {Op: 1}, {Op: 1, A: 1}, {Op: 30, A: 3}, {Op: 13}, {Op: 11, A: 1}, {Op: 30, A: 4, Flags: 1}, {Op: 30, A: 3}, {Op: 13}, {Op: 1, A: 2}, {Op: 30, A: 3}, {Op: 13}, {Op: 30, A: 5, B: 2}, {Op: 26, A: 29}, {Op: 2}, {Op: 9}, {Op: 11, A: 1}, {Op: 30, A: 6, Flags: 1}, {Op: 1, A: 3}, {Op: 22}, {Op: 25}, {Op: 28, A: 51}, {Op: 5}, {Op: 33, A: 7}, {Op: 1, A: 4}, {Op: 1, A: 5}, {Op: 30, A: 3}, {Op: 13}, {Op: 11, A: 1}, {Op: 30, A: 6, Flags: 1}, {Op: 30, A: 3}, {Op: 13}, {Op: 1, A: 6}, {Op: 30, A: 3}, {Op: 13}, {Op: 30, A: 5, B: 2}, {Op: 26, A: 52}, {Op: 2}, {Op: 9}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 1, A: 7}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 1, A: 8}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 30, A: 9, B: 2, Flags: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 52}, {Op: 28, A: 5}, {Op: 11}, {Op: 48, A: 1}, {Op: 26, A: 6}, {Op: 11}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 33}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 25}, {Op: 28, A: 28}, {Op: 5}, {Op: 33, A: 2}, {Op: 1}, {Op: 1, A: 1}, {Op: 30, A: 3}, {Op: 13}, {Op: 11, A: 1}, {Op: 30, A: 4, Flags: 1}, {Op: 30, A: 3}, {Op: 13}, {Op: 1, A: 2}, {Op: 30, A: 3}, {Op: 13}, {Op: 30, A: 5, B: 2}, {Op: 26, A: 29}, {Op: 2}, {Op: 9}, {Op: 11, A: 1}, {Op: 30, A: 6, Flags: 1}, {Op: 1, A: 3}, {Op: 22}, {Op: 25}, {Op: 28, A: 51}, {Op: 5}, {Op: 33, A: 7}, {Op: 1, A: 4}, {Op: 1, A: 5}, {Op: 30, A: 3}, {Op: 13}, {Op: 11, A: 1}, {Op: 30, A: 6, Flags: 1}, {Op: 30, A: 3}, {Op: 13}, {Op: 1, A: 6}, {Op: 30, A: 3}, {Op: 13}, {Op: 30, A: 5, B: 2}, {Op: 26, A: 52}, {Op: 2}, {Op: 9}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 1, A: 7}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 1, A: 8}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 12, A: 2}, {Op: 30, A: 9, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 53}}, Consts: []object.Value{ object.NewString(""), object.NewString("wrong element type "), @@ -237,8 +237,8 @@ func embeddedPrelude() *bytecode.ISeq { SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 2, - Locals: []string{"x", "pair"}, + NumLocals: 3, + Locals: []string{"x", "pair", ""}, Super: "", }, }, @@ -976,15 +976,15 @@ func embeddedPrelude() *bytecode.ISeq { Children: []*bytecode.ISeq{ &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 11}, {Op: 48, A: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 30, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 13}, {Op: 9}, {Op: 6}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 11}, {Op: 30, A: 2, B: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11}, {Op: 48, A: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 30, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 13}, {Op: 9}, {Op: 6}, {Op: 12, A: 2}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 11}, {Op: 30, A: 2, B: 1}, {Op: 53}}, Names: []string{"[]", "[]=", "<<"}, Params: []string{"x"}, NumRequired: 1, SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 2, - Locals: []string{"x", "k"}, + NumLocals: 3, + Locals: []string{"x", "k", ""}, Super: "", }, }, @@ -1003,7 +1003,7 @@ func embeddedPrelude() *bytecode.ISeq { Children: []*bytecode.ISeq{ &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 11, B: 1}, {Op: 11}, {Op: 11, B: 1}, {Op: 11}, {Op: 30, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 9}, {Op: 9}, {Op: 1}, {Op: 1, A: 1}, {Op: 13}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11, B: 1}, {Op: 11}, {Op: 11, B: 1}, {Op: 11}, {Op: 30, B: 1, Flags: 1}, {Op: 10}, {Op: 27, A: 9}, {Op: 9}, {Op: 1}, {Op: 1, A: 1}, {Op: 13}, {Op: 12, A: 1}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 53}}, Consts: []object.Value{ object.Integer(0), object.Integer(1), @@ -1014,8 +1014,8 @@ func embeddedPrelude() *bytecode.ISeq { SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 1, - Locals: []string{"x"}, + NumLocals: 2, + Locals: []string{"x", ""}, Super: "", }, }, @@ -1690,7 +1690,7 @@ func embeddedPrelude() *bytecode.ISeq { }, &bytecode.ISeq{ Name: "bump", - Insns: []bytecode.Instr{{Op: 5}, {Op: 30}, {Op: 30, A: 1, Flags: 1}, {Op: 12}, {Op: 9}, {Op: 11}, {Op: 30, A: 2, Flags: 1}, {Op: 25}, {Op: 10}, {Op: 28, A: 16}, {Op: 9}, {Op: 11}, {Op: 30, A: 3, Flags: 1}, {Op: 33, A: 4}, {Op: 30, A: 5, B: 1, Flags: 1}, {Op: 25}, {Op: 28, A: 21}, {Op: 11}, {Op: 30, A: 6, Flags: 1}, {Op: 9}, {Op: 26, A: 5}, {Op: 2}, {Op: 9}, {Op: 11}, {Op: 30, A: 7, Flags: 1}, {Op: 1}, {Op: 19}, {Op: 28, A: 31}, {Op: 11}, {Op: 30, A: 6, Flags: 1}, {Op: 26, A: 32}, {Op: 2}, {Op: 9}, {Op: 11}, {Op: 1, A: 1}, {Op: 11}, {Op: 1, A: 1}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 1}, {Op: 13}, {Op: 30, A: 9, B: 2, Flags: 1}, {Op: 9}, {Op: 5}, {Op: 30, A: 10}, {Op: 11}, {Op: 1, A: 2}, {Op: 30, A: 11, B: 1, Flags: 1}, {Op: 30, A: 12, B: 1, Flags: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 5}, {Op: 30}, {Op: 30, A: 1, Flags: 1}, {Op: 12}, {Op: 9}, {Op: 11}, {Op: 30, A: 2, Flags: 1}, {Op: 25}, {Op: 10}, {Op: 28, A: 16}, {Op: 9}, {Op: 11}, {Op: 30, A: 3, Flags: 1}, {Op: 33, A: 4}, {Op: 30, A: 5, B: 1, Flags: 1}, {Op: 25}, {Op: 28, A: 21}, {Op: 11}, {Op: 30, A: 6, Flags: 1}, {Op: 9}, {Op: 26, A: 5}, {Op: 2}, {Op: 9}, {Op: 11}, {Op: 30, A: 7, Flags: 1}, {Op: 1}, {Op: 19}, {Op: 28, A: 31}, {Op: 11}, {Op: 30, A: 6, Flags: 1}, {Op: 26, A: 32}, {Op: 2}, {Op: 9}, {Op: 11}, {Op: 1, A: 1}, {Op: 11}, {Op: 1, A: 1}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 1}, {Op: 13}, {Op: 12, A: 1}, {Op: 30, A: 9, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 9}, {Op: 5}, {Op: 30, A: 10}, {Op: 11}, {Op: 1, A: 2}, {Op: 30, A: 11, B: 1, Flags: 1}, {Op: 30, A: 12, B: 1, Flags: 1}, {Op: 53}}, Consts: []object.Value{ object.Integer(1), object.Integer(-1), @@ -1701,8 +1701,8 @@ func embeddedPrelude() *bytecode.ISeq { SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 1, - Locals: []string{"segs"}, + NumLocals: 2, + Locals: []string{"segs", ""}, Super: "", }, &bytecode.ISeq{ @@ -2116,15 +2116,15 @@ func embeddedPrelude() *bytecode.ISeq { Children: []*bytecode.ISeq{ &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 31}, {Op: 11}, {Op: 30, A: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 30, A: 2, B: 2, Flags: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 31}, {Op: 11}, {Op: 30, A: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 12, A: 2}, {Op: 30, A: 2, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 53}}, Names: []string{"@table", "to_sym", "[]="}, Params: []string{"k", "v"}, NumRequired: 2, SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 2, - Locals: []string{"k", "v"}, + NumLocals: 3, + Locals: []string{"k", "v", ""}, Super: "", }, }, @@ -2145,15 +2145,15 @@ func embeddedPrelude() *bytecode.ISeq { }, &bytecode.ISeq{ Name: "[]=", - Insns: []bytecode.Instr{{Op: 31}, {Op: 11}, {Op: 30, A: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 30, A: 2, B: 2, Flags: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 31}, {Op: 11}, {Op: 30, A: 1, Flags: 1}, {Op: 11, A: 1}, {Op: 12, A: 2}, {Op: 30, A: 2, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 53}}, Names: []string{"@table", "to_sym", "[]="}, Params: []string{"name", "value"}, NumRequired: 2, SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 2, - Locals: []string{"name", "value"}, + NumLocals: 3, + Locals: []string{"name", "value", ""}, Super: "", }, &bytecode.ISeq{ @@ -2224,7 +2224,7 @@ func embeddedPrelude() *bytecode.ISeq { }, &bytecode.ISeq{ Name: "method_missing", - Insns: []bytecode.Instr{{Op: 11}, {Op: 30, Flags: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 11, A: 2}, {Op: 1}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 28, A: 43}, {Op: 11, A: 1}, {Op: 30, A: 2, Flags: 1}, {Op: 1, A: 1}, {Op: 22}, {Op: 25}, {Op: 28, A: 29}, {Op: 5}, {Op: 33, A: 3}, {Op: 1, A: 2}, {Op: 1, A: 3}, {Op: 30}, {Op: 13}, {Op: 11, A: 1}, {Op: 30, A: 2, Flags: 1}, {Op: 30}, {Op: 13}, {Op: 1, A: 4}, {Op: 30}, {Op: 13}, {Op: 30, A: 4, B: 2}, {Op: 26, A: 30}, {Op: 2}, {Op: 9}, {Op: 31, A: 5}, {Op: 11, A: 2}, {Op: 1, A: 5}, {Op: 1, A: 6}, {Op: 8}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 30, A: 7, Flags: 1}, {Op: 11, A: 1}, {Op: 1, A: 5}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 30, A: 8, B: 2, Flags: 1}, {Op: 26, A: 51}, {Op: 11, A: 1}, {Op: 30, A: 9, Flags: 1}, {Op: 28, A: 50}, {Op: 31, A: 5}, {Op: 11}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 26, A: 51}, {Op: 47, B: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11}, {Op: 30, Flags: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 11, A: 2}, {Op: 1}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 28, A: 46}, {Op: 11, A: 1}, {Op: 30, A: 2, Flags: 1}, {Op: 1, A: 1}, {Op: 22}, {Op: 25}, {Op: 28, A: 29}, {Op: 5}, {Op: 33, A: 3}, {Op: 1, A: 2}, {Op: 1, A: 3}, {Op: 30}, {Op: 13}, {Op: 11, A: 1}, {Op: 30, A: 2, Flags: 1}, {Op: 30}, {Op: 13}, {Op: 1, A: 4}, {Op: 30}, {Op: 13}, {Op: 30, A: 4, B: 2}, {Op: 26, A: 30}, {Op: 2}, {Op: 9}, {Op: 31, A: 5}, {Op: 11, A: 2}, {Op: 1, A: 5}, {Op: 1, A: 6}, {Op: 8}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 30, A: 7, Flags: 1}, {Op: 11, A: 1}, {Op: 1, A: 5}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 12, A: 3}, {Op: 30, A: 8, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 3}, {Op: 26, A: 54}, {Op: 11, A: 1}, {Op: 30, A: 9, Flags: 1}, {Op: 28, A: 53}, {Op: 31, A: 5}, {Op: 11}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 26, A: 54}, {Op: 47, B: 1}, {Op: 53}}, Consts: []object.Value{ object.NewString("="), object.Integer(1), @@ -2240,8 +2240,8 @@ func embeddedPrelude() *bytecode.ISeq { SplatIndex: 1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 3, - Locals: []string{"name", "args", "n"}, + NumLocals: 4, + Locals: []string{"name", "args", "n", ""}, Super: "", }, &bytecode.ISeq{ @@ -3492,28 +3492,28 @@ func embeddedPrelude() *bytecode.ISeq { Children: []*bytecode.ISeq{ &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 11, A: 2, B: 1}, {Op: 11}, {Op: 30, Flags: 1}, {Op: 11, A: 1}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11, A: 2, B: 1}, {Op: 11}, {Op: 30, Flags: 1}, {Op: 11, A: 1}, {Op: 12, A: 2}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 53}}, Names: []string{"to_sym", "[]="}, Params: []string{"k", "v"}, NumRequired: 2, SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 2, - Locals: []string{"k", "v"}, + NumLocals: 3, + Locals: []string{"k", "v", ""}, Super: "", }, &bytecode.ISeq{ Name: "", - Insns: []bytecode.Instr{{Op: 11, A: 2, B: 1}, {Op: 11}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 30, B: 1, Flags: 1}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11, A: 2, B: 1}, {Op: 11}, {Op: 11, B: 1}, {Op: 11, A: 1}, {Op: 30, B: 1, Flags: 1}, {Op: 12, A: 2}, {Op: 30, A: 1, B: 2, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 53}}, Names: []string{"[]", "[]="}, Params: []string{"k", "i"}, NumRequired: 2, SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 2, - Locals: []string{"k", "i"}, + NumLocals: 3, + Locals: []string{"k", "i", ""}, Super: "", }, }, @@ -3521,7 +3521,7 @@ func embeddedPrelude() *bytecode.ISeq { }, &bytecode.ISeq{ Name: "merge", - Insns: []bytecode.Instr{{Op: 11}, {Op: 33}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 25}, {Op: 28, A: 11}, {Op: 33, A: 2}, {Op: 11}, {Op: 30, A: 3, Flags: 1}, {Op: 30, A: 4, B: 1, Flags: 1}, {Op: 12}, {Op: 26, A: 12}, {Op: 2}, {Op: 9}, {Op: 11}, {Op: 30, A: 5, Flags: 1}, {Op: 28, A: 19}, {Op: 11}, {Op: 53}, {Op: 26, A: 20}, {Op: 2}, {Op: 9}, {Op: 33, A: 2}, {Op: 5}, {Op: 30, A: 3}, {Op: 30, A: 4, B: 1, Flags: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 11}, {Op: 30, A: 6, Flags: 1}, {Op: 30, A: 7, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 11}, {Op: 30, A: 8, Flags: 1}, {Op: 30, A: 9, B: 1, Flags: 1}, {Op: 9}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 30, A: 11, Flags: 1}, {Op: 10}, {Op: 27, A: 46}, {Op: 9}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 30, A: 12, Flags: 1}, {Op: 28, A: 50}, {Op: 11, A: 1}, {Op: 53}, {Op: 26, A: 82}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 1}, {Op: 30, A: 13, B: 1, Flags: 1}, {Op: 28, A: 60}, {Op: 11, A: 1}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 30, A: 14, B: 1, Flags: 1}, {Op: 26, A: 82}, {Op: 31, A: 15}, {Op: 30, A: 12, Flags: 1}, {Op: 28, A: 65}, {Op: 1, A: 1}, {Op: 26, A: 66}, {Op: 31, A: 15}, {Op: 12, A: 2}, {Op: 9}, {Op: 11, A: 2}, {Op: 68, A: 16, B: 17}, {Op: 1, A: 2}, {Op: 30, A: 18, B: 2, Flags: 1}, {Op: 12, A: 3}, {Op: 9}, {Op: 11, A: 1}, {Op: 33, A: 2}, {Op: 11, A: 3}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 13}, {Op: 30, A: 19, B: 1, Flags: 1}, {Op: 30, A: 14, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 11}, {Op: 33}, {Op: 30, A: 1, B: 1, Flags: 1}, {Op: 25}, {Op: 28, A: 11}, {Op: 33, A: 2}, {Op: 11}, {Op: 30, A: 3, Flags: 1}, {Op: 30, A: 4, B: 1, Flags: 1}, {Op: 12}, {Op: 26, A: 12}, {Op: 2}, {Op: 9}, {Op: 11}, {Op: 30, A: 5, Flags: 1}, {Op: 28, A: 19}, {Op: 11}, {Op: 53}, {Op: 26, A: 20}, {Op: 2}, {Op: 9}, {Op: 33, A: 2}, {Op: 5}, {Op: 30, A: 3}, {Op: 30, A: 4, B: 1, Flags: 1}, {Op: 12, A: 1}, {Op: 9}, {Op: 11, A: 1}, {Op: 11}, {Op: 30, A: 6, Flags: 1}, {Op: 12, A: 2}, {Op: 30, A: 7, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 2}, {Op: 9}, {Op: 11, A: 1}, {Op: 11}, {Op: 30, A: 8, Flags: 1}, {Op: 12, A: 3}, {Op: 30, A: 9, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 3}, {Op: 9}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 30, A: 11, Flags: 1}, {Op: 10}, {Op: 27, A: 52}, {Op: 9}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 30, A: 12, Flags: 1}, {Op: 28, A: 56}, {Op: 11, A: 1}, {Op: 53}, {Op: 26, A: 94}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 1}, {Op: 30, A: 13, B: 1, Flags: 1}, {Op: 28, A: 69}, {Op: 11, A: 1}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 12, A: 4}, {Op: 30, A: 14, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 4}, {Op: 26, A: 94}, {Op: 31, A: 15}, {Op: 30, A: 12, Flags: 1}, {Op: 28, A: 74}, {Op: 1, A: 1}, {Op: 26, A: 75}, {Op: 31, A: 15}, {Op: 12, A: 5}, {Op: 9}, {Op: 11, A: 5}, {Op: 68, A: 16, B: 17}, {Op: 1, A: 2}, {Op: 30, A: 18, B: 2, Flags: 1}, {Op: 12, A: 6}, {Op: 9}, {Op: 11, A: 1}, {Op: 33, A: 2}, {Op: 11, A: 6}, {Op: 11}, {Op: 30, A: 10, Flags: 1}, {Op: 13}, {Op: 30, A: 19, B: 1, Flags: 1}, {Op: 12, A: 7}, {Op: 30, A: 14, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 7}, {Op: 9}, {Op: 11, A: 1}, {Op: 53}}, Consts: []object.Value{ object.NewString("/"), object.NewString("/"), @@ -3533,8 +3533,8 @@ func embeddedPrelude() *bytecode.ISeq { SplatIndex: -1, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 4, - Locals: []string{"rel", "merged", "base", "dir"}, + NumLocals: 8, + Locals: []string{"rel", "merged", "", "", "", "base", "dir", ""}, Super: "", }, }, @@ -4529,7 +4529,7 @@ func embeddedPrelude() *bytecode.ISeq { }, &bytecode.ISeq{ Name: "initialize", - Insns: []bytecode.Instr{{Op: 63}, {Op: 27, A: 5}, {Op: 2}, {Op: 12, A: 1}, {Op: 9}, {Op: 63, A: 1}, {Op: 27, A: 10}, {Op: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 2}, {Op: 32}, {Op: 9}, {Op: 1, A: 1}, {Op: 32, A: 1}, {Op: 9}, {Op: 33, A: 2}, {Op: 11, A: 1}, {Op: 30, A: 3, B: 1, Flags: 1}, {Op: 12, A: 3}, {Op: 9}, {Op: 11, A: 3}, {Op: 11, A: 2}, {Op: 30, A: 4, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 3}, {Op: 1, A: 2}, {Op: 1, A: 3}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 2}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 4}, {Op: 30, A: 5}, {Op: 13}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 3}, {Op: 1, A: 5}, {Op: 1, A: 6}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 2}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 7}, {Op: 30, A: 5}, {Op: 13}, {Op: 30, A: 7, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 3}, {Op: 11}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 60}, {Op: 10}, {Op: 59, A: 2}, {Op: 12, A: 4}, {Op: 9}, {Op: 12, A: 5}, {Op: 9}, {Op: 9}, {Op: 1, A: 8}, {Op: 32, A: 9}, {Op: 9}, {Op: 1, A: 9}, {Op: 1, A: 10}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 4}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 11}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 5}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 12}, {Op: 30, A: 5}, {Op: 13}, {Op: 32, A: 10}, {Op: 53}}, + Insns: []bytecode.Instr{{Op: 63}, {Op: 27, A: 5}, {Op: 2}, {Op: 12, A: 1}, {Op: 9}, {Op: 63, A: 1}, {Op: 27, A: 10}, {Op: 1}, {Op: 12, A: 2}, {Op: 9}, {Op: 2}, {Op: 32}, {Op: 9}, {Op: 1, A: 1}, {Op: 32, A: 1}, {Op: 9}, {Op: 33, A: 2}, {Op: 11, A: 1}, {Op: 30, A: 3, B: 1, Flags: 1}, {Op: 12, A: 3}, {Op: 9}, {Op: 11, A: 3}, {Op: 11, A: 2}, {Op: 12, A: 4}, {Op: 30, A: 4, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 4}, {Op: 9}, {Op: 11, A: 3}, {Op: 1, A: 2}, {Op: 1, A: 3}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 2}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 4}, {Op: 30, A: 5}, {Op: 13}, {Op: 12, A: 5}, {Op: 30, A: 6, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 5}, {Op: 9}, {Op: 11, A: 3}, {Op: 1, A: 5}, {Op: 1, A: 6}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 2}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 7}, {Op: 30, A: 5}, {Op: 13}, {Op: 12, A: 6}, {Op: 30, A: 7, B: 1, Flags: 1}, {Op: 9}, {Op: 11, A: 6}, {Op: 9}, {Op: 11, A: 3}, {Op: 11}, {Op: 30, A: 8, B: 1, Flags: 1}, {Op: 60}, {Op: 10}, {Op: 59, A: 2}, {Op: 12, A: 7}, {Op: 9}, {Op: 12, A: 8}, {Op: 9}, {Op: 9}, {Op: 1, A: 8}, {Op: 32, A: 9}, {Op: 9}, {Op: 1, A: 9}, {Op: 1, A: 10}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 7}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 11}, {Op: 30, A: 5}, {Op: 13}, {Op: 11, A: 8}, {Op: 30, A: 5}, {Op: 13}, {Op: 1, A: 12}, {Op: 30, A: 5}, {Op: 13}, {Op: 32, A: 10}, {Op: 53}}, Consts: []object.Value{ object.NewString("_erbout"), object.Integer(0), @@ -4553,8 +4553,8 @@ func embeddedPrelude() *bytecode.ISeq { KwRequired: []bool{false, false}, KwRestSlot: -1, BlockSlot: -1, - NumLocals: 6, - Locals: []string{"str", "trim_mode", "eoutvar", "compiler", "enc", "code"}, + NumLocals: 9, + Locals: []string{"str", "trim_mode", "eoutvar", "compiler", "", "", "", "enc", "code"}, Super: "", }, &bytecode.ISeq{ diff --git a/internal/vm/puppet_apply_primitives_test.go b/internal/vm/puppet_apply_primitives_test.go new file mode 100644 index 00000000..4b052219 --- /dev/null +++ b/internal/vm/puppet_apply_primitives_test.go @@ -0,0 +1,148 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package vm_test + +import ( + "strings" + "testing" +) + +// TestPuppetApplyPrimitives covers the small primitive fixes that together let +// `Puppet::Parser::Compiler#compile` evaluate a trivial manifest: each case was +// reduced from the apply trace to a minimal rbgo-vs-MRI snippet and is asserted +// against MRI Ruby 4.0.5. +func TestPuppetApplyPrimitives(t *testing.T) { + cases := []struct{ src, want string }{ + // Module.new builds a real module: def self.x is a singleton method, the + // result is includable, and methods define into it. + {`m = Module.new { def self.f; 7; end }; p m.f`, "7\n"}, + {`m = Module.new { def g; 5; end }; c = Class.new { include m }; p c.new.g`, "5\n"}, + {`p Module.new.class`, "Module\n"}, + {`p Module.new.is_a?(Class)`, "false\n"}, + {`module Foo; end; p Foo.class`, "Module\n"}, + {`p String.class`, "Class\n"}, + + // extend mixes the module's whole include tree (transitive) as class methods. + {`module I; def desc(x); @d=x; end; end +module O; include I; end +class T; extend O; end +T.desc("hi"); p T.instance_variable_get(:@d)`, "\"hi\"\n"}, + // public_methods / singleton_methods enumerate those extended methods. + {`module I2; def m2; end; end +module O2; include I2; end +class T2; extend O2; end +p T2.public_methods.include?(:m2)`, "true\n"}, + // An object that extends a module reports that module's methods in + // #singleton_methods (the object-singleton include path). + {`module M3; def sm; end; end; o = Object.new; o.extend(M3); p o.singleton_methods.include?(:sm)`, "true\n"}, + // extend a module that prepends another: the prepended module's methods are + // enumerated too (the prepend walk in the singleton-method collector). + {`module P4; def pm; end; end +module O4; prepend P4; end +class T4; extend O4; end +p T4.singleton_methods.include?(:pm)`, "true\n"}, + // A subclass inherits class methods contributed by a module its SUPERCLASS + // extended (and that module's prepends), via the super-chain meta walk. + {`module P5; def pm5; end; end +module O5; prepend P5; def om5; end; end +class Base5; extend O5; end +class Sub5 < Base5; end +p [Sub5.singleton_methods.include?(:om5), Sub5.singleton_methods.include?(:pm5)]`, "[true, true]\n"}, + + // SystemExit#status defaults to 0; with @status unset (allocate skips + // initialize) it falls back to 0 rather than nil. + {`p SystemExit.new.status`, "0\n"}, + {`p SystemExit.allocate.status`, "0\n"}, + + // A class method contributed via `class << SomeClass; prepend Mod; end` + // (the metaclass-prepend path) appears in #singleton_methods. + {`module Pre6; def pp6; end; end; class K6; end +class << K6; prepend Pre6; end +p K6.singleton_methods.include?(:pp6)`, "true\n"}, + + // public/private/protected_method_defined?. + {`class A; def a; end; private def b; end; protected def c; end; end +p [A.public_method_defined?(:a), A.private_method_defined?(:b), A.protected_method_defined?(:c), A.public_method_defined?(:zz)]`, + "[true, true, true, false]\n"}, + + // Proc#source_location: nil for a Proc compiled without a source file. + {`p proc { 1 }.source_location`, "nil\n"}, + + // Array#insert (positive, negative, end-padding, multi-value). + {`a=[1,2,3]; a.insert(1,:x); p a`, "[1, :x, 2, 3]\n"}, + {`a=[1,2,3]; a.insert(-1,:y); p a`, "[1, 2, 3, :y]\n"}, + {`a=[1,2,3]; a.insert(-2,:z); p a`, "[1, 2, :z, 3]\n"}, + {`a=[]; a.insert(2,:g); p a`, "[nil, nil, :g]\n"}, + {`a=[1,2]; a.insert(1,:a,:b); p a`, "[1, :a, :b, 2]\n"}, + {`a=[1,2,3]; p a.insert(1).equal?(a)`, "true\n"}, // no values: returns self unchanged + + // MatchData#[] Range and (start, length) slice the group array. + {`m = /(\w+)(\d+)/.match("abc123"); p m[1..]`, "[\"abc12\", \"3\"]\n"}, + {`m = /(\w+)(\d+)/.match("abc123"); p m[1, 1]`, "[\"abc12\"]\n"}, + {`m = /(\w+)(\d+)/.match("abc123"); p m[1]`, "\"abc12\"\n"}, // integer still works + {`m = /(\w+)(\d+)/.match("abc123"); p m[5..]`, "nil\n"}, // Range out of bounds -> nil + {`m = /(\w+)(\d+)/.match("abc123"); p m[5, 1]`, "nil\n"}, // start past end -> nil + {`m = /(\w+)(\d+)/.match("abc123"); p m[-1, 1]`, "[\"3\"]\n"}, // negative start + {`m = /(\w+)(\d+)/.match("abc123"); p m[0, 9]`, "[\"abc123\", \"abc12\", \"3\"]\n"}, // length clamps to end + {`m = /(\w+)(\d+)/.match("abc123"); p m[1, -1]`, "nil\n"}, // negative length -> nil + + // ObjectSpace: finalizer API + reflective no-ops. + {`p ObjectSpace.define_finalizer(Object.new) { |i| }`, "[0, #]\n"}, + {`pr = proc { }; p ObjectSpace.define_finalizer(Object.new, pr)[1].equal?(pr)`, "true\n"}, // explicit callable + {`o = Object.new; p ObjectSpace.undefine_finalizer(o).equal?(o)`, "true\n"}, + {`p ObjectSpace.each_object`, "0\n"}, + {`p ObjectSpace.garbage_collect`, "nil\n"}, + {`p ObjectSpace.count_objects`, "{}\n"}, + {`require "fiber"; require "objspace"; p :ok`, ":ok\n"}, + + // Concurrent::ThreadLocalVar honours a default block lazily. + {`require "concurrent"; t = Concurrent::ThreadLocalVar.new { [9] }; p t.value`, "[9]\n"}, + {`require "concurrent"; t = Concurrent::ThreadLocalVar.new(5); p t.value`, "5\n"}, + {`require "concurrent"; t = Concurrent::ThreadLocalVar.new; p t.value`, "nil\n"}, + {`require "concurrent"; t = Concurrent::ThreadLocalVar.new { [9] }; t.value = [1]; p t.value`, "[1]\n"}, + + // A syntactic setter expression evaluates to the assigned value, not the + // setter method's return value (attribute and index forms). + {`class C; def foo=(v); @v=v; :ignored; end; end; p(C.new.foo = 7)`, "7\n"}, + {`class D; def []=(k,v); @h=v; :ignored; end; end; p(D.new[1] = 99)`, "99\n"}, + {`class E; attr_accessor :x; def set; self.x = 3; end; end; p E.new.set`, "3\n"}, + // An explicitly-written operator method (=='s name ends in '=') is NOT a + // setter and returns the method's own value. + {`p 1.==(2)`, "false\n"}, + {`p "a".=~(/b/)`, "nil\n"}, + {`o = Object.new; def o.x=(v); 42; end; o.x = 9; p o.send(:x=, 9)`, "42\n"}, // explicit send keeps method return + + // A Set holds arbitrary objects keyed by identity (the apply path adds + // Puppet resources to a Set). + {`require "set"; s = Set.new; s << Object.new << Object.new; p s.size`, "2\n"}, + {`require "set"; o = Object.new; s = Set.new; s << o << o; p s.size`, "1\n"}, + } + for _, c := range cases { + if got := eval(t, c.src); got != c.want { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want) + } + } + // Proc#source_location reports [file, 0] when the block was written in a file + // with a known path (here the script path the VM was given). + if got, err := runScript(t, `p proc { }.source_location`, "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/scripts/main.rb"); err != nil || got != "[\"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/scripts/main.rb\", 0]\n" { + t.Errorf("Proc#source_location got=%q err=%v", got, err) + } + // ObjectSpace.start aliases garbage_collect; both no-ops. + if got := eval(t, `p ObjectSpace.start`); got != "nil\n" { + t.Errorf("ObjectSpace.start got=%q", got) + } + // The finalizer methods require an object argument; Array#insert requires at + // least an index, and rejects an index more negative than -(len+1). + for _, c := range []struct{ src, want string }{ + {`ObjectSpace.define_finalizer`, "ArgumentError"}, + {`ObjectSpace.undefine_finalizer`, "ArgumentError"}, + {`[1].insert`, "ArgumentError"}, + {`[1, 2].insert(-5, :x)`, "IndexError"}, + } { + if err := runErr(t, c.src); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("src=%q got %v want %s", c.src, err, c.want) + } + } +} diff --git a/internal/vm/reflection.go b/internal/vm/reflection.go index 675822c6..5199f6ba 100644 --- a/internal/vm/reflection.go +++ b/internal/vm/reflection.go @@ -46,6 +46,25 @@ func (vm *VM) registerReflection() { return object.Bool(m != nil && !m.undefined) }) + // Module#public_method_defined?/#private_method_defined?/#protected_method_defined?: + // like #method_defined? but additionally require the resolved method's + // effective access level (honouring any inherited visibility override) to + // match. Used by Puppet's metaprogramming to probe accessor visibility. + definedWithVis := func(want visibility) NativeFn { + return func(vm *VM, self object.Value, args []object.Value, _ *Proc) object.Value { + mod := self.(*RClass) + name := nameArg(args[0]) + m := lookupMethod(mod, name) + if m == nil || m.undefined { + return object.False + } + return object.Bool(instanceVisibility(mod, name, m) == want) + } + } + vm.cModule.define("public_method_defined?", definedWithVis(visPublic)) + vm.cModule.define("private_method_defined?", definedWithVis(visPrivate)) + vm.cModule.define("protected_method_defined?", definedWithVis(visProtected)) + cUnbound.define("name", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { return object.Symbol(self.(*UnboundMethod).name) }) diff --git a/internal/vm/regexp.go b/internal/vm/regexp.go index 9089817a..19f91b3d 100644 --- a/internal/vm/regexp.go +++ b/internal/vm/regexp.go @@ -1010,7 +1010,43 @@ func (vm *VM) installRegexp() { return h }) vm.cMatchData.define("[]", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value { - return mdArg(self).at(args[0]) + m := mdArg(self) + // The Range and (start, length) forms slice the array of all groups + // (whole match at 0, then each capture) — md[1..] is how callers grab just + // the captures, as Puppet's parse_title does. + groups := func() []object.Value { + out := make([]object.Value, 0, m.md.NGroups()+1) + for i := 0; i <= m.md.NGroups(); i++ { + out = append(out, groupValue(m, i)) + } + return out + } + if rng, ok := args[0].(*object.Range); ok { + all := groups() + start, length, ok := sliceRange(len(all), rng) + if !ok { + return object.NilV + } + out := make([]object.Value, length) + copy(out, all[start:start+length]) + return &object.Array{Elems: out} + } + if len(args) == 2 { // md[start, length] + all := groups() + start := normIndex(intArg(args[0]), len(all)) + length := int(intArg(args[1])) + if start < 0 || start > len(all) || length < 0 { + return object.NilV + } + end := start + length + if end > len(all) { + end = len(all) + } + out := make([]object.Value, end-start) + copy(out, all[start:end]) + return &object.Array{Elems: out} + } + return m.at(args[0]) }) } diff --git a/internal/vm/require.go b/internal/vm/require.go index 786523d2..8c928379 100644 --- a/internal/vm/require.go +++ b/internal/vm/require.go @@ -32,7 +32,7 @@ var providedFeatures = map[string]bool{ "concurrent": true, "syslog": true, "cgi": true, "monitor": true, "net/http": true, "net/https": true, "resolv": true, "singleton": true, "optparse": true, "ripper": true, "erb": true, "find": true, - "strscan": true, + "strscan": true, "fiber": true, "objspace": true, } // registerRequire installs Kernel#require and #require_relative — the runtime diff --git a/internal/vm/set.go b/internal/vm/set.go index 9a186a78..8549a8d5 100644 --- a/internal/vm/set.go +++ b/internal/vm/set.go @@ -69,8 +69,12 @@ func setKey(v object.Value) any { case *object.String: return "str:" + string(x.B) } - raise("TypeError", "%s is not a valid Set member", v.Inspect()) - return nil + // Any other object is a valid Set member in Ruby — it keys by identity + // (object equality / hash), exactly like a Hash key. A reference-typed Ruby + // value is a Go pointer, which is comparable, so the pointer itself is a + // stable per-object key. This lets a Set hold arbitrary objects (e.g. Puppet + // resources, model nodes) the way MRI does. + return v } // add inserts a Ruby value, preserving first-insertion order (idempotent). diff --git a/internal/vm/set_test.go b/internal/vm/set_test.go index 82ed8d9c..bf1dbf6a 100644 --- a/internal/vm/set_test.go +++ b/internal/vm/set_test.go @@ -24,6 +24,13 @@ func TestSet(t *testing.T) { // Heterogeneous comparable members (String distinct from Symbol, Bignum, etc.). {`p Set.new(["a", :a, 1, 1.5, true, nil])`, "#\n"}, {`p Set.new([10 ** 30, 10 ** 30]).size`, "1\n"}, // Bignum keying + // Arbitrary objects are valid members, keyed by identity like Ruby (an + // Array member is allowed; two distinct objects stay distinct). + {`p Set.new([[1, 2]]).size`, "1\n"}, + {`s = Set.new([1]); s.add([2]); p s.size`, "2\n"}, + {`p Set.new([1]).include?([2])`, "false\n"}, + {`class O; end; p Set.new([O.new, O.new]).size`, "2\n"}, + {`o = Object.new; s = Set.new; s << o << o; p s.size`, "1\n"}, // same object collapses // add / << / add? {`s = Set.new([1]); s.add(2); p s`, "#\n"}, {`s = Set.new([1]); s << 2 << 3; p s`, "#\n"}, @@ -189,10 +196,7 @@ func TestSetAggregateErrors(t *testing.T) { // each without a block. func TestSetErrors(t *testing.T) { for _, c := range []struct{ src, want string }{ - {`Set.new([[1, 2]])`, "TypeError"}, // Array member (non-comparable) - {`Set.new({})`, "TypeError"}, // Hash is not enumerable here - {`Set.new([1]).add([2])`, "TypeError"}, // non-comparable add - {`Set.new([1]).include?([2])`, "TypeError"}, + {`Set.new({})`, "TypeError"}, // Hash is not enumerable here {`Set.new([1]) | [2]`, "TypeError"}, // non-Set operand {`Set.new([1]) & 3`, "TypeError"}, // non-Set operand {`Set.new([1]).merge(5)`, "TypeError"}, diff --git a/internal/vm/singleton.go b/internal/vm/singleton.go index fffab7ae..a58d11d6 100644 --- a/internal/vm/singleton.go +++ b/internal/vm/singleton.go @@ -45,10 +45,15 @@ func (vm *VM) registerSingleton() { raise("TypeError", "wrong argument type %s (expected Module)", classNameOf(a)) } if t, ok := self.(*RClass); ok { - // C.extend(M): M's instance methods become class methods of C. - for n, m := range mod.methods { - t.smethods[n] = m - } + // C.extend(M): M (and every module M itself includes) becomes part of + // C's singleton-class ancestry, so M's own instance methods *and* the + // methods of its transitively-included modules all become class methods + // of C. Mixing M into the metaclass's includes (rather than copying + // M.methods) lets lookupSMethod walk the whole include tree — matching + // MRI, where `extend M` inserts M's full ancestry into the singleton + // class. + mc := t.metaClass() + mc.includes = append(mc.includes, mod) } else { // Any other object (incl. builtin-backed Array/String/… such as // $LOAD_PATH) mixes the module into its singleton class.