Skip to content

Commit

Permalink
Merge pull request #2285 from herwinw/instance_eval_fixed_strings
Browse files Browse the repository at this point in the history
  • Loading branch information
seven1m authored Nov 15, 2024
2 parents 2c2fc23 + 9754b91 commit d02eced
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 94 deletions.
43 changes: 29 additions & 14 deletions lib/natalie/compiler/macro_expander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def initialize(load_path:, interpret:, compiler_context:, log_load_error:)
autoload
eval
include_str!
instance_eval
load
nat_ignore_require
nat_ignore_require_relative
Expand Down Expand Up @@ -59,6 +60,8 @@ def get_macro_name(node)
def get_hidden_macro_name(node)
if node.type == :call_node && node.receiver&.type == :global_variable_read_node && %i[$LOAD_PATH $:].include?(node.receiver.name) && %i[<< unshift].include?(node.name)
:update_load_path
elsif node.type == :call_node && node.name == :instance_eval && node.block.nil? && node.arguments&.arguments&.size == 1 && compile_time_string?(node.arguments.arguments.first)
:instance_eval
end
end

Expand Down Expand Up @@ -159,21 +162,9 @@ def macro_eval(expr:, current_path:, locals:, **)
args = expr.arguments&.arguments || []
node = args.first
$stderr.puts 'FIXME: binding passed to eval() will be ignored.' if args.size > 1
if node.type == :interpolated_string_node && node.parts.all? { |subnode| subnode.type == :string_node }
node = Prism::StringNode.new(
nil,
nil,
node.location,
0,
node.opening_loc,
node.opening_loc,
node.closing_loc,
node.parts.map(&:unescaped).join,
)
end
if node.type == :string_node
if compile_time_string?(node)
begin
Natalie::Parser.new(node.unescaped, current_path, locals: locals).ast
Natalie::Parser.new(string_node_to_string(node), current_path, locals: locals).ast
rescue Parser::ParseError => e
drop_error(:SyntaxError, e.message, location: node.location)
end
Expand Down Expand Up @@ -229,6 +220,16 @@ def macro_update_load_path(expr:, current_path:, depth:, **)
Prism.nil_node(location: expr.location)
end

def macro_instance_eval(expr:, current_path:, depth:, locals:, **)
return expr unless compile_time_string?(expr.arguments&.child_nodes&.first)

ast = Natalie::Parser.new(string_node_to_string(expr.arguments.child_nodes.first), current_path, locals:).ast
block = Prism::BlockNode.new(ast.child_nodes.first, nil, expr.arguments.location, 0, nil, nil, ast.statements, expr.arguments.child_nodes.first.opening_loc, expr.arguments.child_nodes.first.closing_loc)
Prism::CallNode.new(expr.__send__(:source), expr.node_id, expr.location, expr.__send__(:flags), expr.receiver, expr.call_operator_loc, expr.name, expr.message_loc, expr.opening_loc, nil, expr.closing_loc, block)
rescue Parser::ParseError => e
drop_error(:SyntaxError, e.message, location: expr.location)
end

def interpret?
!!@interpret
end
Expand Down Expand Up @@ -321,6 +322,20 @@ def location(expr)
raise "unknown node type: #{expr.inspect}"
end
end

def compile_time_string?(expr)
expr&.type == :string_node || expr&.type == :interpolated_string_node && expr.parts.all? { |subexpr| subexpr.type == :string_node }
end

def string_node_to_string(expr)
case expr.type
when :string_node
return expr.unescaped
when :interpolated_string_node
return expr.parts.map(&:unescaped).join if expr.parts.all? { |subexpr| subexpr.type == :string_node }
end
raise "Not a compile time string: #{expr.location.slice}"
end
end
end
end
116 changes: 48 additions & 68 deletions spec/core/basicobject/instance_eval_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
end

it "evaluates strings" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
a = BasicObject.new
a.instance_eval('self').equal?(a).should be_true
end
a = BasicObject.new
a.instance_eval('self').equal?(a).should be_true
end

it "raises an ArgumentError when no arguments and no block are given" do
Expand Down Expand Up @@ -74,35 +72,29 @@ def foo
it "binds self to the receiver" do
s = "hola"
(s == s.instance_eval { self }).should be_true
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
o = mock('o')
(o == o.instance_eval("self")).should be_true
end
o = mock('o')
(o == o.instance_eval("self")).should be_true
end

it "executes in the context of the receiver" do
"Ruby-fu".instance_eval { size }.should == 7
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
"hola".instance_eval("size").should == 4
Object.class_eval { "hola".instance_eval("to_s") }.should == "hola"
end
"hola".instance_eval("size").should == 4
Object.class_eval { "hola".instance_eval("to_s") }.should == "hola"
Object.class_eval { "Ruby-fu".instance_eval{ to_s } }.should == "Ruby-fu"
end

ruby_version_is "3.3" do
it "uses the caller location as default location" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
f = Object.new
f = Object.new
NATFIXME 'it uses the caller location as default location', exception: SpecFailedException do
f.instance_eval("[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1]
end
end
end

it "has access to receiver's instance variables" do
BasicObjectSpecs::IVars.new.instance_eval { @secret }.should == 99
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
BasicObjectSpecs::IVars.new.instance_eval("@secret").should == 99
end
BasicObjectSpecs::IVars.new.instance_eval("@secret").should == 99
end

it "raises TypeError for frozen objects when tries to set receiver's instance variables" do
Expand All @@ -118,25 +110,23 @@ def foo
end

it "treats block-local variables as local to the block" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
prc = instance_eval <<-CODE
proc do |x, prc|
if x
n = 2
else
n = 1
prc.call(true, prc)
n
end
prc = instance_eval <<-CODE
proc do |x, prc|
if x
n = 2
else
n = 1
prc.call(true, prc)
n
end
CODE
end
CODE

prc.call(false, prc).should == 1
end
prc.call(false, prc).should == 1
end

it "makes the receiver metaclass the scoped class when used with a string" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
NATFIXME 'it makes the receiver metaclass the scoped class when used with a string', exception: NameError, message: /uninitialized constant \S+::B/ do
obj = Object.new
obj.instance_eval %{
class B; end
Expand All @@ -148,10 +138,10 @@ class B; end

describe "constants lookup when a String given" do
it "looks in the receiver singleton class first" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::CallerScope::Caller.new
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::CallerScope::Caller.new

NATFIXME 'it looks in the receiver singleton class first', exception: SpecFailedException do
caller.get_constant_with_string(receiver).should == :singleton_class
end
end
Expand All @@ -167,38 +157,36 @@ class B; end

ruby_version_is "3.1" do
it "looks in the receiver class next" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::CallerScope::Caller.new
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::CallerScope::Caller.new

caller.get_constant_with_string(receiver).should == :Receiver
end
caller.get_constant_with_string(receiver).should == :Receiver
end
end

it "looks in the caller class next" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::CallerScope::Caller.new
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::CallerScope::Caller.new

NATFIXME 'it looks in the caller class next', exception: SpecFailedException do
caller.get_constant_with_string(receiver).should == :Caller
end
end

it "looks in the caller outer scopes next" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::CallerScope::Caller.new
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::CallerScope::Caller.new

NATFIXME 'it looks in the caller outer scopes next', exception: SpecFailedException do
caller.get_constant_with_string(receiver).should == :CallerScope
end
end

it "looks in the receiver class hierarchy next" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::CallerScope::Caller.new
receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::ReceiverScope::Receiver.new
caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::CallerScope::Caller.new

NATFIXME 'it looks in the receiver class hierarchy next', exception: SpecFailedException do
caller.get_constant_with_string(receiver).should == :ReceiverParent
end
end
Expand All @@ -219,12 +207,10 @@ class B; end

describe "class variables lookup" do
it "gets class variables in the caller class when called with a String" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new

caller.get_class_variable_with_string(receiver).should == :value_defined_in_caller_scope
end
caller.get_class_variable_with_string(receiver).should == :value_defined_in_caller_scope
end

it "gets class variables in the block definition scope when called with a block" do
Expand Down Expand Up @@ -255,19 +241,15 @@ class B; end
end

it "does not have access to class variables in the receiver class when called with a String" do
NATFIXME 'Natalie does not support instance_eval with string', exception: SpecFailedException do
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
-> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/)
end
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
-> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/)
end

it "does not have access to class variables in the receiver's singleton class when called with a String" do
NATFIXME 'Natalie does not support instance_eval with string', exception: SpecFailedException do
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverWithCVarDefinedInSingletonClass
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
-> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/)
end
receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverWithCVarDefinedInSingletonClass
caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
-> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/)
end
end

Expand Down Expand Up @@ -312,12 +294,10 @@ def meth(arg); arg; end
end

it "has access to the caller's local variables" do
NATFIXME 'Natalie does not support instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
x = nil
x = nil

instance_eval "x = :value"
x.should == :value
end
instance_eval "x = :value"
x.should == :value
end

it "converts string argument with #to_str method" do
Expand Down
22 changes: 10 additions & 12 deletions spec/language/keyword_arguments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,18 @@ def m(*args, **kwargs)
end

it "works with (...)" do
NATFIXME 'instance_eval with string', exception: ArgumentError, message: 'Natalie only supports instance_eval with a block' do
instance_eval <<~DEF
def m(...)
target(...)
end
DEF
instance_eval <<~DEF
def m(...)
target(...)
end
DEF

empty = {}
m(**empty).should == [[], {}]
m(empty).should == [[{}], {}]
empty = {}
m(**empty).should == [[], {}]
m(empty).should == [[{}], {}]

m(a: 1).should == [[], {a: 1}]
m({a: 1}).should == [[{a: 1}], {}]
end
m(a: 1).should == [[], {a: 1}]
m({a: 1}).should == [[{a: 1}], {}]
end

it "works with call(*ruby2_keyword_args)" do
Expand Down

0 comments on commit d02eced

Please sign in to comment.