trac_lang 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+
2
+ module TracLang
3
+
4
+ # Executable TRAC commands
5
+ class Executor
6
+
7
+ # Initialize executor, giving block to store forms in
8
+ def initialize(d = nil)
9
+ @parser = Parser.new
10
+ @dispatch = d || Dispatch.new
11
+ end
12
+
13
+ # Executes TRAC from an interactive prompt.
14
+ def prompt
15
+ puts "TRAC Emulator #{VERSION}"
16
+ puts
17
+ catch :done do
18
+ loop do
19
+ idle = "#(PS,#(RS)(\n))"
20
+ catch :reset do
21
+ execute(idle)
22
+ end
23
+ if @dispatch.trace
24
+ @dispatch.trace = false
25
+ puts 'Exiting trace...'
26
+ end
27
+ end
28
+ end
29
+ puts 'Exiting...'
30
+ puts
31
+ end
32
+
33
+ # Executes TRAC from a file.
34
+ def load_file(filename)
35
+ full_file = File.expand_path(filename, @dispatch.save_dir)
36
+ save_dir(full_file)
37
+ begin
38
+ File.new(full_file, 'r').each do |line|
39
+ break unless load(full_file, $., line)
40
+ end
41
+ rescue
42
+ puts "Error loading file #{full_file}"
43
+ end
44
+ restore_dir
45
+ end
46
+
47
+ # Saves original save_dir from dispatch, set dispatch save_dir to
48
+ # dir of given filename.
49
+ def save_dir(filename)
50
+ @save_save_dir = @dispatch.save_dir
51
+ @dispatch.save_dir = File.dirname(filename)
52
+ end
53
+
54
+ # Restores saved directory.
55
+ def restore_dir()
56
+ @dispatch.save_dir = @save_save_dir
57
+ end
58
+
59
+ # Executes a line of TRAC loaded from a file. If an error occurs, an error
60
+ # message will be printed with the line number and filename.
61
+ def load(filename, lineno, line)
62
+ @code ||= ''
63
+ to_exe = ''
64
+ catch :reset do
65
+ @code += line
66
+ i = @code.index(@dispatch.meta)
67
+ # explanation of odd indexing:
68
+ # slice everything off code including meta character
69
+ # then execute that slice, without the meta character
70
+ if i
71
+ to_exe = @code.slice!(0..i)[0...-1]
72
+ execute(to_exe)
73
+ end
74
+ return true
75
+ end
76
+ puts to_exe
77
+ puts "Error on or before line #{lineno} of #{filename}"
78
+ return false
79
+ end
80
+
81
+ # Executes a string of TRAC. If we are in trace mode, wait for user input
82
+ # after executing string.
83
+ def execute(str)
84
+ @parser.parse(str) do |to_call|
85
+ if @dispatch.trace
86
+ puts to_call
87
+ c = ImmediateRead.new.getch
88
+ throw :reset unless c == "\n"
89
+ puts
90
+ end
91
+ @dispatch.dispatch(to_call)
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,58 @@
1
+
2
+ module TracLang
3
+
4
+ # Model of TRAC neutral string expression. An expression is an array of strings,
5
+ # corresponding to the arguments of a TRAC expression. Expressions are active
6
+ # if they were called by #(...) and not active (neutral) if they were
7
+ # called by ##(...).
8
+ class Expression
9
+
10
+ # Flag to tell if this expression is active or neutral. The result of
11
+ # active expressions are copied to the active string, while the result of
12
+ # neutral expressions are copied to the enclosing expression.
13
+ attr_accessor :active
14
+
15
+ # List of arguments for this expression.
16
+ attr_accessor :args
17
+
18
+ alias_method :active?, :active
19
+
20
+ # Creates new active expression.
21
+ def initialize
22
+ @args = ['']
23
+ @active = true
24
+ end
25
+
26
+ # Command for TRAC processor.
27
+ def command
28
+ @args[0].downcase.to_sym
29
+ end
30
+
31
+ # Arguments for TRAC command.
32
+ def trac_args
33
+ @args[1..-1]
34
+ end
35
+
36
+ # Adds a string to current argument of the expression.
37
+ def concat(str)
38
+ @args.last.concat str
39
+ end
40
+
41
+ # Signals a new argument is starting.
42
+ def newarg
43
+ @args.push ''
44
+ end
45
+
46
+ # Returns current number of arguments in expression.
47
+ def size
48
+ @args.size
49
+ end
50
+
51
+ # String version of expression, used when trace is on
52
+ def to_s
53
+ (@active ? '#/' : '##/') + @args.join('*') + '/'
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,253 @@
1
+
2
+ require_relative "decimal"
3
+
4
+ # A form is a defined string in TRAC, along with spaces in it for the
5
+ # insertion of parameters.
6
+ class TracLang::Form
7
+
8
+ # Error when trying to read form off the end of its string.
9
+ class EndOfStringError < StandardError; end
10
+
11
+ # Segment positions of form. This is stored as an array of arrays.
12
+ # Each position in the array corresponds to a character in the form
13
+ # string, with an additional entry at the end. If the entry is non-empty
14
+ # it will have a list of the segment numbers at that location.
15
+ attr_reader :segments
16
+
17
+ # String value of form.
18
+ attr_reader :value
19
+
20
+ # Creates a new form with the given value.
21
+ def initialize(value)
22
+ @value = value
23
+ @segments = Array.new(@value.length + 1) { [] }
24
+ # character pointer is the index into the segments array
25
+ @cp = 0
26
+ # segment pointer is the index into @segments[@cp]
27
+ # if @sp == @segments[@cp].lenth then it is pointing to @value[@cp]
28
+ @sp = 0
29
+ end
30
+
31
+ # Finds the given search string in the string portion of the form, starting
32
+ # at the given space. A successful match cannot span segment gaps.
33
+ def find(search, start = 0)
34
+ loop do
35
+ i = @value.index(search, start)
36
+ return nil unless i
37
+ # don't find over segment boundaries
38
+ boundary = @segments.slice(i + 1, search.length - 1)
39
+ unless boundary.all? { |v| v.empty? }
40
+ start = i + 1
41
+ next
42
+ end
43
+ return i
44
+ end
45
+ end
46
+
47
+ # Adds segement gaps for one punch.
48
+ def punch(punch_in, n)
49
+ return if punch_in.empty?
50
+ start = 0
51
+ punch = punch_in.dup
52
+ len = punch.length
53
+ loop do
54
+ found = find(punch)
55
+ break unless found
56
+ if @segments[found].empty?
57
+ @segments.slice!(found, len)
58
+ @segments[found].unshift(n)
59
+ else
60
+ @segments[found].push(n)
61
+ @segments[found] += @segments[found + len]
62
+ @segments.slice!(found + 1, len)
63
+ end
64
+ @value.slice!(found, len)
65
+ end
66
+ end
67
+
68
+ # Add segment gaps for multiple punches.
69
+ def segment_string(*punches)
70
+ punches.each.with_index { |p, n| punch(p, n + 1) }
71
+ end
72
+
73
+ # Increments the character pointer by the given amount.
74
+ def increment(n = 1)
75
+ @cp += n
76
+ @cp = @value.length if @cp > @value.length
77
+ @cp = 0 if @cp < 0
78
+ @sp = n > 0 ? 0 : @segments[@cp].length
79
+ end
80
+
81
+ # Returns the pointers to the start of the form.
82
+ def call_return(*)
83
+ @sp = 0
84
+ @cp = 0
85
+ end
86
+
87
+ # Returns the character being pointed to and moves the pointer
88
+ # one unit to the right. Raises a EndOfStringError if the pointer
89
+ # is already at the end of the form.
90
+ def call_character(*)
91
+ raise EndOfStringError if @cp == @value.length
92
+ result = @value[@cp, 1]
93
+ increment
94
+ result
95
+ end
96
+
97
+ # Returns the given number of characters starting at the current
98
+ # pointer. If the number is negative, returns characters before
99
+ # the current pointer, but in the same order the characters are in
100
+ # the form. Raises an EndOfStringError if a negative number is given
101
+ # and you are at the start of the form, or if a positive number is
102
+ # given and you are at the end of the form. A value of zero can be
103
+ # given to test where the pointer is without changing it.
104
+ def call_n(nstr, *)
105
+ tn = TracLang::Decimal.new(nstr)
106
+ n = tn.value
107
+ if tn.negative?
108
+ raise EndOfStringError if @cp == 0 && @sp == 0
109
+ # move left of seg gaps
110
+ @sp = 0
111
+ return '' if n == 0
112
+ raise EndOfStringError if @cp == 0
113
+ n = -@cp if n < -@cp
114
+ result = @value.slice(@cp + n, -n)
115
+ increment(n)
116
+ else
117
+ raise EndOfStringError if @value.length - @cp == 0 && @sp == @segments[@cp].length
118
+ # move right of seg gaps
119
+ @sp = @segments[@cp].length
120
+ return '' if n == 0
121
+ raise EndOfStringError if @value.length - @cp == 0
122
+ n = @value.length - @cp if n > @value.length - @cp
123
+ result = @value.slice(@cp, n)
124
+ increment(n)
125
+ end
126
+ result
127
+ end
128
+
129
+ # Searches for the given string in the form. If found, returns all
130
+ # characters between the current pointer and the start of the match,
131
+ # while moving the character pointer past the match. Raises an
132
+ # EndOfStringError if you are at the end of the form or no match is
133
+ # found. An empty search string counts as always not matching.
134
+ def in_neutral(search, *)
135
+ raise EndOfStringError if @cp == @value.length || search.empty?
136
+ found = find(search, @cp)
137
+ if found
138
+ result = @value[@cp...found]
139
+ increment(found - @cp + search.length)
140
+ return result
141
+ else
142
+ # form pointer is not moved if not found
143
+ raise EndOfStringError
144
+ end
145
+ end
146
+
147
+ # Returns characters from the given position to the next segment gap.
148
+ def find_chars(start)
149
+ # don't test start position because you might be at the end of the segment list
150
+ len = 1 + @segments[(start + 1)..-2].take_while { |s| s.empty? }.count
151
+ @value.slice(start, len)
152
+ end
153
+
154
+ # Returns characters between the current pointer and the
155
+ # next segment gap.
156
+ def call_segment(*)
157
+ raise EndOfStringError if @cp == @value.length + 1 # would this ever be true?
158
+ # on a character
159
+ if @sp == @segments[@cp].length # may be zero
160
+ raise EndOfStringError if @cp == @value.length
161
+ result = find_chars(@cp)
162
+ @cp += result.length
163
+ # need check if you're at end of string
164
+ @sp = @segments[@cp].empty? ? 0 : 1
165
+ # else within segment list
166
+ else
167
+ result = ''
168
+ @sp += 1
169
+ end
170
+ result
171
+ end
172
+
173
+ # Returns form string with segment gaps filled with the given arguments.
174
+ def call_lookup(*args)
175
+ trimmed = @segments.dup
176
+ # trim off everything before current pointer
177
+ trimmed.slice!(0...@cp) unless @cp == 0
178
+ trimmed[0].slice!(0...@sp) unless trimmed[0].empty? || @sp == 0
179
+ trimmed.map.with_index do |a, i|
180
+ a.map { |v| args[v - 1] || '' }.join + (@value[@cp + i] || '')
181
+ end.join
182
+ end
183
+
184
+ # Checks if any punches have been done on this form.
185
+ def punched
186
+ @segments.any? { |s| !s.empty? }
187
+ end
188
+
189
+ # Finds the biggest punch index.
190
+ def max_punch
191
+ @segments.map { |s| s.max }.compact.max
192
+ end
193
+
194
+ # Tests if matched pair of symbols is used anywhere in this form. Used to
195
+ # find an unused pair for writing this form to a file.
196
+ def matched_pair_used?(open, close)
197
+ max_punch.times.map { |i| "#{open}#{i + 1}#{close}" }.any? { |s| @value.include?(s) }
198
+ end
199
+
200
+ # Test if given special character is used anywhere in this form. Used
201
+ # to find an unused special character for writing this form to a file.
202
+ def special_used?(char)
203
+ max_punch.times.map { |i| "#{char}#{i + 1}" }.any? { |s| @value.include?(s) }
204
+ end
205
+
206
+ # Find format of args that works
207
+ def format
208
+ pair = [['<','>'],['[',']'],['{','}']].find { |p| !matched_pair_used?(*p) }
209
+ return pair if pair
210
+ special = (126..255).find { |n| !special_used?(n.chr) }
211
+ return [special.chr, special.chr] if special
212
+ # what to do if nothing works?
213
+ end
214
+
215
+ # Converts current state of this form into TRAC commands.
216
+ def to_trac(name)
217
+ cp, sp = @cp, @sp
218
+ @cp, @sp = 0, 0
219
+ if punched
220
+ pair = format
221
+ args = max_punch.times.map { |i| "#{pair[0]}#{i + 1}#{pair[1]}"}
222
+ trac = "#(DS,#{name},#{call_lookup(*args)})\n"
223
+ trac += "#(SS,#{name},#{args.join(',')})\n" unless args.empty?
224
+ else
225
+ trac = "#(DS,#{name},#{@value})\n"
226
+ end
227
+ trac += "#(CN,#{name},#{cp})\n" unless cp == 0
228
+ trac += "#(CS,#{name})" * sp + "\n" unless sp == 0
229
+ trac += "\n"
230
+ @cp, @sp = cp, sp
231
+ trac
232
+ end
233
+
234
+ # Converts this form into a string for display. Follows format of TRAC
235
+ # display defined in language definition.
236
+ def to_s
237
+ str = ''
238
+ @segments.each.with_index do |s, cp|
239
+ s.each.with_index do |n, sp|
240
+ str += '<^>' if @cp == cp && @sp == sp
241
+ str += "<#{n}>"
242
+ end
243
+ str += '<^>' if @cp == cp && @sp == s.length
244
+ if cp < @value.length
245
+ c = @value[cp]
246
+ # escape non-printable characters
247
+ str << (c =~ /[[:print:]]/ ? c : sprintf("\\x%02.2x", c.ord))
248
+ end
249
+ end
250
+ str
251
+ end
252
+
253
+ end
@@ -0,0 +1,52 @@
1
+
2
+ require 'io/console'
3
+ require 'highline/system_extensions'
4
+ include HighLine::SystemExtensions
5
+
6
+ module TracLang
7
+
8
+ # Class to do console input. This is put in a separate
9
+ # class to make it easier to switch between implementations,
10
+ # since this seems to be seomthing that has some incompatibilities
11
+ # between operating systems.
12
+ class ImmediateRead
13
+
14
+ # Creates class with console input handler depending on operating system.
15
+ def initialize
16
+ if (/mingw|win|emx/=~RUBY_PLATFORM)!=nil
17
+ @getchar=lambda{WinAPI._getch} # Windows
18
+ else
19
+ @getchar=lambda{STDIN.getbyte} # Unix
20
+ end
21
+ @method_name = :highline
22
+ end
23
+
24
+ # Get character from console input.
25
+ def getch
26
+ self.send(@method_name)
27
+ end
28
+
29
+ def console_io
30
+ c = IO.console.getch
31
+ print c
32
+ c
33
+ end
34
+
35
+ # Get character from console input, doing any translation necessary.
36
+ def highline
37
+ chr = @getchar[].chr
38
+ case chr
39
+ when "\r"
40
+ chr = "\n"
41
+ puts
42
+ when "\u0003"
43
+ throw :done
44
+ else
45
+ STDOUT.write chr
46
+ end
47
+ chr
48
+ end
49
+
50
+ end
51
+
52
+ end