tildeath 0.0.1

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/README.md ADDED
@@ -0,0 +1,4 @@
1
+ tildeath
2
+ ========
3
+
4
+ An esoteric language interpreter for the imminently deceased.
data/bin/tildeath ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ require 'tildeath'
3
+ require 'optparse'
4
+
5
+ options = {}
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: #{__FILE__} [options] [file]"
8
+ opts.on('-v', '--[no-]verbose', 'Verbose mode') do |v|
9
+ options[:verbose] = v
10
+ end
11
+ opts.on_tail('--version', 'Show version') do
12
+ puts Tildeath::VERSION
13
+ exit
14
+ end
15
+ end.parse!
16
+
17
+ input = nil
18
+ filename = nil
19
+ if ARGV.length == 0
20
+ puts "No file given on command line, reading from stdin... (EOF to finish)"
21
+ input = STDIN.read
22
+ filename = '-'
23
+ else
24
+ filename = ARGV.first
25
+ input = File.open(filename, 'r').read
26
+ end
27
+
28
+ Tildeath::Interpreter.interpret(input, filename: filename, verbose: options[:verbose])
data/bin/tildeath~ ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ require 'tildeath'
3
+ require 'optparse'
4
+
5
+ options = {}
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: #{__FILE__} [options] file"
8
+ opts.on('-v', '--[no-]verbose', 'Verbose mode') do |v|
9
+ options[:verbose] = v
10
+ end
11
+ opts.on_tail('--version', 'Show version') do
12
+ puts Tildeath::VERSION
13
+ exit
14
+ end
15
+ end.parse!
16
+
17
+ input = nil
18
+ filename = nil
19
+ if ARGV.length == 0
20
+ puts "No file given on command line, reading from stdin... (EOF to finish)"
21
+ input = STDIN.read
22
+ filename = '-'
23
+ else
24
+ filename = ARGV.first
25
+ input = File.open(filename, 'r').read
26
+ end
27
+
28
+ Tildeath::Interpreter.interpret(input, filename: filename, verbose: options[:verbose])
data/bin/~ath~ ADDED
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env ruby
2
+ $VERBOSE = true
3
+
4
+ require 'optparse'
5
+
6
+ class TilDeathError < StandardError
7
+ attr_reader :line_number, :column
8
+
9
+ def initialize(line_number, column)
10
+ super()
11
+ @line_number = line_number
12
+ @column = column
13
+ end
14
+ end
15
+
16
+ class Token
17
+ attr_reader :name, :value, :line_number, :column
18
+
19
+ def initialize(name, line_number, column, value = nil)
20
+ @name = name
21
+ @value = value #only used by IDENT tokens, to store the actual identifier
22
+ @line_number = line_number
23
+ @column = column
24
+ end
25
+ end
26
+
27
+ module Lexer
28
+ # token types
29
+ TOKENS = {
30
+ COMMENT: /\/\/.*$/,
31
+ IMPORT: /import/,
32
+ TILDEATH: /~ATH/,
33
+ EXECUTE: /EXECUTE/,
34
+ THIS: /THIS/,
35
+ BIFURC8: /bifurcate/,
36
+ SPLIT: /split/,
37
+ LBRACE: /\{/,
38
+ RBRACE: /\}/,
39
+ LPAREN: /\(/,
40
+ RPAREN: /\)/,
41
+ LBRACKET: /\[/,
42
+ RBRACKET: /\]/,
43
+ DOT: /\./,
44
+ DIE: /DIE/,
45
+ NULL: /NULL/,
46
+ COMMA: /,/,
47
+ SEMI: /;/,
48
+ BANG: /!/,
49
+ # one or more whitespace characters
50
+ WS: /\s+/,
51
+ # a letter or underscore followed by zero or more letters, underscores, and digits
52
+ IDENT: /[a-z_][a-z_0-9]*/i
53
+ }
54
+
55
+ def self.lex(input)
56
+ # tokens found so far
57
+ tokens = []
58
+ # current position in script
59
+ pos = 0
60
+ line_number = 0
61
+ column = 0
62
+ match = nil
63
+ # while pos isn't outside the script...
64
+ while pos < input.length
65
+ if input[pos] != "\n"
66
+ column += 1
67
+ else
68
+ line_number += 1
69
+ column = 0
70
+ end
71
+ # for each token type...
72
+ good = TOKENS.any? do |sym, regex|
73
+ # try to match it at the current position in the script
74
+ match = input.match(regex, pos)
75
+ # skip to next token type unless it matched at the current position
76
+ next unless match && match.begin(0) == pos
77
+ # ignore whitespace and comments
78
+ unless [:WS, :COMMENT].include?(sym)
79
+ # add new token to list of found tokens
80
+ # if it's an identifier, save the actual text found as well
81
+ tokens << Token.new(sym, line_number, column, sym == :IDENT ? match[0] : nil)
82
+ end
83
+ # move current position to just after the end of the found token
84
+ pos += match[0].length
85
+ true
86
+ end
87
+ fail TilDeathError.new(line_number, column), "error: unrecognized token #{input[pos]}" unless good
88
+ end
89
+ return tokens
90
+ end
91
+ end
92
+
93
+ class ImminentlyDeceasedObject
94
+ attr_reader :type, :name
95
+
96
+ def initialize(type, name)
97
+ @type = type
98
+ @name = name
99
+ @alive = true
100
+ end
101
+
102
+ def die
103
+ @alive = false
104
+ end
105
+
106
+ def alive?
107
+ @alive
108
+ end
109
+ end
110
+
111
+ class Value
112
+ def initialize(value)
113
+ @value = value
114
+ end
115
+
116
+ def execute(context)
117
+ context[@value].alive?
118
+ end
119
+
120
+ def to_s
121
+ @value
122
+ end
123
+ end
124
+
125
+ class Bang
126
+ def initialize(value)
127
+ @value = value
128
+ end
129
+
130
+ def execute(context)
131
+ !@value.execute(context)
132
+ end
133
+
134
+ def to_s
135
+ "!#{@value}"
136
+ end
137
+ end
138
+
139
+ class Import
140
+ def initialize(type, name)
141
+ @type = type
142
+ @name = name
143
+ end
144
+
145
+ def execute(context)
146
+ return unless context[:THIS].alive?
147
+ # Create new object of the specified type and name and store it in context
148
+ context[@name] = ImminentlyDeceasedObject.new(@type, @name)
149
+ end
150
+
151
+ def to_s
152
+ "import #{@type} #{@name}"
153
+ end
154
+ end
155
+
156
+ class TilDeath
157
+ def initialize(victim, tildeath_body, execute_body)
158
+ @victim = victim
159
+ @tildeath_body = tildeath_body
160
+ @execute_body = execute_body
161
+ end
162
+
163
+ def execute(context)
164
+ fail "error: no such object: #{@victim}" unless context[@victim]
165
+ # loop over first set of statements while victim is alive
166
+ while context[@victim].alive?
167
+ return unless context[:THIS].alive?
168
+ @tildeath_body.execute(context)
169
+ end
170
+ # run second set of statements when victim dies
171
+ return unless context[:THIS].alive?
172
+ @execute_body.execute(context)
173
+ end
174
+
175
+ def to_s
176
+ "~ATH(#{@victim}) {
177
+ #{@tildeath_body}
178
+ } EXECUTE(#{@execute_body})"
179
+ end
180
+ end
181
+
182
+ class DotDie
183
+ def initialize(victim)
184
+ @victim = victim
185
+ end
186
+
187
+ def execute(context)
188
+ return unless context[:THIS].alive?
189
+ context[@victim].die
190
+ end
191
+
192
+ def to_s
193
+ "#{@victim}.DIE()"
194
+ end
195
+ end
196
+
197
+ class Null
198
+ def execute(context); end
199
+
200
+ def to_s
201
+ 'NULL'
202
+ end
203
+ end
204
+
205
+ class Bifurcate
206
+ def initialize(orig, parts)
207
+ @orig = orig
208
+ @parts = parts
209
+ end
210
+
211
+ def execute(context)
212
+ type = context[@orig].type
213
+ @parts.each do |part|
214
+ context[part] = ImminentlyDeceasedObject.new(type, part)
215
+ end
216
+ end
217
+
218
+ def to_s
219
+ @parts[1..-1].reduce("bifurcate #{@orig}[#{@parts[0]}") {|memo, part|
220
+ memo << ', ' << part.to_s
221
+ } << ']'
222
+ end
223
+ end
224
+
225
+ class Statements
226
+ def initialize(statements=[])
227
+ @statements = statements
228
+ end
229
+
230
+ def execute(context)
231
+ return unless context[:THIS].alive?
232
+ @statements.each do |statement|
233
+ return unless context[:THIS].alive?
234
+ statement.execute(context)
235
+ end
236
+ end
237
+
238
+ def to_s
239
+ @statements.reduce('') do |memo, stmt|
240
+ memo << stmt.to_s << ";\n"
241
+ end
242
+ end
243
+ end
244
+
245
+ class Program
246
+ def initialize(statements)
247
+ @statements = statements
248
+ end
249
+
250
+ def execute
251
+ context = {
252
+ THIS: ImminentlyDeceasedObject.new(:program, :THIS)
253
+ }
254
+ @statements.execute(context)
255
+ end
256
+
257
+ def to_s
258
+ @statements.to_s
259
+ end
260
+ end
261
+
262
+ module Parser
263
+ # null: NULL
264
+ def self.parse_null(tokens)
265
+ # shift the NULL off the token queue
266
+ tokens.shift
267
+ # return a new Null object
268
+ Null.new
269
+ end
270
+
271
+ # import: IMPORT IDENT IDENT
272
+ def self.parse_import(tokens)
273
+ # Shift the IMPORT IDENT IDENT tokens off the queue, saving the type and
274
+ # name as symbols
275
+ type, name = tokens.shift(3)[1..2].map{|token| token.value.to_sym}
276
+ # return a new Import with the given type and name
277
+ Import.new(type, name)
278
+ end
279
+
280
+ # value: THIS | IDENT
281
+ def self.parse_value(tokens)
282
+ Value.new(tokens.shift.value.to_sym)
283
+ end
284
+
285
+ # bang: BANG value
286
+ def self.parse_bang(tokens)
287
+ # shift off BANG
288
+ tokens.shift
289
+ value = parse_value(tokens)
290
+ Bang.new(operand)
291
+ end
292
+
293
+ # expression: bang | value
294
+ def self.parse_expression(tokens)
295
+ return parse_bang(tokens) if tokens[0].name == :BANG
296
+ parse_value(tokens)
297
+ end
298
+
299
+ # tildeath: TILDEATH LPAREN expression RPAREN LBRACE statements RBRACE EXECUTE LPAREN statements RPAREN
300
+ def self.parse_tildeath(tokens)
301
+ # shift off the first five tokens, saving the IDENT
302
+ victim = tokens.shift(5)[2]
303
+ victim = victim == :THIS ? victim.name : victim.value.to_sym
304
+ # parse the first statements
305
+ tildeath_body = parse_statements(tokens)
306
+ # shift off some punctuation
307
+ tokens.shift(3)
308
+ # parse the EXECUTE statements (or NULL)
309
+ execute_body = tokens[0].name == :NULL ? parse_null(tokens) : parse_statements(tokens)
310
+ # shift off the last RPAREN
311
+ tokens.shift
312
+ # return a new TilDeath with the parsed victim and statements
313
+ TilDeath.new(victim, tildeath_body, execute_body)
314
+ end
315
+
316
+ # array: LBRACKET expression (COMMA expression)* RBRACKET
317
+ def self.parse_array(tokens)
318
+ elements = []
319
+ elements << tokens.shift(2)[1].value.to_sym
320
+ Array.new(elements)
321
+ end
322
+
323
+ # dot_die: expression DOT DIE LPAREN RPAREN
324
+ def self.parse_dot_die(tokens)
325
+ # shift off all the tokens, keeping the IDENT's value
326
+ victim = tokens.shift(5)[0]
327
+ victim = victim.name == :THIS ? victim.name.to_sym : victim.value.to_sym
328
+ DotDie.new(victim)
329
+ end
330
+
331
+ # bifurcate: BIFURC8 IDENT LBRACKET IDENT COMMA IDENT RBRACKET
332
+ def self.parse_bifurcate(tokens)
333
+ orig = tokens.shift(3)[1].value.to_sym
334
+ parts = tokens.shift(4).values_at(0, 2).map do |token|
335
+ token.value.to_sym
336
+ end
337
+ Bifurcate.new(orig, parts)
338
+ end
339
+
340
+ # split: SPLIT IDENT LBRACKET IDENT (COMMA IDENT)* RBRACKET
341
+ def self.parse_split(tokens)
342
+ orig = tokens.shift(3)[1].value.to_sym
343
+ parts = [tokens.shift.value.to_sym]
344
+ while true
345
+ part = tokens.shift
346
+ case part.name
347
+ when :COMMA
348
+ parts << tokens.shift.value.to_sym
349
+ when :RBRACKET
350
+ break
351
+ end
352
+ end
353
+ Split.new(orig, parts)
354
+ end
355
+
356
+ # statement: (import | tildeath | dot_die | bifurcate | split) SEMI
357
+ # TODO: make [THIS, THIS].DIE() legal
358
+ def self.parse_statement(tokens)
359
+ # Determine statement type based on first token, and parse it
360
+ token = tokens[0].name
361
+ ret = case token
362
+ when :IMPORT then parse_import(tokens)
363
+ when :TILDEATH then parse_tildeath(tokens)
364
+ when :THIS, :IDENT then parse_dot_die(tokens)
365
+ when :BIFURC8 then parse_bifucate(tokens)
366
+ when :SPLIT then parse_split(tokens)
367
+ when :RBRACE then return
368
+ else fail TilDeathError.new(token.line_number, token.column), "error: unexpected token #{token}"
369
+ end
370
+ # shift off SEMI
371
+ fail TilDeathError.new(tokens[0].line_number, tokens[0].column), 'missing semicolon' unless tokens[0].name == :SEMI
372
+ tokens.shift
373
+ ret
374
+ end
375
+
376
+ # statements: statement*
377
+ def self.parse_statements(tokens)
378
+ statements = []
379
+ # while there are tokens left and parse_statement returns non-nil...
380
+ while tokens.length > 0 && statement = parse_statement(tokens)
381
+ # add parsed statement to list of parsed statements
382
+ statements << statement
383
+ end
384
+ # return a new Statements object with the list of parsed statements
385
+ Statements.new(statements)
386
+ end
387
+
388
+ # program: statements
389
+ def self.parse(tokens)
390
+ Program.new(parse_statements(tokens))
391
+ end
392
+ end
393
+
394
+ module Interpreter
395
+ def self.interpret(script, filename:, verbose: false)
396
+ # discard shebang line if present
397
+ script.slice!(0, script.index("\n") + 1) if script[0..1] == "#!"
398
+ # scan string into tokens
399
+ tokens = Lexer.lex(script)
400
+ # parse tokens into abstract syntax tree
401
+ tree = Parser.parse(tokens)
402
+ # show gussed original source based on AST
403
+ puts tree if verbose
404
+ # execute AST starting at its root
405
+ tree.execute
406
+ rescue TilDeathError => ex
407
+ puts "#{filename}:#{ex.line_number}:#{ex.column}: #{ex.message}"
408
+ end
409
+ end
410
+
411
+ options = {}
412
+ OptionParser.new do |opts|
413
+ opts.banner = "Usage: #{__FILE__} [options] file"
414
+ opts.on('-v', '--[no-]verbose', 'Verbose mode') do |v|
415
+ options[:verbose] = v
416
+ end
417
+ opts.on_tail('--version', 'Show version') do
418
+ puts '0.0.0'
419
+ exit
420
+ end
421
+ end.parse!
422
+
423
+ input = nil
424
+ filename = nil
425
+ if ARGV.length == 0
426
+ puts "No file given on command line, reading from stdin... (EOF to finish)"
427
+ input = STDIN.read
428
+ filename = '-'
429
+ else
430
+ filename = ARGV.first
431
+ input = File.open(filename, 'r').read
432
+ end
433
+
434
+ Interpreter.interpret(input, filename: filename, verbose: options[:verbose])