rubylabs 0.5.5 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/marslab.rb ADDED
@@ -0,0 +1,1147 @@
1
+
2
+ =begin rdoc
3
+
4
+ == Core Wars Lab
5
+
6
+ Ruby implementation of the MARS virtual machine used in Core Wars.
7
+
8
+ The machine implemented here conforms as close as possible to the ICWS'88 standard,
9
+ as described by Mark Durham in his Introduction to Redcode (http://corewar.co.uk/guides.htm).
10
+
11
+ Currently there is no attempt to translate instructions into a numerical format. MARS
12
+ code is a set of Word objects. Each Word has an opcode and two operands (A and B), all of
13
+ which are represented as strings. Integer data is represented by a Word where the opcode
14
+ field is DAT. Other Word objects have executable machine instructions for opcodes.
15
+
16
+ To test a single program students can make an instance of a class named MiniMARS, passing
17
+ it the name of a test program. The test machine will have a small memory, only big enough
18
+ to hold the test machine, and special versions of the step, dump and other methods.
19
+
20
+ The main machine that runs one, two, or three programs simultaneously is in the module
21
+ named MARS. Methods that assemble, load, and run programs are module methods, e.g.
22
+ to assemble a program call MARS.assemble(foo).
23
+
24
+ =end
25
+
26
+ # TODO add [] and []= methods to Word, use it to extract/assign bits or ranges of bits
27
+ # TODO define ranges for fields, e.g. @@opcode = 0..3; use to get opcode of Word, e.g. instr[@@opcode]
28
+ # TODO make sure all code uses [] interface (e.g. no more instr.amode[0] == ?#)
29
+ # TODO pack words into binary -- will kill several birds at once -- speed, trim values to 0..4095, ..
30
+ # TODO color a cell black when a thread halts after executing an instruction in that cell
31
+ # TODO MARS.dump
32
+ # TODO pass load address to contest (which passes it on to Warrior.load)?
33
+
34
+ module RubyLabs
35
+
36
+ module MARSLab
37
+
38
+ class MARSRuntimeException < StandardError
39
+ end
40
+
41
+ class RedcodeSyntaxError < StandardError
42
+ end
43
+
44
+ # An object of the Word class represents a single item from memory, either a machine
45
+ # instruction or a piece of data. Attributes are the opcode, the A operand mode, and
46
+ # the B operand (all strings). Instruction execution proceeds according to the description
47
+ # in Durham's spec.
48
+
49
+ class Word
50
+
51
+ attr_reader :op, :lineno
52
+ attr_accessor :a, :b
53
+
54
+ def initialize(*args)
55
+ @op, @a, @b, @lineno = args
56
+ end
57
+
58
+ def inspect
59
+ sprintf "%s %s %s", @op, @a, @b
60
+ end
61
+
62
+ alias to_s inspect
63
+
64
+ # two Words are the same if the strings representing the opcode and
65
+ # both operands are the same
66
+
67
+ def ==(other)
68
+ return (op == other.op && a == other.a && b == other.b)
69
+ end
70
+
71
+ def clone
72
+ Word.new(@op.clone, @a.clone, @b.clone, @lineno)
73
+ end
74
+
75
+ # Convert a field specification into an integer value, stripping off a leading
76
+ # addressing mode character if it's there
77
+
78
+ def field_value(field)
79
+ return @@modes.include?(field[0]) ? field[1..-1].to_i : field.to_i
80
+ end
81
+
82
+ # Return the address of an operand; note that for immediate operands the
83
+ # address is the address of the current instruction.
84
+
85
+ def dereference(field, pc, mem)
86
+ case field[0]
87
+ when ?#
88
+ return pc.current[:addr]
89
+ when ?@
90
+ ptrloc = (field_value(field) + pc.current[:addr]) % mem.size
91
+ ptr = mem.fetch(ptrloc)
92
+ return (field_value(ptr.b) + ptrloc) % mem.size
93
+ when ?<
94
+ ptrloc = (field_value(field) + pc.current[:addr]) % mem.size
95
+ ptr = mem.fetch(ptrloc)
96
+ newb = field_value(ptr.b) - 1
97
+ mem.store_field(ptrloc, (newb % mem.size), :b)
98
+ return (newb + ptrloc) % mem.size
99
+ else
100
+ return (field_value(field) + pc.current[:addr]) % mem.size
101
+ end
102
+ end
103
+
104
+ # Execute an instruction. The first parameter is the program counter used
105
+ # to fetch the instruction, the second is the machine's memory array.
106
+
107
+ def execute(pc, mem)
108
+ self.send(@op, pc, mem)
109
+ return (@op == "DAT") ? (:halt) : (:continue)
110
+ end
111
+
112
+ private
113
+
114
+ # The DAT instruction is effectively a "halt", but we still need to dereference
115
+ # both its operands to generate the side effects in auto-decrement modes.
116
+
117
+ def DAT(pc, mem)
118
+ dereference(@a, pc, mem)
119
+ dereference(@b, pc, mem)
120
+ end
121
+
122
+ # Durham isn't clear on how to handle immediate moves -- does the immediate value
123
+ # go in the A or B field of the destination? Guess: B, in case the destination
124
+ # is a DAT.
125
+
126
+ def MOV(pc, mem)
127
+ raise MARSRuntimeException, "MOV: immediate B-field not allowed" if @b[0] == ?#
128
+ src = dereference(@a, pc, mem)
129
+ val = mem.fetch(src)
130
+ dest = dereference(@b, pc, mem)
131
+ if @a[0] == ?#
132
+ mem.store_field(dest, field_value(val.a), :b)
133
+ else
134
+ mem.store(dest, val)
135
+ end
136
+ pc.log(src)
137
+ pc.log(dest)
138
+ end
139
+
140
+ # Ambiguity on how to handle immediate A values: add operand to the A or B
141
+ # field of the destination? Guess: B (for same reasons given for MOV)
142
+
143
+ def ADD(pc, mem)
144
+ raise MARSRuntimeException, "ADD: immediate B-field not allowed" if @b[0] == ?#
145
+ src = dereference(@a, pc, mem)
146
+ left_operand = mem.fetch(src)
147
+ dest = dereference(@b, pc, mem)
148
+ right_operand = mem.fetch(dest)
149
+ if @a[0] == ?#
150
+ mem.store_field(dest, field_value(left_operand.a) + field_value(right_operand.b), :b)
151
+ else
152
+ mem.store_field(dest, field_value(left_operand.a) + field_value(right_operand.a), :a)
153
+ mem.store_field(dest, field_value(left_operand.b) + field_value(right_operand.b), :b)
154
+ end
155
+ pc.log(src)
156
+ pc.log(dest)
157
+ end
158
+
159
+ # See note for ADD, re immediate A operand.
160
+
161
+ def SUB(pc, mem)
162
+ raise MARSRuntimeException, "SUB: immediate B-field not allowed" if @b[0] == ?#
163
+ src = dereference(@a, pc, mem)
164
+ right_operand = mem.fetch(src)
165
+ dest = dereference(@b, pc, mem)
166
+ left_operand = mem.fetch(dest)
167
+ if @a[0] == ?#
168
+ mem.store_field(dest, field_value(left_operand.b) - field_value(right_operand.a), :b)
169
+ else
170
+ mem.store_field(dest, field_value(left_operand.a) - field_value(right_operand.a), :a)
171
+ mem.store_field(dest, field_value(left_operand.b) - field_value(right_operand.b), :b)
172
+ end
173
+ pc.log(src)
174
+ pc.log(dest)
175
+ end
176
+
177
+ # Durham doesn't mention this explicitly, but since a B operand is allowed it implies
178
+ # we have to dereference it in case it has a side effect.
179
+
180
+ def JMP(pc, mem)
181
+ raise MARSRuntimeException, "JMP: immediate A-field not allowed" if @a[0] == ?#
182
+ target = dereference(@a, pc, mem) % mem.size
183
+ dereference(@b, pc, mem)
184
+ pc.branch(target)
185
+ end
186
+
187
+ # Branch to address specified by A if the B-field of the B operand is zero.
188
+
189
+ def JMZ(pc, mem)
190
+ raise MARSRuntimeException, "JMZ: immediate A-field not allowed" if @a[0] == ?#
191
+ target = dereference(@a, pc, mem) % mem.size
192
+ operand = mem.fetch(dereference(@b, pc, mem))
193
+ if field_value(operand.b) == 0
194
+ pc.branch(target)
195
+ end
196
+ end
197
+
198
+ # As in JMZ, but branch if operand is non-zero
199
+
200
+ def JMN(pc, mem)
201
+ raise MARSRuntimeException, "JMN: immediate A-field not allowed" if @a[0] == ?#
202
+ target = dereference(@a, pc, mem) % mem.size
203
+ operand = mem.fetch(dereference(@b, pc, mem))
204
+ if field_value(operand.b) != 0
205
+ pc.branch(target)
206
+ end
207
+ end
208
+
209
+ # DJN combines the auto-decrement mode dereference logic with a branch -- take
210
+ # the branch if the new value of the B field of the pointer is non-zero.
211
+
212
+ def DJN(pc, mem)
213
+ raise MARSRuntimeException, "DJN: immediate A-field not allowed" if @a[0] == ?#
214
+ target = dereference(@a, pc, mem)
215
+ operand_addr = dereference(@b, pc, mem)
216
+ operand = mem.fetch(operand_addr)
217
+ newb = field_value(operand.b) - 1
218
+ mem.store_field(operand_addr, (newb % mem.size), :b)
219
+ if newb != 0
220
+ pc.branch(target)
221
+ end
222
+ pc.log(operand_addr)
223
+ end
224
+
225
+ # Durham just says "compare two fields" if the operand is immediate. Since
226
+ # B can't be immediate, we just need to look at the A operand and, presumably,
227
+ # compare it to the A operand of the dereferenced operand fetched by the B field.
228
+ # If A is not immediate compare two full Words -- including op codes.
229
+ # The call to pc.next increments the program counter for this thread, which causes
230
+ # the skip.
231
+
232
+ def CMP(pc, mem)
233
+ raise MARSRuntimeException, "CMP: immediate B-field not allowed" if @b[0] == ?#
234
+ right = mem.fetch(dereference(@b, pc, mem))
235
+ if @a[0] == ?#
236
+ left = field_value(@a)
237
+ right = field_value(right.a)
238
+ else
239
+ left = mem.fetch(dereference(@a, pc, mem))
240
+ end
241
+ if left == right
242
+ pc.next
243
+ end
244
+ end
245
+
246
+ # Major ambiguity here -- what does it mean for an word A to be "less than"
247
+ # word B? First assumption, don't compare opcodes. Second, we're just going
248
+ # to implement one-field comparisons of B fields. Otherwise for full-word operands
249
+ # we'd need to do a lexicographic compare of A and B fields of both operands, skipping
250
+ # modes and just comparing values.
251
+
252
+ def SLT(pc, mem)
253
+ raise MARSRuntimeException, "SLT: immediate B-field not allowed" if @b[0] == ?#
254
+ if @a[0] == ?#
255
+ left = field_value(@a)
256
+ else
257
+ left = field_value(mem.fetch(dereference(@a, pc, mem)).b)
258
+ end
259
+ right = field_value(mem.fetch(dereference(@b, pc, mem)).b)
260
+ if left < right
261
+ pc.next
262
+ end
263
+ end
264
+
265
+ # Fork a new thread at the address specified by A. The new thread goes at the end
266
+ # of the queue. Immediate operands are not allowed. Durham doesn't mention it, but
267
+ # implies only A is dereferenced, so ignore B.
268
+
269
+ def SPL(pc, mem)
270
+ raise MARSRuntimeException, "SPL: immediate A-field not allowed" if @a[0] == ?#
271
+ target = dereference(@a, pc, mem)
272
+ pc.add_thread(target)
273
+ end
274
+
275
+ end # class Word
276
+
277
+
278
+ =begin rdoc
279
+ A Warrior is a core warrior -- an object of this class is a simple struct that
280
+ has the program name, assembled code, the starting location, and the symbol table.
281
+
282
+ A static method (Warrior.load) will load an assembled Warrior into memory, making
283
+ sure the max number of programs is not exceeded. The addr attribute is set to the
284
+ starting location of the program when it is loaded.
285
+ =end
286
+
287
+ class Warrior
288
+ attr_reader :name, :code, :symbols, :errors
289
+
290
+ def initialize(filename)
291
+ if filename.class == Symbol
292
+ filename = File.join(@@dataDirectory, filename.to_s + ".txt")
293
+ end
294
+
295
+ if ! File.exists?(filename)
296
+ raise "Can't find file: #{filename}"
297
+ end
298
+
299
+ @name, @code, @symbols, @errors = MARS.assemble( File.open(filename).readlines )
300
+
301
+ if @errors.length > 0
302
+ puts "Syntax errors in #{filename}:"
303
+ puts errors
304
+ @code.clear
305
+ end
306
+ end
307
+
308
+ def inspect
309
+ sprintf "Name: #{@name} Code: #{@code.inspect}"
310
+ end
311
+
312
+ alias to_s inspect
313
+
314
+ def Warrior.load(app, addr = :random)
315
+ if app.class != Warrior
316
+ puts "Argument must be a Warrior object, not a #{app.class}"
317
+ return nil
318
+ end
319
+
320
+ if @@entries.length == @@maxEntries
321
+ puts "Maximum #{@@maxEntries} programs can be loaded at one time"
322
+ return nil
323
+ end
324
+
325
+ if addr == :random
326
+ loop do
327
+ addr = rand(@@mem.size)
328
+ break if Warrior.check_loc(addr, addr + app.code.length - 1)
329
+ end
330
+ else
331
+ if ! Warrior.check_loc(addr, addr + app.code.length - 1)
332
+ puts "Address #{addr} too close to another program; #{app.name} not loaded"
333
+ return nil
334
+ end
335
+ end
336
+
337
+ loc = addr
338
+ app.code.each do |x|
339
+ @@mem.store(loc, x)
340
+ loc = (loc + 1) % @@mem.size
341
+ end
342
+
343
+ id = @@entries.length
344
+ @@pcs << PC.new(id, addr + app.symbols[:start])
345
+ @@entries << app
346
+
347
+ # save the range of memory locations reserved by this app
348
+
349
+ lb = addr - @@params[:buffer]
350
+ ub = addr + app.code.length + @@params[:buffer] - 1
351
+ if lb < 0
352
+ @@in_use << ( 0 .. (addr+app.code.length-1) )
353
+ @@in_use << ( (@@params[:memSize] + lb) .. (@@params[:memSize]-1) )
354
+ elsif ub > @@params[:memSize]
355
+ @@in_use << ( addr .. (@@params[:memSize]-1) )
356
+ @@in_use << ( 0 .. (ub - @@params[:memSize]) )
357
+ else
358
+ @@in_use << (lb..ub)
359
+ end
360
+
361
+ return addr
362
+ end
363
+
364
+ @@in_use = Array.new
365
+
366
+ def Warrior.reset
367
+ @@in_use = Array.new
368
+ end
369
+
370
+ private
371
+
372
+ # Return true if a program loaded between lb and ub would not overlap another
373
+ # program already loaded (including a buffer surrounding loaded programs).
374
+
375
+ def Warrior.check_loc(lb, ub)
376
+ @@in_use.each do |range|
377
+ return false if range.include?(lb) || range.include?(ub)
378
+ end
379
+ return true
380
+ end
381
+
382
+ end # class Warrior
383
+
384
+
385
+ =begin rdoc
386
+ The PC class is used to represent the set of program counters for a running program.
387
+ It has an array of locations to hold the next instruction from each thread, plus the
388
+ index of the thread to use on the next instruction fetch cycle.
389
+
390
+ Call next to get the address of the next instruction to execute; as a side effect this
391
+ call bumps the thread pointer to the next thread in the program. Call add_thread(n)
392
+ to start a new thread running at location n. Call kill_thread to remove the thread
393
+ that was used in the most recent call to next (i.e. when that program dies).
394
+
395
+ To help visualize the operation of a program the class keeps a history of memory
396
+ references made by each thread. A call to next automatically records the program counter
397
+ value, but a semantic routine can also all log(x) to append location x to a history.
398
+ Call history to get the history vectors for all threads.
399
+ =end
400
+
401
+ class PC
402
+ attr_reader :id, :thread, :addrs, :current
403
+
404
+ @@hmax = 10 # see also @@threadMax in Draw -- allowed to be a different value
405
+
406
+ def initialize(id, addr)
407
+ @id = id
408
+ @addrs = [addr]
409
+ @history = [ Array.new ]
410
+ @thread = 0
411
+ @current = {:thread => nil, :addr => nil}
412
+ @first = addr
413
+ end
414
+
415
+ def reset
416
+ @addrs = [@first]
417
+ @history.clear
418
+ @thread = 0
419
+ @current = {:thread => nil, :addr => nil}
420
+ return @first
421
+ end
422
+
423
+ def inspect
424
+ s = "[ "
425
+ @addrs.each_with_index do |x,i|
426
+ s << "*" if i == @thread
427
+ s << x.to_s
428
+ s << " "
429
+ end
430
+ s << "]"
431
+ end
432
+
433
+ alias to_s inspect
434
+
435
+ # Not sure if this is right -- new thread appended to list, as per Durham, but
436
+ # the execution stays with the current thread (thread switch happens in 'next' method)
437
+
438
+ def add_thread(addr)
439
+ @addrs << addr
440
+ @history << Array.new
441
+ self
442
+ end
443
+
444
+ def next
445
+ return nil if @addrs.empty?
446
+ addr = @addrs[@thread]
447
+ @current[:thread] = @thread
448
+ @current[:addr] = addr
449
+ @addrs[@thread] = (@addrs[@thread] + 1) % @@mem.size
450
+ @thread = (@thread + 1) % @addrs.size
451
+ log(addr)
452
+ return addr
453
+ end
454
+
455
+ def branch(loc)
456
+ @addrs[@current[:thread]] = loc
457
+ end
458
+
459
+ def kill_thread
460
+ return 0 if @addrs.empty?
461
+ @addrs.slice!(@current[:thread])
462
+ @history.slice!(@current[:thread])
463
+ @thread -= 1
464
+ return @addrs.length
465
+ end
466
+
467
+ =begin rdoc
468
+ record the location of a memory operation in the history vector for the current thread
469
+ =end
470
+
471
+ def log(loc)
472
+ a = @history[@current[:thread]]
473
+ a << loc
474
+ a.shift if a.length > @@hmax
475
+ end
476
+
477
+ def history
478
+ return @history
479
+ end
480
+
481
+ end # class PC
482
+
483
+ =begin rdoc
484
+ An object of the Memory class is a 1-D array of the specified size. Methods namde
485
+ store and fetch operate on full words, while store_field and fetch_field operate on partial words.
486
+ =end
487
+
488
+ class Memory
489
+
490
+ attr_reader :array
491
+
492
+ def initialize(size)
493
+ @array = Array.new(size)
494
+ end
495
+
496
+ def to_s
497
+ sprintf "Memory [0..#{@array.size-1}]"
498
+ end
499
+
500
+ =begin rdoc
501
+ dump(loc,n) -- print the n words in memory starting at loc
502
+ =end
503
+
504
+ def dump(loc, n)
505
+ (loc...(loc+n)).each do |x|
506
+ addr = x % @array.size
507
+ printf "%04d: %s\n", addr, self.fetch(addr)
508
+ end
509
+ end
510
+
511
+ def size
512
+ return @array.size
513
+ end
514
+
515
+ # Methods for fetching and storing data in memory. According to the standard,
516
+ # memory is initialized with DAT #0 instructions, but our array has nils; the
517
+ # fetch method returns a DAT #0 from an uninitialized location.
518
+
519
+ def fetch(loc)
520
+ return ( @array[loc] || Word.new("DAT", "#0", "#0", nil) )
521
+ end
522
+
523
+ def store(loc, val)
524
+ @array[loc] = val.clone
525
+ end
526
+
527
+ # fetch and store operations that work on partial words
528
+
529
+ def fetch_field(loc, field)
530
+ instr = self.fetch(loc)
531
+ return field == :a ? instr.a : instr.b
532
+ end
533
+
534
+ def store_field(loc, val, field)
535
+ instr = self.fetch(loc)
536
+ part = (field == :a) ? instr.a : instr.b
537
+ mode = @@modes.include?(part[0]) ? part[0].chr : ""
538
+ part.replace(mode + val.to_s)
539
+ self.store(loc, instr)
540
+ end
541
+
542
+ end # class Memory
543
+
544
+ =begin rdoc
545
+ A miniature machine (MiniMARS) object is used to test a program. Pass the name of
546
+ a Recode program to the constructor and get back a VM that has a memory where this
547
+ program has been loaded into location 0. Pass an extra parameter to define the size
548
+ of the memory (otherwise the memory is just big enough to hold the program). Call the
549
+ step method to execute a single instruction, or run to run the program until it hits
550
+ a DAT instruction or executes max instructions.
551
+ =end
552
+
553
+ class MiniMARS
554
+ attr_reader :mem, :pc, :state
555
+
556
+ def initialize(file, size = nil)
557
+ w = Warrior.new(file)
558
+ @mem = size ? Memory.new(size) : Memory.new(w.code.length)
559
+ loc = 0
560
+ w.code.each do |x|
561
+ @mem.store(loc, x)
562
+ loc = loc + 1
563
+ end
564
+ @pc = PC.new(w.name, w.symbols[:start])
565
+ @state = :ready
566
+ end
567
+
568
+ def inspect
569
+ return "#<MiniMARS mem = #{@mem.array.inspect} pc = #{@pc}>"
570
+ end
571
+
572
+ alias to_s inspect
573
+
574
+ def status
575
+ s = "Run: #{@state}"
576
+ s += " PC: #{@pc}" unless @state == :halt
577
+ puts s
578
+ end
579
+
580
+ def step
581
+ return "machine halted" if @state == :halt
582
+ instr = @mem.fetch(@pc.next)
583
+ @state = instr.execute(@pc, @mem)
584
+ return instr
585
+ end
586
+
587
+ def run(nsteps = 1000)
588
+ count = 0
589
+ loop do
590
+ break if @state == :halt || nsteps <= 0
591
+ self.step
592
+ nsteps -= 1
593
+ count += 1
594
+ end
595
+ return count
596
+ end
597
+
598
+ def reset
599
+ @pc.reset
600
+ puts "warning: memory may not be in initial state"
601
+ end
602
+
603
+ def dump(*args)
604
+ if args.empty?
605
+ @mem.dump(0, @mem.array.length)
606
+ else
607
+ x = args[0]
608
+ y = args[1] || x
609
+ @mem.dump(x, y-x+1)
610
+ end
611
+ return nil
612
+ end
613
+
614
+ end # MiniMARS
615
+
616
+ =begin rdoc
617
+ Make a window to display the state of the machine
618
+ =end
619
+
620
+ class Draw
621
+
622
+ @@cellSize = 8
623
+ @@cellRows = 32
624
+ @@cellCols = 128
625
+ @@padding = @@cellSize
626
+ @@traceSize = 10
627
+ @@cellColor = '#CCCCCC'
628
+
629
+ def paintCell(i, color)
630
+ @cells[i]['fill'] = color
631
+ end
632
+
633
+ def fillCanvas(mem)
634
+ @cells = []
635
+ mem.each_with_index do |val, i|
636
+ x = (i % @@cellCols) * @@cellSize + @@padding
637
+ y = (i / @@cellCols) * @@cellSize + @@padding
638
+ @cells << TkcRectangle.new( @canvas, x, y, x+@@cellSize, y+@@cellSize, :outline => "#888888", :fill => @@cellColor )
639
+ end
640
+ end
641
+
642
+ def reset
643
+ @cells.each do |cell|
644
+ cell['fill'] = @@cellColor
645
+ end
646
+ end
647
+
648
+ # Make a range of colors starting from first and going to last in n steps.
649
+ # First and last are expected to be 3-tuples of integer RGB values. The
650
+ # result is an array that starts with first, has n-1 intermediate colors,
651
+ # and ends with last. Example:
652
+ # makePalette( [255,0,0], [0,0,0], 10)
653
+ # makes 11 colors starting with red and ending with black.
654
+
655
+ def makePalette(first, last, n)
656
+ d = Array.new(3)
657
+ 3.times { |i| d[i] = (first[i] - last[i]) / n }
658
+ a = [first]
659
+ (n-1).times do |i|
660
+ a << a.last.clone
661
+ 3.times { |j| a.last[j] -= d[j] }
662
+ end
663
+ a << last
664
+ a.map { |c| sprintf("#%02X%02X%02X",c[0],c[1],c[2]) }
665
+ end
666
+
667
+ def updateCells(pc)
668
+ id = pc.id
669
+ a = pc.history[pc.current[:thread]]
670
+ d = @palette[id].length - a.length
671
+ a.each_with_index do |x, i|
672
+ paintCell(x, @palette[id][i+d])
673
+ end
674
+ end
675
+
676
+ def initialize(parent)
677
+ content = TkFrame.new(parent)
678
+ @canvas = TkCanvas.new(content, :borderwidth => 1,
679
+ :width => @@cellCols*@@cellSize+@@padding, :height => @@cellRows*@@cellSize+@@padding)
680
+ fillCanvas(Array.new(4096))
681
+
682
+ @canvas.grid :column => 0, :row => 0, :columnspan => 4, :padx => 10, :pady => 10
683
+ content.pack :pady => 20
684
+
685
+ # initialize palettes with blends from a dingy color to gray, then
686
+ # add a bright color as the last item
687
+
688
+ @palette = [
689
+ makePalette( [204,204,204], [204,100,100], @@traceSize ),
690
+ makePalette( [204,204,204], [100,100,204], @@traceSize ),
691
+ makePalette( [204,204,204], [100,204,100], @@traceSize ),
692
+ ]
693
+ @palette[0] << "#FF0000"
694
+ @palette[1] << "#0000FF"
695
+ @palette[2] << "#00FF00"
696
+ end
697
+
698
+ end # class Draw
699
+
700
+
701
+ =begin rdoc
702
+ The MARS module is a package that has the methods used to assemble, load, and execute
703
+ up to three programs at a time.
704
+ =end
705
+
706
+ class MARS
707
+
708
+ # -------- Assembler ----------
709
+
710
+ # line format:
711
+ # <label> <opcode> <A-mode><A-field>, <B-mode><B-field> <comment>
712
+
713
+ # Parsing methods strip off and return the item they are looking for, or raise
714
+ # an error if the line doesn't start with a well-formed item
715
+
716
+ def MARS.parseLabel(s) # labels start in column 0, can be any length
717
+ return nil if s =~ /^\s/
718
+ x = s[/^\w+/] # set x to the label
719
+ if x.nil? # x is nil if label didn't start with a word char
720
+ raise RedcodeSyntaxError, "illegal label in '#{s}'"
721
+ end
722
+ if @@opcodes.include?(x.upcase)
723
+ raise RedcodeSyntaxError, "can't use opcode '#{x}' as a label"
724
+ end
725
+ s.slice!(x) # remove it from the line
726
+ return x # return it
727
+ end
728
+
729
+ def MARS.parseOpCode(s)
730
+ if s !~ /^\s/ # expect opcode to be separated from label (or start of line) by white space
731
+ raise RedcodeSyntaxError, "illegal label in '#{s}'"
732
+ end
733
+ s.lstrip!
734
+ x = s[/^\w+/] # set x to the opcode
735
+ if x.nil? || !@@opcodes.include?(x.upcase)
736
+ raise RedcodeSyntaxError, "unknown opcode '#{x}'"
737
+ end
738
+ s.slice!(x) # remove it from the line
739
+ return x.upcase # return it
740
+ end
741
+
742
+ def MARS.parseOperand(s)
743
+ s.lstrip!
744
+ return s if s.length == 0
745
+ x = s[/^[#{@@modes}]?[+-]?\w+/]
746
+ if x.nil?
747
+ raise RedcodeSyntaxError, "illegal operand in '#{s}'"
748
+ end
749
+ s.slice!(x)
750
+ return x.upcase
751
+ end
752
+
753
+ def MARS.parseSeparator(s)
754
+ s.lstrip!
755
+ return if s.length == 0
756
+ if s[0] == ?,
757
+ s.slice!(0)
758
+ else
759
+ raise RedcodeSyntaxError, "operands must be separated by a comma"
760
+ end
761
+ end
762
+
763
+ def MARS.parse(s)
764
+ label = MARS.parseLabel(s)
765
+ op = MARS.parseOpCode(s)
766
+ a = MARS.parseOperand(s)
767
+ MARS.parseSeparator(s)
768
+ b = MARS.parseOperand(s)
769
+ return [label,op,a,b]
770
+ end
771
+
772
+ def MARS.saveWord(instr, label, code, symbols)
773
+ if instr.op == "EQU"
774
+ operand = instr.a
775
+ arg = operand[/[+-]?\d+/]
776
+ operand.slice!(arg)
777
+ raise RedcodeSyntaxError, "EQU operand must be an integer" unless operand.length == 0
778
+ raise RedcodeSyntaxError, "EQU must have a label" if label.nil?
779
+ symbols[label.upcase] = arg.to_i
780
+ elsif instr.op == "END"
781
+ if n = symbols[instr.a]
782
+ symbols[:start] = n
783
+ else
784
+ raise RedcodeSyntaxError, "unknown operand in END: #{instr.a}" if instr.a.length > 0
785
+ end
786
+ else
787
+ if label
788
+ symbols[label.upcase] = code.length # "PC" value is number of codes saved so far
789
+ end
790
+ code << instr
791
+ end
792
+ end
793
+
794
+ def MARS.translate(s, symbols, loc)
795
+ if s.length == 0
796
+ s.insert(0, "#0")
797
+ return true
798
+ end
799
+ if md = s.match(/[#{@@modes}]?(\w+)/)
800
+ sym = md[1]
801
+ return true if sym =~ /^[+-]?\d+$/
802
+ return false unless symbols.has_key?(sym)
803
+ val = symbols[sym] - loc
804
+ s.sub!(sym, val.to_s)
805
+ return true
806
+ end
807
+ return false
808
+ end
809
+
810
+ =begin rdoc
811
+ Assembler -- pass in an array of strings, one instruction per string, get back
812
+ the program name, an array of Word objects, a symbol table, and an array
813
+ of error messages.
814
+ =end
815
+
816
+ def MARS.assemble(strings)
817
+ code = Array.new # save Word objects here
818
+ symbols = Hash.new # symbol table
819
+ errors = Array.new # put error strings here
820
+ name = "unknown" # default program name
821
+ name += @@entries.size.to_s
822
+ symbols[:start] = 0 # default starting address
823
+
824
+ # Pass 1 -- create list of Word objects, build the symbol table:
825
+
826
+ strings.each_with_index do |line, lineno|
827
+ line.rstrip!
828
+ next if line.length == 0 # skip blank lines
829
+ if line[0] == ?; # comments have ; in column 0
830
+ if md = line.match(/;\s*name\s+(\w+)/) # does comment have program name?
831
+ name = md[1] # yes -- save it
832
+ end
833
+ next
834
+ end
835
+ if n = line.index(";") # if comment at end remove it
836
+ line.slice!(n..-1)
837
+ end
838
+ begin
839
+ label, op, a, b = MARS.parse(line)
840
+ # puts "parse '#{label}' '#{op}' '#{a}' '#{b}'"
841
+ instr = Word.new(op, a, b, lineno+1)
842
+ MARS.saveWord(instr, label, code, symbols)
843
+ rescue RedcodeSyntaxError
844
+ errors << " line #{lineno+1}: #{$!}"
845
+ end
846
+ end
847
+
848
+ # Pass 2 -- translate labels into ints on each instruction:
849
+ # TODO -- deal with expressions, e.g. "JMP label + 1"
850
+
851
+ code.each_with_index do |instr, loc|
852
+ if instr.op == "DAT" && instr.b.length == 0
853
+ instr.a, instr.b = instr.b, instr.a
854
+ end
855
+ begin
856
+ MARS.translate(instr.a, symbols, loc) or raise RedcodeSyntaxError, "unknown/illegal label"
857
+ MARS.translate(instr.b, symbols, loc) or raise RedcodeSyntaxError, "unknown/illegal label"
858
+ rescue RedcodeSyntaxError
859
+ errors << " line #{instr.lineno}: #{$!}"
860
+ end
861
+ end
862
+
863
+ return [name, code, symbols, errors]
864
+ end
865
+
866
+ # -------- End Assembler ----------
867
+
868
+
869
+ =begin rdoc
870
+ step() -- execute one instruction from each active program. If a thread dies remove the PC
871
+ from the array for that program. A program dies when its last thread dies.
872
+ =end
873
+
874
+ def MARS.step
875
+ if @@entries.empty?
876
+ puts "no programs loaded"
877
+ return 0
878
+ end
879
+
880
+ # A list of program ids that terminate on this cycle
881
+ done = []
882
+
883
+ # This loop gets the current instruction from each active program and executes it.
884
+ # The return value from the execute method is true if the thread executes a DAT
885
+ # instruction. The other way to stop the thread is if it has a runtime exception.
886
+
887
+ @@pcs.each_with_index do |pc, id|
888
+ loc = pc.next
889
+ instr = @@mem.fetch(loc)
890
+ printf("%04d: %s\n", pc.current[:addr], instr) if @@params[:tracing]
891
+
892
+ if @@drawing
893
+ @@drawing.updateCells(pc)
894
+ sleep(@@params[:pace])
895
+ end
896
+
897
+ begin
898
+ signal = instr.execute(pc, @@mem)
899
+ rescue MARSRuntimeException
900
+ puts "runtime error: #{$!} in instruction on line #{instr.lineno}"
901
+ signal = :halt
902
+ end
903
+
904
+ # If this thread halted delete the thread location from the program counter's list.
905
+ # The return value from kill_thread is the number of threads left -- if 0 the
906
+ # program is done.
907
+
908
+ if signal == :halt
909
+ if pc.kill_thread == 0
910
+ done << id # old C++ habit -- don't delete from container while iterating....
911
+ end
912
+ end
913
+
914
+ end
915
+
916
+ # Remove any newly terminated programs from the list of program counters, stop the machine
917
+ # when no programs are left.
918
+
919
+ done.each do |x|
920
+ @@pcs.slice!(x)
921
+ end
922
+
923
+ return @@pcs.size
924
+
925
+ end
926
+
927
+ =begin rdoc
928
+ run(n) -- run the programs, stopping when the number of surviving programs is n. For a
929
+ contest n will be 1, but for testing it can be 0. Another way to return from this method
930
+ is when a specified number of rounds of instruction executions have taken place.
931
+ =end
932
+
933
+ def MARS.run(nleft = 0)
934
+ nrounds = @@params[:maxRounds]
935
+ loop do
936
+ nprog = MARS.step
937
+ nrounds -= 1
938
+ break if nprog == nleft || nrounds <= 0
939
+ end
940
+ end
941
+
942
+ =begin rdoc
943
+ state() -- print info about the machine and any programs in memory
944
+ =end
945
+
946
+ def MARS.state
947
+ puts "MARS CPU with #{@@mem.size}-word memory"
948
+ if @@entries.length == 0
949
+ puts "no programs loaded"
950
+ else
951
+ for i in 0...@@entries.length
952
+ puts "Program: #{@@entries[i].name}"
953
+ puts " code: #{@@entries[i].code.inspect}"
954
+ puts " threads: #{@@pcs[i]}"
955
+ end
956
+ end
957
+ puts "Options:"
958
+ @@params.each do |key, val|
959
+ puts " #{key}: #{val}"
960
+ end
961
+ return true
962
+ end
963
+
964
+
965
+ =begin rdoc
966
+ set_option(key, val) -- set the value of one of the run-time options (valid
967
+ only when no programs are loaded)
968
+ =end
969
+
970
+ def MARS.set_option(key, val)
971
+ if ! @@entries.empty?
972
+ puts "Options can be set only when no programs are loaded (call 'reset' to clear programs)"
973
+ return false
974
+ end
975
+ case key
976
+ when :memSize
977
+ if val.class != Fixnum || val < 1024 || val > 16536
978
+ puts ":memSize must be an integer between 1024 and 16536"
979
+ return nil
980
+ end
981
+ when :maxRounds, :buffer
982
+ if val.class != Fixnum
983
+ puts ":#{key} must be an integer"
984
+ return nil
985
+ end
986
+ when :pace
987
+ if val.class != Float
988
+ puts ":#{key} must be an floating point number (e.g. 0.05)"
989
+ return nil
990
+ end
991
+ when :tracing
992
+ if ! (val.class == TrueClass || val.class == FalseClass)
993
+ puts ":tracing must be true or false"
994
+ return nil
995
+ end
996
+ else
997
+ puts "Unknown option: #{key}"
998
+ return nil
999
+ end
1000
+ @@params[key] = val
1001
+ end
1002
+
1003
+ =begin rdoc
1004
+ view() -- open a visual view of the machine state
1005
+ =end
1006
+
1007
+ def MARS.view
1008
+ require 'tk'
1009
+
1010
+ if ! defined? @@tkroot
1011
+ @@tkroot = TkRoot.new { title "MARS" }
1012
+ end
1013
+
1014
+ if @@drawing == nil
1015
+ @@drawing = Draw.new(@tkroot)
1016
+ @@threads = []
1017
+ @@threads << Thread.new() do
1018
+ Tk.mainloop
1019
+ end
1020
+ end
1021
+ end
1022
+
1023
+ def MARS.close_view
1024
+ @@threads[0].kill
1025
+ @@tkroot.destroy
1026
+ @@drawing = nil
1027
+ end
1028
+
1029
+ =begin rdoc
1030
+ Run a contest. Parameters are names of up to three programs. Make a Warrior
1031
+ object for each one, load them, and call run.
1032
+ =end
1033
+
1034
+ def MARS.contest(*args)
1035
+ MARS.reset
1036
+ if args.length < 1 || args.length > 3
1037
+ puts "Pass one, two, or three program names"
1038
+ return nil
1039
+ end
1040
+ args.each do |x|
1041
+ Warrior.load(Warrior.new(x))
1042
+ end
1043
+ MARS.run(1) # the 1 means stop when only 1 program left running
1044
+ end
1045
+
1046
+ =begin rdoc
1047
+ reset() -- stop the current contest; clear all PCs, reset memory to all nils
1048
+ =end
1049
+
1050
+ def MARS.reset
1051
+ @@pcs = Array.new
1052
+ @@mem = Memory.new(@@params[:memSize])
1053
+ @@entries = Array.new
1054
+ Warrior.reset
1055
+ @@drawing.reset if @@drawing
1056
+ end
1057
+
1058
+ =begin rdoc
1059
+ Print the code for one of the Redcode source programs.
1060
+ =end
1061
+
1062
+ def MARS.listing(prog, dest = nil)
1063
+ filename = prog.to_s
1064
+ filename += ".txt" unless filename =~ /\.txt$/
1065
+ filename = File.join(@@dataDirectory, filename)
1066
+ dest = STDOUT if dest.nil?
1067
+ if !File.exists?(filename)
1068
+ puts "File not found: #{filename}"
1069
+ else
1070
+ File.open(filename).each do |line|
1071
+ dest.puts line.chomp
1072
+ end
1073
+ end
1074
+ return nil
1075
+ end
1076
+
1077
+ =begin rdoc
1078
+ Save a copy of a Redcode source program; if no output file name specified
1079
+ make a file name from the program name.
1080
+ =end
1081
+
1082
+ def MARS.checkout(prog, filename = nil)
1083
+ filename = prog.to_s + ".txt" if filename.nil?
1084
+ dest = File.open(filename, "w")
1085
+ MARS.listing(prog, dest)
1086
+ dest.close
1087
+ puts "Copy of #{prog} saved in #{filename}"
1088
+ end
1089
+
1090
+ =begin rdoc
1091
+ Print a list of programs
1092
+ =end
1093
+
1094
+ def MARS.dir
1095
+ puts "Redcode programs in #{@@dataDirectory}:"
1096
+ Dir.open(@@dataDirectory).each do |file|
1097
+ next if file[0] == ?.
1098
+ file.slice!(/\.txt/)
1099
+ puts " " + file
1100
+ end
1101
+ return nil
1102
+ end
1103
+
1104
+ end # class MARS
1105
+
1106
+
1107
+ =begin rdoc
1108
+ Make a test machine
1109
+ =end
1110
+
1111
+ def make_test_machine(file, size = nil)
1112
+ begin
1113
+ return MiniMARS.new(file, size)
1114
+ rescue Exception => e
1115
+ puts "Failed to make test machine: #{$!}"
1116
+ return nil
1117
+ end
1118
+ end
1119
+
1120
+ # -- Initializations -- These are "global" vars in the outer MARSLab scope that are
1121
+ # accessible to all the classes and modules defined inside MARSLab
1122
+
1123
+ @@dataDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'mars')
1124
+
1125
+ @@opcodes = ["DAT", "MOV", "ADD", "SUB", "JMP", "JMZ", "JMN", "DJN", "CMP", "SPL", "END", "SLT", "EQU"]
1126
+ @@modes = "@<#"
1127
+
1128
+ @@maxEntries = 3
1129
+ @@displayMemSize = 4096
1130
+
1131
+ @@params = {
1132
+ :maxRounds => 1000,
1133
+ :memSize => @@displayMemSize,
1134
+ :buffer => 100,
1135
+ :tracing => false,
1136
+ :pace => 0.01,
1137
+ }
1138
+
1139
+ @@drawing = nil
1140
+
1141
+ @@pcs = Array.new
1142
+ @@mem = Memory.new(@@params[:memSize])
1143
+ @@entries = Array.new
1144
+
1145
+ end # MARSLab
1146
+
1147
+ end # RubyLabs