tildeath 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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])