Skip to content

Commit a84b16f

Browse files
committed
[GR-15916] Interactive sources should share the same binding.
PullRequest: truffleruby/885
2 parents 2f8100c + e3d23ad commit a84b16f

File tree

8 files changed

+120
-11
lines changed

8 files changed

+120
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Compatibility:
1414
* Implement more `Ripper` methods as no-ops (#1694).
1515
* Implemented `rb_enc_sprintf` (#1702).
1616

17+
Changes:
18+
19+
* Interactive sources (like the GraalVM polyglot shell) now all share the same binding (#1695).
20+
1721
# 20.0.0 beta 1
1822

1923
Bug fixes:

doc/user/polyglot.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ If you are using the native configuration, you will need to use the `--polyglot`
1212
flag to get access to other languages. The JVM configuration automatically has
1313
access to other languages.
1414

15+
* [Running Ruby code from another language](#running-ruby-code-from-another-language)
1516
* [Loading code written in foreign languages](#loading-code-written-in-foreign-languages)
1617
* [Exporting Ruby objects to foreign languages](#exporting-ruby-objects-to-foreign-languages)
1718
* [Importing foreign objects to Ruby](#importing-foreign-objects-to-ruby)
@@ -22,6 +23,17 @@ access to other languages.
2223
* [Threading and interop](#threading-and-interop)
2324
* [Embedded configuration](#embedded-configuration)
2425

26+
## Running Ruby code from another language
27+
28+
When you `eval` Ruby code from the [Context API](https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/Context.html)
29+
in another language and mark the `Source` as interactive, the same interactive
30+
top-level binding is used each time. This means that if you set a local variable
31+
in one `eval`, you will be able to use it from the next.
32+
33+
The semantics are the same as the Ruby semantics of calling
34+
`INTERACTIVE_BINDING.eval(code)` for every `Context.eval()` call with an
35+
interactive `Source`. This is similar to most REPL semantics.
36+
2537
## Loading code written in foreign languages
2638

2739
`Polyglot.eval(id, string)` executes code in a foreign language identified by

src/launcher/java/org/truffleruby/launcher/RubyLauncher.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,13 @@ private int runRubyMain(Context.Builder contextBuilder, CommandLineOptions confi
203203
Metrics.printTime("before-run");
204204

205205
if (config.executionAction == ExecutionAction.PATH) {
206-
String path = context.eval(TruffleRuby.LANGUAGE_ID,
206+
final Source source = Source.newBuilder(TruffleRuby.LANGUAGE_ID,
207207
// language=ruby
208-
"-> name { Truffle::Boot.find_s_file(name) }").execute(config.toExecute).asString();
208+
"-> name { Truffle::Boot.find_s_file(name) }",
209+
TruffleRuby.BOOT_SOURCE_NAME).internal(true).buildLiteral();
210+
209211
config.executionAction = ExecutionAction.FILE;
210-
config.toExecute = path;
212+
config.toExecute = context.eval(source).execute(config.toExecute).asString();
211213
}
212214

213215
final Source source = Source.newBuilder(TruffleRuby.LANGUAGE_ID,

src/main/java/org/truffleruby/language/RubyInlineParsingRequestNode.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ public Object execute(VirtualFrame frame) {
6464

6565
// We use the current frame as the lexical scope to parse, but then we may run with a new frame in the future
6666

67-
final RubyRootNode rootNode = translator.parse(new RubySource(source), ParserContext.INLINE, null, currentFrame, false, null);
67+
final RubyRootNode rootNode = translator.parse(
68+
new RubySource(source),
69+
ParserContext.INLINE,
70+
null,
71+
currentFrame,
72+
false,
73+
null);
6874

6975
final RootCallTarget callTarget = Truffle.getRuntime().createCallTarget(rootNode);
7076

src/main/java/org/truffleruby/language/RubyParsingRequestNode.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919
import com.oracle.truffle.api.nodes.DirectCallNode;
2020
import com.oracle.truffle.api.object.DynamicObject;
2121
import com.oracle.truffle.api.source.Source;
22+
import org.jcodings.specific.UTF8Encoding;
23+
import org.truffleruby.Layouts;
2224
import org.truffleruby.RubyContext;
2325
import org.truffleruby.RubyLanguage;
26+
import org.truffleruby.core.rope.Rope;
27+
import org.truffleruby.core.string.StringOperations;
2428
import org.truffleruby.language.arguments.RubyArguments;
2529
import org.truffleruby.language.backtrace.InternalRootNode;
2630
import org.truffleruby.language.methods.DeclarationContext;
@@ -36,8 +40,10 @@ public class RubyParsingRequestNode extends RubyBaseRootNode implements Internal
3640

3741
private final TruffleLanguage.ContextReference<RubyContext> contextReference;
3842
private final Source source;
43+
private final boolean interactive;
3944
private final String[] argumentNames;
4045

46+
@CompilationFinal private Rope sourceRope;
4147
@CompilationFinal private RubyContext cachedContext;
4248
@CompilationFinal private DynamicObject mainObject;
4349
@CompilationFinal private InternalMethod method;
@@ -46,8 +52,9 @@ public class RubyParsingRequestNode extends RubyBaseRootNode implements Internal
4652

4753
public RubyParsingRequestNode(RubyLanguage language, Source source, String[] argumentNames) {
4854
super(language, null, null);
49-
contextReference = language.getContextReference();
55+
this.contextReference = language.getContextReference();
5056
this.source = source;
57+
this.interactive = source.isInteractive();
5158
this.argumentNames = argumentNames;
5259
}
5360

@@ -56,6 +63,19 @@ public Object execute(VirtualFrame frame) {
5663
printTimeMetric("before-script");
5764
final RubyContext context = contextReference.get();
5865

66+
if (interactive) {
67+
if (sourceRope == null) {
68+
CompilerDirectives.transferToInterpreterAndInvalidate();
69+
sourceRope = StringOperations.encodeRope(source.getCharacters().toString(), UTF8Encoding.INSTANCE);
70+
}
71+
72+
// Just do Truffle::Boot.INTERACTIVE_BINDING.eval(code) for interactive sources.
73+
// It's the semantics we want and takes care of caching correctly based on the Binding's FrameDescriptor.
74+
final Object interactiveBinding = Layouts.MODULE.getFields(context.getCoreLibrary().getTruffleBootModule())
75+
.getConstant("INTERACTIVE_BINDING").getValue();
76+
return context.send(interactiveBinding, "eval", StringOperations.createString(context, sourceRope));
77+
}
78+
5979
if (cachedContext == null) {
6080
CompilerDirectives.transferToInterpreterAndInvalidate();
6181
cachedContext = context;
@@ -66,7 +86,13 @@ public Object execute(VirtualFrame frame) {
6686

6787
final TranslatorDriver translator = new TranslatorDriver(context);
6888

69-
final RubyRootNode rootNode = translator.parse(new RubySource(source), ParserContext.TOP_LEVEL, argumentNames, null, true, null);
89+
final RubyRootNode rootNode = translator.parse(
90+
new RubySource(source),
91+
ParserContext.TOP_LEVEL,
92+
argumentNames,
93+
null,
94+
true,
95+
null);
7096

7197
final RootCallTarget callTarget = Truffle.getRuntime().createCallTarget(rootNode);
7298

@@ -80,15 +106,14 @@ public Object execute(VirtualFrame frame) {
80106
sharedMethodInfo.getName(), context.getCoreLibrary().getObjectClass(), Visibility.PUBLIC, callTarget);
81107
}
82108

83-
Object[] arguments = RubyArguments.pack(
109+
final Object value = callNode.call(RubyArguments.pack(
84110
null,
85111
null,
86112
method,
87113
null,
88114
mainObject,
89115
null,
90-
frame.getArguments());
91-
final Object value = callNode.call(arguments);
116+
frame.getArguments()));
92117

93118
// The return value will be leaked to Java, so share it if the Context API is used.
94119
// We share conditionally on EMBEDDED to avoid sharing return values used in RubyLauncher.

src/main/java/org/truffleruby/parser/TranslatorDriver.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ public TranslatorDriver(RubyContext context) {
9696

9797
public RubyRootNode parse(RubySource rubySource, ParserContext parserContext, String[] argumentNames,
9898
MaterializedFrame parentFrame, boolean ownScopeForAssignments, Node currentNode) {
99-
10099
assert parserContext.isTopLevel() == (parentFrame == null) : "A frame should be given iff the context is not toplevel: " + parserContext + " " + parentFrame;
101100

102101
final Source source = rubySource.getSource();

src/main/ruby/truffleruby/core/truffle/boot.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
# as the TOPLEVEL_BINDING should be empty until the main script is executed.
1313
TOPLEVEL_BINDING = binding
1414

15+
# The Binding used for sharing top-level locals of interactive Sources
16+
Truffle::Boot::INTERACTIVE_BINDING = binding
17+
1518
module Truffle::Boot
1619

1720
def self.check_syntax(source_or_file)

src/test/java/org/truffleruby/PolyglotInteropTest.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. This
2+
* Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. This
33
* code is released under a tri EPL/GPL/LGPL license. You can use it,
44
* redistribute it and/or modify it under the terms of the:
55
*
@@ -10,6 +10,7 @@
1010
package org.truffleruby;
1111

1212
import org.graalvm.polyglot.Context;
13+
import org.graalvm.polyglot.Source;
1314
import org.graalvm.polyglot.Value;
1415
import org.junit.Test;
1516
import org.truffleruby.fixtures.FluidForce;
@@ -21,6 +22,8 @@
2122
import java.util.function.IntConsumer;
2223

2324
import static org.junit.Assert.assertEquals;
25+
import static org.junit.Assert.assertFalse;
26+
import static org.junit.Assert.assertTrue;
2427

2528
public class PolyglotInteropTest {
2629

@@ -147,4 +150,59 @@ public void testParseOnceRunMany() {
147150
}
148151
}
149152

153+
@Test
154+
public void testLocalVariablesNotSharedBetweenNonInteractiveEval() {
155+
try (Context polyglot = Context.newBuilder()
156+
.option(OptionsCatalog.HOME.getName(), System.getProperty("user.dir"))
157+
.allowAllAccess(true)
158+
.build()) {
159+
polyglot.eval("ruby", "a = 14");
160+
assertTrue(polyglot.eval("ruby", "defined?(a).nil?").asBoolean());
161+
}
162+
}
163+
164+
@Test
165+
public void testLocalVariablesSharedBetweenInteractiveEval() {
166+
try (Context polyglot = Context.newBuilder()
167+
.option(OptionsCatalog.HOME.getName(), System.getProperty("user.dir"))
168+
.allowAllAccess(true)
169+
.build()) {
170+
polyglot.eval(Source.newBuilder("ruby", "a = 14", "test").interactive(true).buildLiteral());
171+
assertFalse(polyglot.eval(Source.newBuilder("ruby", "defined?(a).nil?", "test").interactive(true).buildLiteral()).asBoolean());
172+
polyglot.eval(Source.newBuilder("ruby", "b = 2", "test").interactive(true).buildLiteral());
173+
assertEquals(16, polyglot.eval(Source.newBuilder("ruby", "a + b", "test").interactive(true).buildLiteral()).asInt());
174+
}
175+
}
176+
177+
@Test
178+
public void testLocalVariablesSharedBetweenInteractiveEvalChangesParsing() {
179+
try (Context polyglot = Context.newBuilder()
180+
.option(OptionsCatalog.HOME.getName(), System.getProperty("user.dir"))
181+
.allowAllAccess(true)
182+
.build()) {
183+
polyglot.eval(Source.newBuilder("ruby", "def foo; 12; end", "test").interactive(true).buildLiteral());
184+
assertEquals(12, polyglot.eval(Source.newBuilder("ruby", "foo", "test").interactive(true).buildLiteral()).asInt());
185+
polyglot.eval(Source.newBuilder("ruby", "foo = 42", "test").interactive(true).buildLiteral());
186+
assertEquals(42, polyglot.eval(Source.newBuilder("ruby", "foo", "test").interactive(true).buildLiteral()).asInt());
187+
}
188+
}
189+
190+
@Test
191+
public void testLocalVariablesAreNotSharedBetweenInteractiveAndNonInteractive() {
192+
try (Context polyglot = Context.newBuilder()
193+
.option(OptionsCatalog.HOME.getName(), System.getProperty("user.dir"))
194+
.allowAllAccess(true)
195+
.build()) {
196+
polyglot.eval(Source.newBuilder("ruby", "a = 14", "test").interactive(false).buildLiteral());
197+
polyglot.eval(Source.newBuilder("ruby", "b = 2", "test").interactive(true).buildLiteral());
198+
assertTrue(polyglot.eval(Source.newBuilder("ruby", "defined?(a).nil?", "test").interactive(true).buildLiteral()).asBoolean());
199+
assertTrue(polyglot.eval(Source.newBuilder("ruby", "defined?(b).nil?", "test").interactive(false).buildLiteral()).asBoolean());
200+
assertFalse(polyglot.eval(Source.newBuilder("ruby", "defined?(b).nil?", "test").interactive(true).buildLiteral()).asBoolean());
201+
assertTrue(polyglot.eval(Source.newBuilder("ruby", "TOPLEVEL_BINDING.eval('defined?(a).nil?')", "test").interactive(false).buildLiteral()).asBoolean());
202+
assertTrue(polyglot.eval(Source.newBuilder("ruby", "TOPLEVEL_BINDING.eval('defined?(b).nil?')", "test").interactive(false).buildLiteral()).asBoolean());
203+
assertTrue(polyglot.eval(Source.newBuilder("ruby", "TOPLEVEL_BINDING.eval('defined?(a).nil?')", "test").interactive(true).buildLiteral()).asBoolean());
204+
assertTrue(polyglot.eval(Source.newBuilder("ruby", "TOPLEVEL_BINDING.eval('defined?(b).nil?')", "test").interactive(true).buildLiteral()).asBoolean());
205+
}
206+
}
207+
150208
}

0 commit comments

Comments
 (0)