From b490acffee2dd525448a8d0bf23c52bf477aaa4d Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 16 Aug 2025 12:49:18 -0400 Subject: [PATCH] Tweak an error message --- src/parse/AST.zig | 21 ++-- src/parse/Parser.zig | 74 +++++++++++---- test/snapshots/expr_if_missing_else.md | 2 +- test/snapshots/file/underscore_type_decl.md | 95 ++++++++++++++++--- test/snapshots/match_expr/guards_1.md | 4 +- test/snapshots/match_expr/guards_2.md | 4 +- .../record_different_fields_reserved_error.md | 2 +- 7 files changed, 157 insertions(+), 45 deletions(-) diff --git a/src/parse/AST.zig b/src/parse/AST.zig index 33f27ca6b0..a985e154df 100644 --- a/src/parse/AST.zig +++ b/src/parse/AST.zig @@ -143,17 +143,18 @@ pub fn tokenizeDiagnosticToReport(self: *AST, diagnostic: tokenize.Diagnostic, a var report = reporting.Report.init(allocator, title, .runtime_error); try report.document.addText(body); - + // Add the region information from the diagnostic if valid - if (diagnostic.region.start.offset < diagnostic.region.end.offset and - diagnostic.region.end.offset <= self.env.source.len) { - + if (diagnostic.region.start.offset < diagnostic.region.end.offset and + diagnostic.region.end.offset <= self.env.source.len) + { + // Calculate line starts if not already done var env = self.env.*; if (env.line_starts.items.items.len == 0) { try env.calcLineStarts(allocator); } - + // Convert region to RegionInfo const region_info = base.RegionInfo.position( self.env.source, @@ -164,7 +165,7 @@ pub fn tokenizeDiagnosticToReport(self: *AST, diagnostic: tokenize.Diagnostic, a // If we can't calculate region info, just return the report without source context return report; }; - + // Add source region to the report try report.document.addSourceRegion( region_info, @@ -174,7 +175,7 @@ pub fn tokenizeDiagnosticToReport(self: *AST, diagnostic: tokenize.Diagnostic, a env.line_starts.items.items, ); } - + return report; } @@ -541,9 +542,11 @@ pub fn parseDiagnosticToReport(self: *AST, env: *const CommonEnv, diagnostic: Di try report.document.addLineBreak(); try report.document.addReflowingText("When "); try report.document.addKeyword("if"); - try report.document.addReflowingText(" is used as an expression (to produce a value), it must have an "); + try report.document.addReflowingText(" is used as an expression (to evaluate to a value), it must have an "); try report.document.addKeyword("else"); - try report.document.addReflowingText(" branch to handle the case when the condition is false."); + try report.document.addReflowingText(" branch to specify what value to use when the condition is "); + try report.document.addKeyword("False"); + try report.document.addReflowingText("."); }, else => { const tag_name = @tagName(diagnostic.tag); diff --git a/src/parse/Parser.zig b/src/parse/Parser.zig index d55f34fc9e..de1f4b8e9f 100644 --- a/src/parse/Parser.zig +++ b/src/parse/Parser.zig @@ -1168,29 +1168,65 @@ fn parseStmtByType(self: *Parser, statementType: StatementType) Error!?AST.State // continue to parse final expression } }, - // Expect to parse a Type Annotation, e.g. `Foo a : (a,a)` + // Could be a Type Annotation (e.g. `Foo a : (a,a)`) or a pattern assignment (e.g. `Pair(x, y) = expr`) .UpperIdent => { const start = self.pos; if (statementType == .top_level) { - const header = try self.parseTypeHeader(); - if (self.peek() != .OpColon and self.peek() != .OpColonEqual) { - // Point to the unexpected token (e.g., "U8" in "List U8") - return try self.pushMalformed(AST.Statement.Idx, .expected_colon_after_type_annotation, self.pos); + // Look ahead to determine if this is a type declaration or pattern assignment + // We need to check what comes after the identifier and any parentheses + var lookahead_pos = self.pos + 1; + var paren_depth: u32 = 0; + + // Skip past any parentheses to find what operator follows + while (lookahead_pos < self.tok_buf.tokens.len) { + const tok = self.tok_buf.tokens.items(.tag)[lookahead_pos]; + if (tok == .OpenRound or tok == .NoSpaceOpenRound) { + paren_depth += 1; + } else if (tok == .CloseRound) { + if (paren_depth == 0) break; + paren_depth -= 1; + if (paren_depth == 0) { + lookahead_pos += 1; + break; + } + } else if (paren_depth == 0) { + // We're not in parentheses, so check what token we have + break; + } + lookahead_pos += 1; + } + + // Check what token follows the identifier (and any parentheses) + const next_tok = if (lookahead_pos < self.tok_buf.tokens.len) + self.tok_buf.tokens.items(.tag)[lookahead_pos] + else + .EndOfFile; + + // If it's OpAssign, this is a pattern assignment, not a type declaration + if (next_tok == .OpAssign) { + // Fall through to parse as a pattern assignment + } else { + // Parse as a type declaration + const header = try self.parseTypeHeader(); + if (self.peek() != .OpColon and self.peek() != .OpColonEqual) { + // Point to the unexpected token (e.g., "U8" in "List U8") + return try self.pushMalformed(AST.Statement.Idx, .expected_colon_after_type_annotation, self.pos); + } + const kind: AST.TypeDeclKind = if (self.peek() == .OpColonEqual) .nominal else .alias; + self.advance(); + const anno = try self.parseTypeAnno(.not_looking_for_args); + const where_clause = try self.parseWhereConstraint(); + // Use the type annotation's end position if there's no where clause, + // otherwise use the current position (after parsing where clause) + const statement_idx = try self.store.addStatement(.{ .type_decl = .{ + .header = header, + .anno = anno, + .where = where_clause, + .kind = kind, + .region = .{ .start = start, .end = self.pos }, + } }); + return statement_idx; } - const kind: AST.TypeDeclKind = if (self.peek() == .OpColonEqual) .nominal else .alias; - self.advance(); - const anno = try self.parseTypeAnno(.not_looking_for_args); - const where_clause = try self.parseWhereConstraint(); - // Use the type annotation's end position if there's no where clause, - // otherwise use the current position (after parsing where clause) - const statement_idx = try self.store.addStatement(.{ .type_decl = .{ - .header = header, - .anno = anno, - .where = where_clause, - .kind = kind, - .region = .{ .start = start, .end = self.pos }, - } }); - return statement_idx; } }, .OpenCurly, .OpenRound => { diff --git a/test/snapshots/expr_if_missing_else.md b/test/snapshots/expr_if_missing_else.md index e86ac4efde..9369df01b7 100644 --- a/test/snapshots/expr_if_missing_else.md +++ b/test/snapshots/expr_if_missing_else.md @@ -16,7 +16,7 @@ UNKNOWN OPERATOR - expr_if_missing_else.md:3:7:3:15 **IF WITHOUT ELSE** This `if` is being used as an expression, but it doesn't have an `else`. -When `if` is used as an expression (to produce a value), it must have an `else` branch to handle the case when the condition is false. +When `if` is used as an expression (to evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. Here is the problematic code: **expr_if_missing_else.md:3:7:3:9:** diff --git a/test/snapshots/file/underscore_type_decl.md b/test/snapshots/file/underscore_type_decl.md index 014a5b6295..b7bf503354 100644 --- a/test/snapshots/file/underscore_type_decl.md +++ b/test/snapshots/file/underscore_type_decl.md @@ -14,6 +14,12 @@ Pair2(_, y) = Pair(0, 1) Pair3(_, _) = Pair(0, 1) ~~~ # EXPECTED +PARSE ERROR - underscore_type_decl.md:5:1:5:6 +PARSE ERROR - underscore_type_decl.md:5:6:5:7 +PARSE ERROR - underscore_type_decl.md:5:7:5:8 +PARSE ERROR - underscore_type_decl.md:5:8:5:9 +PARSE ERROR - underscore_type_decl.md:5:10:5:11 +PARSE ERROR - underscore_type_decl.md:5:11:5:12 PARSE ERROR - underscore_type_decl.md:5:13:5:14 PARSE ERROR - underscore_type_decl.md:5:20:5:21 PARSE ERROR - underscore_type_decl.md:5:23:5:24 @@ -39,20 +45,80 @@ PARSE ERROR - underscore_type_decl.md:7:25:7:25 MODULE NOT FOUND - underscore_type_decl.md:3:1:3:30 # PROBLEMS **PARSE ERROR** -Type applications require parentheses around their type arguments. +A parsing error occurred: `statement_unexpected_token` +This is an unexpected parsing error. Please check your syntax. -I found a type followed by what looks like a type argument, but they need to be connected with parentheses. +Here is the problematic code: +**underscore_type_decl.md:5:1:5:6:** +```roc +Pair1(x, _) = Pair(0, 1) +``` +^^^^^ -Instead of: - **List U8** -Use: - **List(U8)** +**PARSE ERROR** +A parsing error occurred: `statement_unexpected_token` +This is an unexpected parsing error. Please check your syntax. -Other valid examples: - `Dict(Str, Num)` - `Result(a, Str)` - `Maybe(List(U64))` +Here is the problematic code: +**underscore_type_decl.md:5:6:5:7:** +```roc +Pair1(x, _) = Pair(0, 1) +``` + ^ + + +**PARSE ERROR** +A parsing error occurred: `statement_unexpected_token` +This is an unexpected parsing error. Please check your syntax. + +Here is the problematic code: +**underscore_type_decl.md:5:7:5:8:** +```roc +Pair1(x, _) = Pair(0, 1) +``` + ^ + + +**PARSE ERROR** +A parsing error occurred: `statement_unexpected_token` +This is an unexpected parsing error. Please check your syntax. + +Here is the problematic code: +**underscore_type_decl.md:5:8:5:9:** +```roc +Pair1(x, _) = Pair(0, 1) +``` + ^ + + +**PARSE ERROR** +A parsing error occurred: `statement_unexpected_token` +This is an unexpected parsing error. Please check your syntax. + +Here is the problematic code: +**underscore_type_decl.md:5:10:5:11:** +```roc +Pair1(x, _) = Pair(0, 1) +``` + ^ + + +**PARSE ERROR** +A parsing error occurred: `statement_unexpected_token` +This is an unexpected parsing error. Please check your syntax. + +Here is the problematic code: +**underscore_type_decl.md:5:11:5:12:** +```roc +Pair1(x, _) = Pair(0, 1) +``` + ^ + + +**PARSE ERROR** +A parsing error occurred: `statement_unexpected_token` +This is an unexpected parsing error. Please check your syntax. Here is the problematic code: **underscore_type_decl.md:5:13:5:14:** @@ -378,7 +444,13 @@ UpperIdent(7:1-7:6),NoSpaceOpenRound(7:6-7:7),Underscore(7:7-7:8),Comma(7:8-7:9) (s-import @3.1-3.30 (raw "Module") (exposing (exposed-upper-ident @3.25-3.29 (text "Pair")))) - (s-malformed @5.13-5.14 (tag "expected_colon_after_type_annotation")) + (s-malformed @5.1-5.6 (tag "statement_unexpected_token")) + (s-malformed @5.6-5.7 (tag "statement_unexpected_token")) + (s-malformed @5.7-5.8 (tag "statement_unexpected_token")) + (s-malformed @5.8-5.9 (tag "statement_unexpected_token")) + (s-malformed @5.10-5.11 (tag "statement_unexpected_token")) + (s-malformed @5.11-5.12 (tag "statement_unexpected_token")) + (s-malformed @5.13-5.14 (tag "statement_unexpected_token")) (s-malformed @6.1-6.6 (tag "expected_colon_after_type_annotation")) (s-malformed @6.6-6.7 (tag "statement_unexpected_token")) (s-malformed @6.7-6.8 (tag "statement_unexpected_token")) @@ -403,6 +475,7 @@ import Module exposing [Pair] + ~~~ # CANONICALIZE ~~~clojure diff --git a/test/snapshots/match_expr/guards_1.md b/test/snapshots/match_expr/guards_1.md index 67fe71ba64..deef2b69a4 100644 --- a/test/snapshots/match_expr/guards_1.md +++ b/test/snapshots/match_expr/guards_1.md @@ -67,7 +67,7 @@ Here is the problematic code: **IF WITHOUT ELSE** This `if` is being used as an expression, but it doesn't have an `else`. -When `if` is used as an expression (to produce a value), it must have an `else` branch to handle the case when the condition is false. +When `if` is used as an expression (to evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. Here is the problematic code: **guards_1.md:2:7:2:9:** @@ -212,7 +212,7 @@ Here is the problematic code: **IF WITHOUT ELSE** This `if` is being used as an expression, but it doesn't have an `else`. -When `if` is used as an expression (to produce a value), it must have an `else` branch to handle the case when the condition is false. +When `if` is used as an expression (to evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. Here is the problematic code: **guards_1.md:3:7:3:9:** diff --git a/test/snapshots/match_expr/guards_2.md b/test/snapshots/match_expr/guards_2.md index d559e60b5f..30585b3a49 100644 --- a/test/snapshots/match_expr/guards_2.md +++ b/test/snapshots/match_expr/guards_2.md @@ -68,7 +68,7 @@ Here is the problematic code: **IF WITHOUT ELSE** This `if` is being used as an expression, but it doesn't have an `else`. -When `if` is used as an expression (to produce a value), it must have an `else` branch to handle the case when the condition is false. +When `if` is used as an expression (to evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. Here is the problematic code: **guards_2.md:2:25:2:27:** @@ -213,7 +213,7 @@ Here is the problematic code: **IF WITHOUT ELSE** This `if` is being used as an expression, but it doesn't have an `else`. -When `if` is used as an expression (to produce a value), it must have an `else` branch to handle the case when the condition is false. +When `if` is used as an expression (to evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. Here is the problematic code: **guards_2.md:3:12:3:14:** diff --git a/test/snapshots/records/record_different_fields_reserved_error.md b/test/snapshots/records/record_different_fields_reserved_error.md index 29d2ce9710..6d2d7a5730 100644 --- a/test/snapshots/records/record_different_fields_reserved_error.md +++ b/test/snapshots/records/record_different_fields_reserved_error.md @@ -52,7 +52,7 @@ Here is the problematic code: **IF WITHOUT ELSE** This `if` is being used as an expression, but it doesn't have an `else`. -When `if` is used as an expression (to produce a value), it must have an `else` branch to handle the case when the condition is false. +When `if` is used as an expression (to evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. Here is the problematic code: **record_different_fields_reserved_error.md:2:5:2:7:**