n65 0.5.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 +23 -0
- data/Gemfile +4 -0
- data/LICENSE +340 -0
- data/README.md +126 -0
- data/Rakefile +2 -0
- data/bin/n65 +11 -0
- data/data/opcodes.yaml +1030 -0
- data/examples/beep.asm +24 -0
- data/examples/mario2.asm +260 -0
- data/examples/mario2.char +0 -0
- data/examples/music_driver.asm +202 -0
- data/examples/noise.asm +93 -0
- data/examples/pulse_chord.asm +213 -0
- data/images/assembler_demo.png +0 -0
- data/lib/n65.rb +243 -0
- data/lib/n65/directives/ascii.rb +42 -0
- data/lib/n65/directives/bytes.rb +102 -0
- data/lib/n65/directives/dw.rb +86 -0
- data/lib/n65/directives/enter_scope.rb +55 -0
- data/lib/n65/directives/exit_scope.rb +35 -0
- data/lib/n65/directives/inc.rb +67 -0
- data/lib/n65/directives/incbin.rb +51 -0
- data/lib/n65/directives/ines_header.rb +53 -0
- data/lib/n65/directives/label.rb +46 -0
- data/lib/n65/directives/org.rb +47 -0
- data/lib/n65/directives/segment.rb +45 -0
- data/lib/n65/directives/space.rb +46 -0
- data/lib/n65/front_end.rb +90 -0
- data/lib/n65/instruction.rb +308 -0
- data/lib/n65/instruction_base.rb +29 -0
- data/lib/n65/memory_space.rb +150 -0
- data/lib/n65/opcodes.rb +9 -0
- data/lib/n65/parser.rb +85 -0
- data/lib/n65/regexes.rb +33 -0
- data/lib/n65/symbol_table.rb +198 -0
- data/lib/n65/version.rb +3 -0
- data/n65.gemspec +23 -0
- data/nes_lib/nes.sym +105 -0
- data/test/test_memory_space.rb +82 -0
- data/test/test_symbol_table.rb +238 -0
- data/utils/midi/Makefile +3 -0
- data/utils/midi/c_scale.mid +0 -0
- data/utils/midi/convert +0 -0
- data/utils/midi/guitar.mid +0 -0
- data/utils/midi/include/event.h +93 -0
- data/utils/midi/include/file.h +57 -0
- data/utils/midi/include/helpers.h +14 -0
- data/utils/midi/include/track.h +45 -0
- data/utils/midi/lil_melody.mid +0 -0
- data/utils/midi/mi_feabhra.mid +0 -0
- data/utils/midi/midi_to_nes.rb +204 -0
- data/utils/midi/source/convert.cpp +16 -0
- data/utils/midi/source/event.cpp +96 -0
- data/utils/midi/source/file.cpp +37 -0
- data/utils/midi/source/helpers.cpp +46 -0
- data/utils/midi/source/track.cpp +37 -0
- data/utils/opcode_table_to_yaml.rb +91 -0
- metadata +133 -0
data/examples/noise.asm
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
;------------------------------------------------------------------------------
|
2
|
+
; This is a direct port of Michael Martin's tutorial project for NES101
|
3
|
+
; With some modifications to the tile map, and extra comments, and ported to
|
4
|
+
; suit my assembler. - Saf
|
5
|
+
; See:
|
6
|
+
; http://hackipedia.org/Platform/Nintendo/NES/tutorial,%20NES%20programming%20101/NES101.html
|
7
|
+
;
|
8
|
+
;;;;
|
9
|
+
; Create an iNES header
|
10
|
+
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
|
11
|
+
|
12
|
+
|
13
|
+
;;;;
|
14
|
+
; Setup the interrupt vectors
|
15
|
+
.org $FFFA
|
16
|
+
.dw vblank
|
17
|
+
.dw main
|
18
|
+
.dw irq
|
19
|
+
|
20
|
+
|
21
|
+
.org $C000
|
22
|
+
;;;;
|
23
|
+
; Here is our code entry point, which we'll call main.
|
24
|
+
.scope main
|
25
|
+
; Disable interrupts and decimal flag
|
26
|
+
sei
|
27
|
+
cld
|
28
|
+
|
29
|
+
; Wait for 2 vblanks
|
30
|
+
wait_vb1:
|
31
|
+
lda $2002
|
32
|
+
bpl wait_vb1
|
33
|
+
wait_vb2:
|
34
|
+
lda $2002
|
35
|
+
bpl wait_vb2
|
36
|
+
|
37
|
+
; Now we want to initialize the hardware to a known state
|
38
|
+
lda #$00
|
39
|
+
ldx #$00
|
40
|
+
clear_segments:
|
41
|
+
sta $00, X
|
42
|
+
sta $0100, X
|
43
|
+
sta $0200, X
|
44
|
+
sta $0300, X
|
45
|
+
sta $0400, X
|
46
|
+
sta $0500, X
|
47
|
+
sta $0600, X
|
48
|
+
sta $0700, X
|
49
|
+
inx
|
50
|
+
bne clear_segments
|
51
|
+
|
52
|
+
; Reset the stack pointer
|
53
|
+
ldx #$FF
|
54
|
+
txs
|
55
|
+
|
56
|
+
; Disable all graphics.
|
57
|
+
lda #$00
|
58
|
+
sta $2000
|
59
|
+
sta $2001
|
60
|
+
|
61
|
+
; Init APU
|
62
|
+
ldx #$0F
|
63
|
+
stx $4015
|
64
|
+
|
65
|
+
; Turn on noise tone
|
66
|
+
ldx #$85
|
67
|
+
stx $400E
|
68
|
+
|
69
|
+
; Set volume to max
|
70
|
+
ldx #$3F
|
71
|
+
stx $400C
|
72
|
+
|
73
|
+
; Load Length counter
|
74
|
+
ldx #$01
|
75
|
+
stx $400F
|
76
|
+
|
77
|
+
; Resume interrupts and loop here forever
|
78
|
+
cli
|
79
|
+
forever:
|
80
|
+
jmp forever
|
81
|
+
.
|
82
|
+
|
83
|
+
|
84
|
+
;;;;
|
85
|
+
; Update everything on every vblank
|
86
|
+
vblank:
|
87
|
+
rti
|
88
|
+
|
89
|
+
|
90
|
+
;;;;
|
91
|
+
; Don't do anything on IRQ
|
92
|
+
irq:
|
93
|
+
rti
|
@@ -0,0 +1,213 @@
|
|
1
|
+
;;;;
|
2
|
+
; Create an iNES header
|
3
|
+
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
|
4
|
+
|
5
|
+
|
6
|
+
;;;;
|
7
|
+
; Include all the symbols in the nes library
|
8
|
+
.inc <nes.sym>
|
9
|
+
|
10
|
+
|
11
|
+
;;;;
|
12
|
+
; Open the prog section bank 0
|
13
|
+
.segment prog 0
|
14
|
+
|
15
|
+
|
16
|
+
;;;;
|
17
|
+
; Structure to keep track of input
|
18
|
+
.org $0000
|
19
|
+
.scope controller_state
|
20
|
+
.space b 1
|
21
|
+
.space a 1
|
22
|
+
.
|
23
|
+
|
24
|
+
|
25
|
+
;;;;
|
26
|
+
; Setup the interrupt vectors
|
27
|
+
.org $FFFA
|
28
|
+
.dw vblank
|
29
|
+
.dw reset
|
30
|
+
.dw irq
|
31
|
+
|
32
|
+
|
33
|
+
;;;;
|
34
|
+
; Here is our code entry point
|
35
|
+
.org $C000
|
36
|
+
.scope reset
|
37
|
+
sei ; SEt Interrupt (Disables them)
|
38
|
+
cld ; CLear Decimal Mode
|
39
|
+
|
40
|
+
ldx #$ff
|
41
|
+
txs ; Set the stack pointer
|
42
|
+
|
43
|
+
ldx #$00
|
44
|
+
stx nes.ppu.control
|
45
|
+
stx nes.ppu.mask ; Disable Vblank & Rendering
|
46
|
+
|
47
|
+
jsr zero_apu ; Zero all APU registers
|
48
|
+
|
49
|
+
; We need to wait for at least 2 Vblanks to happen
|
50
|
+
; before we know the PPU has stabilized at startup
|
51
|
+
; Here we wait for the first one.
|
52
|
+
wait_vblank1:
|
53
|
+
bit nes.ppu.status
|
54
|
+
bpl wait_vblank1
|
55
|
+
|
56
|
+
; Before we wait for the second vblank, lets
|
57
|
+
; zero all of the working RAM $0 to $800
|
58
|
+
; The $200s are sprite OAM, and should be set to $ff
|
59
|
+
clear_ram:
|
60
|
+
lda #$00
|
61
|
+
sta $00, x
|
62
|
+
sta $100, x
|
63
|
+
sta $300, x
|
64
|
+
sta $400, x
|
65
|
+
sta $500, x
|
66
|
+
sta $600, x
|
67
|
+
sta $700, x
|
68
|
+
lda #$ff
|
69
|
+
sta $200, x
|
70
|
+
inx
|
71
|
+
bne clear_ram
|
72
|
+
|
73
|
+
; Now wait for the second vblank
|
74
|
+
wait_vblank2:
|
75
|
+
bit nes.ppu.status
|
76
|
+
bpl wait_vblank2
|
77
|
+
|
78
|
+
jsr initialize
|
79
|
+
|
80
|
+
forever:
|
81
|
+
jmp forever
|
82
|
+
rti
|
83
|
+
.
|
84
|
+
|
85
|
+
|
86
|
+
;;;;
|
87
|
+
; Initialize everything
|
88
|
+
.scope initialize
|
89
|
+
; Enable pulse1 and pulse2 in the APU
|
90
|
+
lda #%00000011
|
91
|
+
sta nes.apu.channel_enable
|
92
|
+
|
93
|
+
; Initialize the controller states
|
94
|
+
lda #$00
|
95
|
+
sta controller_state.a zp
|
96
|
+
sta controller_state.b zp
|
97
|
+
|
98
|
+
; Reenable interrupts, Turn Vblank back on
|
99
|
+
lda #%10000000
|
100
|
+
sta nes.ppu.control
|
101
|
+
cli
|
102
|
+
rts
|
103
|
+
.
|
104
|
+
|
105
|
+
|
106
|
+
;;;;
|
107
|
+
; VBlank is called 60 times per second
|
108
|
+
.scope vblank
|
109
|
+
jsr read_input
|
110
|
+
rti
|
111
|
+
.
|
112
|
+
|
113
|
+
|
114
|
+
;;;;
|
115
|
+
; IRQ, we are not using
|
116
|
+
.scope irq
|
117
|
+
rti
|
118
|
+
.
|
119
|
+
|
120
|
+
|
121
|
+
;;;;
|
122
|
+
; Zero all the APU registers
|
123
|
+
.scope zero_apu
|
124
|
+
lda #$00
|
125
|
+
ldx #$00
|
126
|
+
loop:
|
127
|
+
sta $4000, x
|
128
|
+
inx
|
129
|
+
cpx $18
|
130
|
+
bne loop
|
131
|
+
rts
|
132
|
+
.
|
133
|
+
|
134
|
+
|
135
|
+
;;;;
|
136
|
+
; Read input from controller 1
|
137
|
+
.scope read_input
|
138
|
+
lda #$01 ; strobe joypad
|
139
|
+
sta nes.controller1
|
140
|
+
lda #$00
|
141
|
+
sta nes.controller1
|
142
|
+
|
143
|
+
; Handle Button A
|
144
|
+
lda nes.controller1
|
145
|
+
and #$01
|
146
|
+
beq update_a_state
|
147
|
+
|
148
|
+
; A is pressed, but did it just change to being pressed now?
|
149
|
+
ldx controller_state.a zp
|
150
|
+
bne update_a_state
|
151
|
+
|
152
|
+
; do the thing A does
|
153
|
+
jsr play_e329
|
154
|
+
|
155
|
+
update_a_state:
|
156
|
+
sta controller_state.a zp
|
157
|
+
|
158
|
+
; Handle Button B
|
159
|
+
lda nes.controller1
|
160
|
+
and #$01
|
161
|
+
beq update_b_state
|
162
|
+
|
163
|
+
; B is pressed, but did it just change to being pressed now?
|
164
|
+
ldx controller_state.b zp
|
165
|
+
bne update_b_state
|
166
|
+
|
167
|
+
; Do the thing B does
|
168
|
+
jsr play_a220
|
169
|
+
|
170
|
+
update_b_state:
|
171
|
+
sta controller_state.b zp
|
172
|
+
|
173
|
+
rts
|
174
|
+
.
|
175
|
+
|
176
|
+
|
177
|
+
;;;;
|
178
|
+
;; This will play an A 220hz note
|
179
|
+
;; On the pulse1 generator
|
180
|
+
.scope play_a220
|
181
|
+
pha
|
182
|
+
lda #%10011111
|
183
|
+
sta nes.apu.pulse1.control
|
184
|
+
|
185
|
+
lda #%11111011
|
186
|
+
sta nes.apu.pulse1.ft
|
187
|
+
|
188
|
+
lda #%11111001
|
189
|
+
sta nes.apu.pulse1.ct
|
190
|
+
|
191
|
+
pla
|
192
|
+
rts
|
193
|
+
.
|
194
|
+
|
195
|
+
|
196
|
+
;;;;
|
197
|
+
;; This will play an E 329.63hz note
|
198
|
+
;; On the pulse2 generator
|
199
|
+
.scope play_e329
|
200
|
+
pha
|
201
|
+
lda #%10011111
|
202
|
+
sta nes.apu.pulse2.control
|
203
|
+
|
204
|
+
lda #%01010010
|
205
|
+
sta nes.apu.pulse2.ft
|
206
|
+
|
207
|
+
lda #%11111001
|
208
|
+
sta nes.apu.pulse2.ct
|
209
|
+
|
210
|
+
pla
|
211
|
+
rts
|
212
|
+
.
|
213
|
+
|
Binary file
|
data/lib/n65.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
require_relative 'n65/version'
|
2
|
+
require_relative 'n65/symbol_table'
|
3
|
+
require_relative 'n65/memory_space'
|
4
|
+
require_relative 'n65/parser'
|
5
|
+
|
6
|
+
module N65
|
7
|
+
|
8
|
+
class Assembler
|
9
|
+
attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises
|
10
|
+
|
11
|
+
##### Custom exceptions
|
12
|
+
class AddressOutOfRange < StandardError; end
|
13
|
+
class InvalidSegment < StandardError; end
|
14
|
+
class WriteOutOfBounds < StandardError; end
|
15
|
+
class INESHeaderAlreadySet < StandardError; end
|
16
|
+
class FileNotFound < StandardError; end
|
17
|
+
|
18
|
+
|
19
|
+
####
|
20
|
+
## Assemble from an asm file to a nes ROM
|
21
|
+
def self.from_file(infile, outfile)
|
22
|
+
fail(FileNotFound, infile) unless File.exists?(infile)
|
23
|
+
|
24
|
+
assembler = self.new
|
25
|
+
program = File.read(infile)
|
26
|
+
|
27
|
+
puts "Building #{infile}"
|
28
|
+
## Process each line in the file
|
29
|
+
program.split(/\n/).each_with_index do |line, line_number|
|
30
|
+
begin
|
31
|
+
assembler.assemble_one_line(line)
|
32
|
+
rescue StandardError => e
|
33
|
+
STDERR.puts("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
|
34
|
+
exit(1)
|
35
|
+
end
|
36
|
+
print '.'
|
37
|
+
end
|
38
|
+
puts
|
39
|
+
|
40
|
+
## Second pass to resolve any missing symbols.
|
41
|
+
print "Second pass, resolving symbols..."
|
42
|
+
assembler.fulfill_promises
|
43
|
+
puts " Done."
|
44
|
+
|
45
|
+
## Let's not export the symbol table to a file anymore
|
46
|
+
## Will add an option for this later.
|
47
|
+
#print "Writing symbol table to #{outfile}.yaml..."
|
48
|
+
#File.open("#{outfile}.yaml", 'w') do |fp|
|
49
|
+
#fp.write(assembler.symbol_table.export_to_yaml)
|
50
|
+
#end
|
51
|
+
#puts "Done."
|
52
|
+
|
53
|
+
## For right now, let's just emit the first prog bank
|
54
|
+
File.open(outfile, 'w') do |fp|
|
55
|
+
fp.write(assembler.emit_binary_rom)
|
56
|
+
end
|
57
|
+
puts "All Done :)"
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
####
|
62
|
+
## Initialize with a bank 1 of prog space for starters
|
63
|
+
def initialize
|
64
|
+
@ines_header = nil
|
65
|
+
@program_counter = 0x0
|
66
|
+
@current_segment = :prog
|
67
|
+
@current_bank = 0x0
|
68
|
+
@symbol_table = SymbolTable.new
|
69
|
+
@promises = []
|
70
|
+
@virtual_memory = {
|
71
|
+
:prog => [MemorySpace.create_prog_rom],
|
72
|
+
:char => []
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
####
|
78
|
+
## Return an object that contains the assembler's current state
|
79
|
+
def get_current_state
|
80
|
+
saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank
|
81
|
+
saved_scope = symbol_table.scope_stack.dup
|
82
|
+
OpenStruct.new(program_counter: saved_program_counter, segment: saved_segment, bank: saved_bank, scope: saved_scope)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
####
|
87
|
+
## Set the current state from an OpenStruct
|
88
|
+
def set_current_state(struct)
|
89
|
+
@program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank
|
90
|
+
symbol_table.scope_stack = struct.scope.dup
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
####
|
95
|
+
## This is the main assemble method, it parses one line into an object
|
96
|
+
## which when given a reference to this assembler, controls the assembler
|
97
|
+
## itself through public methods, executing assembler directives, and
|
98
|
+
## emitting bytes into our virtual memory spaces. Empty lines or lines
|
99
|
+
## with only comments parse to nil, and we just ignore them.
|
100
|
+
def assemble_one_line(line)
|
101
|
+
parsed_object = Parser.parse(line)
|
102
|
+
|
103
|
+
unless parsed_object.nil?
|
104
|
+
exec_result = parsed_object.exec(self)
|
105
|
+
|
106
|
+
## If we have returned a promise save it for the second pass
|
107
|
+
@promises << exec_result if exec_result.kind_of?(Proc)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
####
|
113
|
+
## This will empty out our promise queue and try to fullfil operations
|
114
|
+
## that required an undefined symbol when first encountered.
|
115
|
+
def fulfill_promises
|
116
|
+
while promise = @promises.pop
|
117
|
+
promise.call
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
####
|
123
|
+
## This rewinds the state of the assembler, so a promise can be
|
124
|
+
## executed with a previous state, for example if we can't resolve
|
125
|
+
## a symbol right now, and want to try during the second pass
|
126
|
+
def with_saved_state(&block)
|
127
|
+
## Save the current state of the assembler
|
128
|
+
old_state = get_current_state
|
129
|
+
|
130
|
+
lambda do
|
131
|
+
|
132
|
+
## Set the assembler state back to the old state and run the block like that
|
133
|
+
set_current_state(old_state)
|
134
|
+
block.call(self)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
####
|
140
|
+
## Write to memory space. Typically, we are going to want to write
|
141
|
+
## to the location of the current PC, current segment, and current bank.
|
142
|
+
## Bounds check is inside MemorySpace#write
|
143
|
+
def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank)
|
144
|
+
memory_space = get_virtual_memory_space(segment, bank)
|
145
|
+
memory_space.write(pc, bytes)
|
146
|
+
@program_counter += bytes.size
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
####
|
151
|
+
## Set the iNES header
|
152
|
+
def set_ines_header(ines_header)
|
153
|
+
fail(INESHeaderAlreadySet) unless @ines_header.nil?
|
154
|
+
@ines_header = ines_header
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
####
|
159
|
+
## Set the program counter
|
160
|
+
def program_counter=(address)
|
161
|
+
fail(AddressOutOfRange) unless address_within_range?(address)
|
162
|
+
@program_counter = address
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
####
|
167
|
+
## Set the current segment, prog or char.
|
168
|
+
def current_segment=(segment)
|
169
|
+
segment = segment.to_sym
|
170
|
+
unless valid_segment?(segment)
|
171
|
+
fail(InvalidSegment, "#{segment} is not a valid segment. Try prog or char")
|
172
|
+
end
|
173
|
+
@current_segment = segment
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
####
|
178
|
+
## Set the current bank, create it if it does not exist
|
179
|
+
def current_bank=(bank_number)
|
180
|
+
memory_space = get_virtual_memory_space(@current_segment, bank_number)
|
181
|
+
if memory_space.nil?
|
182
|
+
@virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment)
|
183
|
+
end
|
184
|
+
@current_bank = bank_number
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
####
|
189
|
+
## Emit a binary ROM
|
190
|
+
def emit_binary_rom
|
191
|
+
progs = @virtual_memory[:prog]
|
192
|
+
chars = @virtual_memory[:char]
|
193
|
+
puts "iNES Header"
|
194
|
+
puts "+ #{progs.size} PROG ROM bank#{progs.size != 1 ? 's' : ''}"
|
195
|
+
puts "+ #{chars.size} CHAR ROM bank#{chars.size != 1 ? 's' : ''}"
|
196
|
+
|
197
|
+
rom_size = 0x10
|
198
|
+
rom_size += MemorySpace::BankSizes[:prog] * progs.size
|
199
|
+
rom_size += MemorySpace::BankSizes[:char] * chars.size
|
200
|
+
|
201
|
+
puts "= Output ROM will be #{rom_size} bytes"
|
202
|
+
rom = MemorySpace.new(rom_size, :rom)
|
203
|
+
|
204
|
+
offset = 0x0
|
205
|
+
offset += rom.write(0x0, @ines_header.emit_bytes)
|
206
|
+
|
207
|
+
progs.each do |prog|
|
208
|
+
offset += rom.write(offset, prog.read(0x8000, MemorySpace::BankSizes[:prog]))
|
209
|
+
end
|
210
|
+
|
211
|
+
chars.each do |char|
|
212
|
+
offset += rom.write(offset, char.read(0x0, MemorySpace::BankSizes[:char]))
|
213
|
+
end
|
214
|
+
rom.emit_bytes.pack('C*')
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
|
221
|
+
####
|
222
|
+
## Get virtual memory space
|
223
|
+
def get_virtual_memory_space(segment, bank_number)
|
224
|
+
@virtual_memory[segment][bank_number]
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
####
|
229
|
+
## Is this a 16-bit address within range?
|
230
|
+
def address_within_range?(address)
|
231
|
+
address >= 0 && address < 2**16
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
####
|
236
|
+
## Is this a valid segment?
|
237
|
+
def valid_segment?(segment)
|
238
|
+
[:prog, :char].include?(segment)
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|