rubylabs 0.5.5 → 0.6.2

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.
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