NCPrePatcher 0.2.0-x64-mingw-ucrt

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.
@@ -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