diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs index 5609266c41..9e9b10d0f5 100644 --- a/crates/compiler/can/src/expr.rs +++ b/crates/compiler/can/src/expr.rs @@ -1367,6 +1367,14 @@ pub fn canonicalize_expr<'a>( (RuntimeError(problem), Output::default()) } + ast::Expr::UnappliedRecordBuilder(sub_expr) => { + use roc_problem::can::RuntimeError::*; + + let problem = UnappliedRecordBuilder(sub_expr.region); + env.problem(Problem::RuntimeError(problem.clone())); + + (RuntimeError(problem), Output::default()) + } &ast::Expr::NonBase10Int { string, base, diff --git a/crates/compiler/can/src/operator.rs b/crates/compiler/can/src/operator.rs index baba063d15..1e8dceba02 100644 --- a/crates/compiler/can/src/operator.rs +++ b/crates/compiler/can/src/operator.rs @@ -138,6 +138,7 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc>) -> &'a Loc | MalformedClosure | PrecedenceConflict { .. } | MultipleRecordBuilders { .. } + | UnappliedRecordBuilder { .. } | Tag(_) | OpaqueRef(_) | IngestedFile(_, _) @@ -253,9 +254,10 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc>) -> &'a Loc } } } - RecordBuilder(_) => { - todo!("Compiler error: Record builders must be applied to functions") - } + RecordBuilder(_) => arena.alloc(Loc { + value: UnappliedRecordBuilder(loc_expr), + region: loc_expr.region, + }), BinOps(lefts, right) => desugar_bin_ops(arena, loc_expr.region, lefts, right), Defs(defs, loc_ret) => { let mut defs = (*defs).clone(); diff --git a/crates/compiler/can/tests/test_can.rs b/crates/compiler/can/tests/test_can.rs index eb855da7ec..8363b8b89e 100644 --- a/crates/compiler/can/tests/test_can.rs +++ b/crates/compiler/can/tests/test_can.rs @@ -799,6 +799,30 @@ mod test_can { )); } + #[test] + fn hanging_record_builder() { + let src = indoc!( + r#" + { a <- apply "a" } + "# + ); + let arena = Bump::new(); + let CanExprOut { + problems, loc_expr, .. + } = can_expr_with(&arena, test_home(), src); + + assert_eq!(problems.len(), 1); + assert!(problems.iter().all(|problem| matches!( + problem, + Problem::RuntimeError(roc_problem::can::RuntimeError::UnappliedRecordBuilder { .. }) + ))); + + assert!(matches!( + loc_expr.value, + Expr::RuntimeError(roc_problem::can::RuntimeError::UnappliedRecordBuilder { .. }) + )); + } + // TAIL CALLS fn get_closure(expr: &Expr, i: usize) -> roc_can::expr::Recursive { match expr { diff --git a/crates/compiler/fmt/src/expr.rs b/crates/compiler/fmt/src/expr.rs index e9a201fd08..e9ab4f6190 100644 --- a/crates/compiler/fmt/src/expr.rs +++ b/crates/compiler/fmt/src/expr.rs @@ -79,7 +79,8 @@ impl<'a> Formattable for Expr<'a> { | PrecedenceConflict(roc_parse::ast::PrecedenceConflict { expr: loc_subexpr, .. }) - | MultipleRecordBuilders(loc_subexpr) => loc_subexpr.is_multiline(), + | MultipleRecordBuilders(loc_subexpr) + | UnappliedRecordBuilder(loc_subexpr) => loc_subexpr.is_multiline(), ParensAround(subexpr) => subexpr.is_multiline(), @@ -499,9 +500,8 @@ impl<'a> Formattable for Expr<'a> { } MalformedClosure => {} PrecedenceConflict { .. } => {} - MultipleRecordBuilders(sub_expr) => { - sub_expr.format_with_options(buf, parens, newlines, indent) - } + MultipleRecordBuilders { .. } => {} + UnappliedRecordBuilder { .. } => {} IngestedFile(_, _) => {} } } diff --git a/crates/compiler/fmt/src/spaces.rs b/crates/compiler/fmt/src/spaces.rs index b8eb353338..eb7c96b927 100644 --- a/crates/compiler/fmt/src/spaces.rs +++ b/crates/compiler/fmt/src/spaces.rs @@ -747,9 +747,8 @@ impl<'a> RemoveSpaces<'a> for Expr<'a> { Expr::MalformedIdent(a, b) => Expr::MalformedIdent(a, remove_spaces_bad_ident(b)), Expr::MalformedClosure => Expr::MalformedClosure, Expr::PrecedenceConflict(a) => Expr::PrecedenceConflict(a), - Expr::MultipleRecordBuilders(a) => { - Expr::MultipleRecordBuilders(arena.alloc(a.remove_spaces(arena))) - } + Expr::MultipleRecordBuilders(a) => Expr::MultipleRecordBuilders(a), + Expr::UnappliedRecordBuilder(a) => Expr::UnappliedRecordBuilder(a), Expr::SpaceBefore(a, _) => a.remove_spaces(arena), Expr::SpaceAfter(a, _) => a.remove_spaces(arena), Expr::SingleQuote(a) => Expr::Num(a), diff --git a/crates/compiler/parse/src/ast.rs b/crates/compiler/parse/src/ast.rs index f303179100..3b01744371 100644 --- a/crates/compiler/parse/src/ast.rs +++ b/crates/compiler/parse/src/ast.rs @@ -328,6 +328,7 @@ pub enum Expr<'a> { // We should tell the author to disambiguate by grouping them with parens. PrecedenceConflict(&'a PrecedenceConflict<'a>), MultipleRecordBuilders(&'a Loc>), + UnappliedRecordBuilder(&'a Loc>), } #[derive(Clone, Copy, Debug, PartialEq)] @@ -1535,7 +1536,8 @@ impl<'a> Malformed for Expr<'a> { MalformedIdent(_, _) | MalformedClosure | PrecedenceConflict(_) | - MultipleRecordBuilders(_) => true, + MultipleRecordBuilders(_) | + UnappliedRecordBuilder(_) => true, } } } diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs index 14e50094db..de394779fe 100644 --- a/crates/compiler/parse/src/expr.rs +++ b/crates/compiler/parse/src/expr.rs @@ -1929,6 +1929,7 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result return Err(()), diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index 94f3cba5c0..a81784b239 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -362,6 +362,7 @@ impl Problem { | Problem::RuntimeError(RuntimeError::MultipleCharsInSingleQuote(region)) | Problem::RuntimeError(RuntimeError::DegenerateBranch(region)) | Problem::RuntimeError(RuntimeError::MultipleRecordBuilders(region)) + | Problem::RuntimeError(RuntimeError::UnappliedRecordBuilder(region)) | Problem::InvalidAliasRigid { region, .. } | Problem::InvalidInterpolation(region) | Problem::InvalidHexadecimal(region) @@ -591,6 +592,7 @@ pub enum RuntimeError { DegenerateBranch(Region), MultipleRecordBuilders(Region), + UnappliedRecordBuilder(Region), } impl RuntimeError { diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index 4c71a13830..25da873585 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -2147,6 +2147,18 @@ fn pretty_runtime_error<'b>( title = "MULTIPLE RECORD BUILDERS"; } + RuntimeError::UnappliedRecordBuilder(region) => { + doc = alloc.stack([ + alloc.reflow("This record builder was not applied to a function:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("However, we need a function to construct the record."), + alloc.note( + "Functions must be applied directly. The pipe operator (|>) cannot be used.", + ), + ]); + + title = "UNAPPLIED RECORD BUILDER"; + } } (doc, title) diff --git a/crates/reporting/tests/test_reporting.rs b/crates/reporting/tests/test_reporting.rs index 02f1b73cc4..49a871560c 100644 --- a/crates/reporting/tests/test_reporting.rs +++ b/crates/reporting/tests/test_reporting.rs @@ -10204,6 +10204,27 @@ I recommend using camelCase. It's the standard style in Roc code! "### ); + test_report!( + unapplied_record_builder, + indoc!( + r#" + { a <- apply "a" } + "# + ), + @r###" + ── UNAPPLIED RECORD BUILDER ────────────────────────────── /code/proj/Main.roc ─ + + This record builder was not applied to a function: + + 4│ { a <- apply "a" } + ^^^^^^^^^^^^^^^^^^ + + However, we need a function to construct the record. + + Note: Functions must be applied directly. The pipe operator (|>) cannot be used. + "### + ); + test_report!( destructure_assignment_introduces_no_variables_nested, indoc!(