Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: better error messages #31

Merged
merged 9 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions src/bin/build.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Licensed under the Apache License, Version 2.0 as described in the file LICENSE.
Authors: Júnior Nascimento
*)
open Lang
open Sedlexing

type compiler_options = {
input_files : string list;
Expand Down Expand Up @@ -41,22 +40,19 @@ let read_until_eof () =
read_loop()

let parse_from_stdin json_output =
let str = read_until_eof () in
let buf = Sedlexing.Utf8.from_string str in
try
let ast = MenhirLib.Convert.Simplified.traditional2revised Grammar.program (Lexer.provider buf) in
let source_code = read_until_eof () in
let parsed = Parser.parse_from_source "stdin" source_code in

match parsed with
| Ok ast ->
if json_output then
failwith "TODO implement json output"
else
print_endline (Ast.show_program ast);
`Ok()
with
| Lexer.Invalid_token msg ->
Printf.eprintf "Lexical error: %s at position %d\n" msg (lexeme_start buf);
exit 1
| Grammar.Error ->
Printf.eprintf "Parse error starting at %d, ending at %d\n" (lexeme_start buf) (lexeme_end buf);
exit 1
| Error error ->
Errors.print_compiler_error Format.err_formatter error source_code;
exit 1

let compile options () =
Printf.printf "Compiling files: %s\n" (String.concat ", " options.input_files);
Expand All @@ -71,7 +67,7 @@ let compile options () =

let process options =
match options with
| { features = true; _ } -> print_features ()
| { features = true; _ } -> print_features ()
| { from_stdin = true; json_output; _ } -> parse_from_stdin json_output
| { input_files = []; _ } ->
prerr_endline "Error: No input files provided.";
Expand Down
15 changes: 1 addition & 14 deletions src/lib/ast.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,7 @@ Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Davi William, Sofia Rodrigues
*)

(* A point in the source code defined using line and column. *)
type point = {
line: int;
column: int
}
[@@deriving show]

(* A position represents a location in the source code. *)
type position = {
start: point;
end': point;
}
[@@deriving show]
open Location

(* An identifier is a string with a position in the source code, usually representing a name. *)
type identifier = string * position
Expand Down
15 changes: 1 addition & 14 deletions src/lib/ast.mli
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,7 @@ Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Davi William, Sofia Rodrigues
*)

(* A point in the source code defined using line and column. *)
type point = {
line: int;
column: int
}
[@@deriving show]

(* A position represents a location in the source code. *)
type position = {
start: point;
end': point;
}
[@@deriving show]
open Location

(* An identifier is a string with a position in the source code, usually representing a name. *)
type identifier = string * position
Expand Down
53 changes: 53 additions & 0 deletions src/lib/errors.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
(*
Copyright 2025 Lemonadic. All rights reserved.
Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Sofia Rodrigues
*)
open Location

type compiler_error = {
id : int;
message : string;
file : string;
location : position;
hints : (string * position) list;
additional_info : (string * string) list;
}

(* ANSI escape codes for color formatting *)
let bold = "\027[1m"
let reset = "\027[0m"
let red = "\027[31m"
let white = "\027[37m"

(* Pretty prints an error message with a more beautiful layout. *)
let print_compiler_error fmt error source =
let pp_range fmt { start_pos; end_pos } = Format.fprintf fmt "%d:%d~%d:%d" start_pos.line start_pos.column end_pos.line end_pos.column in

Format.fprintf fmt "\n %s%sERROR E%04d%s: %s%s\n" bold red error.id white error.message reset;

Format.fprintf fmt " --> %s:" error.file;
pp_range fmt error.location;
Format.fprintf fmt "\n\n";

(* Split source code into lines and highlight relevant lines *)
let relevant_lines = expand_positions (error.location :: (List.map (fun (_, pos) -> pos) error.hints)) in
let sorted_lines = List.sort_uniq compare relevant_lines in
let source_lines = String.split_on_char '\n' source |> Array.of_list in

(* Print the relevant lines of code with error highlighting *)
List.iter (fun line_num ->
if line_num > 0 && line_num <= Array.length source_lines then
let line = source_lines.(line_num - 1) in
Format.fprintf fmt "%4d | %s\n" line_num line;

if line_num = error.location.start_pos.line then
let start_col = error.location.start_pos.column in
let end_col = error.location.end_pos.column in
let error_length = end_col - start_col in
let caret_line = String.make (start_col) ' ' ^ String.make error_length '^' in
Format.fprintf fmt " | %s%s%s%s\n" red bold caret_line reset
) sorted_lines;

Format.fprintf fmt "\n";
19 changes: 19 additions & 0 deletions src/lib/errors.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
(*
Copyright 2025 Lemonadic. All rights reserved.
Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Sofia Rodrigues
*)
open Location

type compiler_error = {
id : int;
message : string;
file : string;
location : position;
hints : (string * position) list;
additional_info : (string * string) list;
}

(* Pretty prints an error message. *)
val print_compiler_error : Format.formatter -> compiler_error -> string -> unit
9 changes: 2 additions & 7 deletions src/lib/grammar.mly
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ Authors: Davi William, Sofia Rodrigues
*)
%{
open Ast

(* Creates an AST position using the Menhir internal position *)
let loc startpos endpos = {
start = { column = startpos.Lexing.pos_cnum - startpos.Lexing.pos_bol; line = startpos.Lexing.pos_lnum };
end' = { column = endpos.Lexing.pos_cnum - endpos.Lexing.pos_bol; line = endpos.Lexing.pos_lnum }
}
open Location

%}

Expand All @@ -36,7 +31,7 @@ let loc startpos endpos = {

(* Macro for creating some rule with a position in a tuple. *)
localized(x):
| d = x { (d, (loc $startpos $endpos)) }
| d = x { (d, (mk_ast_position $loc)) }

(* The entrypoint of the entire parser, it parses a sequence of top level definitons. *)
program:
Expand Down
1 change: 0 additions & 1 deletion src/lib/lexer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ let provider buf () =
token, start, stop

(* `from_string` converts an input string into a sequence of tokens using the provided lexer. *)

let from_string f string =
provider (from_string string)
|> MenhirLib.Convert.Simplified.traditional2revised f
51 changes: 51 additions & 0 deletions src/lib/location.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
(*
Copyright 2024 Lemonadic. All rights reserved.
Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Sofia Rodrigues
*)

module IntSet = Set.Make(struct type t = int let compare = compare end)

(* A point in the source code defined using line and column. *)
type point = {
line: int;
column: int
}
[@@deriving show]

(* A position represents a location in the source code. *)
type position = {
start_pos: point;
end_pos: point;
}
[@@deriving show]

(** Expands a list of ranges into a set of selected line numbers. *)
let expand_positions ranges =
let add_if_valid acc line = if line > 0 then IntSet.add line acc else acc in

let determine_lines l1 l2 =
match l2 - l1 with
| 0 -> [l1 - 1; l1; l1 + 1]
| _ -> [l1 - 1; l1; l2; l2 + 1] in

let process_range acc {start_pos; end_pos} =
determine_lines start_pos.line end_pos.line
|> List.fold_left add_if_valid acc in

ranges
|> List.fold_left process_range IntSet.empty
|> IntSet.to_list

(* Creates an AST position using the Menhir internal position *)
let mk_ast_position (startpos, endpos) = {
start_pos = {
column = startpos.Lexing.pos_cnum - startpos.Lexing.pos_bol;
line = startpos.Lexing.pos_lnum
};
end_pos = {
column = endpos.Lexing.pos_cnum - endpos.Lexing.pos_bol;
line = endpos.Lexing.pos_lnum
}
}
26 changes: 26 additions & 0 deletions src/lib/location.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
(*
Copyright 2024 Lemonadic. All rights reserved.
Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Sofia Rodrigues
*)

(* A point in the source code defined using line and column. *)
type point = {
line: int;
column: int
}
[@@deriving show]

(* A position represents a location in the source code. *)
type position = {
start_pos: point;
end_pos: point;
}
[@@deriving show]

(** Expands a list of ranges into a set of selected line numbers. *)
val expand_positions : position list -> int list

(* Creates an AST position using the Menhir internal position *)
val mk_ast_position : Lexing.position * Lexing.position -> position
33 changes: 33 additions & 0 deletions src/lib/parser.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
(*
Copyright 2024 Lemonadic. All rights reserved.
Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Sofia Rodrigues
*)
open Errors

let handle_error file buf id message hints =
{
id;
message;
file;
location = Location.mk_ast_position (Sedlexing.lexing_positions buf);
hints;
additional_info = [];
}

let parse file (buf : Sedlexing.lexbuf) =
try
let ast = MenhirLib.Convert.Simplified.traditional2revised Grammar.program (Lexer.provider buf) in
Ok ast
with
| Lexer.Invalid_token msg -> Error (handle_error file buf 1 ("Lexical error: " ^ msg) [])
| Grammar.Error -> Error (handle_error file buf 2 "Parse error" [])

(* Parses a source string from the given string. *)
let parse_from_source file source =
parse file (Sedlexing.Utf8.from_string source)

(* Parses input from a channel. *)
let parse_from_channel file channel =
parse file (Sedlexing.Utf8.from_channel channel)
13 changes: 13 additions & 0 deletions src/lib/parser.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
(*
Copyright 2025 Lemonadic. All rights reserved.
Licensed under the Apache License, Version 2.0 as described in the file LICENSE.

Authors: Sofia Rodrigues
*)
open Errors

(* Parses a source string from the given string. *)
val parse_from_channel : string -> in_channel -> (Ast.program, compiler_error) result

(* Parses input from a channel. *)
val parse_from_source : string -> string -> (Ast.program, compiler_error) result