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
66 changes: 66 additions & 0 deletions internal/vm/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,43 @@ func (vm *VM) bootstrap() {
exc("ScriptError", "Exception")
exc("SyntaxError", "ScriptError")
exc("LoadError", "ScriptError")
// Exceptions that sit directly under Exception (NOT StandardError), so a bare
// `rescue` does not catch them — matching MRI. SystemExit additionally carries
// an exit status (defined below).
exc("NoMemoryError", "Exception")
exc("SecurityError", "Exception")
exc("SignalException", "Exception")
exc("Interrupt", "SignalException")
systemExit := exc("SystemExit", "Exception")
// SystemExit#initialize accepts (status=0, message=nil) or (message); it stores
// @status (the process exit code) and the message. SystemExit#status returns it.
systemExit.define("initialize", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
o := self.(*RObject)
status := object.Value(object.Integer(0))
switch {
case len(args) >= 2:
status = args[0]
o.ivars["@message"] = object.NewString(args[1].ToS())
case len(args) == 1:
if i, ok := args[0].(object.Integer); ok {
status = i
} else {
o.ivars["@message"] = object.NewString(args[0].ToS())
}
}
o.ivars["@status"] = status
return object.NilV
})
systemExit.define("status", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
if s := getIvar(self, "@status"); s != object.NilV {
return s
}
return object.Integer(0)
})
systemExit.define("success?", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
s, ok := getIvar(self, "@status").(object.Integer)
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)
Expand All @@ -302,10 +339,13 @@ func (vm *VM) bootstrap() {
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
Expand Down Expand Up @@ -711,6 +751,9 @@ func (vm *VM) bootstrap() {
vm.cSymbol.define("to_sym", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return self
})
vm.cSymbol.define("intern", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return self // MRI: Symbol#intern is an alias of Symbol#to_sym (returns self)
})
symStr := func(self object.Value) string { return string(self.(object.Symbol)) }
vm.cSymbol.define("<=>", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
o, ok := args[0].(object.Symbol)
Expand Down Expand Up @@ -3651,6 +3694,29 @@ func strArg(v object.Value) string {
return ""
}

// pathArg coerces a path-like argument to a String the way MRI's File/IO entry
// points do: a String is taken directly; otherwise the value is converted via
// #to_path (Pathname and friends), falling back to #to_str. Anything that
// responds to neither raises TypeError, matching MRI.
func pathArg(vm *VM, v object.Value) string {
if s, ok := v.(*object.String); ok {
return s.Str()
}
for _, m := range []string{"to_path", "to_str"} {
if vm.respondsTo(v, m) {
r := vm.send(v, m, nil, nil)
s, ok := r.(*object.String)
if !ok {
raise("TypeError", "can't convert %s to String (%s#%s gives %s)",
vm.classOf(v).name, vm.classOf(v).name, m, vm.classOf(r).name)
}
return s.Str()
}
}
raise("TypeError", "no implicit conversion of %s into String", vm.classOf(v).name)
return ""
}

// scrubUTF8 returns s with every invalid UTF-8 byte sequence replaced by repl,
// so the result is valid UTF-8 (backing String#scrub). Valid runs are copied
// verbatim; each invalid byte collapses to repl, as MRI does.
Expand Down
72 changes: 42 additions & 30 deletions internal/vm/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"path" // always '/'-separated, as Ruby's File is — not path/filepath
"strings"

gotime "github.com/go-composites/time/src"
"github.com/go-embedded-ruby/ruby/internal/object"
)

Expand Down Expand Up @@ -38,8 +39,8 @@ func (vm *VM) registerFile() {
cFile.consts["NULL"] = object.NewString("/dev/null")
def := func(name string, fn NativeFn) { cFile.smethods[name] = &Method{name: name, owner: cFile, native: fn} }

def("basename", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
base := path.Base(strArg(args[0]))
def("basename", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
base := path.Base(pathArg(vm, args[0]))
if len(args) > 1 {
suf := strArg(args[1])
if suf == ".*" {
Expand All @@ -52,17 +53,17 @@ func (vm *VM) registerFile() {
}
return object.NewString(base)
})
def("dirname", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(path.Dir(strArg(args[0])))
def("dirname", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(path.Dir(pathArg(vm, args[0])))
})
def("extname", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(fileExt(path.Base(strArg(args[0]))))
def("extname", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(fileExt(path.Base(pathArg(vm, args[0]))))
})
def("split", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := strArg(args[0])
def("split", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := pathArg(vm, args[0])
return &object.Array{Elems: []object.Value{object.NewString(path.Dir(p)), object.NewString(path.Base(p))}}
})
def("join", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
def("join", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
// MRI's File.join flattens nested Array arguments, so File.join([a, b], c)
// behaves like File.join(a, b, c).
var parts []string
Expand All @@ -74,65 +75,76 @@ func (vm *VM) registerFile() {
}
return
}
parts = append(parts, strArg(v))
parts = append(parts, pathArg(vm, v))
}
for _, a := range args {
add(a)
}
return object.NewString(fileJoin(parts))
})
def("expand_path", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(fileExpand(strArg(args[0]), args[1:]))
def("expand_path", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(fileExpand(pathArg(vm, args[0]), args[1:]))
})
// absolute_path resolves a path to an absolute one against an optional base
// directory (defaulting to the CWD), like expand_path but without ~ expansion.
// Puppet uses it with relative paths and an explicit base, where the two agree.
def("absolute_path", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(fileExpand(strArg(args[0]), args[1:]))
def("absolute_path", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(fileExpand(pathArg(vm, args[0]), args[1:]))
})
def("absolute_path?", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.Bool(path.IsAbs(toSlash(strArg(args[0]))))
def("absolute_path?", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.Bool(path.IsAbs(toSlash(pathArg(vm, args[0]))))
})

def("exist?", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
_, err := os.Stat(strArg(args[0]))
def("exist?", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
_, err := os.Stat(pathArg(vm, args[0]))
return object.Bool(err == nil)
})
def("file?", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
fi, err := os.Stat(strArg(args[0]))
def("file?", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
fi, err := os.Stat(pathArg(vm, args[0]))
return object.Bool(err == nil && fi.Mode().IsRegular())
})
def("directory?", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
fi, err := os.Stat(strArg(args[0]))
def("directory?", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
fi, err := os.Stat(pathArg(vm, args[0]))
return object.Bool(err == nil && fi.IsDir())
})
def("read", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := strArg(args[0])
def("read", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := pathArg(vm, args[0])
b, err := os.ReadFile(p)
if err != nil {
raise("Errno::ENOENT", "No such file or directory @ rb_sysopen - %s", p)
}
return object.NewString(string(b))
})
def("write", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := strArg(args[0])
def("write", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := pathArg(vm, args[0])
data := []byte(strArg(args[1]))
if err := os.WriteFile(p, data, 0o644); err != nil {
raise("Errno::ENOENT", "No such file or directory @ rb_sysopen - %s", p)
}
return object.Integer(len(data))
})
def("size", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := strArg(args[0])
def("size", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := pathArg(vm, args[0])
fi, err := os.Stat(p)
if err != nil {
raise("Errno::ENOENT", "No such file or directory @ rb_file_s_stat - %s", p)
}
return object.Integer(fi.Size())
})
delete := func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
// File.mtime returns the file's last-modification Time (whole-second
// resolution, matching the Time class's granularity). A missing file raises
// Errno::ENOENT, as in MRI.
def("mtime", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
p := pathArg(vm, args[0])
fi, err := os.Stat(p)
if err != nil {
raise("Errno::ENOENT", "No such file or directory @ rb_file_s_stat - %s", p)
}
return &Time{t: gotime.FromUnix(fi.ModTime().Unix())}
})
delete := func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
for _, a := range args {
p := strArg(a)
p := pathArg(vm, a)
if err := os.Remove(p); err != nil {
raise("Errno::ENOENT", "No such file or directory @ apply2files - %s", p)
}
Expand Down
20 changes: 19 additions & 1 deletion internal/vm/object_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,30 @@ func cvarOwner(c *RClass, name string) *RClass {
}

// lookupSMethod finds a singleton (class) method, walking the superclass chain
// so a subclass inherits its ancestors' class methods.
// so a subclass inherits its ancestors' class methods. At each level it also
// consults the modules mixed into that class's singleton (metaclass) — i.e.
// `class << self; include M; end` / `extend M` — so a module's instance methods
// become class methods, matching MRI's singleton-class ancestor order
// (prepends, then own smethods, then includes).
func lookupSMethod(c *RClass, name string) *Method {
for ; c != nil; c = c.super {
if c.meta != nil {
for i := len(c.meta.prepends) - 1; i >= 0; i-- {
if m := lookupOwnOrIncluded(c.meta.prepends[i], name); m != nil {
return m
}
}
}
if m, ok := c.smethods[name]; ok {
return m
}
if c.meta != nil {
for i := len(c.meta.includes) - 1; i >= 0; i-- {
if m := lookupOwnOrIncluded(c.meta.includes[i], name); m != nil {
return m
}
}
}
}
return nil
}
Expand Down
79 changes: 79 additions & 0 deletions internal/vm/optparse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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"
)

// registerOptParse installs a loadable shell for the optparse standard library
// (require "optparse"). A complete OptionParser is a substantial parser in its
// own right; for now the class and its error tree exist so `require "optparse"`
// completes and load-time references resolve (Puppet requires optparse at load
// but only builds and runs a parser from method bodies). The declaration surface
// that merely records options without parsing — on / on_tail / on_head /
// separator / banner= — is real and chainable, while the methods that actually
// consume argv (parse / parse! / order / order! / permute / permute!) raise
// NotImplementedError until the real parser lands.
func (vm *VM) registerOptParse() {
std := vm.consts["StandardError"].(*RClass)

op := newClass("OptionParser", vm.cObject)
vm.consts["OptionParser"] = op
// MRI also exposes the short alias OptParse for OptionParser.
vm.consts["OptParse"] = op

// Error tree: ParseError < StandardError, with the concrete subclasses raised
// by a real parse (so `rescue OptionParser::InvalidOption` resolves).
parseErr := newClass("OptionParser::ParseError", std)
op.consts["ParseError"] = parseErr
for _, name := range []string{
"InvalidOption", "MissingArgument", "InvalidArgument",
"AmbiguousOption", "AmbiguousArgument", "NeedlessArgument",
} {
op.consts[name] = newClass("OptionParser::"+name, parseErr)
}

op.smethods["new"] = &Method{name: "new", owner: op,
native: func(_ *VM, _ object.Value, args []object.Value, blk *Proc) object.Value {
o := &RObject{class: op, ivars: map[string]object.Value{}}
if len(args) > 0 {
o.ivars["@banner"] = object.NewString(args[0].ToS())
}
// A block form (OptionParser.new { |opts| ... }) yields the parser, as in
// MRI, so option declarations inside the block run.
if blk != nil {
vm.callBlock(blk, []object.Value{o})
}
return o
}}

// Declaration methods: record nothing meaningful yet but accept any arguments
// and a block, returning self so chaining works. on captures the block so a
// future real parser can invoke it.
for _, m := range []string{"on", "on_tail", "on_head", "separator", "accept", "reject"} {
op.define(m, func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return self
})
}
op.define("banner", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return getIvar(self, "@banner")
})
op.define("banner=", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
self.(*RObject).ivars["@banner"] = args[0]
return args[0]
})

// Parsing methods need real argv handling — the large subsystem — so they raise
// until that lands.
notImpl := func(what string) NativeFn {
return func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
return raise("NotImplementedError", "OptionParser#%s not yet supported (real argv parser pending)", what)
}
}
for _, m := range []string{"parse", "parse!", "order", "order!", "permute", "permute!", "to_a", "help"} {
op.define(m, notImpl(m))
}
}
23 changes: 23 additions & 0 deletions internal/vm/prelude.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1372,3 +1372,26 @@ def URI(uri)
end
module_function :URI
end

# Singleton turns its includer into a single-instance class (require "singleton").
# Mixing it in privatizes .new/.allocate and adds a memoizing .instance, matching
# MRI's lib/singleton.rb behavior on the surface code relies on (Puppet::Runtime
# is `include Singleton`).
module Singleton
# The class methods Singleton grafts onto the includer via extend.
module SingletonClassMethods
# instance returns the one-and-only instance, building it on first call and
# caching it on the class thereafter.
def instance
@__singleton_instance__ ||= new
end
end

# included is the mix-in hook: it extends the includer with the class methods
# and makes the constructors private, so the instance is reachable only via
# .instance — calling .new raises NoMethodError, as in MRI.
def self.included(klass)
klass.extend(SingletonClassMethods)
klass.private_class_method(:new, :allocate)
end
end
Loading
Loading