Repository navigation
Embedding API: an Interp that keeps state, exchanges values with Go, and lets Go define Ruby - #823
Merged
Merged
Conversation
…and lets Go define Ruby New(...) *Interp with Eval(src, file, line), whose top-level locals persist across calls as one binding; errors as *ruby.Error carrying class, message, file:line and backtrace; separate, switchable stdout and stderr. Set/Get convert nil, bool, integers (Bignum <-> *big.Int), floats, complex, rationals, strings, symbols, slices, maps, and *ndarray.Array <-> NDArray as the same array; other Go values cross as opaque objects that come back as themselves. DefineModule/DefineClass/DefineMethod/DefineSingletonMethod take Go funcs receiving self, positional args, keyword args and a block Go can call. The VM side is internal/vm/embed.go, reached only through an Interp; the one change on the shared path is that callNative keeps the call site's keyword verdict for a host method to read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Binary operators, unary minus and == on an object of a host-defined class dispatch to the methods the class defines; == keeps its result as it is (a data frame's == answers a mask), and != is its negation unless the class defines != too. Reported by the go-scikit/ruby bindings, where Series + 1 raised "can't be coerced". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e host go-ruby-parser v0.13.1 panics (index out of range) on an unterminated string that ends in a backslash. Kernel#eval is protected by callNative; Interp.Eval reached the parser directly, so the panic killed the host. Found by fuzzing the notebook's Ruby-cell analysis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Rebased on #819, whose classOf cases sit next to this PR's. Added a commit: a panic of go-ruby-parser on a malformed source (go-ruby-parser/parser#54) is now a SyntaxError from |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An embedding API that keeps state
Until now the public API was one function,
ruby.Run(src, out), which runs a program on a fresh VM. This addsInterp, for hosts that evaluate Ruby piece by piece, exchange values with it and define Ruby from Go. Its first users are the go-scikit Ruby bindings and the Ruby cells of go-notebook.State:
New(WithStdout(w), WithStderr(w)) *Interp.Eval(src, file, line) (Value, error)evaluates top-level code. Its top-level locals persist from one call to the next, as one binding, and methods, constants and globals persist in the VM as before.SetStdout/SetStderrswitch the two streams, which are kept separate.Closeruns theat_exithandlers.Errors: a Ruby exception, a
SyntaxErrorincluded, comes back as an*ruby.Errorwith Class, Message, Backtrace, File and Line. File and Line are those of the innermost frame in the evaluated file, so an exception raised deep in a method is placed at the line of the source that led to it.exitis an*ErrorwhoseExitStatus()holds the status, and the interpreter stays usable after any error.Values:
Set/Get/Lookup/Locals/ValueOfconvert between Go and Ruby:*big.Int;*big.Rat, string/[]byte,ruby.Symbol;*ndarray.Arrayto NDArray, as the same array;Module.Bindnames for its type, or ofGoValue.ValuehasGo,Inspect,String,ClassNameandCall.Defining Ruby from Go:
DefineModuleandDefineClasstake qualified names;DefineMethod,DefineSingletonMethod,SetConst,Bind,Wrap.Func(*Call)receives Self, Args, Kwargs and a Block.Block.Callreturns a Ruby exception as an*Error; returning it re-raises the same exception.==.==keeps its result, so a data frame's==can answer a mask.Scope of the change: the VM side is
internal/vm/embed.go, reached only through anInterp. On the shared path there are three small changes, none of which changes how existing Ruby code runs:callNativekeeps the call site's keyword verdict for a host method to read;classOfgets a case for the host object type;binaryOpandOpNegdispatch when the operand is a host object, and nothing else can be one.The README's "Embedding in a Go program" section is rewritten for it.
Verified locally, every step of
ci.yml, before the pushgo vet ./...,go build ./...,GOOS=jsandGOOS=wasip1builds.go test -race -coverpkg=./internal/... ./...: all green. The coverage ratchet is OK, with no function newly below 100%.rbgo_core: vet, theinternal/vm+cmd/wasmsuite, the 23.2 MB module andweb/smoke.mjs --core; the default module's smoke too.RBGO_BUILD_IT=1 go test ./cmd/rbgo: ok.Found while doing this, not changed here
A user-defined
-@on a plain Ruby class is never called:class A; def -@ = 42; end; p(-A.new)raises NoMethodError.OpNeggoes tonegate(), which knows only built-in values. Fixing it changes how existing Ruby programs behave, so it belongs in a conformance PR of its own.🤖 Generated with Claude Code