~tim/scheme-vm

633d0f52f4ace23a1d56d099b6562b758851cf69 — Tim Morgan 2 years ago ad59b0a
Show better message on syntax error
4 files changed, 75 insertions(+), 23 deletions(-)

M parser.rb
M program.rb
M spec/program_spec.rb
M src/lib.rs
M parser.rb => parser.rb +6 -3
@@ 4,14 4,17 @@ require 'pathname'

class Parser
  class ParseError < StandardError
    attr_reader :line
    attr_reader :filename, :line, :column, :expected

    def initialize(line)
    def initialize(filename, line, column, expected)
      @filename = filename
      @line = line
      @column = column
      @expected = expected
    end

    def message
      "error parsing at line #{line}"
      "expected one of: #{expected.sort.inspect}"
    end
  end


M program.rb => program.rb +27 -5
@@ 1,26 1,42 @@
require_relative 'parser'
require_relative 'compiler'
require 'time'

class Program
  EXIT_CODE_VAR_UNDEFINED  = 1
  EXIT_CODE_STACK_TOO_DEEP = 2
  EXIT_CODE_SYNTAX_ERROR   = 3

  def initialize(code, filename: '(unknown)', args: [], stdout: $stdout)
    @filename = filename
    @args = args
    @stdout = stdout
    @code = code
    @compiler = Compiler.new(code, filename: filename)
  rescue Parser::ParseError => e
    print_syntax_error(e)
    @error_parsing = true
  end

  def run(code: nil, debug: 0)
    return EXIT_CODE_SYNTAX_ERROR if @error_parsing
    start_compile = Time.now
    @instr = @compiler.compile(code)
    total_compile = Time.now - start_compile
    VM::PrettyPrinter.new(@instr, grouped: true, ip: true).print if debug >= 1
    vm.debug = debug
    start_execute = Time.now
    vm.execute(@instr)
    total_execute = Time.now - start_execute
    puts "compile: #{total_compile}" if false # TEMP
    puts "execute: #{total_execute}" if false
    vm.return_value
  rescue VM::VariableUndefined => e
    print_variable_undefined_error(e)
    1
    EXIT_CODE_VAR_UNDEFINED
  rescue VM::CallStackTooDeep => e
    print_call_stack_too_deep_error(e)
    2
    EXIT_CODE_STACK_TOO_DEEP
  end

  def filename=(f)


@@ 45,10 61,11 @@ class Program
    @stdout.puts(message)
  end

  def error_details_to_s(e)
    return '' unless e.filename && e.filename != '' && @compiler.source[e.filename]
  def error_details_to_s(e, code = nil)
    return '' unless e.filename && e.filename != ''
    return '' unless (code ||= @compiler.source[e.filename])
    lines_range = (e.line - 2)..(e.line - 1)
    code = @compiler.source[e.filename].split("\n")[lines_range].map { |l| "  #{l}" }.join("\n")
    code = code.split("\n")[lines_range].map { |l| "  #{l}" }.join("\n")
    line = "#{e.filename}##{e.line}"
    pointer = " #{' ' * e.column}^ #{e.message}"
    "\n\n#{line}\n\n#{code}\n#{pointer}"


@@ 65,4 82,9 @@ class Program
      @stdout.puts " #{' ' * name.column}^"
    end
  end

  def print_syntax_error(e)
    message = 'Syntax Error:' + error_details_to_s(e, @code)
    @stdout.puts(message)
  end
end

M spec/program_spec.rb => spec/program_spec.rb +25 -0
@@ 231,5 231,30 @@ describe Program do
        )
      end
    end

    context 'when a syntax error occurs' do
      let(:code) do
        "(foo)\n" \
        "\n" \
        '      (()'
      end

      it 'sets the exit code to 3' do
        expect(subject.run).to eq(3)
      end

      it 'shows the filename, line and column of the error' do
        subject.run
        stdout.rewind
        expected = ["\"", "#;", "#|", "'", "(", ")", ",", ",@", ";", "[ \t\n]", "[^() \t\n[]{}|\"]", "`", "|"]
        expect(stdout.read).to eq(
          "Syntax Error:\n\n" \
            "#{__FILE__}#3\n\n" \
            "  \n" \
            "        (()\n" \
            "           ^ expected one of: #{expected.inspect}\n"
        )
      end
    end
  end
end

M src/lib.rs => src/lib.rs +17 -15
@@ 15,7 15,8 @@ mod lisp {

fn parse_native(rself: Value) -> Value {
    let code = rbstr2str!(&rb::ivar_get(&rself, "@code"));
    let filename = rbstr2str!(&rb::ivar_get(&rself, "@filename"));
    let filename_rbstr = rb::ivar_get(&rself, "@filename");
    let filename = rbstr2str!(&filename_rbstr);
    let newlines: Vec<usize> = code.match_indices("\n").map(|(i, _s)| i).collect();
    rb::gc_disable();
    match lisp::program(&code, &filename, &newlines) {


@@ 24,25 25,26 @@ fn parse_native(rself: Value) -> Value {
            ast
        },
        Err(err) => {
            rb::gc_enable();
            //let expected = rb::vec2rbarr(
                //err.expected.iter().cloned().map(|e| rb::str_new(&e.to_string())).collect()
            //);
            println!("{}", err.line);
            println!("{}", err.column);
            println!("{:?}", err.expected);
            println!("{:?}", &code);
            println!("{:?}", &code[err.column..]);
            let c_parser = rb::const_get("Parser", &RB_NIL);
            let c_parse_error = rb::const_get("ParseError", &c_parser);
            let line = int2rbnum!(err.line);
            let error = rb::class_new_instance(&c_parse_error, vec![line]);
            rb::raise_instance(&error);
            raise_syntax_error(err, filename_rbstr);
            RB_NIL
        }
    }
}

fn raise_syntax_error(err: lisp::ParseError, filename_rbstr: Value) {
    let c_parser = rb::const_get("Parser", &RB_NIL);
    let c_parse_error = rb::const_get("ParseError", &c_parser);
    let line = int2rbnum!(err.line);
    let column = int2rbnum!(err.column);
    let mut expected = rb::ary_new();
    for token in err.expected {
        expected = rb::ary_push(expected, rb::str_new(token));
    }
    let error = rb::class_new_instance(&c_parse_error, vec![filename_rbstr, line, column, expected]);
    rb::gc_enable();
    rb::raise_instance(&error);
}

#[no_mangle]
pub extern fn init_parser() {
    let c_parser = rb::const_get("Parser", &RB_NIL);