trac_lang 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +58 -0
- data/Rakefile +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/README.trl +633 -0
- data/examples/golf.trl +113 -0
- data/examples/list.trl +125 -0
- data/examples/math.trl +351 -0
- data/examples/meta.trl +254 -0
- data/examples/ratio.trl +107 -0
- data/examples/struct.trl +176 -0
- data/examples/term.trl +129 -0
- data/examples/util.trl +366 -0
- data/exe/trac_lang +74 -0
- data/lib/trac_lang.rb +16 -0
- data/lib/trac_lang/bindings.rb +53 -0
- data/lib/trac_lang/block.rb +46 -0
- data/lib/trac_lang/decimal.rb +79 -0
- data/lib/trac_lang/dispatch.rb +421 -0
- data/lib/trac_lang/executor.rb +97 -0
- data/lib/trac_lang/expression.rb +58 -0
- data/lib/trac_lang/form.rb +253 -0
- data/lib/trac_lang/immediate_read.rb +52 -0
- data/lib/trac_lang/octal.rb +88 -0
- data/lib/trac_lang/parser.rb +114 -0
- data/lib/trac_lang/version.rb +4 -0
- data/trac_lang.gemspec +42 -0
- metadata +177 -0
@@ -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
|