diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index e2003fa7ce..32ca1b867f 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -35,6 +35,10 @@ pub const AutoImportedType = struct { env: *ModuleEnv, parse_ir: *AST, +/// Track whether we're in statement position (true) or expression position (false) +/// Statement position: if without else is OK (default) +/// Expression position: if without else is ERROR (explicitly set in assignments, etc.) +in_statement_position: bool = true, scopes: std.ArrayList(Scope) = .{}, /// Special scope for rigid type variables in annotations type_vars_scope: base.Scratch(TypeVarScope), @@ -712,8 +716,11 @@ fn canonicalizeAssociatedDecl( } }; - // Canonicalize the body expression + // Canonicalize the body expression in expression context (RHS of assignment) + const saved_stmt_pos = self.in_statement_position; + self.in_statement_position = false; const can_expr = try self.canonicalizeExprOrMalformed(decl.body); + self.in_statement_position = saved_stmt_pos; // Create the def with no annotation (type annotations are handled via canonicalizeAssociatedDeclWithAnno) const def = CIR.Def{ @@ -749,8 +756,11 @@ fn canonicalizeAssociatedDeclWithAnno( } }; - // Canonicalize the body expression + // Canonicalize the body expression in expression context (RHS of assignment) + const saved_stmt_pos = self.in_statement_position; + self.in_statement_position = false; const can_expr = try self.canonicalizeExprOrMalformed(decl.body); + self.in_statement_position = saved_stmt_pos; // Create the annotation structure const annotation = CIR.Annotation{ @@ -4279,6 +4289,68 @@ pub fn canonicalizeExpr( const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_span.len > 0) free_vars_span else null }; }, + .if_without_else => |e| { + // Statement form: if without else + const region = self.parse_ir.tokenizedRegionToRegion(e.region); + + // Check if we're in expression context (e.g., assignment, function call) + // If so, emit error explaining that if-expressions need else + if (!self.in_statement_position) { + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ + .if_expr_without_else = .{ .region = region }, + }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; + } + + // Desugar to if-then-else with empty record {} as the final else + // Type checking will ensure the then-branch also has type {} + + const free_vars_start = self.scratch_free_vars.top(); + + // Canonicalize condition + const can_cond = try self.canonicalizeExpr(e.condition) orelse { + const ast_cond = self.parse_ir.store.getExpr(e.condition); + const cond_region = self.parse_ir.tokenizedRegionToRegion(ast_cond.to_tokenized_region()); + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ + .if_condition_not_canonicalized = .{ .region = cond_region }, + }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; + }; + + // Canonicalize then branch + const can_then = try self.canonicalizeExpr(e.then) orelse { + const ast_then = self.parse_ir.store.getExpr(e.then); + const then_region = self.parse_ir.tokenizedRegionToRegion(ast_then.to_tokenized_region()); + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ + .if_then_not_canonicalized = .{ .region = then_region }, + }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; + }; + + // Create an empty record {} as the implicit else + const empty_record_idx = try self.env.addExpr(CIR.Expr{ .e_empty_record = .{} }, region); + + // Create single if branch + const scratch_top = self.env.store.scratchIfBranchTop(); + const if_branch = Expr.IfBranch{ + .cond = can_cond.idx, + .body = can_then.idx, + }; + const if_branch_idx = try self.env.addIfBranch(if_branch, region); + try self.env.store.addScratchIfBranch(if_branch_idx); + const branches_span = try self.env.store.ifBranchSpanFrom(scratch_top); + + // Create if expression with empty record as final else + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_if = .{ + .branches = branches_span, + .final_else = empty_record_idx, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_span.len > 0) free_vars_span else null }; + }, .match => |m| { const region = self.parse_ir.tokenizedRegionToRegion(m.region); diff --git a/src/canonicalize/Diagnostic.zig b/src/canonicalize/Diagnostic.zig index 6778f72b14..a1168f5e7d 100644 --- a/src/canonicalize/Diagnostic.zig +++ b/src/canonicalize/Diagnostic.zig @@ -76,6 +76,9 @@ pub const Diagnostic = union(enum) { if_else_not_canonicalized: struct { region: Region, }, + if_expr_without_else: struct { + region: Region, + }, malformed_type_annotation: struct { region: Region, }, diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index b2cc2675d0..ae0ed98938 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -764,6 +764,16 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st try report.document.addReflowingText(")."); break :blk report; }, + .if_then_not_canonicalized => |_| blk: { + var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error); + try report.document.addReflowingText("The branch in this "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" expression could not be processed."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("The branch must contain a valid expression. Check for syntax errors or missing values."); + break :blk report; + }, .if_else_not_canonicalized => |_| blk: { var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error); try report.document.addReflowingText("The "); @@ -777,12 +787,33 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st try report.document.addKeyword("else"); try report.document.addReflowingText(" branch must contain a valid expression. Check for syntax errors or missing values."); try report.document.addLineBreak(); - try report.document.addLineBreak(); - try report.document.addReflowingText("Note: Every "); + break :blk report; + }, + .if_expr_without_else => |_| blk: { + var report = Report.init(allocator, "IF EXPRESSION WITHOUT ELSE", .runtime_error); + try report.document.addReflowingText("This "); try report.document.addKeyword("if"); - try report.document.addReflowingText(" expression in Roc must have an "); + try report.document.addReflowingText(" has no "); try report.document.addKeyword("else"); - try report.document.addReflowingText(" branch, and both branches must have the same type."); + try report.document.addReflowingText(" branch, but it's being used as an expression (assigned to a variable, passed to a function, etc.)."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("You can only use "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" without "); + try report.document.addKeyword("else"); + try report.document.addReflowingText(" when it's a statement. When "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" is used as an expression that evaluates to a value, "); + try report.document.addKeyword("else"); + try report.document.addReflowingText(" is required because otherwise there wouldn't always be a value available."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("Either add an "); + try report.document.addKeyword("else"); + try report.document.addReflowingText(" branch, or use this "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" as a standalone statement."); break :blk report; }, .pattern_not_canonicalized => |_| blk: { diff --git a/src/canonicalize/Node.zig b/src/canonicalize/Node.zig index 705014894e..69bb7fca57 100644 --- a/src/canonicalize/Node.zig +++ b/src/canonicalize/Node.zig @@ -208,4 +208,5 @@ pub const Tag = enum { diag_underscore_in_type_declaration, diagnostic_exposed_but_not_implemented, diag_redundant_exposed, + diag_if_expr_without_else, }; diff --git a/src/canonicalize/NodeStore.zig b/src/canonicalize/NodeStore.zig index 9554ae069a..2c02e04ad0 100644 --- a/src/canonicalize/NodeStore.zig +++ b/src/canonicalize/NodeStore.zig @@ -128,7 +128,7 @@ pub fn deinit(store: *NodeStore) void { /// when adding/removing variants from ModuleEnv unions. Update these when modifying the unions. /// /// Count of the diagnostic nodes in the ModuleEnv -pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 58; +pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 59; /// Count of the expression nodes in the ModuleEnv pub const MODULEENV_EXPR_NODE_COUNT = 35; /// Count of the statement nodes in the ModuleEnv @@ -2746,6 +2746,10 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) Allocator.Error! node.tag = .diag_if_else_not_canonicalized; region = r.region; }, + .if_expr_without_else => |r| { + node.tag = .diag_if_expr_without_else; + region = r.region; + }, .malformed_type_annotation => |r| { node.tag = .diag_malformed_type_annotation; region = r.region; @@ -3088,6 +3092,9 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .diag_if_else_not_canonicalized => return CIR.Diagnostic{ .if_else_not_canonicalized = .{ .region = store.getRegionAt(node_idx), } }, + .diag_if_expr_without_else => return CIR.Diagnostic{ .if_expr_without_else = .{ + .region = store.getRegionAt(node_idx), + } }, .diag_var_across_function_boundary => return CIR.Diagnostic{ .var_across_function_boundary = .{ .region = store.getRegionAt(node_idx), } }, diff --git a/src/canonicalize/test/node_store_test.zig b/src/canonicalize/test/node_store_test.zig index 34bf6345c3..5f601b456f 100644 --- a/src/canonicalize/test/node_store_test.zig +++ b/src/canonicalize/test/node_store_test.zig @@ -505,6 +505,12 @@ test "NodeStore round trip - Diagnostics" { }, }); + try diagnostics.append(gpa, CIR.Diagnostic{ + .if_expr_without_else = .{ + .region = rand_region(), + }, + }); + try diagnostics.append(gpa, CIR.Diagnostic{ .var_across_function_boundary = .{ .region = rand_region(), diff --git a/src/fmt/fmt.zig b/src/fmt/fmt.zig index ba850294d7..6d89544fbc 100644 --- a/src/fmt/fmt.zig +++ b/src/fmt/fmt.zig @@ -1141,6 +1141,27 @@ const Formatter = struct { } _ = try fmt.formatExpr(i.@"else"); }, + .if_without_else => |i| { + try fmt.pushAll("if"); + const cond_region = fmt.nodeRegion(@intFromEnum(i.condition)); + var flushed = try fmt.flushCommentsBefore(cond_region.start); + if (flushed) { + fmt.curr_indent += 1; + try fmt.pushIndent(); + } else { + try fmt.push(' '); + } + _ = try fmt.formatExpr(i.condition); + const then_region = fmt.nodeRegion(@intFromEnum(i.then)); + flushed = try fmt.flushCommentsBefore(then_region.start); + if (flushed) { + fmt.curr_indent += 1; + try fmt.pushIndent(); + } else { + try fmt.push(' '); + } + _ = try fmt.formatExpr(i.then); + }, .match => |m| { try fmt.pushAll("match "); _ = try fmt.formatExpr(m.expr); @@ -2186,6 +2207,13 @@ const Formatter = struct { return fmt.nodeWillBeMultiline(AST.Expr.Idx, i.@"else"); }, + .if_without_else => |i| { + if (fmt.nodeWillBeMultiline(AST.Expr.Idx, i.condition)) { + return true; + } + + return fmt.nodeWillBeMultiline(AST.Expr.Idx, i.then); + }, .local_dispatch => |l| { if (fmt.nodeWillBeMultiline(AST.Expr.Idx, l.left)) { return true; diff --git a/src/parse/AST.zig b/src/parse/AST.zig index a8eefeec19..355863a1d4 100644 --- a/src/parse/AST.zig +++ b/src/parse/AST.zig @@ -2336,6 +2336,11 @@ pub const Expr = union(enum) { @"else": Expr.Idx, region: TokenizedRegion, }, + if_without_else: struct { + condition: Expr.Idx, + then: Expr.Idx, + region: TokenizedRegion, + }, match: struct { expr: Expr.Idx, branches: MatchBranch.Span, @@ -2401,6 +2406,7 @@ pub const Expr = union(enum) { .suffix_single_question => |e| e.region, .apply => |e| e.region, .if_then_else => |e| e.region, + .if_without_else => |e| e.region, .match => |e| e.region, .dbg => |e| e.region, .block => |e| e.region, @@ -2603,6 +2609,17 @@ pub const Expr = union(enum) { try tree.endNode(begin, attrs); }, + .if_without_else => |stmt| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-if-without-else"); + try ast.appendRegionInfoToSexprTree(env, tree, stmt.region); + const attrs = tree.beginNode(); + + try ast.store.getExpr(stmt.condition).pushToSExprTree(gpa, env, ast, tree); + try ast.store.getExpr(stmt.then).pushToSExprTree(gpa, env, ast, tree); + + try tree.endNode(begin, attrs); + }, .match => |a| { const begin = tree.beginNode(); try tree.pushStaticAtom("e-match"); diff --git a/src/parse/Node.zig b/src/parse/Node.zig index 0de7ff6f0b..33c4f4a640 100644 --- a/src/parse/Node.zig +++ b/src/parse/Node.zig @@ -402,6 +402,11 @@ pub const Tag = enum { /// * lhs - node index of expr /// * rhs - RHS DESCRIPTION if_then_else, + /// If-statement (no else branch) - statement form of if, returns unit type + /// Example: if Bool.true {} + /// * lhs - node index of condition expr + /// * rhs - node index of then branch expr + if_without_else, /// DESCRIPTION /// Example: EXAMPLE /// * lhs - start index of extra_data diff --git a/src/parse/NodeStore.zig b/src/parse/NodeStore.zig index 7fd691cc36..c724aa2284 100644 --- a/src/parse/NodeStore.zig +++ b/src/parse/NodeStore.zig @@ -687,6 +687,12 @@ pub fn addExpr(store: *NodeStore, expr: AST.Expr) std.mem.Allocator.Error!AST.Ex try store.extra_data.append(store.gpa, @intFromEnum(i.then)); try store.extra_data.append(store.gpa, @intFromEnum(i.@"else")); }, + .if_without_else => |i| { + node.tag = .if_without_else; + node.region = i.region; + node.data.lhs = @intFromEnum(i.condition); + node.data.rhs = @intFromEnum(i.then); + }, .match => |m| { node.tag = .match; node.region = m.region; @@ -1580,6 +1586,13 @@ pub fn getExpr(store: *const NodeStore, expr_idx: AST.Expr.Idx) AST.Expr { .@"else" = @enumFromInt(else_ed), } }; }, + .if_without_else => { + return .{ .if_without_else = .{ + .region = node.region, + .condition = @enumFromInt(node.data.lhs), + .then = @enumFromInt(node.data.rhs), + } }; + }, .match => { return .{ .match = .{ .region = node.region, diff --git a/src/parse/Parser.zig b/src/parse/Parser.zig index 5e3bb90006..835cb05fdd 100644 --- a/src/parse/Parser.zig +++ b/src/parse/Parser.zig @@ -2089,17 +2089,22 @@ pub fn parseExprWithBp(self: *Parser, min_bp: u8) Error!AST.Expr.Idx { const condition = try self.parseExpr(); const then = try self.parseExpr(); if (self.peek() != .KwElse) { - // Point to the if keyword for missing else error - return try self.pushMalformed(AST.Expr.Idx, .no_else, start); + // Statement form: if without else + expr = try self.store.addExpr(.{ .if_without_else = .{ + .region = .{ .start = start, .end = self.pos }, + .condition = condition, + .then = then, + } }); + } else { + self.advance(); + const else_idx = try self.parseExpr(); + expr = try self.store.addExpr(.{ .if_then_else = .{ + .region = .{ .start = start, .end = self.pos }, + .condition = condition, + .then = then, + .@"else" = else_idx, + } }); } - self.advance(); - const else_idx = try self.parseExpr(); - expr = try self.store.addExpr(.{ .if_then_else = .{ - .region = .{ .start = start, .end = self.pos }, - .condition = condition, - .then = then, - .@"else" = else_idx, - } }); }, .KwMatch => { self.advance(); diff --git a/test/snapshots/expr_if_missing_else.md b/test/snapshots/expr_if_missing_else.md index 2b704b93bd..d172b8f807 100644 --- a/test/snapshots/expr_if_missing_else.md +++ b/test/snapshots/expr_if_missing_else.md @@ -8,31 +8,38 @@ type=snippet foo = if tru 0 ~~~ # EXPECTED -IF WITHOUT ELSE - expr_if_missing_else.md:1:7:1:9 -UNRECOGNIZED SYNTAX - expr_if_missing_else.md:1:7:1:15 +UNDEFINED VARIABLE - expr_if_missing_else.md:1:10:1:13 +INCOMPATIBLE IF BRANCHES - expr_if_missing_else.md:1:7:1:7 # PROBLEMS -**IF WITHOUT ELSE** -This `if` is being used as an expression, but it doesn't have an `else`. +**UNDEFINED VARIABLE** +Nothing is named `tru` in this scope. +Is there an `import` or `exposing` missing up-top? -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`. - -**expr_if_missing_else.md:1:7:1:9:** +**expr_if_missing_else.md:1:10:1:13:** ```roc foo = if tru 0 ``` - ^^ + ^^^ -**UNRECOGNIZED SYNTAX** -I don't recognize this syntax. - -**expr_if_missing_else.md:1:7:1:15:** +**INCOMPATIBLE IF BRANCHES** +This `if` has an `else` branch with a different type from it's `then` branch: +**expr_if_missing_else.md:1:7:** ```roc foo = if tru 0 ``` ^^^^^^^^ -This might be a syntax error, an unsupported language feature, or a typo. +The `else` branch has the type: + _{}_ + +But the `then` branch has the type: + _Num(_size)_ + +All branches in an `if` must have compatible types. + +Note: You can wrap branches in a tag to make them compatible. +To learn about tags, see # TOKENS ~~~zig @@ -46,18 +53,26 @@ EndOfFile, (statements (s-decl (p-ident (raw "foo")) - (e-malformed (reason "no_else"))))) + (e-if-without-else + (e-ident (raw "tru")) + (e-int (raw "0")))))) ~~~ # FORMATTED ~~~roc -foo = +NO CHANGE ~~~ # CANONICALIZE ~~~clojure (can-ir (d-let (p-assign (ident "foo")) - (e-runtime-error (tag "expr_not_canonicalized")))) + (e-if + (if-branches + (if-branch + (e-runtime-error (tag "ident_not_in_scope")) + (e-num (value "0")))) + (if-else + (e-empty_record))))) ~~~ # TYPES ~~~clojure diff --git a/test/snapshots/fuzz_crash/fuzz_crash_026.md b/test/snapshots/fuzz_crash/fuzz_crash_026.md index 6ae9223b5d..41d8fee78e 100644 Binary files a/test/snapshots/fuzz_crash/fuzz_crash_026.md and b/test/snapshots/fuzz_crash/fuzz_crash_026.md differ diff --git a/test/snapshots/match_expr/guards_1.md b/test/snapshots/match_expr/guards_1.md index fab82951ed..f0f648bad5 100644 --- a/test/snapshots/match_expr/guards_1.md +++ b/test/snapshots/match_expr/guards_1.md @@ -14,11 +14,7 @@ match value { # EXPECTED PARSE ERROR - guards_1.md:2:7:2:7 UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:2:16:2:18 -IF WITHOUT ELSE - guards_1.md:2:7:2:9 -UNEXPECTED TOKEN IN PATTERN - guards_1.md:2:20:2:30 -PARSE ERROR - guards_1.md:2:30:2:30 -UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:2:30:2:32 -UNEXPECTED TOKEN IN PATTERN - guards_1.md:2:32:2:35 +PARSE ERROR - guards_1.md:2:19:2:20 PARSE ERROR - guards_1.md:2:43:2:43 UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:2:43:2:44 UNEXPECTED TOKEN IN PATTERN - guards_1.md:2:44:2:44 @@ -26,19 +22,15 @@ PARSE ERROR - guards_1.md:2:44:2:44 UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:2:44:2:45 PARSE ERROR - guards_1.md:3:7:3:7 UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:3:16:3:18 -IF WITHOUT ELSE - guards_1.md:3:7:3:9 -UNEXPECTED TOKEN IN PATTERN - guards_1.md:3:20:3:30 -PARSE ERROR - guards_1.md:3:30:3:30 -UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:3:30:3:32 -UNEXPECTED TOKEN IN PATTERN - guards_1.md:3:32:3:35 +PARSE ERROR - guards_1.md:3:19:3:20 PARSE ERROR - guards_1.md:3:43:3:43 UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:3:43:3:44 UNEXPECTED TOKEN IN PATTERN - guards_1.md:3:44:3:44 PARSE ERROR - guards_1.md:3:44:3:44 UNEXPECTED TOKEN IN EXPRESSION - guards_1.md:3:44:3:45 UNDEFINED VARIABLE - guards_1.md:1:7:1:12 -UNRECOGNIZED SYNTAX - guards_1.md:2:7:2:20 -UNUSED VARIABLE - guards_1.md:2:5:2:6 +INVALID IF BRANCH - :0:0:0:0 +UNRECOGNIZED SYNTAX - guards_1.md:2:43:2:44 # PROBLEMS **PARSE ERROR** A parsing error occurred: `match_branch_missing_arrow` @@ -62,60 +54,15 @@ Expressions can be identifiers, literals, function calls, or operators. ^^ -**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 evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. - -**guards_1.md:2:7:2:9:** -```roc - x if x > 0 => "positive: ${Num.toStr x}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **positive: ** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_1.md:2:20:2:30:** -```roc - x if x > 0 => "positive: ${Num.toStr x}" -``` - ^^^^^^^^^^ - - **PARSE ERROR** -A parsing error occurred: `match_branch_missing_arrow` +A parsing error occurred: `string_expected_close_interpolation` This is an unexpected parsing error. Please check your syntax. -**guards_1.md:2:30:2:30:** +**guards_1.md:2:19:2:20:** ```roc x if x > 0 => "positive: ${Num.toStr x}" ``` - ^ - - -**UNEXPECTED TOKEN IN EXPRESSION** -The token **${** is not expected in an expression. -Expressions can be identifiers, literals, function calls, or operators. - -**guards_1.md:2:30:2:32:** -```roc - x if x > 0 => "positive: ${Num.toStr x}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **Num** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_1.md:2:32:2:35:** -```roc - x if x > 0 => "positive: ${Num.toStr x}" -``` - ^^^ + ^ **PARSE ERROR** @@ -195,60 +142,15 @@ Expressions can be identifiers, literals, function calls, or operators. ^^ -**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 evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. - -**guards_1.md:3:7:3:9:** -```roc - x if x < 0 => "negative: ${Num.toStr x}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **negative: ** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_1.md:3:20:3:30:** -```roc - x if x < 0 => "negative: ${Num.toStr x}" -``` - ^^^^^^^^^^ - - **PARSE ERROR** -A parsing error occurred: `match_branch_missing_arrow` +A parsing error occurred: `string_expected_close_interpolation` This is an unexpected parsing error. Please check your syntax. -**guards_1.md:3:30:3:30:** +**guards_1.md:3:19:3:20:** ```roc x if x < 0 => "negative: ${Num.toStr x}" ``` - ^ - - -**UNEXPECTED TOKEN IN EXPRESSION** -The token **${** is not expected in an expression. -Expressions can be identifiers, literals, function calls, or operators. - -**guards_1.md:3:30:3:32:** -```roc - x if x < 0 => "negative: ${Num.toStr x}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **Num** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_1.md:3:32:3:35:** -```roc - x if x < 0 => "negative: ${Num.toStr x}" -``` - ^^^ + ^ **PARSE ERROR** @@ -317,29 +219,22 @@ match value { ^^^^^ +**INVALID IF BRANCH** +The branch in this `if` expression could not be processed. + +The branch must contain a valid expression. Check for syntax errors or missing values. + **UNRECOGNIZED SYNTAX** I don't recognize this syntax. -**guards_1.md:2:7:2:20:** +**guards_1.md:2:43:2:44:** ```roc x if x > 0 => "positive: ${Num.toStr x}" ``` - ^^^^^^^^^^^^^ + ^ This might be a syntax error, an unsupported language feature, or a typo. -**UNUSED VARIABLE** -Variable `x` is not used anywhere in your code. - -If you don't need this variable, prefix it with an underscore like `_x` to suppress this warning. -The unused variable is declared here: -**guards_1.md:2:5:2:6:** -```roc - x if x > 0 => "positive: ${Num.toStr x}" -``` - ^ - - # TOKENS ~~~zig KwMatch,LowerIdent,OpenCurly, @@ -356,24 +251,26 @@ EndOfFile, (branches (branch (p-ident (raw "x")) - (e-malformed (reason "no_else"))) + (e-if-without-else + (e-binop (op ">") + (e-ident (raw "x")) + (e-int (raw "0"))) + (e-malformed (reason "expr_unexpected_token")))) (branch - (p-malformed (tag "pattern_unexpected_token")) - (e-malformed (reason "expr_unexpected_token"))) - (branch - (p-malformed (tag "pattern_unexpected_token")) + (p-string (raw """)) (e-malformed (reason "expr_unexpected_token"))) (branch (p-malformed (tag "pattern_unexpected_token")) (e-malformed (reason "expr_unexpected_token"))) (branch (p-ident (raw "x")) - (e-malformed (reason "no_else"))) + (e-if-without-else + (e-binop (op "<") + (e-ident (raw "x")) + (e-int (raw "0"))) + (e-malformed (reason "expr_unexpected_token")))) (branch - (p-malformed (tag "pattern_unexpected_token")) - (e-malformed (reason "expr_unexpected_token"))) - (branch - (p-malformed (tag "pattern_unexpected_token")) + (p-string (raw """)) (e-malformed (reason "expr_unexpected_token"))) (branch (p-malformed (tag "pattern_unexpected_token")) @@ -386,12 +283,10 @@ EndOfFile, # FORMATTED ~~~roc match value { - x => + x => if x > 0 => => - => - x => - => + x => if x < 0 => => _ => "other" diff --git a/test/snapshots/match_expr/guards_2.md b/test/snapshots/match_expr/guards_2.md index 95cc94faa0..0d16a56ab7 100644 --- a/test/snapshots/match_expr/guards_2.md +++ b/test/snapshots/match_expr/guards_2.md @@ -14,11 +14,7 @@ match value { # EXPECTED PARSE ERROR - guards_2.md:2:25:2:25 UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:2:47:2:49 -IF WITHOUT ELSE - guards_2.md:2:25:2:27 -UNEXPECTED TOKEN IN PATTERN - guards_2.md:2:51:2:75 -PARSE ERROR - guards_2.md:2:75:2:75 -UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:2:75:2:77 -UNEXPECTED TOKEN IN PATTERN - guards_2.md:2:77:2:80 +PARSE ERROR - guards_2.md:2:50:2:51 PARSE ERROR - guards_2.md:2:92:2:92 UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:2:92:2:93 UNEXPECTED TOKEN IN PATTERN - guards_2.md:2:93:2:93 @@ -26,20 +22,16 @@ PARSE ERROR - guards_2.md:2:93:2:93 UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:2:93:2:94 PARSE ERROR - guards_2.md:3:12:3:12 UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:3:22:3:24 -IF WITHOUT ELSE - guards_2.md:3:12:3:14 -UNEXPECTED TOKEN IN PATTERN - guards_2.md:3:26:3:48 -PARSE ERROR - guards_2.md:3:48:3:48 -UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:3:48:3:50 -UNEXPECTED TOKEN IN PATTERN - guards_2.md:3:50:3:53 +PARSE ERROR - guards_2.md:3:25:3:26 PARSE ERROR - guards_2.md:3:61:3:61 UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:3:61:3:62 UNEXPECTED TOKEN IN PATTERN - guards_2.md:3:62:3:62 PARSE ERROR - guards_2.md:3:62:3:62 UNEXPECTED TOKEN IN EXPRESSION - guards_2.md:3:62:3:63 UNDEFINED VARIABLE - guards_2.md:1:7:1:12 -UNRECOGNIZED SYNTAX - guards_2.md:2:25:2:51 +INVALID IF BRANCH - :0:0:0:0 UNUSED VARIABLE - guards_2.md:2:6:2:11 -UNUSED VARIABLE - guards_2.md:1:1:1:1 +UNRECOGNIZED SYNTAX - guards_2.md:2:92:2:93 # PROBLEMS **PARSE ERROR** A parsing error occurred: `match_branch_missing_arrow` @@ -63,60 +55,15 @@ Expressions can be identifiers, literals, function calls, or operators. ^^ -**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 evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. - -**guards_2.md:2:25:2:27:** -```roc - [first, .. as rest] if List.len(rest) > 5 => "long list starting with ${Num.toStr first}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **long list starting with ** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_2.md:2:51:2:75:** -```roc - [first, .. as rest] if List.len(rest) > 5 => "long list starting with ${Num.toStr first}" -``` - ^^^^^^^^^^^^^^^^^^^^^^^^ - - **PARSE ERROR** -A parsing error occurred: `match_branch_missing_arrow` +A parsing error occurred: `string_expected_close_interpolation` This is an unexpected parsing error. Please check your syntax. -**guards_2.md:2:75:2:75:** +**guards_2.md:2:50:2:51:** ```roc [first, .. as rest] if List.len(rest) > 5 => "long list starting with ${Num.toStr first}" ``` - ^ - - -**UNEXPECTED TOKEN IN EXPRESSION** -The token **${** is not expected in an expression. -Expressions can be identifiers, literals, function calls, or operators. - -**guards_2.md:2:75:2:77:** -```roc - [first, .. as rest] if List.len(rest) > 5 => "long list starting with ${Num.toStr first}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **Num** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_2.md:2:77:2:80:** -```roc - [first, .. as rest] if List.len(rest) > 5 => "long list starting with ${Num.toStr first}" -``` - ^^^ + ^ **PARSE ERROR** @@ -196,60 +143,15 @@ Expressions can be identifiers, literals, function calls, or operators. ^^ -**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 evaluate to a value), it must have an `else` branch to specify what value to use when the condition is `False`. - -**guards_2.md:3:12:3:14:** -```roc - [x, y] if x == y => "pair of equal values: ${Num.toStr x}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **pair of equal values: ** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_2.md:3:26:3:48:** -```roc - [x, y] if x == y => "pair of equal values: ${Num.toStr x}" -``` - ^^^^^^^^^^^^^^^^^^^^^^ - - **PARSE ERROR** -A parsing error occurred: `match_branch_missing_arrow` +A parsing error occurred: `string_expected_close_interpolation` This is an unexpected parsing error. Please check your syntax. -**guards_2.md:3:48:3:48:** +**guards_2.md:3:25:3:26:** ```roc [x, y] if x == y => "pair of equal values: ${Num.toStr x}" ``` - ^ - - -**UNEXPECTED TOKEN IN EXPRESSION** -The token **${** is not expected in an expression. -Expressions can be identifiers, literals, function calls, or operators. - -**guards_2.md:3:48:3:50:** -```roc - [x, y] if x == y => "pair of equal values: ${Num.toStr x}" -``` - ^^ - - -**UNEXPECTED TOKEN IN PATTERN** -The token **Num** is not expected in a pattern. -Patterns can contain identifiers, literals, lists, records, or tags. - -**guards_2.md:3:50:3:53:** -```roc - [x, y] if x == y => "pair of equal values: ${Num.toStr x}" -``` - ^^^ + ^ **PARSE ERROR** @@ -318,16 +220,10 @@ match value { ^^^^^ -**UNRECOGNIZED SYNTAX** -I don't recognize this syntax. +**INVALID IF BRANCH** +The branch in this `if` expression could not be processed. -**guards_2.md:2:25:2:51:** -```roc - [first, .. as rest] if List.len(rest) > 5 => "long list starting with ${Num.toStr first}" -``` - ^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This might be a syntax error, an unsupported language feature, or a typo. +The branch must contain a valid expression. Check for syntax errors or missing values. **UNUSED VARIABLE** Variable `first` is not used anywhere in your code. @@ -341,17 +237,16 @@ The unused variable is declared here: ^^^^^ -**UNUSED VARIABLE** -Variable `rest` is not used anywhere in your code. +**UNRECOGNIZED SYNTAX** +I don't recognize this syntax. -If you don't need this variable, prefix it with an underscore like `_rest` to suppress this warning. -The unused variable is declared here: -**guards_2.md:1:1:1:1:** +**guards_2.md:2:92:2:93:** ```roc -match value { + [first, .. as rest] if List.len(rest) > 5 => "long list starting with ${Num.toStr first}" ``` -^ + ^ +This might be a syntax error, an unsupported language feature, or a typo. # TOKENS ~~~zig @@ -371,12 +266,15 @@ EndOfFile, (p-list (p-ident (raw "first")) (p-list-rest (name "rest"))) - (e-malformed (reason "no_else"))) + (e-if-without-else + (e-binop (op ">") + (e-apply + (e-ident (raw "List.len")) + (e-ident (raw "rest"))) + (e-int (raw "5"))) + (e-malformed (reason "expr_unexpected_token")))) (branch - (p-malformed (tag "pattern_unexpected_token")) - (e-malformed (reason "expr_unexpected_token"))) - (branch - (p-malformed (tag "pattern_unexpected_token")) + (p-string (raw """)) (e-malformed (reason "expr_unexpected_token"))) (branch (p-malformed (tag "pattern_unexpected_token")) @@ -385,12 +283,13 @@ EndOfFile, (p-list (p-ident (raw "x")) (p-ident (raw "y"))) - (e-malformed (reason "no_else"))) + (e-if-without-else + (e-binop (op "==") + (e-ident (raw "x")) + (e-ident (raw "y"))) + (e-malformed (reason "expr_unexpected_token")))) (branch - (p-malformed (tag "pattern_unexpected_token")) - (e-malformed (reason "expr_unexpected_token"))) - (branch - (p-malformed (tag "pattern_unexpected_token")) + (p-string (raw """)) (e-malformed (reason "expr_unexpected_token"))) (branch (p-malformed (tag "pattern_unexpected_token")) @@ -403,12 +302,10 @@ EndOfFile, # FORMATTED ~~~roc match value { - [first, .. as rest] => + [first, .. as rest] => if List.len(rest) > 5 => => - => - [x, y] => - => + [x, y] => if x == y => => _ => "other" diff --git a/test/snapshots/records/record_different_fields_reserved_error.md b/test/snapshots/records/record_different_fields_reserved_error.md index caada2cac8..9dd2d70089 100644 --- a/test/snapshots/records/record_different_fields_reserved_error.md +++ b/test/snapshots/records/record_different_fields_reserved_error.md @@ -16,7 +16,7 @@ type=expr ~~~ # EXPECTED UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:2:7:2:8 -IF WITHOUT ELSE - record_different_fields_reserved_error.md:2:5:2:7 +UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:2:22:2:23 UNEXPECTED TOKEN IN TYPE ANNOTATION - record_different_fields_reserved_error.md:3:11:3:12 UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:3:12:3:25 UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:3:25:3:26 @@ -32,7 +32,8 @@ UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:6:19: UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:7:5:7:7 UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:7:7:7:8 UNEXPECTED TOKEN IN EXPRESSION - record_different_fields_reserved_error.md:7:19:7:20 -UNRECOGNIZED SYNTAX - record_different_fields_reserved_error.md:2:5:2:23 +INVALID IF CONDITION - :0:0:0:0 +UNRECOGNIZED SYNTAX - record_different_fields_reserved_error.md:2:22:2:23 MALFORMED TYPE - record_different_fields_reserved_error.md:3:11:3:12 UNRECOGNIZED SYNTAX - record_different_fields_reserved_error.md:3:12:3:25 UNRECOGNIZED SYNTAX - record_different_fields_reserved_error.md:3:25:3:26 @@ -64,16 +65,15 @@ Expressions can be identifiers, literals, function calls, or operators. ^ -**IF WITHOUT ELSE** -This `if` is being used as an expression, but it doesn't have an `else`. +**UNEXPECTED TOKEN IN EXPRESSION** +The token **,** is not expected in an expression. +Expressions can be identifiers, literals, function calls, or operators. -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`. - -**record_different_fields_reserved_error.md:2:5:2:7:** +**record_different_fields_reserved_error.md:2:22:2:23:** ```roc if: "conditional", ``` - ^^ + ^ **UNEXPECTED TOKEN IN TYPE ANNOTATION** @@ -241,14 +241,19 @@ Expressions can be identifiers, literals, function calls, or operators. ^ +**INVALID IF CONDITION** +The condition in this `if` expression could not be processed. + +The condition must be a valid expression that evaluates to a `Bool` value (`Bool.true` or `Bool.false`). + **UNRECOGNIZED SYNTAX** I don't recognize this syntax. -**record_different_fields_reserved_error.md:2:5:2:23:** +**record_different_fields_reserved_error.md:2:22:2:23:** ```roc if: "conditional", ``` - ^^^^^^^^^^^^^^^^^^ + ^ This might be a syntax error, an unsupported language feature, or a typo. @@ -481,7 +486,11 @@ EndOfFile, ~~~clojure (e-block (statements - (e-malformed (reason "no_else")) + (e-if-without-else + (e-malformed (reason "expr_unexpected_token")) + (e-string + (e-string-part (raw "conditional")))) + (e-malformed (reason "expr_unexpected_token")) (s-type-anno (name "when") (ty-malformed (tag "ty_anno_unexpected_token"))) (e-malformed (reason "expr_unexpected_token")) @@ -509,6 +518,7 @@ EndOfFile, # FORMATTED ~~~roc { + if "conditional" when : @@ -526,6 +536,8 @@ EndOfFile, # CANONICALIZE ~~~clojure (e-block + (s-expr + (e-runtime-error (tag "if_condition_not_canonicalized"))) (s-expr (e-runtime-error (tag "expr_not_canonicalized"))) (s-let