From d3737706080dc841bb853640fb98380a65c96a1b Mon Sep 17 00:00:00 2001 From: Tau Date: Tue, 23 Jul 2024 21:00:19 +0200 Subject: [PATCH] Report mismatched parentheses before more specific errors Addresses #18597 (the specific case presented there, but not the general goal of that issue) by checking for mismatched parentheses after a parser error. Unclosed parens are often a root cause of otherwise much-more-cryptic errors, so reporting them should have priority. --- lib/std/zig/AstGen.zig | 62 ++++++++++++++++--- lib/std/zig/Parse.zig | 31 ++++++++++ .../compile_errors/invalid_unicode_escape.zig | 2 +- test/compile_errors.zig | 47 +++++++++++++- 4 files changed, 130 insertions(+), 12 deletions(-) diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index a6be743c2bc0..501591c6188a 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -1,6 +1,8 @@ //! Ingests an AST and produces ZIR code. const AstGen = @This(); +const Parse = @import("Parse.zig"); + const std = @import("std"); const Ast = std.zig.Ast; const mem = std.mem; @@ -13816,21 +13818,65 @@ fn lowerAstErrors(astgen: *AstGen) !void { const gpa = astgen.gpa; const parse_err = tree.errors[0]; - - var msg: std.ArrayListUnmanaged(u8) = .{}; - defer msg.deinit(gpa); + const err_tok = parse_err.token + @intFromBool(parse_err.token_is_prev); const token_starts = tree.tokens.items(.start); const token_tags = tree.tokens.items(.tag); + if (try Parse.findUnmatchedParen(astgen.gpa, token_tags)) |tok| { + const text: []const u8 = switch (token_tags[tok]) { + .l_paren => "unclosed parenthesis", + .l_brace => "unclosed curly brace", + .l_bracket => "unclosed bracket", + .r_paren => "unmatched parenthesis", + .r_brace => "unmatched curly brace", + .r_bracket => "unmatched bracket", + else => unreachable, + }; + try astgen.appendErrorTok(tok, "{s}", .{text}); + // Unmatched parentheses are often an underlying cause of + // otherwise more obscure errors, so we only report the parse + // error if it probably wasn't caused by this. + switch (parse_err.tag) { + .asterisk_after_ptr_deref, + .chained_comparison_operators, + .expected_inlinable, + .expected_labelable, + .expected_prefix_expr, + .expected_return_type, + .extern_fn_body, + .extra_addrspace_qualifier, + .extra_align_qualifier, + .extra_allowzero_qualifier, + .extra_const_qualifier, + .extra_volatile_qualifier, + .ptr_mod_on_array_child_type, + .invalid_bit_range, + .same_line_doc_comment, + .test_doc_comment, + .comptime_doc_comment, + .varargs_nonfinal, + .expected_continue_expr, + .mismatched_binary_op_whitespace, + .invalid_ampersand_ampersand, + .extra_for_capture, + .for_input_not_captured, + => {}, + .expected_token => if (token_tags[err_tok] != .invalid) return, + else => return, + } + } + + var msg: std.ArrayListUnmanaged(u8) = .{}; + defer msg.deinit(gpa); + var notes: std.ArrayListUnmanaged(u32) = .{}; defer notes.deinit(gpa); - const tok = parse_err.token + @intFromBool(parse_err.token_is_prev); - if (token_tags[tok] == .invalid) { - const bad_off: u32 = @intCast(tree.tokenSlice(tok).len); - const byte_abs = token_starts[tok] + bad_off; - try notes.append(gpa, try astgen.errNoteTokOff(tok, bad_off, "invalid byte: '{'}'", .{ + if (token_tags[err_tok] == .invalid) { + const bad_off: u32 = @intCast(tree.tokenSlice(err_tok).len); + const byte_abs = token_starts[err_tok] + bad_off; + try notes.append(gpa, try astgen.errNoteTokOff(err_tok, bad_off, "invalid byte: '{'}'", .{ std.zig.fmtEscapes(tree.source[byte_abs..][0..1]), })); } diff --git a/lib/std/zig/Parse.zig b/lib/std/zig/Parse.zig index c43868a7d4c7..a1d54ed41c58 100644 --- a/lib/std/zig/Parse.zig +++ b/lib/std/zig/Parse.zig @@ -205,6 +205,37 @@ pub fn parseZon(p: *Parse) !void { }; } +pub fn findUnmatchedParen(gpa: Allocator, token_tags: []const Token.Tag) !?TokenIndex { + var stack = std.ArrayList(struct { + tag: Token.Tag, + idx: TokenIndex, + }).init(gpa); + defer stack.deinit(); + + for (token_tags, 0..) |t, i| { + switch (t) { + .l_paren, .l_brace, .l_bracket => try stack.append(.{ .tag = t, .idx = @intCast(i) }), + .r_paren, .r_brace, .r_bracket => { + if (stack.items.len == 0 or t != closingParen(stack.pop().tag)) + return @intCast(i); + }, + else => {}, + } + } + if (stack.items.len > 0) + return stack.pop().idx; + return null; +} + +fn closingParen(a: Token.Tag) Token.Tag { + return switch (a) { + .l_paren => .r_paren, + .l_brace => .r_brace, + .l_bracket => .r_bracket, + else => unreachable, + }; +} + /// ContainerMembers <- ContainerDeclaration* (ContainerField COMMA)* (ContainerField / ContainerDeclaration*) /// /// ContainerDeclaration <- TestDecl / ComptimeDecl / doc_comment? KEYWORD_pub? Decl diff --git a/test/cases/compile_errors/invalid_unicode_escape.zig b/test/cases/compile_errors/invalid_unicode_escape.zig index 1555f2be801a..9251022e1f23 100644 --- a/test/cases/compile_errors/invalid_unicode_escape.zig +++ b/test/cases/compile_errors/invalid_unicode_escape.zig @@ -1,5 +1,5 @@ export fn entry() void { - const a = '\u{12z34}'; + const a = '\u{12z34'; } // error diff --git a/test/compile_errors.zig b/test/compile_errors.zig index 5c5a574caf5b..f7dadc5389d3 100644 --- a/test/compile_errors.zig +++ b/test/compile_errors.zig @@ -28,13 +28,54 @@ pub fn addCases(ctx: *Cases, b: *std.Build) !void { \\ ); \\} , &[_][]const u8{ - \\:2:5: error: + \\:2:5: error: \\ hello! \\ I'm a multiline error message. \\ I hope to be very useful! - \\ + \\ \\ also I will leave this trailing newline here if you don't mind - \\ + \\ + }); + } + + { + const case = ctx.obj("unmatched parentheses", b.graph.host); + + case.addError( + \\export fn a() void { + \\} + \\} + , &[_][]const u8{ + ":3:1: error: unmatched curly brace", + }); + } + + { + const case = ctx.obj("unmatched parentheses #2", b.graph.host); + + case.addError( + \\const c = { + \\) + \\}; + , &[_][]const u8{ + ":2:1: error: unmatched parenthesis", + }); + } + + { + const case = ctx.obj("unmatched parentheses #3", b.graph.host); + + case.addError( + \\pub fn bar() void { + \\ // Oops... + \\ } + \\ + \\ if (true) { + \\ return; + \\ } + \\} + , &[_][]const u8{ + ":8:1: error: unmatched curly brace", }); }