Skip to content

Commit

Permalink
Report mismatched parentheses before more specific errors
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tau-dev committed Jul 24, 2024
1 parent 7aaebd1 commit d373770
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 12 deletions.
62 changes: 54 additions & 8 deletions lib/std/zig/AstGen.zig
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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]),
}));
}
Expand Down
31 changes: 31 additions & 0 deletions lib/std/zig/Parse.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/cases/compile_errors/invalid_unicode_escape.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export fn entry() void {
const a = '\u{12z34}';
const a = '\u{12z34';
}

// error
Expand Down
47 changes: 44 additions & 3 deletions test/compile_errors.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
}

Expand Down

0 comments on commit d373770

Please sign in to comment.