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.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +4 -0
- data/bin/tildeath +28 -0
- data/bin/tildeath~ +28 -0
- data/bin/~ath~ +434 -0
- data/lib/tildeath.rb +2 -0
- data/lib/tildeath/ast_nodes.rb +10 -0
- data/lib/tildeath/ast_nodes/bang.rb +17 -0
- data/lib/tildeath/ast_nodes/bifurcate.rb +25 -0
- data/lib/tildeath/ast_nodes/dot_die.rb +18 -0
- data/lib/tildeath/ast_nodes/import.rb +22 -0
- data/lib/tildeath/ast_nodes/null.rb +11 -0
- data/lib/tildeath/ast_nodes/program.rb +22 -0
- data/lib/tildeath/ast_nodes/split.rb +25 -0
- data/lib/tildeath/ast_nodes/statements.rb +23 -0
- data/lib/tildeath/ast_nodes/tildeath.rb +29 -0
- data/lib/tildeath/ast_nodes/value.rb +17 -0
- data/lib/tildeath/imminently_deceased_object.rb +19 -0
- data/lib/tildeath/interpreter.rb +22 -0
- data/lib/tildeath/lexer.rb +70 -0
- data/lib/tildeath/parser.rb +136 -0
- data/lib/tildeath/tildeath_error.rb +11 -0
- data/lib/tildeath/token.rb +12 -0
- data/lib/tildeath/version.rb +3 -0
- metadata +69 -0
data/README.md
ADDED
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])
|