You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ruby实现Nand2Tetris VM转Jack翻译器遇NilClass错误求助

问题

在进行Nand2Tetris项目时,用Ruby编写VM到Hack汇编的翻译器,始终触发以下错误:

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

错误定位在VMTranslator类初始化方法的这一行:

path = path[0...-1] if path[-1] == "/"

错误会在程序核心逻辑运行前触发,确认是对象未定义导致,但无法定位根源。完整代码如下:

#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
解决方案

问题根源

当运行程序时未传入命令行参数,ARGV[0]会返回nil,此时VMTranslator.new(ARGV[0])将nil传入initialize方法的path参数,执行path[-1]时就会触发NoMethodError。此外当前代码存在流程冲突:初始化阶段处理的路径会被compile方法中用户输入的路径覆盖,完全没必要提前处理。

修复步骤

  1. 重构初始化与编译流程
    把路径处理逻辑从initialize移到compile方法中,避免初始化时的空参数问题:

    class VMTranslator
      def initialize
        # 初始化阶段不处理路径
      end
    
      def compile
        puts "Input the path to a file: "
        path = gets.chomp
        # 处理路径末尾的斜杠,同时避免空字符串报错
        path = path[0...-1] if !path.empty? && path[-1] == "/"
        @vm_path = File.expand_path(path)
        
        # 重新判断单文件/目录模式
        if @vm_path.end_with?(".vm")
          @asm_path = "#{@vm_path[0..-4]}.asm"
          @single_file = true
        else
          dir_name = File.basename(@vm_path)
          @asm_path = "#{@vm_path}/#{dir_name}.asm"
          @single_file = false
        end
        
        @writer = CodeWriter.new(@asm_path, @single_file)
        @single_file ? translate(@vm_path) : translate_all
        @writer.close
      end
    
      # 剩余方法保持不变
    end
    
    # 启动代码修改为无需传入参数
    if __FILE__ == $0
      VMTranslator.new.compile
    end
    
  2. 修复Parser类的注释匹配正则
    原代码中advance方法的正则\/\.+无法正确匹配//开头的注释,修改为\/\/.+:

    def advance
      line = @vm_file.gets
      # 处理文件读完的情况,避免gsub调用在nil上出错
      @current_command = line ? line.gsub(/\/\/.+|\n|\r/, "").strip : ""
    end
    
  3. 添加空命令防御性检查
    在CodeWriter的write方法中,跳过空命令的处理:

    def write
      while @parser.has_more_commands?
        command = @parser.advance.strip
        next if command.empty?
        @parser.current_command = command
        translate
      end
    end
    

内容的提问来源于stack exchange,提问作者Ellie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 06:42:00