I am doing the nand2tetris project in Ruby, and I am programming a translator from VM to Jack.
I keep getting the error:
in initialize': undefined method
[]' for nil:NilClass (NoMethodError)
path = path[0...-1] if path[-1] == "/"\r
^^^^
No matter what I try to change in the program.
#parse a vm file
class Parser
attr_reader :current_command #returns the current VM command that was parsed
#constructor takes a path to a vm file and opens it in read-only mode
def initialize(path_to_vm_file)
@vm_file = File.open(path_to_vm_file, "r")
end
#checks if there are more command to parse
def has_more_commands?
!@vm_file.eof?
end
#reads the next command and sets current_command to the clean version of that command
# (gsub removes comments, newlines...)
def advance
@current_command = @vm_file.gets.gsub(/\/\.+|\n|\r/, "")
end
#allows the user to access a specific part of the current command by index
def [](index)
split_command[index]
end
#returns the current line number
def line_number
@vm_file.lineno
end
#returns the name of the vm file without extension
def file_name
File.basename(@vm_file.path, ".vm")
end
#helper method that splits the current command into an array of strings based on whitespaces
# (used by the [] method)
private
def split_command
@current_command.split
end
end
#translate VM code into Hack
class CodeWriter
#constructor: takes path to the output and opens it in write mode
def initialize(path_to_asm_file, single_file)
@asm_file = File.open(path_to_asm_file, "w")
end
#sets the name of the current vm file being translated
def set_file_name(path_to_vm_file)
@parser = Parser.new(path_to_vm_file)
end
# end
#reads each command form the parser and translates it into Hack using translate
def write
while @parser.has_more_commands?
if !@parser.advance.empty?
translate
end
end
end
#translates a VM command into Hack
# first determines the type of command and then calls the appropriate method
def translate
case @parser[0]
when "add","sub","eq","gt","lt","and","or","neg","not"
write_arithmetic
when "push"
write_push
when "pop"
write_pop
end
end
#translates vm arithmetic commands into Hack
def write_arithmetic
case @parser[0]
when "add"
arithmetic(calc: "+")
when "sub"
arithmetic(calc: "-")
when "eq"
arithmetic(calc: "-", jump_type: "JEQ")
when "gt"
arithmetic(calc: "-", jump_type: "JGT")
when "lt"
arithmetic(calc: "-", jump_type: "JLT")
when "and"
arithmetic(calc: "&")
when "or"
arithmetic(calc: "|")
when "neg"
arithmetic(calc: "-", unary: true)
when "not"
arithmetic(calc: "!", unary: true)
end
end
#pushes a value onto the stack based on the segment specified in the vm command
def write_push
case @parser[1]
when "constant"
push_stack(constant:@parser[2])
when "static"
load_static
push_stack
else
load_memory
push_stack
end
end
#pops a value from the stack and stores it in the specific segment
def write_pop
pop_stack
#if static, loads the address of the static variable and stores popped value at that address
if @parser[1] == "static"
load_static(pop: true)
else
#else, stores in D register
write_file(string: "@13\nM=D")
load_memory(save_from_r13: true)
end
end
#loads the value of a static variable
# (if pop=true, stores the value at the top of the stack into the static variable)
def load_static(pop: false)
write_file(string: "@#{@parser.file_name.upcase}.#{@parser[2]}")
write_file(string: "#{pop ? "M=D" : "D=M"}")
end
#loads value from memory onto the top of the stack
def load_memory(pop: false, save_from_r13: false)
symbol_hash = Hash["local", "LCL", "argument", "ARG", "this", "THIS", "that", "THAT",
"pointer", "THIS", "temp", "5"]
write_file(string: "@#{@parser[2]}")
write_file(string: "D=A")
write_file(string: "@#{symbol_hash[@parser[1]]}")
write_file(string: "#{(@parser[1] == "temp" || @parser[1] == "pointer") ? "AD=A+D" : "AD=M+D"}")
write_file(string: "#{save_from_r13 ? "@14\nM=D\n@13\nD=M\n@14\nA=M\nM=D" : "D=M"}")
end
#pushes a value onto the stack
def push_stack(constant: nil)
write_file(string: "@#{constant}\nD=A") if constant
#if constant, then load that value in D and push it onto the stack
# otherwise just pushes the value in the D register
write_file(string: "@SP\nA=M\nM=D\n@SP\nM=M+1")
end
#pops a value and optionally stores it in the D register
# decrements SP and accesses the value and the new top
def pop_stack(save_to_d: true)
write_file(string: "@SP\nM=M-1\nA=M#{save_to_d ? "\nD=M" : ""}")
end
#performs a jump instruction according to jump_type parameter
# sets D register to -1 if the jump condition is met or 0 if not, by jumping to either the true or false label that marks the jump location.
def jump(jump_type)
write_file(string: "@TRUE_JUMP", set_file_name: true, label: "@")
write_file(string: "D; #{jump_type}\nD=0")
write_file(string: "@FALSE_NO_JUMP", set_file_name: true, label: "@")
write_file(string: "0;JMP")
write_file(string: "(TRUE_JUMP", set_file_name: true, label: "(")
write_file(string: "D=-1")
write_file(string: "(FALSE_NO_JUMP", set_file_name: true, label: "(")
end
#pops top 2 values from the stack and performs the calculation
def arithmetic(calc:, jump_type: nil, unary: false)
pop_stack
pop_stack(save_to_d: false) if !unary
write_file(string: "D=#{unary ? "" : "M"}#{calc}D")
jump(jump_type) if jump_type
push_stack
end
# #initializes by putting the stack pointer at memory location 256
# def write_init
# write_file(string: "@256\nD=A\n@SP\nM=D")
# write_call(init: true) #init: initializes vm
# end
#closes the asm file when done
def close
@asm_file.close
end
#writes in the output asm file the new command
private
def write_file(string:"", set_line_number: false, comment: "", set_file_name: false, label: "")
line_number = set_line_number ? @parser.line_number : ""
if !set_file_name
@asm_file.write("#{string}#{line_number}#{comment == "" ? "\n" : "//#{comment}\n"}")
elsif label == "@"
@asm_file.write("#{string}.#{@parser.file_name.upcase}.#{@parser.line_number}#{comment == "" ? "\n" : "//#{comment}\n"}")
else
@asm_file.write("#{string}.#{@parser.file_name.upcase}.#{@parser.line_number}#{comment == "" ? ")\n" : ")//#{comment}\n"}")
end
end
end
class VMTranslator
def initialize(path)
print(path)
path = path[0...-1] if path[-1] == "/"
@vm_path = File.expand_path(path)
if path[-3..-1] == ".vm"
file_name = path.split("/")[-1][0..-4]
@asm_path = "#{@vm_path[0..-4]}.asm"
@single_file = true
else #if more than 1 file, all vm files in the directory will be translated
@asm_path = "#{@vm_path}/#{@vm_path.split("/")[-1]}.asm"
@single_file = false
end
@writer = CodeWriter.new(@asm_path, @single_file)
end
def compile
puts "Input the path to a file: "
@vm_path = gets.chomp
@single_file ? translate(@vm_path) : translate_all
@writer.close
end
#sets file name in codeWriter
private
def translate(vm_path)
@writer.set_file_name(vm_path)
@writer.write
end
#if more than 1 vm file, iterate over them and translate them all
def translate_all
Dir["#{@vm_path}/*.vm"].each {|file| translate(file)}
end
end
#pass the file path to the constructor of VM translator to start the translation
if __FILE__ == $0
VMTranslator.new(ARGV[0]).compile
end
I know it happens when the object is not defined, however, I get the error before even running the program and getting to that point.
Do you have any help about that? Thank you very much!
path = path[0...-1] if path[-1] == "/" ^^^^
First of all, the String#[] operator can return nil. This may or may not be your issue, but it's definitely worth keeping in mind. Consider:
nil.to_s[-1]
#=> nil
Trying to #slice nil (which is what #[] is doing) must raise a NoMethodError exception since it's neither a String nor an Array, and NilClass doesn't define a #[] method. Rather than trying to debug your big wall of code, I would suggest three things:
First, try to ensure that path isn't nil. Note that this won't help you find out why it's nil, but will prevent the exception so you can deal with a nil path in some other way.
path = path[...-1] if !!path && path[-1].eql?("/")
VMTranslator#new requires an argument to populate path, but there's nothing stopping it from being passed a literal nil
value. You should guard against that by raising your own exception at the top of your #initialize method, and then inspect the backtrace to see if you can find the culprit.
raise ArgumentError, "caller passed nil to VMTranslator#new"
Strip out all the non-essentials, and create a minimal and reproducible subset of your code so you can debug it more easily.
You can do lots of other things to guard against nils or handle exceptions. Options may include coercing values into a String (e.g. String(path)
or path.to_s
), or rescuing NoMethodError exceptions. But unless path.empty?
or path.nil?
are valid options for your code's logic, you'll need to either fix your caller or bail when you receive unacceptable input.