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方法中用户输入的路径覆盖,完全没必要提前处理。
修复步骤
重构初始化与编译流程
把路径处理逻辑从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修复Parser类的注释匹配正则
原代码中advance方法的正则\/\.+无法正确匹配//开头的注释,修改为\/\/.+:def advance line = @vm_file.gets # 处理文件读完的情况,避免gsub调用在nil上出错 @current_command = line ? line.gsub(/\/\/.+|\n|\r/, "").strip : "" end添加空命令防御性检查
在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
相关产品推荐
相关产品推荐

