hackasm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +139 -0
  8. data/README.md +40 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/exe/hackasm +18 -0
  13. data/hackasm.gemspec +64 -0
  14. data/lib/hackasm.rb +6 -0
  15. data/lib/hackasm/assembler/constants/dests.rb +1 -0
  16. data/lib/hackasm/assembler/constants/jumps.rb +1 -0
  17. data/lib/hackasm/assembler/hack_parser.rb +50 -0
  18. data/lib/hackasm/assembler/instructions/address_instruction.rb +18 -0
  19. data/lib/hackasm/assembler/instructions/command_instruction.rb +76 -0
  20. data/lib/hackasm/assembler/symbol_table.rb +53 -0
  21. data/lib/hackasm/assembler/translator.rb +51 -0
  22. data/lib/hackasm/cli.rb +45 -0
  23. data/lib/hackasm/command.rb +121 -0
  24. data/lib/hackasm/commands/.gitkeep +1 -0
  25. data/lib/hackasm/commands/asm2binary.rb +26 -0
  26. data/lib/hackasm/commands/vm2asm.rb +36 -0
  27. data/lib/hackasm/templates/.gitkeep +1 -0
  28. data/lib/hackasm/templates/assemble/.gitkeep +1 -0
  29. data/lib/hackasm/templates/vm2asm/.gitkeep +1 -0
  30. data/lib/hackasm/version.rb +3 -0
  31. data/lib/hackasm/vm/instructions/arithmetic_instruction.rb +30 -0
  32. data/lib/hackasm/vm/instructions/arithmetic_operations/binary_operation.rb +47 -0
  33. data/lib/hackasm/vm/instructions/arithmetic_operations/comparison.rb +50 -0
  34. data/lib/hackasm/vm/instructions/arithmetic_operations/unary_operation.rb +34 -0
  35. data/lib/hackasm/vm/instructions/memory_access_instruction.rb +36 -0
  36. data/lib/hackasm/vm/instructions/memory_access_operations/constant_operation.rb +30 -0
  37. data/lib/hackasm/vm/instructions/memory_access_operations/fixed_segment_operation.rb +62 -0
  38. data/lib/hackasm/vm/instructions/memory_access_operations/static_operation.rb +43 -0
  39. data/lib/hackasm/vm/instructions/memory_access_operations/virtual_segment_operation.rb +65 -0
  40. data/lib/hackasm/vm/translator.rb +46 -0
  41. data/lib/hackasm/vm/vm_code_parser.rb +39 -0
  42. metadata +467 -0
@@ -0,0 +1,18 @@
1
+ module Assembler
2
+ module Instructions
3
+ class AddressInstruction
4
+ def initialize(instruction, symbol_table)
5
+ @instruction = instruction.compact.map { |key, value| [key, value.to_s] }.to_h
6
+ @symbol_table = symbol_table
7
+ end
8
+
9
+ def to_b
10
+ if memory_address = @instruction[:memory_address]
11
+ memory_address.to_i
12
+ elsif identifier = @instruction[:identifier]
13
+ @symbol_table[identifier]
14
+ end.to_s(2).rjust(16, '0')
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,76 @@
1
+ require_relative "../constants/jumps"
2
+ require_relative "../constants/dests"
3
+
4
+ module Assembler
5
+ module Instructions
6
+ class CommandInstruction
7
+ JUMPS_TABLE = [nil, *JUMPS].zip(
8
+ (0..8).map { |i| i.to_s(2).rjust(3, "0") }
9
+ ).to_h.freeze
10
+ DESTS_TABLE = [nil, *DESTS].zip(
11
+ (0..8).map { |i| i.to_s(2).rjust(3, "0") }
12
+ ).to_h.freeze
13
+ COMPS_TABLE = {
14
+ "0" => "0101010",
15
+ "1" => "0111111",
16
+ "-1" => "0111010",
17
+ "D" => "0001100",
18
+ "A" => "0110000",
19
+ "!D" => "0001101",
20
+ "!A" => "0110001",
21
+ "-D" => "0001111",
22
+ "-A" => "0110011",
23
+ "D+1" => "0011111",
24
+ "A+1" => "0110111",
25
+ "D-1" => "0001110",
26
+ "A-1" => "0110010",
27
+ "D+A" => "0000010",
28
+ "D-A" => "0010011",
29
+ "A-D" => "0000111",
30
+ "D&A" => "0000000",
31
+ "D|A" => "0010101",
32
+ "M" => "1110000",
33
+ "!M" => "1110001",
34
+ "-M" => "1110011",
35
+ "M+1" => "1110111",
36
+ "M-1" => "1110010",
37
+ "D+M" => "1000010",
38
+ "D-M" => "1010011",
39
+ "M-D" => "1000111",
40
+ "D&M" => "1000000",
41
+ "D|M" => "1010101",
42
+ }
43
+
44
+ def initialize(instruction)
45
+ @instruction = instruction
46
+ end
47
+
48
+ def to_b
49
+ comp_bits = COMPS_TABLE[comp]
50
+
51
+ binary_instruction = "111"
52
+ binary_instruction << COMPS_TABLE[comp]
53
+ binary_instruction << DESTS_TABLE[dest]
54
+ binary_instruction << JUMPS_TABLE[jump]
55
+
56
+ binary_instruction
57
+ rescue
58
+ binding.pry
59
+ end
60
+
61
+ private
62
+
63
+ def dest
64
+ @instruction[:dest] && @instruction[:dest].to_s
65
+ end
66
+
67
+ def jump
68
+ @instruction[:jump] && @instruction[:jump].to_s
69
+ end
70
+
71
+ def comp
72
+ @instruction[:comp] && @instruction[:comp].to_s.tr(" ", "")
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,53 @@
1
+ module Assembler
2
+ class SymbolTable
3
+ DEFAULT_SYMBOL_TABLE = {
4
+ "SP" => 0,
5
+ "LCL" => 1,
6
+ "ARG" => 2,
7
+ "THIS" => 3,
8
+ "THAT" => 4,
9
+ "SCREEN" => 16384,
10
+ "KBD" => 24576,
11
+ }.merge(("0".."15").map { |i| ["R" + i, i.to_i] }.to_h).freeze
12
+
13
+ def initialize(expressions)
14
+ @expressions = expressions
15
+ end
16
+
17
+ def to_h
18
+ DEFAULT_SYMBOL_TABLE.merge(
19
+ label_symbols
20
+ ).merge(
21
+ variable_symbols
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ def label_symbols
28
+ @label_symbols ||= begin
29
+ label_count = 0
30
+ @expressions.each.with_index.with_object({}) do |(expression, index), symbol_table|
31
+ symbol = expression.dig(:jump_label, :identifier)
32
+ symbol = symbol && symbol.to_s
33
+ if symbol
34
+ symbol_table[symbol] = index - label_count
35
+ label_count += 1
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def variable_symbols
42
+ memory_location = 16
43
+ @expressions.each.with_index.with_object({}) do |(expression, index), symbol_table|
44
+ symbol = expression.dig(:address_instruction, :identifier)
45
+ symbol = symbol && symbol.to_s
46
+ if symbol && symbol_table[symbol].nil? && DEFAULT_SYMBOL_TABLE[symbol].nil? && label_symbols[symbol].nil?
47
+ symbol_table[symbol] = memory_location
48
+ memory_location += 1
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ require "pry"
2
+ require_relative "./hack_parser"
3
+ require_relative "./symbol_table"
4
+ require_relative "./instructions/address_instruction"
5
+ require_relative "./instructions/command_instruction"
6
+
7
+ module Assembler
8
+ class Translator
9
+ def initialize(text)
10
+ @text = text
11
+ end
12
+
13
+ def translate
14
+ expressions = parser.parse(@text)
15
+ expressions.map do |expression|
16
+ process_instruction(expression)
17
+ end.compact.join("\n") << "\n"
18
+ rescue Parslet::ParseFailed => failure
19
+ failure.parse_failure_cause.ascii_tree
20
+ end
21
+
22
+ private
23
+
24
+ def process_instruction(instruction)
25
+ instruction_name = instruction.keys.first
26
+ instruction_body = instruction.values.first
27
+
28
+ case instruction_name
29
+ when :address_instruction
30
+ Instructions::AddressInstruction.new(instruction_body, symbol_table).to_b
31
+ when :command_instruction
32
+ Instructions::CommandInstruction.new(instruction_body).to_b
33
+ when :jump_label
34
+ nil
35
+ else raise "Unknown instruction!"
36
+ end
37
+ end
38
+
39
+ def parser
40
+ @parser ||= HackParser.new
41
+ end
42
+
43
+ def expressions
44
+ @expressions ||= parser.parse(@text)
45
+ end
46
+
47
+ def symbol_table
48
+ @symbol_table ||= SymbolTable.new(expressions).to_h
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Hackasm
6
+ # Handle the application command line parsing
7
+ # and the dispatch to various command objects
8
+ #
9
+ # @api public
10
+ class CLI < Thor
11
+ # Error raised by this runner
12
+ Error = Class.new(StandardError)
13
+
14
+ desc 'version', 'hackasm version'
15
+ def version
16
+ require_relative 'version'
17
+ puts "v#{Hackasm::VERSION}"
18
+ end
19
+ map %w(--version -v) => :version
20
+
21
+ desc 'vm2asm FILE', 'Translate *.vm file with virtual machine code or a folder of *.vm files to the single *.asm file of assembler commands'
22
+ method_option :help, aliases: '-h', type: :boolean,
23
+ desc: 'Display usage information'
24
+ def vm2asm(base_path)
25
+ if options[:help]
26
+ invoke :help, ['vm2asm']
27
+ else
28
+ require_relative 'commands/vm2asm'
29
+ Hackasm::Commands::Vm2Asm.new(base_path, options).execute
30
+ end
31
+ end
32
+
33
+ desc 'asm2binary FILE', 'Translate *.asm file of assembler commands to *.hack file of binary commands'
34
+ method_option :help, aliases: '-h', type: :boolean,
35
+ desc: 'Display usage information'
36
+ def asm2binary(file)
37
+ if options[:help]
38
+ invoke :help, ['asm2binary']
39
+ else
40
+ require_relative 'commands/asm2binary'
41
+ Hackasm::Commands::Asm2Binary.new(file, options).execute
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Hackasm
6
+ class Command
7
+ extend Forwardable
8
+
9
+ def_delegators :command, :run
10
+
11
+ # Execute this command
12
+ #
13
+ # @api public
14
+ def execute(*)
15
+ raise(
16
+ NotImplementedError,
17
+ "#{self.class}##{__method__} must be implemented"
18
+ )
19
+ end
20
+
21
+ # The external commands runner
22
+ #
23
+ # @see http://www.rubydoc.info/gems/tty-command
24
+ #
25
+ # @api public
26
+ def command(**options)
27
+ require 'tty-command'
28
+ TTY::Command.new(options)
29
+ end
30
+
31
+ # The cursor movement
32
+ #
33
+ # @see http://www.rubydoc.info/gems/tty-cursor
34
+ #
35
+ # @api public
36
+ def cursor
37
+ require 'tty-cursor'
38
+ TTY::Cursor
39
+ end
40
+
41
+ # Open a file or text in the user's preferred editor
42
+ #
43
+ # @see http://www.rubydoc.info/gems/tty-editor
44
+ #
45
+ # @api public
46
+ def editor
47
+ require 'tty-editor'
48
+ TTY::Editor
49
+ end
50
+
51
+ # File manipulation utility methods
52
+ #
53
+ # @see http://www.rubydoc.info/gems/tty-file
54
+ #
55
+ # @api public
56
+ def generator
57
+ require 'tty-file'
58
+ TTY::File
59
+ end
60
+
61
+ # Terminal output paging
62
+ #
63
+ # @see http://www.rubydoc.info/gems/tty-pager
64
+ #
65
+ # @api public
66
+ def pager(**options)
67
+ require 'tty-pager'
68
+ TTY::Pager.new(options)
69
+ end
70
+
71
+ # Terminal platform and OS properties
72
+ #
73
+ # @see http://www.rubydoc.info/gems/tty-pager
74
+ #
75
+ # @api public
76
+ def platform
77
+ require 'tty-platform'
78
+ TTY::Platform.new
79
+ end
80
+
81
+ # The interactive prompt
82
+ #
83
+ # @see http://www.rubydoc.info/gems/tty-prompt
84
+ #
85
+ # @api public
86
+ def prompt(**options)
87
+ require 'tty-prompt'
88
+ TTY::Prompt.new(options)
89
+ end
90
+
91
+ # Get terminal screen properties
92
+ #
93
+ # @see http://www.rubydoc.info/gems/tty-screen
94
+ #
95
+ # @api public
96
+ def screen
97
+ require 'tty-screen'
98
+ TTY::Screen
99
+ end
100
+
101
+ # The unix which utility
102
+ #
103
+ # @see http://www.rubydoc.info/gems/tty-which
104
+ #
105
+ # @api public
106
+ def which(*args)
107
+ require 'tty-which'
108
+ TTY::Which.which(*args)
109
+ end
110
+
111
+ # Check if executable exists
112
+ #
113
+ # @see http://www.rubydoc.info/gems/tty-which
114
+ #
115
+ # @api public
116
+ def exec_exist?(*args)
117
+ require 'tty-which'
118
+ TTY::Which.exist?(*args)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+ require_relative '../assembler/translator'
5
+
6
+ module Hackasm
7
+ module Commands
8
+ class Asm2Binary < Hackasm::Command
9
+ FILE_NAME = "a.hack".freeze
10
+
11
+ def initialize(file, options)
12
+ @file = file
13
+ @options = options
14
+ end
15
+
16
+ def execute(input: $stdin, output: $stdout)
17
+ assembler_code = File.read(@file)
18
+
19
+ binary = Assembler::Translator.new(assembler_code).translate
20
+
21
+ output.puts binary
22
+ File.write(FILE_NAME, binary)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+ require_relative '../vm/translator'
5
+
6
+ module Hackasm
7
+ module Commands
8
+ class Vm2Asm < Hackasm::Command
9
+ attr_reader :base_path
10
+
11
+ def initialize(base_path, options)
12
+ @base_path = base_path
13
+ @options = options
14
+ end
15
+
16
+ def execute(input: $stdin, output: $stdout)
17
+ if File.directory?(base_path)
18
+ assembler_code = Dir[File.join(base_path, "*")].inject do |code_buffer, path|
19
+ object_name = File.basename(path, ".vm")
20
+ vm_code = File.read(path)
21
+ code_buffer + Vm::Translator.new(vm_code, object_name).translate
22
+ end
23
+ file_name = "#{base_path}.asm"
24
+ else
25
+ object_name = File.basename(base_path, ".vm")
26
+ vm_code = File.read(base_path)
27
+ assembler_code = Vm::Translator.new(vm_code, object_name).translate
28
+ file_name = File.join(File.dirname(base_path), "#{object_name}.asm")
29
+ end
30
+
31
+ output.puts assembler_code
32
+ File.write(file_name, assembler_code)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #