Class: NCPP::CFileInterpreter

Inherits:
Interpreter show all
Defined in:
lib/ncpp/interpreter.rb

Overview

Scans C/C++ source files for commands and expands them in place

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Interpreter

#call, #commands, #def_command, #def_variable, #eval_expr, #eval_str, #get_binding, #get_cacheable_cache, #get_command, #get_new_commands, #get_new_variables, #get_variable, #node_impure?, #unknown_command_error, #unknown_variable_error, #variables

Constructor Details

#initialize(file_list, out_path, cmd_prefix = COMMAND_PREFIX, extra_cmds = {}, extra_vars = {}, template_args = [], safe: false, puritan: false, no_cache: false, cmd_cache: {}) ⇒ CFileInterpreter

Returns a new instance of CFileInterpreter.



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'lib/ncpp/interpreter.rb', line 481

def initialize(file_list, out_path, cmd_prefix = COMMAND_PREFIX, extra_cmds = {}, extra_vars = {}, template_args=[],
                safe: false, puritan: false, no_cache: false, cmd_cache: {})

  @EXTRA_CMDS, @EXTRA_VARS = extra_cmds, extra_vars
  super(cmd_prefix, extra_cmds, extra_vars, safe: safe, puritan: puritan, no_cache: no_cache, cmd_cache: cmd_cache)

  @file_list = file_list.is_a?(Array) ? file_list : [file_list]
  @current_file = nil
  @out_path = out_path

  @incomplete_files = []

  @lines_parsed = 0

  @template_args = template_args

  @recorded = ''
  @consume_mode = false
  @lick_mode = false

  @commands.merge!({
    embed: ->(filename, newline_steps=nil) {
      dir = File.dirname(@current_file || Dir.pwd)
      path = File.expand_path(filename, dir)
      raise "File not found: #{path}" unless File.exist? path
      File.binread(path).bytes.join(',')
    }.returns(String).impure
     .describe(
      'Reads all bytes from the specified file and joins them into a comma-separated String representation.'
    ),

    embed_hex: ->(filename, newline_steps=nil) {
      dir = File.dirname(@current_file || Dir.pwd)
      path = File.expand_path(filename, dir)
      raise "File not found: #{path}" unless File.exist? path
      bytes = File.binread(path).bytes
      bytes.map! {|b| b.to_i.to_hex }.join(',')
    }.returns(String).impure
     .describe(
      'Reads all bytes from the specified file and joins them as hex into a comma-separated String representation.'
    ),

    read: ->(filename) {
      dir = File.dirname(@current_file || Dir.pwd)
      path = File.expand_path(filename, dir)
      raise "File not found: #{path}" unless File.exist? path
      File.read(path)
    }.returns(String).impure
      .describe('Reads the file specified and returns its contents as a String.'),

    read_lines: ->(filename) {
      dir = File.dirname(@current_file || Dir.pwd)
      path = File.expand_path(filename, dir)
      raise "File not found: #{path}" unless File.exist? path
      File.readlines(path)
    }.returns(Array).impure
      .describe('Reads the file specified and returns an Array containing each line.'),

    read_bytes: ->(filename) {
      dir = File.dirname(@current_file || Dir.pwd)
      path = File.expand_path(filename, dir)
      raise "File not found: #{path}" unless File.exist? path
      File.binread(path).bytes
    }.returns(Array).impure
      .describe('Reads the file specified and returns an Array containing each byte.'),

    import: ->(template_file, *arg_vals) {
      t_interpreter = CFileInterpreter.new(nil,nil,@COMMAND_PREFIX,@EXTRA_CMDS,@EXTRA_VARS,[*arg_vals])
      dir = File.dirname(@current_file || Dir.pwd)
      path = File.expand_path(template_file, dir)
      ret, _, t_args = t_interpreter.process_file(path)
      @lines_parsed += t_interpreter.lines_parsed
      if t_args.length > 0
        puts "WARNING".underline_yellow + ': '.yellow + "#{t_args.length} template arg#{'s' if t_args.length != 1}"\
             " not used.".yellow
      end
      ret
    }.returns(String).impure
     .describe(
      "Takes a template file name and a value for each arg exported by the template. The template file is " \
      "processed by the interpreter, and the generated code is embedded into the current file."
    ),

    expect: ->(*arg_names) {
      argc, targc = @template_args.length, arg_names.length
      if targc != argc
        raise "#{argc} template arg#{'s' if argc != 1} given when #{targc} #{targc==1 ? 'is' : 'are'} required."
      end
      arg_names.each_with_index do |arg, i|
        Utils.valid_identifier_check(arg)
        @variables[arg.to_sym] = @template_args.first
        @template_args = @template_args.drop(1)
      end
    }.impure
      .describe(
      "Declares the variables that should be defined when importing the template. " \
      "This command is specific to CFileInterpreter."
    ),

    start_consume: -> { @consume_mode = true }.impure
      .describe(
        "Starts consume mode; the following parsed lines will stored in a variable held by the interpreter, which "\
        "can only be accessed by the 'spit' command. Consumed lines will not be put in the generated source file."
    ),

    end_consume: -> { @consume_mode = false }.impure
      .describe('Ends consume parse mode.'),

    start_lick: -> { @lick_mode = true }.impure
      .describe('Starts lick parse mode.'),

    end_lick: -> { @lick_mode = false }.impure
      .describe('Ends lick parse mode.'),

    spit: ->(retain = false) {
      ret = @recorded.clone
      @recorded.clear unless retain
      ret
    }.returns(String).impure
      .describe('Gets what was consumed or licked.'),

    clear_consumed: -> { @recorded.clear }.impure
      .describe('Clears the variable containing what was consumed or licked.'),
    
    clear_licked: -> { @recorded.clear }.impure
      .describe('Clears the variable containing what was licked or consumed.'),
  })
end

Instance Attribute Details

#incomplete_filesObject (readonly)

Returns the value of attribute incomplete_files.



479
480
481
# File 'lib/ncpp/interpreter.rb', line 479

def incomplete_files
  @incomplete_files
end

#lines_parsedObject (readonly)

Returns the value of attribute lines_parsed.



479
480
481
# File 'lib/ncpp/interpreter.rb', line 479

def lines_parsed
  @lines_parsed
end

Instance Method Details

#process_file(file_path, verbose: true, debug: false) ⇒ Object



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
# File 'lib/ncpp/interpreter.rb', line 626

def process_file(file_path, verbose: true, debug: false)
  raise "#{file_path} does not exist." unless File.exist?(file_path)

  @current_file = file_path

  success = true

  # cursor state
  in_comment = false
  in_string  = false
  in_expr    = false # TODO: multi-line expression parsing

  output = ''

  File.readlines(file_path).each_with_index do |line, lineno|
    cursor   = 0
    new_line = ""

    while cursor < line.length
      # stop parsing rest of line on single-line comment
      if !in_comment && !in_string && line[cursor, 2] == "//"
        new_line << line[cursor..-1]
        break

      # enter multi-line comment
      elsif !in_comment && !in_string && line[cursor, 2] == "/*"
        in_comment = true
        new_line << "/*"
        cursor += 2
        next

      # leave comment
      elsif in_comment && line[cursor, 2] == "*/"
        in_comment = false
        new_line << "*/"
        cursor += 2
        next

      # enter string
      elsif !in_comment && line[cursor] == '"'
        in_string = !in_string
        new_line << '"'
        cursor += 1
        next
      end

      # enter command
      if !in_comment && !in_string && line[cursor, @COMMAND_PREFIX.length] == @COMMAND_PREFIX &&
          (cursor == 0 || !/[0-9A-Za-z_]/.match?(line[cursor-1]))
        expr_src = line[(cursor + @COMMAND_PREFIX.length)..]
        begin
          tree    = @parser.parse(expr_src)
          rtree_s = tree.to_s.reverse

          # finds the end of the expression (hacky)
          expr_end = /\d+/.match(rtree_s[..rtree_s.index('__last_char__: '.reverse)].reverse).to_s
          if expr_end.empty?
            raise 'Could not find an end to expression on line; multi-line expressions are not yet supported'
          end
          last_paren = Integer(expr_end) + 1

          ast = @transformer.apply(tree)
          value = eval_expr(ast)
          @out_stack << value.to_s unless value.nil?
          new_line << @out_stack.join("\n") unless @out_stack.empty?
          @out_stack.clear

          cursor += @COMMAND_PREFIX.length + last_paren # move cursor past expression
          next

        rescue Parslet::ParseFailed => e
          puts "#{file_path}:#{lineno+1}: parse failed at expression".yellow
          puts 'ERROR'.underline_red + ": #{e.parse_failure_cause.ascii_tree}".red
        rescue Exception => e
          puts "#{file_path}:#{lineno+1}: parse failed at expression".yellow
          puts 'ERROR'.underline_red + ": #{debug ? e.detailed_message : e.to_s}".red
          # fall through, copy raw text instead
        end

        success = false
      end

      new_line << line[cursor]
      cursor += 1
    end

    new_line = (line != new_line && new_line.strip.empty?) ? '' : new_line

    if @consume_mode
      @recorded << new_line
    else
      @recorded << new_line if @lick_mode
      output << new_line
    end
    @lines_parsed += 1
  end
  [output, success, @template_args]
end

#run(verbose: true, debug: false) ⇒ Object



610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/ncpp/interpreter.rb', line 610

def run(verbose: true, debug: false)
  @file_list.each do |file|
    if verbose
      puts "Processing #{file}".cyan
    end

    out, success, _ = process_file(file, verbose: verbose, debug: debug)

    @incomplete_files << file unless success

    new_file_path = @out_path + '/' + file
    FileUtils.mkdir_p(File.dirname(new_file_path))
    File.write(new_file_path, out)
  end
end