NCPrePatcher 0.2.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/LICENSE.txt +674 -0
- data/README.md +66 -0
- data/example/README.md +3 -0
- data/example/disasm.rb +34 -0
- data/exe/ncpp +4 -0
- data/lib/ncpp/commands.rb +903 -0
- data/lib/ncpp/interpreter.rb +919 -0
- data/lib/ncpp/parser.rb +249 -0
- data/lib/ncpp/types.rb +68 -0
- data/lib/ncpp/utils.rb +700 -0
- data/lib/ncpp/version.rb +4 -0
- data/lib/ncpp.rb +478 -0
- data/lib/nitro/nitro.dll +0 -0
- data/lib/nitro/nitro.rb +440 -0
- data/lib/unarm/unarm.dll +0 -0
- data/lib/unarm/unarm.rb +836 -0
- metadata +91 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
require_relative 'parser.rb'
|
|
2
|
+
require_relative 'commands.rb'
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module NCPP
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# NCPP language interpreter
|
|
10
|
+
#
|
|
11
|
+
class Interpreter
|
|
12
|
+
# Interpreter environment-specific commands
|
|
13
|
+
def commands
|
|
14
|
+
CommandRegistry.new({
|
|
15
|
+
put: ->(x, on_new_line=true) {
|
|
16
|
+
if on_new_line
|
|
17
|
+
@out_stack << x.to_s
|
|
18
|
+
else
|
|
19
|
+
@out_stack[-1] << x.to_s
|
|
20
|
+
end
|
|
21
|
+
}.impure
|
|
22
|
+
.describe(
|
|
23
|
+
"Puts the value of the given argument as a String on the end of the out-stack; but if 'on_new_line' is true "\
|
|
24
|
+
"(it's false by default), it's added to the end of the last stack entry."
|
|
25
|
+
),
|
|
26
|
+
|
|
27
|
+
get_out_stack: -> { @out_stack }.returns(Array).impure
|
|
28
|
+
.describe('Gets the out-stack.'),
|
|
29
|
+
|
|
30
|
+
clear_out_stack: -> { @out_stack.clear }.impure
|
|
31
|
+
.describe('Clears the out-stack.'),
|
|
32
|
+
|
|
33
|
+
ruby: ->(code_str) {
|
|
34
|
+
raise "The 'ruby' command can't be run in safe mode" if @safe_mode
|
|
35
|
+
eval(code_str, get_binding)
|
|
36
|
+
}.returns(Object).impure
|
|
37
|
+
.describe('Evaluates the given String as Ruby code.'),
|
|
38
|
+
|
|
39
|
+
eval: ->(expr_str) { eval_str(expr_str) }.returns(Object).impure
|
|
40
|
+
.describe('Evaluates the given String as an NCPP expression.'),
|
|
41
|
+
|
|
42
|
+
do_command: ->(cmd_str, *args) { call(cmd_str,get_command(cmd_str),*args) }.returns(Object)
|
|
43
|
+
.describe('Calls the command named in the given String.'),
|
|
44
|
+
|
|
45
|
+
map_to_command: ->(arr, cmd_str, *args) {
|
|
46
|
+
Utils.array_check(arr,'map_to_command')
|
|
47
|
+
cmd = get_command(cmd_str)
|
|
48
|
+
arr.map {|element| args.nil? ? call(cmd_str,cmd,element) : call(cmd_str,cmd,element,*args) }
|
|
49
|
+
}.returns(Array)
|
|
50
|
+
.describe('Calls the command named in the String given on each element in the provided Array.'),
|
|
51
|
+
|
|
52
|
+
inject_in_command: ->(arr, init_val, cmd_str) {
|
|
53
|
+
Utils.array_check(arr, 'inject_in_command')
|
|
54
|
+
cmd = get_command(cmd_str)
|
|
55
|
+
arr.inject(init_val) {|acc,n| call(cmd_str,cmd,acc,n) }
|
|
56
|
+
}.returns(Object),
|
|
57
|
+
|
|
58
|
+
define_command: ->(name, block) { def_command(name, block) }
|
|
59
|
+
.describe('Defines a command with the name given and a Block or a Ruby Proc.'),
|
|
60
|
+
|
|
61
|
+
alias_command: ->(new_name, name) {
|
|
62
|
+
Utils.valid_identifier_check(name)
|
|
63
|
+
raise "Alias name '#{new_name}' is occupied" if @commands.has_key?(new_name.to_sym)
|
|
64
|
+
@commands[new_name.to_sym] = @commands[name.to_sym]
|
|
65
|
+
}.describe('Adds an alias for an existing command.'),
|
|
66
|
+
|
|
67
|
+
define_variable: ->(var_name, val = nil) { def_variable(var_name, val) }
|
|
68
|
+
.describe('Defines a variable with the name and value given. If no value is given, it is set to nil.'),
|
|
69
|
+
|
|
70
|
+
define: ->(name, val = nil) {
|
|
71
|
+
if val.is_a?(Block) || val.is_a?(Proc)
|
|
72
|
+
def_command(name, val)
|
|
73
|
+
else
|
|
74
|
+
def_variable(name, val)
|
|
75
|
+
end
|
|
76
|
+
}.describe(
|
|
77
|
+
"Defines a variable or command with the name and value given. A command is defined if the value is a Block "\
|
|
78
|
+
"or a Ruby Proc, otherwise it is a variable."
|
|
79
|
+
),
|
|
80
|
+
|
|
81
|
+
delete_variable: ->(var_str, panic_if_missing = true) {
|
|
82
|
+
unknown_variable_error(var_str) if panic_if_missing && !@variables.has_key?(var_str.to_sym)
|
|
83
|
+
@variables.delete(var_str.to_sym)
|
|
84
|
+
}.impure
|
|
85
|
+
.describe('Deletes the variable corresponding to the given name.'),
|
|
86
|
+
|
|
87
|
+
describe: ->(cmd_str) {
|
|
88
|
+
cmd = @commands[cmd_str.to_sym]
|
|
89
|
+
unknown_command_error(cmd_str) if cmd.nil?
|
|
90
|
+
out = ''
|
|
91
|
+
if cmd.is_a?(Proc)
|
|
92
|
+
out << (cmd.description or '')
|
|
93
|
+
params = cmd.parameters.map {|type,arg| "#{arg} (#{type})" }
|
|
94
|
+
out << "#{"\n" if !out.empty?}Param#{'s' if params.length != 1}: #{params.join(', ')}" unless params.empty?
|
|
95
|
+
out << "#{"\n" if !out.empty?}Returns: #{cmd.return_type}" unless cmd.return_type.nil?
|
|
96
|
+
unless cmd.pure? && cmd.ignore_unk_var_args.empty?
|
|
97
|
+
out << "#{"\n" if !out.empty?}Notes:\n"
|
|
98
|
+
out << "* Impure" unless cmd.pure?
|
|
99
|
+
unless cmd.ignore_unk_var_args.empty?
|
|
100
|
+
out << "* Unknown variables ignored at args #{cmd.ignore_unk_var_args.join(', ')}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
elsif !cmd.pure?
|
|
104
|
+
out << "Notes:\n* Impure"
|
|
105
|
+
end
|
|
106
|
+
aliases = @commands.select {|k,v| k.to_s != cmd_str && v == cmd }.keys
|
|
107
|
+
out << "#{"\n" if !out.empty?}Alias#{'es' if aliases.length != 1}: #{aliases.join(', ')}" if !aliases.empty?
|
|
108
|
+
out
|
|
109
|
+
}.returns(String)
|
|
110
|
+
.describe(
|
|
111
|
+
'Describes a given command if a description is present and lists its parameters, return type, and aliases.'
|
|
112
|
+
),
|
|
113
|
+
|
|
114
|
+
get_command_names: -> { @commands.keys.map { it.to_s } }
|
|
115
|
+
.returns(Array)
|
|
116
|
+
.describe('Gets an array containing each command name.'),
|
|
117
|
+
|
|
118
|
+
get_variable_names: -> { @variables.keys.map { it.to_s } }
|
|
119
|
+
.returns(Array)
|
|
120
|
+
.describe('Gets an array containing each variables name.'),
|
|
121
|
+
|
|
122
|
+
benchmark: ->(block_or_cmd, repeats, *args) {
|
|
123
|
+
thing = block_or_cmd.is_a?(String) ? get_command(block_or_cmd) : block_or_cmd
|
|
124
|
+
start_time = Time.now
|
|
125
|
+
if args.nil?
|
|
126
|
+
repeats.times { thing.call }
|
|
127
|
+
else
|
|
128
|
+
repeats.times { thing.call(*args) }
|
|
129
|
+
end
|
|
130
|
+
Time.now - start_time
|
|
131
|
+
}.returns(Float).impure
|
|
132
|
+
.describe('Times how long it takes to do the given Block or Command the amount of times given.'),
|
|
133
|
+
|
|
134
|
+
is_pure: ->(block_proc_or_cmd) {
|
|
135
|
+
if block_proc_or_cmd.is_a? String
|
|
136
|
+
get_command(block_proc_or_cmd).pure?
|
|
137
|
+
else
|
|
138
|
+
block_proc_or_cmd.pure?
|
|
139
|
+
end
|
|
140
|
+
}.returns(Object)
|
|
141
|
+
.describe('Gets whether the given Block, Ruby Proc, or command is pure.'),
|
|
142
|
+
|
|
143
|
+
vow_purity: -> { @puritan_mode = true }.impure
|
|
144
|
+
.describe('Activates puritan mode, disallowing the use of impure commands.'),
|
|
145
|
+
vow_safety: -> { @safe_mode = true }.impure
|
|
146
|
+
.describe('Activates safe mode, disallowing the use of inline Ruby commands.'),
|
|
147
|
+
break_purity_vow: -> { @puritan_mode = false } # impure
|
|
148
|
+
.describe('Deactivates puritan mode, allowing the use of impure commands.'),
|
|
149
|
+
break_safety_vow: -> { @safe_mode = false }.impure
|
|
150
|
+
.describe('Deactivates puritan mode, allpwing the use of impure commands.'),
|
|
151
|
+
break_vows: -> { @puritan_mode = false; @safe_mode = false } # impure
|
|
152
|
+
.describe('Breaks all vows.'),
|
|
153
|
+
|
|
154
|
+
invalidate_cache: -> { @command_cache&.clear }.impure
|
|
155
|
+
.describe('Clears command cache.')
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
aliases: {
|
|
159
|
+
out: :put,
|
|
160
|
+
do_cmd: :do_command,
|
|
161
|
+
map_to_cmd: :map_to_command,
|
|
162
|
+
inject_in_cmd: :inject_in_command,
|
|
163
|
+
define_cmd: :define_command,
|
|
164
|
+
def_cmd: :define_command,
|
|
165
|
+
alias_cmd: :alias_command,
|
|
166
|
+
def_var: :define_variable,
|
|
167
|
+
set_var: :define_variable,
|
|
168
|
+
delete_var: :delete_variable,
|
|
169
|
+
del_var: :delete_variable,
|
|
170
|
+
def: :define,
|
|
171
|
+
desc: :describe,
|
|
172
|
+
get_cmd_names: :get_command_names,
|
|
173
|
+
get_var_names: :get_variable_names
|
|
174
|
+
}).freeze
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def variables
|
|
178
|
+
{
|
|
179
|
+
SYMBOL_COUNT: Unarm.symbols.count,
|
|
180
|
+
SYMBOL_NAMES: Unarm.symbols.map.keys, # TODO: how should I handle ARM7 ??
|
|
181
|
+
DEMANGLED_SYMBOL_NAMES: Unarm.symbols.demangled_map.keys,
|
|
182
|
+
OVERLAY_COUNT: $rom.overlay_count,
|
|
183
|
+
GAME_TITLE: $rom.header.game_title,
|
|
184
|
+
NITRO_SDK_VERSION: $rom.nitro_sdk_version
|
|
185
|
+
# OVERLAY_OFFSETS: Array.new($rom.overlay_count, 0)
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def initialize(cmd_prefix = COMMAND_PREFIX, extra_cmds = {}, extra_vars = {}, safe: false, puritan: false,
|
|
190
|
+
no_cache: false, cmd_cache: {})
|
|
191
|
+
|
|
192
|
+
@parser = Parser.new(cmd_prefix: cmd_prefix)
|
|
193
|
+
@transformer = Transformer.new
|
|
194
|
+
|
|
195
|
+
@COMMAND_PREFIX = cmd_prefix
|
|
196
|
+
|
|
197
|
+
@safe_mode = safe
|
|
198
|
+
@puritan_mode = puritan
|
|
199
|
+
|
|
200
|
+
@commands = commands.merge(CORE_COMMANDS).merge(extra_cmds)
|
|
201
|
+
|
|
202
|
+
@variables = {}
|
|
203
|
+
@variables.merge!(variables) unless $rom.nil?
|
|
204
|
+
@variables.merge!(CORE_VARIABLES).merge!(extra_vars)
|
|
205
|
+
|
|
206
|
+
@added_commands = extra_cmds.keys.to_set
|
|
207
|
+
@added_variables = extra_vars.keys.to_set
|
|
208
|
+
|
|
209
|
+
@out_stack = []
|
|
210
|
+
@command_cache = no_cache ? nil : cmd_cache
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def get_binding
|
|
214
|
+
binding
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def get_new_commands
|
|
218
|
+
@added_commands.to_h {|cmd| [cmd, @commands[cmd]] }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def get_new_variables
|
|
222
|
+
@added_variables.to_h {|var| [var, @variables[var]] }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# get cached commands that can be saved
|
|
226
|
+
def get_cacheable_cache
|
|
227
|
+
@command_cache.delete_if {|k,v| @added_commands.include?(k.to_sym) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def unknown_command_error(cmd_name)
|
|
231
|
+
alt = @commands.suggest_similar_key(cmd_name)
|
|
232
|
+
raise "Unknown command '#{cmd_name}'#{"\nDid you mean '#{alt}'?" unless alt.nil?}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def unknown_variable_error(var_name)
|
|
236
|
+
alt = @variables.suggest_similar_key(var_name)
|
|
237
|
+
raise "Unknown variable '#{var_name}'#{"\nDid you mean '#{alt}'?" unless alt.nil?}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def def_command(cmd_name, block)
|
|
241
|
+
Utils.valid_identifier_check(cmd_name)
|
|
242
|
+
raise 'commands must be either Blocks or Ruby Procs' unless block.is_a?(Block) || block.is_a?(Proc)
|
|
243
|
+
cmd_sym = cmd_name.to_sym
|
|
244
|
+
redef = @commands.has_key?(cmd_sym)
|
|
245
|
+
raise 'Redefining commands is not allowed in puritan mode' if @puritan_mode && redef
|
|
246
|
+
@command_cache.clear if !@command_cache.nil? && redef # command cache must be cleared if any command is redefined
|
|
247
|
+
block.name = cmd_name if block.is_a? Block
|
|
248
|
+
@commands[cmd_sym] = block
|
|
249
|
+
@added_commands.add(cmd_sym)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def get_command(cmd_name)
|
|
253
|
+
cmd = @commands[cmd_name.to_sym]
|
|
254
|
+
unknown_command_error(cmd_name) if cmd.nil?
|
|
255
|
+
cmd
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def call(cmd_name, block_or_proc, *args)
|
|
259
|
+
if !block_or_proc.return_type.nil? && !@command_cache.nil? && block_or_proc.pure?
|
|
260
|
+
return @command_cache[cmd_name][args] if @command_cache.has_key?(cmd_name) && @command_cache[cmd_name].has_key?(args)
|
|
261
|
+
result = block_or_proc.call(*args)
|
|
262
|
+
@command_cache[cmd_name] ||= {}
|
|
263
|
+
@command_cache[cmd_name][args] = result
|
|
264
|
+
result
|
|
265
|
+
else
|
|
266
|
+
block_or_proc.call(*args)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def def_variable(var_name, val)
|
|
271
|
+
Utils.valid_identifier_check(var_name)
|
|
272
|
+
var_sym = var_name.to_sym
|
|
273
|
+
redef = @variables.has_key?(var_sym)
|
|
274
|
+
raise 'Redefining variables is not allowed in puritan mode' if @puritan_mode && redef
|
|
275
|
+
@command_cache.clear if !@command_cache.nil? && redef # command cache must be cleared if any variable is redefined
|
|
276
|
+
@variables[var_sym] = val
|
|
277
|
+
@added_variables.add(var_sym)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def get_variable(var_name)
|
|
281
|
+
unknown_variable_error(var_name) unless @variables.has_key?(var_name.to_sym)
|
|
282
|
+
var = @variables[var_name.to_sym]
|
|
283
|
+
var
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Parses the given String of NCPP code, transforms it, then evaluates resulting AST
|
|
287
|
+
def eval_str(expr_str)
|
|
288
|
+
return nil if expr_str.empty?
|
|
289
|
+
parsed_tree = @parser.parse(expr_str, root: :expression)
|
|
290
|
+
ast = @transformer.apply(parsed_tree)
|
|
291
|
+
eval_expr(ast)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Evaluates the given AST
|
|
295
|
+
def eval_expr(node, subs = nil)
|
|
296
|
+
case node
|
|
297
|
+
when Numeric, String, Array, TrueClass, FalseClass, NilClass
|
|
298
|
+
node
|
|
299
|
+
|
|
300
|
+
when Hash
|
|
301
|
+
if node[:infix] # binary operation
|
|
302
|
+
lhs = eval_expr(node[:lhs], subs)
|
|
303
|
+
rhs = eval_expr(node[:rhs], subs)
|
|
304
|
+
op = node[:op]
|
|
305
|
+
if op == '&&'
|
|
306
|
+
lhs && rhs
|
|
307
|
+
elsif op == '||'
|
|
308
|
+
lhs || rhs
|
|
309
|
+
else
|
|
310
|
+
lhs.send(op, rhs)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
elsif node[:cond] # ternary operation
|
|
314
|
+
eval_expr(node[:cond], subs) ? eval_expr(node[:e1], subs) : eval_expr(node[:e2], subs)
|
|
315
|
+
|
|
316
|
+
elsif node[:op] # unary operation
|
|
317
|
+
op = node[:op]
|
|
318
|
+
# if node[]
|
|
319
|
+
eval_expr(node[:e], subs).send(op == '-' || op == '+' ? op+'@' : op)
|
|
320
|
+
|
|
321
|
+
elsif node[:cmd_name] # normal command call
|
|
322
|
+
cmd_name = node[:cmd_name].to_s
|
|
323
|
+
cmd = get_command(cmd_name)
|
|
324
|
+
raise "Cannot use impure command '#{cmd_name}' in puritan mode." if @puritan_mode && !cmd.pure?
|
|
325
|
+
args = Array(node[:args]).map.with_index do |a,i|
|
|
326
|
+
if cmd.is_a?(Proc) && cmd.ignore_unk_var_args.include?(i) &&
|
|
327
|
+
a[:base].is_a?(Hash) && a[:base][:var_name] && !@variables.include?(a[:base][:var_name].to_sym)
|
|
328
|
+
a[:base][:var_name].to_s
|
|
329
|
+
else
|
|
330
|
+
eval_expr(a, subs)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
ret = call(cmd_name,cmd,*args)
|
|
334
|
+
cmd.return_type.nil? ? nil : (node[:subscript_idx].nil? ? ret : ret[eval_expr(node[:subscript_idx], subs)])
|
|
335
|
+
|
|
336
|
+
elsif node[:base] && node[:chain] # chained command call
|
|
337
|
+
acc = eval_expr(node[:base], subs)
|
|
338
|
+
no_ret = false
|
|
339
|
+
node[:chain].each do |link|
|
|
340
|
+
next_cmd = link[:next]
|
|
341
|
+
cmd_name = next_cmd[:cmd_name].to_s
|
|
342
|
+
cmd = get_command(cmd_name)
|
|
343
|
+
raise "Cannot use impure command '#{cmd_name}' in puritan mode." if @puritan_mode && !cmd.pure?
|
|
344
|
+
args = Array(next_cmd[:args]).map.with_index do |a,i|
|
|
345
|
+
if !cmd.is_a?(Block) && cmd.ignore_unk_var_args.include?(i) &&
|
|
346
|
+
a[:base].is_a?(Hash) && a[:base][:var_name] && !@variables.include?(a[:base][:var_name].to_sym)
|
|
347
|
+
a[:base][:var_name].to_s
|
|
348
|
+
else
|
|
349
|
+
eval_expr(a, subs)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
acc = call(cmd_name,cmd, acc,*args)
|
|
353
|
+
no_ret = cmd.return_type.nil?
|
|
354
|
+
end
|
|
355
|
+
no_ret ? nil : acc
|
|
356
|
+
|
|
357
|
+
elsif node[:var_name]
|
|
358
|
+
unless subs.nil?
|
|
359
|
+
var_name = node[:var_name].to_s
|
|
360
|
+
if subs.has_key?(var_name)
|
|
361
|
+
ret = subs[var_name]
|
|
362
|
+
return (node[:subscript_idx].nil? ? ret : ret[eval_expr(node[:subscript_idx], subs)])
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
var_name = node[:var_name].to_s
|
|
366
|
+
ret = get_variable(var_name)
|
|
367
|
+
node[:subscript_idx].nil? ? ret : ret[eval_expr(node[:subscript_idx], subs)]
|
|
368
|
+
|
|
369
|
+
elsif node[:block]
|
|
370
|
+
Block.new(node[:block], node[:args], self, subs)
|
|
371
|
+
|
|
372
|
+
elsif node[:array]
|
|
373
|
+
arr = Array(node[:array]).map { |a| eval_expr(a, subs) }
|
|
374
|
+
if node[:subscript_idx].nil?
|
|
375
|
+
arr
|
|
376
|
+
else
|
|
377
|
+
arr[eval_expr(node[:subscript_idx], subs)]
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
elsif node[:string]
|
|
381
|
+
node[:string].to_s[eval_expr(node[:subscript_idx], subs)]
|
|
382
|
+
|
|
383
|
+
elsif node[:bool]
|
|
384
|
+
node[:bool].to_s == 'true' ? true : false
|
|
385
|
+
|
|
386
|
+
elsif node[:nil]
|
|
387
|
+
nil
|
|
388
|
+
|
|
389
|
+
else
|
|
390
|
+
raise "Unknown node type: #{node.inspect}"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
else
|
|
394
|
+
raise "Unexpected node: #{node.inspect}"
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# AST purity checking - if a call to an impure command or Block is found, return true
|
|
399
|
+
def node_impure?(node, self_name = nil)
|
|
400
|
+
case node
|
|
401
|
+
when Numeric, String, Array, TrueClass, FalseClass, NilClass
|
|
402
|
+
return false
|
|
403
|
+
|
|
404
|
+
when Hash
|
|
405
|
+
if node[:infix] # binary operation
|
|
406
|
+
return node_impure?(node[:lhs], self_name) || node_impure?(node[:rhs], self_name)
|
|
407
|
+
|
|
408
|
+
elsif node[:cond] # ternary operation
|
|
409
|
+
return node_impure?(node[:cond], self_name) ||
|
|
410
|
+
node_impure?(node[:e1], self_name) ||
|
|
411
|
+
node_impure?(node[:e2], self_name)
|
|
412
|
+
|
|
413
|
+
elsif node[:op] # unary operation
|
|
414
|
+
return node_impure?(node[:e], self_name)
|
|
415
|
+
|
|
416
|
+
elsif node[:cmd_name] # command call
|
|
417
|
+
name = node[:cmd_name].to_s
|
|
418
|
+
|
|
419
|
+
cmd = get_command(name)
|
|
420
|
+
return true unless name == self_name || cmd.pure?
|
|
421
|
+
|
|
422
|
+
# check args
|
|
423
|
+
Array(node[:args]).each do |a|
|
|
424
|
+
return true if node_impure?(a, self_name)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
return false
|
|
428
|
+
|
|
429
|
+
elsif node[:base] && node[:chain] # chained command call
|
|
430
|
+
return true if node_impure?(node[:base], self_name)
|
|
431
|
+
|
|
432
|
+
node[:chain].each do |link|
|
|
433
|
+
next_cmd = link[:next]
|
|
434
|
+
name = next_cmd[:cmd_name].to_s
|
|
435
|
+
|
|
436
|
+
cmd = get_command(name)
|
|
437
|
+
return true unless name == self_name || cmd.pure?
|
|
438
|
+
|
|
439
|
+
Array(next_cmd[:args]).each do |a|
|
|
440
|
+
return true if node_impure?(a, self_name)
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
return false
|
|
445
|
+
|
|
446
|
+
elsif node[:var_name]
|
|
447
|
+
return false # not impure unless a variable is mutated (cache is cleared on variable mutation)
|
|
448
|
+
|
|
449
|
+
elsif node[:block] # a nested Block
|
|
450
|
+
nested_block = Block.new(node[:block], node[:args], self, nil, self_name)
|
|
451
|
+
return !nested_block.pure?
|
|
452
|
+
|
|
453
|
+
elsif node[:array]
|
|
454
|
+
return Array(node[:array]).any? { |a| node_impure?(a, self_name) } ||
|
|
455
|
+
(node[:subscript_idx] && node_impure?(node[:subscript_idx], self_name))
|
|
456
|
+
|
|
457
|
+
elsif node[:string]
|
|
458
|
+
return node[:subscript_idx] && node_impure?(node[:subscript_idx], self_name)
|
|
459
|
+
|
|
460
|
+
elsif node[:bool] || node[:nil]
|
|
461
|
+
return false
|
|
462
|
+
|
|
463
|
+
else
|
|
464
|
+
raise "Unknown node type: #{node.inspect}"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
else
|
|
468
|
+
raise "Unexpected node: #{node.inspect}"
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
#
|
|
475
|
+
# Scans C/C++ source files for commands and expands them in place
|
|
476
|
+
#
|
|
477
|
+
class CFileInterpreter < Interpreter
|
|
478
|
+
attr_reader :lines_parsed, :incomplete_files
|
|
479
|
+
|
|
480
|
+
def initialize(file_list, out_path, cmd_prefix = COMMAND_PREFIX, extra_cmds = {}, extra_vars = {}, template_args=[],
|
|
481
|
+
safe: false, puritan: false, no_cache: false, cmd_cache: {})
|
|
482
|
+
|
|
483
|
+
@EXTRA_CMDS, @EXTRA_VARS = extra_cmds, extra_vars
|
|
484
|
+
super(cmd_prefix, extra_cmds, extra_vars, safe: safe, puritan: puritan, no_cache: no_cache, cmd_cache: cmd_cache)
|
|
485
|
+
|
|
486
|
+
@file_list = file_list.is_a?(Array) ? file_list : [file_list]
|
|
487
|
+
@current_file = nil
|
|
488
|
+
@out_path = out_path
|
|
489
|
+
|
|
490
|
+
@incomplete_files = []
|
|
491
|
+
|
|
492
|
+
@lines_parsed = 0
|
|
493
|
+
|
|
494
|
+
@template_args = template_args
|
|
495
|
+
|
|
496
|
+
@recorded = ''
|
|
497
|
+
@consume_mode = false
|
|
498
|
+
@lick_mode = false
|
|
499
|
+
|
|
500
|
+
@commands.merge!({
|
|
501
|
+
embed: ->(filename, newline_steps=nil) {
|
|
502
|
+
dir = File.dirname(@current_file || Dir.pwd)
|
|
503
|
+
path = File.expand_path(filename, dir)
|
|
504
|
+
raise "File not found: #{path}" unless File.exist? path
|
|
505
|
+
File.binread(path).bytes.join(',')
|
|
506
|
+
}.returns(String).impure
|
|
507
|
+
.describe(
|
|
508
|
+
'Reads all bytes from the specified file and joins them into a comma-separated String representation.'
|
|
509
|
+
),
|
|
510
|
+
|
|
511
|
+
embed_hex: ->(filename, newline_steps=nil) {
|
|
512
|
+
dir = File.dirname(@current_file || Dir.pwd)
|
|
513
|
+
path = File.expand_path(filename, dir)
|
|
514
|
+
raise "File not found: #{path}" unless File.exist? path
|
|
515
|
+
bytes = File.binread(path).bytes
|
|
516
|
+
bytes.map! {|b| b.to_i.to_hex }.join(',')
|
|
517
|
+
}.returns(String).impure
|
|
518
|
+
.describe(
|
|
519
|
+
'Reads all bytes from the specified file and joins them as hex into a comma-separated String representation.'
|
|
520
|
+
),
|
|
521
|
+
|
|
522
|
+
read: ->(filename) {
|
|
523
|
+
dir = File.dirname(@current_file || Dir.pwd)
|
|
524
|
+
path = File.expand_path(filename, dir)
|
|
525
|
+
raise "File not found: #{path}" unless File.exist? path
|
|
526
|
+
File.read(path)
|
|
527
|
+
}.returns(String).impure
|
|
528
|
+
.describe('Reads the file specified and returns its contents as a String.'),
|
|
529
|
+
|
|
530
|
+
read_lines: ->(filename) {
|
|
531
|
+
dir = File.dirname(@current_file || Dir.pwd)
|
|
532
|
+
path = File.expand_path(filename, dir)
|
|
533
|
+
raise "File not found: #{path}" unless File.exist? path
|
|
534
|
+
File.readlines(path)
|
|
535
|
+
}.returns(Array).impure
|
|
536
|
+
.describe('Reads the file specified and returns an Array containing each line.'),
|
|
537
|
+
|
|
538
|
+
read_bytes: ->(filename) {
|
|
539
|
+
dir = File.dirname(@current_file || Dir.pwd)
|
|
540
|
+
path = File.expand_path(filename, dir)
|
|
541
|
+
raise "File not found: #{path}" unless File.exist? path
|
|
542
|
+
File.binread(path).bytes
|
|
543
|
+
}.returns(Array).impure
|
|
544
|
+
.describe('Reads the file specified and returns an Array containing each byte.'),
|
|
545
|
+
|
|
546
|
+
import: ->(template_file, *arg_vals) {
|
|
547
|
+
t_interpreter = CFileInterpreter.new(nil,nil,@COMMAND_PREFIX,@EXTRA_CMDS,@EXTRA_VARS,[*arg_vals])
|
|
548
|
+
dir = File.dirname(@current_file || Dir.pwd)
|
|
549
|
+
path = File.expand_path(template_file, dir)
|
|
550
|
+
ret, _, t_args = t_interpreter.process_file(path)
|
|
551
|
+
@lines_parsed += t_interpreter.lines_parsed
|
|
552
|
+
if t_args.length > 0
|
|
553
|
+
puts "WARNING".underline_yellow + ': '.yellow + "#{t_args.length} template arg#{'s' if t_args.length != 1}"\
|
|
554
|
+
" not used.".yellow
|
|
555
|
+
end
|
|
556
|
+
ret
|
|
557
|
+
}.returns(String).impure
|
|
558
|
+
.describe(
|
|
559
|
+
"Takes a template file name and a value for each arg exported by the template. The template file is " \
|
|
560
|
+
"processed by the interpreter, and the generated code is embedded into the current file."
|
|
561
|
+
),
|
|
562
|
+
|
|
563
|
+
expect: ->(*arg_names) {
|
|
564
|
+
argc, targc = @template_args.length, arg_names.length
|
|
565
|
+
if targc != argc
|
|
566
|
+
raise "#{argc} template arg#{'s' if argc != 1} given when #{targc} #{targc==1 ? 'is' : 'are'} required."
|
|
567
|
+
end
|
|
568
|
+
arg_names.each_with_index do |arg, i|
|
|
569
|
+
Utils.valid_identifier_check(arg)
|
|
570
|
+
@variables[arg.to_sym] = @template_args.first
|
|
571
|
+
@template_args = @template_args.drop(1)
|
|
572
|
+
end
|
|
573
|
+
}.impure
|
|
574
|
+
.describe(
|
|
575
|
+
"Declares the variables that should be defined when importing the template. " \
|
|
576
|
+
"This command is specific to CFileInterpreter."
|
|
577
|
+
),
|
|
578
|
+
|
|
579
|
+
start_consume: -> { @consume_mode = true }.impure
|
|
580
|
+
.describe(
|
|
581
|
+
"Starts consume mode; the following parsed lines will stored in a variable held by the interpreter, which "\
|
|
582
|
+
"can only be accessed by the 'spit' command. Consumed lines will not be put in the generated source file."
|
|
583
|
+
),
|
|
584
|
+
|
|
585
|
+
end_consume: -> { @consume_mode = false }.impure
|
|
586
|
+
.describe('Ends consume parse mode.'),
|
|
587
|
+
|
|
588
|
+
start_lick: -> { @lick_mode = true }.impure
|
|
589
|
+
.describe('Starts lick parse mode.'),
|
|
590
|
+
|
|
591
|
+
end_lick: -> { @lick_mode = false }.impure
|
|
592
|
+
.describe('Ends lick parse mode.'),
|
|
593
|
+
|
|
594
|
+
spit: ->(retain = false) {
|
|
595
|
+
ret = @recorded.clone
|
|
596
|
+
@recorded.clear unless retain
|
|
597
|
+
ret
|
|
598
|
+
}.returns(String).impure
|
|
599
|
+
.describe('Gets what was consumed or licked.'),
|
|
600
|
+
|
|
601
|
+
clear_consumed: -> { @recorded.clear }.impure
|
|
602
|
+
.describe('Clears the variable containing what was consumed or licked.'),
|
|
603
|
+
|
|
604
|
+
clear_licked: -> { @recorded.clear }.impure
|
|
605
|
+
.describe('Clears the variable containing what was licked or consumed.'),
|
|
606
|
+
})
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def run(verbose: true, debug: false)
|
|
610
|
+
@file_list.each do |file|
|
|
611
|
+
if verbose
|
|
612
|
+
puts "Processing #{file}".cyan
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
out, success, _ = process_file(file, verbose: verbose, debug: debug)
|
|
616
|
+
|
|
617
|
+
@incomplete_files << file unless success
|
|
618
|
+
|
|
619
|
+
new_file_path = @out_path + '/' + file
|
|
620
|
+
FileUtils.mkdir_p(File.dirname(new_file_path))
|
|
621
|
+
File.write(new_file_path, out)
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def process_file(file_path, verbose: true, debug: false)
|
|
626
|
+
raise "#{file_path} does not exist." unless File.exist?(file_path)
|
|
627
|
+
|
|
628
|
+
@current_file = file_path
|
|
629
|
+
|
|
630
|
+
success = true
|
|
631
|
+
|
|
632
|
+
# cursor state
|
|
633
|
+
in_comment = false
|
|
634
|
+
in_string = false
|
|
635
|
+
in_expr = false # TODO: multi-line expression parsing
|
|
636
|
+
|
|
637
|
+
output = ''
|
|
638
|
+
|
|
639
|
+
File.readlines(file_path).each_with_index do |line, lineno|
|
|
640
|
+
cursor = 0
|
|
641
|
+
new_line = ""
|
|
642
|
+
|
|
643
|
+
while cursor < line.length
|
|
644
|
+
# stop parsing rest of line on single-line comment
|
|
645
|
+
if !in_comment && !in_string && line[cursor, 2] == "//"
|
|
646
|
+
new_line << line[cursor..-1]
|
|
647
|
+
break
|
|
648
|
+
|
|
649
|
+
# enter multi-line comment
|
|
650
|
+
elsif !in_comment && !in_string && line[cursor, 2] == "/*"
|
|
651
|
+
in_comment = true
|
|
652
|
+
new_line << "/*"
|
|
653
|
+
cursor += 2
|
|
654
|
+
next
|
|
655
|
+
|
|
656
|
+
# leave comment
|
|
657
|
+
elsif in_comment && line[cursor, 2] == "*/"
|
|
658
|
+
in_comment = false
|
|
659
|
+
new_line << "*/"
|
|
660
|
+
cursor += 2
|
|
661
|
+
next
|
|
662
|
+
|
|
663
|
+
# enter string
|
|
664
|
+
elsif !in_comment && line[cursor] == '"'
|
|
665
|
+
in_string = !in_string
|
|
666
|
+
new_line << '"'
|
|
667
|
+
cursor += 1
|
|
668
|
+
next
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# enter command
|
|
672
|
+
if !in_comment && !in_string && line[cursor, @COMMAND_PREFIX.length] == @COMMAND_PREFIX &&
|
|
673
|
+
(cursor == 0 || !/[0-9A-Za-z_]/.match?(line[cursor-1]))
|
|
674
|
+
expr_src = line[(cursor + @COMMAND_PREFIX.length)..]
|
|
675
|
+
begin
|
|
676
|
+
tree = @parser.parse(expr_src)
|
|
677
|
+
rtree_s = tree.to_s.reverse
|
|
678
|
+
|
|
679
|
+
# finds the end of the expression (hacky)
|
|
680
|
+
expr_end = /\d+/.match(rtree_s[..rtree_s.index('__last_char__: '.reverse)].reverse).to_s
|
|
681
|
+
if expr_end.empty?
|
|
682
|
+
raise 'Could not find an end to expression on line; multi-line expressions are not yet supported'
|
|
683
|
+
end
|
|
684
|
+
last_paren = Integer(expr_end) + 1
|
|
685
|
+
|
|
686
|
+
ast = @transformer.apply(tree)
|
|
687
|
+
value = eval_expr(ast)
|
|
688
|
+
@out_stack << value.to_s unless value.nil?
|
|
689
|
+
new_line << @out_stack.join("\n") unless @out_stack.empty?
|
|
690
|
+
@out_stack.clear
|
|
691
|
+
|
|
692
|
+
cursor += @COMMAND_PREFIX.length + last_paren # move cursor past expression
|
|
693
|
+
next
|
|
694
|
+
|
|
695
|
+
rescue Parslet::ParseFailed => e
|
|
696
|
+
puts "#{file_path}:#{lineno+1}: parse failed at expression".yellow
|
|
697
|
+
puts 'ERROR'.underline_red + ": #{e.parse_failure_cause.ascii_tree}".red
|
|
698
|
+
rescue Exception => e
|
|
699
|
+
puts "#{file_path}:#{lineno+1}: parse failed at expression".yellow
|
|
700
|
+
puts 'ERROR'.underline_red + ": #{debug ? e.detailed_message : e.to_s}".red
|
|
701
|
+
# fall through, copy raw text instead
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
success = false
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
new_line << line[cursor]
|
|
708
|
+
cursor += 1
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
new_line = (line != new_line && new_line.strip.empty?) ? '' : new_line
|
|
712
|
+
|
|
713
|
+
if @consume_mode
|
|
714
|
+
@recorded << new_line
|
|
715
|
+
else
|
|
716
|
+
@recorded << new_line if @lick_mode
|
|
717
|
+
output << new_line
|
|
718
|
+
end
|
|
719
|
+
@lines_parsed += 1
|
|
720
|
+
end
|
|
721
|
+
[output, success, @template_args]
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
class ASMFileInterpreter < Interpreter
|
|
726
|
+
# TODO!!!
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
#
|
|
730
|
+
# A read–eval–print loop environment for testing/learning the language
|
|
731
|
+
#
|
|
732
|
+
class REPL < Interpreter
|
|
733
|
+
|
|
734
|
+
def initialize(cmd_prefix = COMMAND_PREFIX, extra_cmds = {}, extra_vars = {}, safe: false, puritan: false,
|
|
735
|
+
no_cache: false, cmd_cache: {})
|
|
736
|
+
@EXTRA_CMDS, @EXTRA_VARS = extra_cmds, extra_vars
|
|
737
|
+
super(cmd_prefix, extra_cmds, extra_vars, safe: safe, puritan: puritan, no_cache: no_cache, cmd_cache: cmd_cache)
|
|
738
|
+
|
|
739
|
+
@running = true
|
|
740
|
+
|
|
741
|
+
@commands.merge!({
|
|
742
|
+
write: ->(x, filename) {
|
|
743
|
+
File.open(filename, "w") do |file|
|
|
744
|
+
@out_stack << x unless x.nil?
|
|
745
|
+
file.write(@out_stack.join("\n"))
|
|
746
|
+
@out_stack.clear
|
|
747
|
+
nil
|
|
748
|
+
end
|
|
749
|
+
}.impure
|
|
750
|
+
.describe(
|
|
751
|
+
"Writes the contents of the out-stack and the given argument to the file specified. " \
|
|
752
|
+
"This command is unique to the REPL interpreter."
|
|
753
|
+
),
|
|
754
|
+
|
|
755
|
+
embed: ->(filename, newline_steps=nil) {
|
|
756
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
757
|
+
File.binread(filename).bytes.join(',')
|
|
758
|
+
}.returns(String).impure
|
|
759
|
+
.describe('Reads all bytes from the given file and joins them into a comma-separated String representation.'),
|
|
760
|
+
|
|
761
|
+
embed_hex: ->(filename, newline_steps=nil) {
|
|
762
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
763
|
+
bytes = File.binread(filename).bytes
|
|
764
|
+
bytes.map! {|b| b.to_i.to_hex }.join(',')
|
|
765
|
+
}.returns(String).impure
|
|
766
|
+
.describe(
|
|
767
|
+
'Reads all bytes from the given file and joins them as hex into a comma-separated String representation.'
|
|
768
|
+
),
|
|
769
|
+
|
|
770
|
+
read: ->(filename) {
|
|
771
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
772
|
+
File.read(filename)
|
|
773
|
+
}.returns(String).impure
|
|
774
|
+
.describe('Reads the file given and returns its contents as a String.'),
|
|
775
|
+
|
|
776
|
+
read_lines: ->(filename) {
|
|
777
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
778
|
+
File.readlines(filename)
|
|
779
|
+
}.returns(Array).impure
|
|
780
|
+
.describe('Reads the file specified and returns an Array containing each line.'),
|
|
781
|
+
|
|
782
|
+
read_bytes: ->(filename) {
|
|
783
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
784
|
+
File.binread(filename).bytes
|
|
785
|
+
}.returns(Array).impure
|
|
786
|
+
.describe('Reads the file specified and returns an Array containing each byte.'),
|
|
787
|
+
|
|
788
|
+
import: ->(template_file, *arg_vals) {
|
|
789
|
+
t_interpreter = CFileInterpreter.new(nil,nil,@COMMAND_PREFIX,@EXTRA_CMDS,@EXTRA_VARS,[*arg_vals])
|
|
790
|
+
ret, _, t_args = t_interpreter.process_file(template_file)
|
|
791
|
+
if t_args.length > 0
|
|
792
|
+
puts "WARNING".underline_yellow + ': '.yellow + "#{t_args.length} template arg#{'s' if t_args.length != 1}"\
|
|
793
|
+
"not used.".yellow
|
|
794
|
+
end
|
|
795
|
+
ret
|
|
796
|
+
}.returns(String).impure
|
|
797
|
+
.describe(
|
|
798
|
+
"Takes a template file name and a value for each arg exported by the template. The template file is " \
|
|
799
|
+
"processed by the interpreter, and the generated code is embedded into the current file."
|
|
800
|
+
),
|
|
801
|
+
|
|
802
|
+
exit: -> { @running = false }.impure
|
|
803
|
+
.describe('Exits the current REPL interpreter session. This command is specific to the REPL interpreter.')
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def run(debug: false)
|
|
809
|
+
loop do
|
|
810
|
+
begin
|
|
811
|
+
print '> '.purple; expr = STDIN.gets&.chomp&.strip
|
|
812
|
+
next if expr.nil? || expr.empty?
|
|
813
|
+
|
|
814
|
+
parsed_tree = @parser.parse(expr, root: :expression)
|
|
815
|
+
puts "Parsed tree: #{parsed_tree.inspect}".blue if debug
|
|
816
|
+
|
|
817
|
+
ast = @transformer.apply(parsed_tree)
|
|
818
|
+
puts "Transf tree: #{ast.inspect}".blue if debug
|
|
819
|
+
|
|
820
|
+
out = eval_expr(ast)
|
|
821
|
+
|
|
822
|
+
@out_stack << out unless out.nil?
|
|
823
|
+
output = @out_stack.join("\n")
|
|
824
|
+
|
|
825
|
+
output = "#<struct NCPP::Block ..." if output.start_with?("#<struct NCPP::Block")
|
|
826
|
+
|
|
827
|
+
puts output.cyan
|
|
828
|
+
@out_stack.clear
|
|
829
|
+
|
|
830
|
+
break unless @running
|
|
831
|
+
|
|
832
|
+
rescue Interrupt # Ctrl+C on windows to exit
|
|
833
|
+
break
|
|
834
|
+
rescue Parslet::ParseFailed => e
|
|
835
|
+
puts 'ERROR'.underline_red + ": #{e.parse_failure_cause.ascii_tree}".red
|
|
836
|
+
rescue Exception => e
|
|
837
|
+
puts 'ERROR'.underline_red + ": #{debug ? e.detailed_message : e.to_s }".red
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
class NCPPFileInterpreter < Interpreter
|
|
846
|
+
|
|
847
|
+
def initialize(cmd_prefix = COMMAND_PREFIX, extra_cmds = {}, extra_vars = {}, safe: false, puritan: false,
|
|
848
|
+
no_cache: false, cmd_cache: {})
|
|
849
|
+
super(cmd_prefix, extra_cmds, extra_vars, safe: safe, puritan: puritan, no_cache: no_cache, cmd_cache: cmd_cache)
|
|
850
|
+
|
|
851
|
+
@running = true
|
|
852
|
+
|
|
853
|
+
@exit_code = 0
|
|
854
|
+
|
|
855
|
+
@commands.merge!({
|
|
856
|
+
write: ->(x, filename) {
|
|
857
|
+
File.open(filename, "w") do |file|
|
|
858
|
+
@out_stack << x unless x.nil?
|
|
859
|
+
file.write(@out_stack.join("\n"))
|
|
860
|
+
@out_stack.clear
|
|
861
|
+
nil
|
|
862
|
+
end
|
|
863
|
+
}.impure
|
|
864
|
+
.describe(
|
|
865
|
+
"Writes the contents of the out-stack and the given argument to the file specified. " \
|
|
866
|
+
"This command is unique to the REPL interpreter."
|
|
867
|
+
),
|
|
868
|
+
|
|
869
|
+
read: ->(filename) {
|
|
870
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
871
|
+
File.read(filename)
|
|
872
|
+
}.returns(String).impure
|
|
873
|
+
.describe('Reads the file given and returns its contents as a String.'),
|
|
874
|
+
|
|
875
|
+
read_lines: ->(filename) {
|
|
876
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
877
|
+
File.readlines(filename)
|
|
878
|
+
}.returns(Array).impure
|
|
879
|
+
.describe('Reads the file specified and returns an Array containing each line.'),
|
|
880
|
+
|
|
881
|
+
read_bytes: ->(filename) {
|
|
882
|
+
raise "File not found: #{filename}" unless File.exist? filename
|
|
883
|
+
File.binread(filename).bytes
|
|
884
|
+
}.returns(Array).impure
|
|
885
|
+
.describe('Reads the file specified and returns an Array containing each byte.'),
|
|
886
|
+
|
|
887
|
+
exit: ->(exit_code = 0) { @exit_code = exit_code; @running = false }.impure
|
|
888
|
+
.describe('Exits the program.')
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def run(ncpp_file, debug: false)
|
|
894
|
+
File.readlines(ncpp_file).each do |line|
|
|
895
|
+
begin
|
|
896
|
+
line = line.strip
|
|
897
|
+
next if line.empty? || line.start_with?('//')
|
|
898
|
+
|
|
899
|
+
eval_str(line)
|
|
900
|
+
|
|
901
|
+
break unless @running
|
|
902
|
+
|
|
903
|
+
rescue Parslet::ParseFailed => e
|
|
904
|
+
puts 'ERROR'.underline_red + ": #{e.parse_failure_cause.ascii_tree}".red
|
|
905
|
+
@exit_code = 1
|
|
906
|
+
break
|
|
907
|
+
rescue Exception => e
|
|
908
|
+
puts 'ERROR'.underline_red + ": #{debug ? e.detailed_message : e.to_s}".red
|
|
909
|
+
@exit_code = 1
|
|
910
|
+
break
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
@exit_code
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
end
|