rubynand2tetris

Error: undefined method `[]' for nil:NilClass (NoMethodError)


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!


Solution

  • 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:

    1. 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?("/")
      
    2. 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"
      
    3. 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.