-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtoyasm.rb
executable file
·242 lines (200 loc) · 8.29 KB
/
toyasm.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# Chris Durtschi
# CS 4820
# David Hart
# Toy Assembler
class ToyAsm
LABEL_SIZE = 7
OPCODES = ['STOP', 'LD', 'STO', 'ADD', 'SUB', 'MPY', 'DIV', 'IN', 'OUT', 'B', 'BGTR', 'BZ']
# Initialize instance variables.
# @instructions will contain a collection of hash objects,
# that represent the contents of each line of the .tas file.
# The following symbols are used in the hash:
# :line => the entire contents of the line
# :line_counter => the line number of the line
# :location_counter => the TOYCOM memory location of the line
# :opcode => the integer opcode value of of the line
# :operand => the integer operand value of the line
# :label => the label at the beginning of the line
# @symbol_table is a hash of labels and their memory locations
def initialize(path)
@tas_path = path
@tas_path += '.tas' if !@tas_path.match(/^\w+\.tas$/)
@lis_path = @tas_path.gsub('.tas', '.lis')
@obj_path = @tas_path.gsub('.tas', '.obj')
@line_counter = 1
@location_counter = 0
@symbol_table = {}
@instructions = []
@errors = []
@warnings = []
first_pass
end
private
# Performs the first pass on the .tas file.
# Creates @symbol_table which we'll use later
# to turn labels into memory addresses.
def first_pass
found_end = false
File.open(@tas_path, "r") do |file|
while (line = file.gets)
line.chomp!
code = {}
code[:line] = line
code[:line_counter] = @line_counter
tokens = line.strip.split(/\s+/)
first = tokens.first
if (first.match(/^\w+:$/))
handle_label(tokens, code)
elsif (first == 'END')
found_end = true
break
elsif (first != 'REM')
handle_opcode(tokens, code)
end
@instructions.push(code)
@line_counter = @line_counter.next
end
end
@warnings.push("END directive not found") if (!found_end)
if (@errors.empty?)
second_pass
else
write_lis
puts "Errors encountered on first pass, .obj file not created. See .lis file for details"
end
end
# Goes through @instructions, and matches labels up with their
# memory addresses using @symbol_table.
# Creates .lis file, and if no @errors were encountered, the .obj file.
def second_pass
@instructions.each do |code|
operand = code[:label]
if (operand)
if (@symbol_table.key?(operand))
code[:operand] = @symbol_table[operand]
else
@errors.push("Undefined symbol on line #{code[:line_counter]}: #{operand}")
end
end
end
write_obj if (@errors.empty?)
write_lis
if ([email protected]?)
puts "Errors encountered on second pass, .obj file not created. See .lis file for details"
elsif ([email protected]?)
puts "Warnings encountered on second pass. See .lis file for details"
end
puts ".tas file successfully assembled" if @errors.empty?
end
# Checks the validity of a label, adds label to @symbol_table, then passes
# on the remaining symbols on the line to handle_opcode.
def handle_label(tokens, code)
# There shouldn't be more than 3 symbols on a label line
if (tokens.length > 3)
@errors.push("Too many symbols on line #{@line_counter}}")
else
# Strip the : and check if label exceeds size limit
label = tokens[0].gsub(':', '')
if (label.length > LABEL_SIZE)
@errors.push("Label has too many characters on line #{@line_counter}: #{label}")
else
# Make sure label hasn't already been defined
if (@symbol_table.has_key?(label))
@errors.push("Duplicate label on line #{@line_counter}: #{label}")
else
@symbol_table[label] = @location_counter
handle_opcode(tokens[1..2], code)
end
end
end
end
# Checks if the two sybols after a label are valid opcode/label pair,
# or a valid DC/value pair.
def handle_opcode(tokens, code)
# There shouldn't be more than 2 symbols on a label line
if (tokens.length > 2)
@errors.push("Too many symbols on line #{@line_counter}")
else
opcode = tokens[0]
operand = tokens[1]
if (OPCODES.include?(opcode))
code[:opcode] = OPCODES.index(opcode)
code[:operand] = 0
code[:label] = operand
record_location(code)
elsif (opcode == 'DC')
@warnings.push("Missing DC value on line #{@line_counter}") if (!operand)
# If the value is not defined, default to 0
operand = operand ? operand.to_i : 0
sign = (operand < 0) ? -1 : 1
code[:opcode] = (operand.abs / 100).to_i
code[:operand] = (operand.abs % 100).to_i * sign
record_location(code)
else
@errors.push("Invalid opcode at line #{@line_counter}: #{opcode}")
end
end
end
# Records the current location of the @location_counter,
# then increments the @location_counter.
def record_location(code)
code[:location_counter] = @location_counter
@location_counter = @location_counter.next
end
# Writes the .lis file using the information stored in @instructions.
# If their are any @errors or @warnings, print them out at the end of the file.
def write_lis
File.open(@lis_path, "w") do |file|
file << " Line Relative Object Assembly\n"
file << "Number Address Code Instruction\n\n"
@instructions.each do |code|
line = code[:line]
line_counter = code[:line_counter]
location_counter = code[:location_counter]
opcode = code[:opcode]
operand = code[:operand]
file << "#{"%05s" % line_counter} "
file << "#{"%02d" % location_counter} " if location_counter
file << " " if !location_counter
if (opcode && operand)
sign = (operand < 0) ? '-' : ' '
file << "#{sign}#{"%02d" % opcode.abs}#{"%02d" % operand.abs} "
else
file << " "
end
file << "#{line}\n"
end
if @errors.empty?
file << "\nAssembly was Successful - Object file Created\n\n"
else
file << "\nErrors Encountered\n\n"
end
@errors.each { |error| file << "ERROR: #{error}\n" }
@warnings.each { |warning| file << "WARNING: #{warning}\n" }
end
end
# Writes the .obj file using the information stored in @instructions.
# Only called if second_pass is successful.
def write_obj
File.open(@obj_path, "w") do |file|
@instructions.each do |code|
opcode = code[:opcode]
operand = code[:operand]
if opcode && operand
sign = (operand < 0) ? '-' : ''
file << "#{sign}#{"%02d" % opcode.abs}#{"%02d" % operand.abs}\n"
end
end
end
end
end
while true
puts "\nEnter assembler filename, with or without .tas extension, or 'quit' to exit:"
path = gets.chomp
exit if path == 'quit'
begin
ToyAsm.new(path)
rescue
puts "Error: #{$!}"
end
end