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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +139 -0
- data/README.md +40 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/hackasm +18 -0
- data/hackasm.gemspec +64 -0
- data/lib/hackasm.rb +6 -0
- data/lib/hackasm/assembler/constants/dests.rb +1 -0
- data/lib/hackasm/assembler/constants/jumps.rb +1 -0
- data/lib/hackasm/assembler/hack_parser.rb +50 -0
- data/lib/hackasm/assembler/instructions/address_instruction.rb +18 -0
- data/lib/hackasm/assembler/instructions/command_instruction.rb +76 -0
- data/lib/hackasm/assembler/symbol_table.rb +53 -0
- data/lib/hackasm/assembler/translator.rb +51 -0
- data/lib/hackasm/cli.rb +45 -0
- data/lib/hackasm/command.rb +121 -0
- data/lib/hackasm/commands/.gitkeep +1 -0
- data/lib/hackasm/commands/asm2binary.rb +26 -0
- data/lib/hackasm/commands/vm2asm.rb +36 -0
- data/lib/hackasm/templates/.gitkeep +1 -0
- data/lib/hackasm/templates/assemble/.gitkeep +1 -0
- data/lib/hackasm/templates/vm2asm/.gitkeep +1 -0
- data/lib/hackasm/version.rb +3 -0
- data/lib/hackasm/vm/instructions/arithmetic_instruction.rb +30 -0
- data/lib/hackasm/vm/instructions/arithmetic_operations/binary_operation.rb +47 -0
- data/lib/hackasm/vm/instructions/arithmetic_operations/comparison.rb +50 -0
- data/lib/hackasm/vm/instructions/arithmetic_operations/unary_operation.rb +34 -0
- data/lib/hackasm/vm/instructions/memory_access_instruction.rb +36 -0
- data/lib/hackasm/vm/instructions/memory_access_operations/constant_operation.rb +30 -0
- data/lib/hackasm/vm/instructions/memory_access_operations/fixed_segment_operation.rb +62 -0
- data/lib/hackasm/vm/instructions/memory_access_operations/static_operation.rb +43 -0
- data/lib/hackasm/vm/instructions/memory_access_operations/virtual_segment_operation.rb +65 -0
- data/lib/hackasm/vm/translator.rb +46 -0
- data/lib/hackasm/vm/vm_code_parser.rb +39 -0
- 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
|
data/lib/hackasm/cli.rb
ADDED
@@ -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
|
+
#
|