ucisc 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 +14 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +217 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/factorial.ucisc +58 -0
- data/examples/fib.ucisc +74 -0
- data/examples/hello_world.ucisc +96 -0
- data/examples/image.ucisc +543 -0
- data/exe/ucisc +25 -0
- data/lib/micro_cisc.rb +48 -0
- data/lib/micro_cisc/compile/compiler.rb +103 -0
- data/lib/micro_cisc/compile/instruction.rb +310 -0
- data/lib/micro_cisc/compile/label_generator.rb +48 -0
- data/lib/micro_cisc/compile/statement.rb +113 -0
- data/lib/micro_cisc/version.rb +3 -0
- data/lib/micro_cisc/vm/device.rb +140 -0
- data/lib/micro_cisc/vm/empty_device.rb +9 -0
- data/lib/micro_cisc/vm/processor.rb +447 -0
- data/lib/micro_cisc/vm/term_device.rb +35 -0
- data/lib/micro_cisc/vm/video.rb +151 -0
- data/ucisc.gemspec +28 -0
- data/ucisc.vim +54 -0
- metadata +74 -0
data/exe/ucisc
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "micro_cisc"
|
5
|
+
require "byebug"
|
6
|
+
|
7
|
+
if ARGV.length < 1
|
8
|
+
puts "Usage:"
|
9
|
+
puts " ucisc <file_name>"
|
10
|
+
exit(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
file_name = ARGV.first
|
14
|
+
puts "Reading #{file_name}"
|
15
|
+
compiler = MicroCisc.load(file_name)
|
16
|
+
puts "Running program with #{compiler.serialize.size} words"
|
17
|
+
|
18
|
+
begin
|
19
|
+
MicroCisc.run(compiler.serialize)
|
20
|
+
rescue StandardError => e
|
21
|
+
puts "Execution terminated: #{e.message}"
|
22
|
+
puts " #{e.backtrace.join("\n ")}"
|
23
|
+
end
|
24
|
+
|
25
|
+
puts "Done."
|
data/lib/micro_cisc.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "json"
|
2
|
+
require "io/wait"
|
3
|
+
require "tty-screen"
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
require "micro_cisc/version"
|
7
|
+
require "micro_cisc/compile/label_generator"
|
8
|
+
require "micro_cisc/compile/instruction"
|
9
|
+
require "micro_cisc/compile/statement"
|
10
|
+
require "micro_cisc/compile/compiler"
|
11
|
+
|
12
|
+
require "micro_cisc/vm/device"
|
13
|
+
require "micro_cisc/vm/processor"
|
14
|
+
require "micro_cisc/vm/term_device"
|
15
|
+
require "micro_cisc/vm/empty_device"
|
16
|
+
|
17
|
+
module MicroCisc
|
18
|
+
class Error < StandardError; end
|
19
|
+
|
20
|
+
def self.load(file_name)
|
21
|
+
text = File.read(file_name)
|
22
|
+
MicroCisc::Compile::Compiler.new(text)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.run(data)
|
26
|
+
rom = Array.new(256).map { 0 }
|
27
|
+
rom[0...data.size] = data
|
28
|
+
terminal = MicroCisc::Vm::TermDevice.new(5)
|
29
|
+
terminal.host_device = 1
|
30
|
+
devices = Array.new(16).map { MicroCisc::Vm::EmptyDevice.new }
|
31
|
+
devices[15] = terminal # first banked device
|
32
|
+
processor = MicroCisc::Vm::Processor.new(1, 256, [rom])
|
33
|
+
processor.devices = devices
|
34
|
+
processor.start(ARGV.include?('-d'))
|
35
|
+
processor
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.logger
|
39
|
+
@logger ||=
|
40
|
+
begin
|
41
|
+
logger = Logger.new(STDOUT)
|
42
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
43
|
+
"#{severity}: #{msg}\n"
|
44
|
+
end
|
45
|
+
logger
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module MicroCisc
|
2
|
+
module Compile
|
3
|
+
class Compiler
|
4
|
+
def initialize(text)
|
5
|
+
@text = text
|
6
|
+
parse
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse
|
10
|
+
line_number = 1
|
11
|
+
@instructions = []
|
12
|
+
address = 0
|
13
|
+
@labels = {}
|
14
|
+
@sugar = {}
|
15
|
+
errors = []
|
16
|
+
lgen = MicroCisc::Compile::LabelGenerator.new
|
17
|
+
@text.each_line do |line|
|
18
|
+
begin
|
19
|
+
statement = MicroCisc::Compile::Statement.new(lgen, line, @sugar)
|
20
|
+
statement.parse.each do |instruction|
|
21
|
+
if instruction.label?
|
22
|
+
@labels[instruction.label] = address
|
23
|
+
elsif instruction.instruction?
|
24
|
+
@instructions << [line_number, instruction]
|
25
|
+
address += 1
|
26
|
+
elsif instruction.data?
|
27
|
+
@instructions += instruction.data.map { |d| [line_number, d] }
|
28
|
+
line_string = []
|
29
|
+
word_counts = instruction.data.map do |d|
|
30
|
+
if d.is_a?(String)
|
31
|
+
line_string << d.unpack("S*").map { |w| '%04x' % w }.join
|
32
|
+
d.size / 2
|
33
|
+
else
|
34
|
+
line_string << d.join('.')
|
35
|
+
1 # 1 16-bit word reference
|
36
|
+
end
|
37
|
+
end
|
38
|
+
address += word_counts.sum
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue ArgumentError => e
|
42
|
+
MicroCisc.logger.error("Error on line #{line_number}: #{e.message}\n #{line}")
|
43
|
+
errors << [line_number, e, line]
|
44
|
+
end
|
45
|
+
line_number += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
if errors.size > 0
|
49
|
+
errors = errors.map do |error|
|
50
|
+
"#{error[0]}: #{error[1]}\n #{error[2]}"
|
51
|
+
end
|
52
|
+
MicroCisc.logger.error("\n\nErrors found:\n\n#{errors.join("\n")}")
|
53
|
+
exit(1)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def serialize(file = nil)
|
58
|
+
@serialize ||=
|
59
|
+
begin
|
60
|
+
words = []
|
61
|
+
dstart = nil
|
62
|
+
MicroCisc.logger.info("ADDRESS: INS-WORD LINE#: SOURCE")
|
63
|
+
@instructions.each do |(line_number, ins)|
|
64
|
+
if ins.is_a?(String)
|
65
|
+
address = words.length
|
66
|
+
ins_words = ins.unpack("S*")
|
67
|
+
dstart ||= address
|
68
|
+
words += ins_words
|
69
|
+
elsif ins.is_a?(Array)
|
70
|
+
dstart ||= address
|
71
|
+
# Address reference in data
|
72
|
+
label_address = @labels[ins.first]
|
73
|
+
if ins.last == 'disp'
|
74
|
+
words << label_address - address
|
75
|
+
elsif ins.last == 'imm'
|
76
|
+
words << (label_address & 0xFFFF)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
address = words.length
|
80
|
+
if dstart
|
81
|
+
MicroCisc.logger.info(" 0x#{'%04x' % dstart}: #{(address - dstart)} words of data")
|
82
|
+
dstart = nil
|
83
|
+
end
|
84
|
+
MicroCisc.logger.info(" 0x#{'%04x' % address}: 0x#{'%04x' % ins.encoded(@labels, address)} #{'% 6d' % line_number}: " + ins.original.gsub("\n", ""))
|
85
|
+
words << ins.encoded(@labels, address)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
if dstart
|
89
|
+
address = words.length
|
90
|
+
MicroCisc.logger.info(" 0x#{'%04x' % dstart}: #{(address - dstart)} words of data")
|
91
|
+
end
|
92
|
+
words
|
93
|
+
end
|
94
|
+
|
95
|
+
File.open(file, 'w') do |file|
|
96
|
+
file.write(@serialize.pack("S*"))
|
97
|
+
end if file
|
98
|
+
|
99
|
+
@serialize
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
module MicroCisc
|
2
|
+
module Compile
|
3
|
+
class Instruction
|
4
|
+
attr_reader :label, :instruction, :data, :imm, :sign, :dir, :reg, :dest, :original, :minimal
|
5
|
+
|
6
|
+
def initialize(label_generator, minimal, original, sugar)
|
7
|
+
@label_generator = label_generator
|
8
|
+
@original = original
|
9
|
+
@label = nil
|
10
|
+
@operation = nil
|
11
|
+
@sign = nil
|
12
|
+
@dir = nil
|
13
|
+
@imm = nil
|
14
|
+
@src = nil
|
15
|
+
@dest = nil
|
16
|
+
@sugar = sugar
|
17
|
+
@data = nil
|
18
|
+
@inc = nil
|
19
|
+
@eff = nil
|
20
|
+
@alu_code = nil
|
21
|
+
parse_ucisc(minimal)
|
22
|
+
end
|
23
|
+
|
24
|
+
def data?
|
25
|
+
!@data.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def label?
|
29
|
+
!@label.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def instruction?
|
33
|
+
!@operation.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def comment?
|
37
|
+
!label? && !instruction?
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_ucisc(minimal_instruction)
|
41
|
+
@components = minimal_instruction.split(/\s/)
|
42
|
+
|
43
|
+
return if @components.empty?
|
44
|
+
|
45
|
+
first = @components.first
|
46
|
+
if first == '{'
|
47
|
+
@label_generator.push_context
|
48
|
+
@label = @label_generator.start_label
|
49
|
+
elsif first == '}'
|
50
|
+
@label = @label_generator.end_label
|
51
|
+
@label_generator.pop_context
|
52
|
+
else
|
53
|
+
label = /(?<name>[^\s]+):/.match(first)
|
54
|
+
@label = label['name'] if label
|
55
|
+
end
|
56
|
+
return if @label
|
57
|
+
|
58
|
+
if @components.first == '%'
|
59
|
+
@components.shift
|
60
|
+
@data = @components.map do |component|
|
61
|
+
if match = /(?<name>[^\s]+)\.(?<type>imm|disp)/.match(component)
|
62
|
+
[match['name'], match['type']]
|
63
|
+
else
|
64
|
+
if component.length % 4 != 0
|
65
|
+
raise ArgumentError, "Data segment length must be a multiple of 2-bytes"
|
66
|
+
end
|
67
|
+
words = []
|
68
|
+
(0...(component.length / 4)).each do |index|
|
69
|
+
words << ((component[index * 4, 4]).to_i(16) & 0xFFFF)
|
70
|
+
end
|
71
|
+
words.pack("S*")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
@opcode = parse_component(@components.first).first
|
78
|
+
case @opcode
|
79
|
+
when 'copy'
|
80
|
+
parse('copy')
|
81
|
+
when 'compute'
|
82
|
+
parse('alu')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_component(component)
|
87
|
+
parts = component.split('.')
|
88
|
+
if /^-{0,1}[0-9A-Fa-f]+$/.match(parts.first)
|
89
|
+
[parts.first.to_i, parts.last.downcase]
|
90
|
+
elsif /^-{0,1}(0x){0,1}[0-9A-Fa-f]+$/.match(parts.first)
|
91
|
+
[parts.first.to_i(16), parts.last.downcase]
|
92
|
+
else
|
93
|
+
[parts.first, parts.last.downcase]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def validate_alu(component, current)
|
98
|
+
raise ArgumentError, "Duplicate #{component.last} value" if current
|
99
|
+
code = component.first
|
100
|
+
unless code >= 0 && code < 16
|
101
|
+
raise ArgumentError, "Value of #{component.last} must be between 0x0 and 0xF instead of #{component.first.to_s(16).upcase}"
|
102
|
+
end
|
103
|
+
component.first
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_effect(component, current)
|
107
|
+
raise ArgumentError, "Duplicate #{component.last} value" if current
|
108
|
+
unless (0..7).include?(component.first)
|
109
|
+
raise ArgumentError, "Value of #{component.last} must be between 0x0 and 0x7 instead of 0x#{component.first.to_s(16).upcase}"
|
110
|
+
end
|
111
|
+
component.first
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_boolean(component, current)
|
115
|
+
raise ArgumentError, "Duplicate #{component.last} value" if current
|
116
|
+
if component.first == component.last
|
117
|
+
# Was used with syntax sugar without the numeric argument
|
118
|
+
component[0] = 1
|
119
|
+
end
|
120
|
+
unless (0..1).include?(component.first)
|
121
|
+
raise ArgumentError, "Value of #{component.last} must be 0x0 or 0x1 instead of 0x#{component.first.to_s(16).upcase}"
|
122
|
+
end
|
123
|
+
component.first
|
124
|
+
end
|
125
|
+
|
126
|
+
def parse(operation)
|
127
|
+
@operation = operation
|
128
|
+
components = @components[1..-1]
|
129
|
+
args = []
|
130
|
+
@immediates = []
|
131
|
+
@uses_mem_arg = false
|
132
|
+
@source_is_mem = false
|
133
|
+
@dest_is_mem = false
|
134
|
+
|
135
|
+
while components.size > 0
|
136
|
+
to_parse = components.shift
|
137
|
+
if(to_parse.start_with?('$') || to_parse.start_with?('&'))
|
138
|
+
raise ArgumentError, "Missing ref #{to_parse}" unless @sugar[to_parse]
|
139
|
+
to_parse = @sugar[to_parse]
|
140
|
+
end
|
141
|
+
parsed = parse_component(to_parse)
|
142
|
+
if ['val', 'reg', 'mem'].include?(parsed.last)
|
143
|
+
@uses_mem_arg = true if parsed.last == 'mem'
|
144
|
+
@source_is_mem = true if args.empty? && parsed.last == 'mem'
|
145
|
+
@dest_is_mem = true if !args.empty? && parsed.last == 'mem'
|
146
|
+
args << parsed
|
147
|
+
@immediates << 0
|
148
|
+
elsif parsed.last == 'op'
|
149
|
+
@alu_code = validate_alu(parsed, @alu_code)
|
150
|
+
elsif parsed.last == 'push'
|
151
|
+
@inc = validate_boolean(parsed, @sign)
|
152
|
+
elsif parsed.last == 'pop'
|
153
|
+
@inc = validate_boolean(parsed, @sign)
|
154
|
+
elsif parsed.last == 'eff'
|
155
|
+
@eff = validate_effect(parsed, @eff)
|
156
|
+
elsif (parsed.last == 'disp' || parsed.last == 'imm')
|
157
|
+
if args.empty?
|
158
|
+
# if immediate is first arg, this is a 4.val source
|
159
|
+
args << [4, 'val']
|
160
|
+
@immediates << 0
|
161
|
+
end
|
162
|
+
|
163
|
+
imm =
|
164
|
+
if parsed.first.is_a?(Numeric)
|
165
|
+
parsed.first
|
166
|
+
else
|
167
|
+
if parsed.first == 'break'
|
168
|
+
[@label_generator.end_label, parsed.last]
|
169
|
+
elsif parsed.first == 'loop'
|
170
|
+
[@label_generator.start_label, parsed.last]
|
171
|
+
else
|
172
|
+
parsed
|
173
|
+
end
|
174
|
+
end
|
175
|
+
@immediates[args.size - 1] = imm
|
176
|
+
else
|
177
|
+
raise ArgumentError, "Invalid argument for #{@operation}: #{to_parse}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
if args.size != 2
|
182
|
+
raise ArgumentError, "Missing source and/or destination arguments"
|
183
|
+
end
|
184
|
+
@eff ||= 3
|
185
|
+
if @inc && !@uses_mem_arg
|
186
|
+
raise ArgumentError, "Memory argument required to use push and pop"
|
187
|
+
end
|
188
|
+
if @operation == 'alu' && !@alu_code
|
189
|
+
raise ArgumentError, "Compute instruction must have ALU op code"
|
190
|
+
end
|
191
|
+
@inc ||= 0
|
192
|
+
@bit_width = 7
|
193
|
+
@bit_width -= 4 if @operation == 'alu'
|
194
|
+
@bit_width -= 1 if @uses_mem_arg
|
195
|
+
@immediates.each_with_index do |imm, index|
|
196
|
+
if imm.is_a?(Numeric)
|
197
|
+
validate_immediate(imm, index)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
if @immediates.last != 0 && (!@source_is_mem || !@dest_is_mem)
|
202
|
+
raise ArgumentError, "Destination immediate is only allowed when both arguments are mem args"
|
203
|
+
end
|
204
|
+
|
205
|
+
@src, @dest, @dir = validate_args(args.first, args.last)
|
206
|
+
nil
|
207
|
+
end
|
208
|
+
|
209
|
+
def validate_immediate(value, index)
|
210
|
+
width = @bit_width
|
211
|
+
width = width / 2 if @source_is_mem && @dest_is_mem
|
212
|
+
if index == 0 && @source_is_mem || index == 1 && @dest_is_mem
|
213
|
+
min = 0
|
214
|
+
max = (2 << @bit_width) - 1
|
215
|
+
else
|
216
|
+
magnitude = 2 << (@bit_width - 1)
|
217
|
+
min = magnitude * -1
|
218
|
+
max = magnitude - 1
|
219
|
+
end
|
220
|
+
if (value < min || value > max)
|
221
|
+
signed = @source_is_mem ? 'unsigned' : 'signed'
|
222
|
+
raise ArgumentError, "Immediate max bits is #{@bit_width} #{signed}; value must be between #{min} and #{max} instead of #{value}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def encoded(label_dictionary = nil, current_address = nil)
|
227
|
+
imms = @immediates.map do |imm|
|
228
|
+
if imm.is_a?(Numeric)
|
229
|
+
imm
|
230
|
+
elsif label_dictionary.nil?
|
231
|
+
0
|
232
|
+
elsif imm.is_a?(Array)
|
233
|
+
raise ArgumentError, 'Current address is missing' if current_address.nil?
|
234
|
+
label_address = label_dictionary[imm.first]
|
235
|
+
raise ArgumentError, "Missing label '#{imm.first}'" if label_address.nil?
|
236
|
+
label_address = label_address & 0xFFFF
|
237
|
+
if imm.last == 'disp'
|
238
|
+
label_address - current_address
|
239
|
+
elsif imm.last == 'imm'
|
240
|
+
label_address & 0xFFFF
|
241
|
+
else
|
242
|
+
raise ArgumentError, "Invalid immediate spec: #{imm.first}.#{imm.last}"
|
243
|
+
end
|
244
|
+
else
|
245
|
+
raise ArgumentError, "Invalid immediate spec: #{imm.first}.#{imm.last}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
op_code = @operation == 'alu' ? 1 : 0
|
250
|
+
msb = (op_code << 7) | (@eff << 5) | (@dest << 2) | (@src >> 1)
|
251
|
+
lsb = ((@src & 0x01) << 7)
|
252
|
+
|
253
|
+
if @uses_mem_arg
|
254
|
+
lsb = lsb | (@inc << 6)
|
255
|
+
end
|
256
|
+
|
257
|
+
imm_mask = ~(-1 << @bit_width)
|
258
|
+
if @operation == 'alu'
|
259
|
+
lsb = lsb | ((imms.first & imm_mask) << 4) | (@alu_code & 0xF)
|
260
|
+
else
|
261
|
+
imm =
|
262
|
+
if @source_is_mem && @dest_is_mem
|
263
|
+
((imms.first & 0x7) << 3) | (imms.last & 0x7)
|
264
|
+
else
|
265
|
+
imms.first
|
266
|
+
end
|
267
|
+
lsb = lsb | (imm & imm_mask)
|
268
|
+
end
|
269
|
+
|
270
|
+
((msb & 0xFF) << 8) | (lsb & 0xFF)
|
271
|
+
end
|
272
|
+
|
273
|
+
def validate_args(first_arg, second_arg)
|
274
|
+
register = validate_reg(first_arg)
|
275
|
+
dest = validate_dest(second_arg)
|
276
|
+
|
277
|
+
[register, dest, dir]
|
278
|
+
end
|
279
|
+
|
280
|
+
def validate_reg(arg)
|
281
|
+
valid = arg.first == 0 && arg.last == 'reg'
|
282
|
+
valid = valid || ([1, 2, 3].include?(arg.first) && arg.last == 'mem')
|
283
|
+
valid = valid || (arg.first == 4 && arg.last == 'val')
|
284
|
+
valid = valid || ([1, 2, 3].include?(arg.first) && arg.last == 'reg')
|
285
|
+
|
286
|
+
if valid
|
287
|
+
reg = arg.first
|
288
|
+
reg += 4 if [1, 2, 3].include?(arg.first) && 'reg' == arg.last
|
289
|
+
reg
|
290
|
+
else
|
291
|
+
raise ArgumentError, "Invalid register: #{arg.first.to_s}.#{arg.last}"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def validate_dest(arg)
|
296
|
+
valid = arg.last == 'mem' && [1, 2, 3].include?(arg.first)
|
297
|
+
valid = valid || (arg.last == 'reg' && [0, 4, 1, 2, 3].include?(arg.first))
|
298
|
+
|
299
|
+
if valid
|
300
|
+
reg = arg.first
|
301
|
+
reg += 4 if [1, 2, 3].include?(arg.first) && arg.last == 'reg'
|
302
|
+
reg
|
303
|
+
else
|
304
|
+
raise ArgumentError, "Invalid destination: #{arg.first.to_s}.#{arg.last}"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|