Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Ruby keeps code evaluation after throwing a NameError exception?

Tags:

ruby

Simple code which I can not explain to myself:

puts a if a = 1

This results in

warning: found = in conditional, should be ==
NameError: undefined local variable or method 'a' for main:Object

Though, now upon checking a we can see, that it has been defined:

a #=> 1

Why does a get assigned to 1 despite the exception thrown?

From the docs:

The confusion comes from the out-of-order execution of the expression. First the local variable is assigned-to then you attempt to call a nonexistent method [a].

This part is still confusing - why does interpreter not detecting already defined local variable a and still tries to call a "nonexisting" method? Should it not check for local variables as well, find defined local variable a and print 1?

like image 793
Andrey Deineko Avatar asked May 13 '18 16:05

Andrey Deineko


People also ask

What is a nameerror in Ruby?

When an error in the code is encountered, an exception is "raised" or "thrown" and the program shuts down by default. Ruby publishes an exception hierarchy with predefined classes. NameErrors are in the StandardError class, along with RuntimeError, ThreadError, RangeError, ArgumentError and others.

How do you handle exceptions in a Ruby program?

We enclose the code that could raise an exception in a begin/end block and use rescue clauses to tell Ruby the types of exceptions we want to handle. Everything from begin to rescue is protected. If an exception occurs during the execution of this block of code, control is passed to the block between rescue and end.

What happens when an error is raised in Ruby?

When an error in the code is encountered, an exception is "raised" or "thrown" and the program shuts down by default. Ruby publishes an exception hierarchy with predefined classes.

Why is my class not showing up in Ruby code?

You'll see this error when the code refers to a class or module that it can't find, often because the code doesn't include require, which instructs the Ruby file to load the class. In Ruby, variables/methods begin with lowercase letters, while classes begin with uppercase letters.


2 Answers

Let's take a look at Ruby's abstract syntax tree for modifier if:

$ ruby --dump=parsetree -e 'puts a if a = 1'

# @ NODE_SCOPE (line: 1, code_range: (1,0)-(1,15))
# +- nd_tbl: :a
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_PRELUDE (line: 1, code_range: (1,0)-(1,15))
#     +- nd_head:
#     |   (null node)
#     +- nd_body:
#     |   @ NODE_IF (line: 1, code_range: (1,0)-(1,15))
#     |   +- nd_cond:
#     |   |   @ NODE_DASGN_CURR (line: 1, code_range: (1,10)-(1,15))
#     |   |   +- nd_vid: :a
#     |   |   +- nd_value:
#     |   |       @ NODE_LIT (line: 1, code_range: (1,14)-(1,15))
#     |   |       +- nd_lit: 1
#     |   +- nd_body:
#     |   |   @ NODE_FCALL (line: 1, code_range: (1,0)-(1,6))
#     |   |   +- nd_mid: :puts
#     |   |   +- nd_args:
#     |   |       @ NODE_ARRAY (line: 1, code_range: (1,5)-(1,6))
#     |   |       +- nd_alen: 1
#     |   |       +- nd_head:
#     |   |       |   @ NODE_VCALL (line: 1, code_range: (1,5)-(1,6))
#     |   |       |   +- nd_mid: :a
#     |   |       +- nd_next:
#     |   |           (null node)
#     |   +- nd_else:
#     |       (null node)
#     +- nd_compile_option:
#         +- coverage_enabled: false

And for standard if:

$ ruby --dump=parsetree -e 'if a = 1 then puts a end'

# @ NODE_SCOPE (line: 1, code_range: (1,0)-(1,24))
# +- nd_tbl: :a
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_PRELUDE (line: 1, code_range: (1,0)-(1,24))
#     +- nd_head:
#     |   (null node)
#     +- nd_body:
#     |   @ NODE_IF (line: 1, code_range: (1,0)-(1,24))
#     |   +- nd_cond:
#     |   |   @ NODE_DASGN_CURR (line: 1, code_range: (1,3)-(1,8))
#     |   |   +- nd_vid: :a
#     |   |   +- nd_value:
#     |   |       @ NODE_LIT (line: 1, code_range: (1,7)-(1,8))
#     |   |       +- nd_lit: 1
#     |   +- nd_body:
#     |   |   @ NODE_FCALL (line: 1, code_range: (1,14)-(1,20))
#     |   |   +- nd_mid: :puts
#     |   |   +- nd_args:
#     |   |       @ NODE_ARRAY (line: 1, code_range: (1,19)-(1,20))
#     |   |       +- nd_alen: 1
#     |   |       +- nd_head:
#     |   |       |   @ NODE_DVAR (line: 1, code_range: (1,19)-(1,20))
#     |   |       |   +- nd_vid: :a
#     |   |       +- nd_next:
#     |   |           (null node)
#     |   +- nd_else:
#     |       (null node)
#     +- nd_compile_option:
#         +- coverage_enabled: false

The only difference is the method argument for puts:

#     |   |       |   @ NODE_VCALL (line: 1, code_range: (1,5)-(1,6))
#     |   |       |   +- nd_mid: :a

vs:

#     |   |       |   @ NODE_DVAR (line: 1, code_range: (1,19)-(1,20))
#     |   |       |   +- nd_vid: :a

With modifier if, the parser treats a as a method call and creates a NODE_VCALL. This instructs the interpreter to make a method call (although there is a local variable a), resulting in a NameError. (because there is no method a)

With standard if, the parser treats a as a local variable and creates a NODE_DVAR. This instructs the interpreter to look up a local variable which works as expected.

As you can see, Ruby recognizes local variables at the parser level. That's why the documentation says: (emphasis added)

the modifier and standard versions [...] are not exact transformations of each other due to parse order.

like image 61
Stefan Avatar answered Oct 03 '22 15:10

Stefan


Ruby parses code left-to-right. Local variables get defined when the first assignment to them is being parsed. At puts a, no assignment to a has been parsed yet, thus the local variable a doesn't exist yet, and Ruby assumes a is a method call. The local variable only exists to the right and below the assignment.

At runtime, Ruby has to evaluate the condition in order to figure out whether to execute the puts, so a gets initialized to 1.

You seem to be executing that code within some kind of REPL. Usually, REPLs rescue exceptions instead of terminating, which is why your code keeps executing instead of terminating, and since we are now below the assignment, the variable is defined, and since the assignment was executed, the variable is initialized.

If the distinction between definition and initialization of a variable is unclear to you, meditate on this:

foo
# NameError

if false
  foo = 42
end

foo
#=> nil

foo = :bar

foo
#=> :bar
like image 27
Jörg W Mittag Avatar answered Oct 03 '22 16:10

Jörg W Mittag