mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-03 11:52:19 +00:00
Merge pull request #5389 from agu-z/agu-z/record-builder-syntax
Record Builder Syntax
This commit is contained in:
commit
922a10db52
20 changed files with 1223 additions and 50 deletions
|
@ -289,6 +289,7 @@ pub fn constrain_expr<'a>(
|
|||
let fn_reason = Reason::FnCall {
|
||||
name: opt_symbol,
|
||||
arity: args.len() as u8,
|
||||
called_via: *called_via,
|
||||
};
|
||||
|
||||
let fn_con = constrain_expr(arena, env, call_expr, fn_expected, region);
|
||||
|
|
|
@ -1052,6 +1052,9 @@ pub fn canonicalize_expr<'a>(
|
|||
can_defs_with_return(env, var_store, inner_scope, env.arena.alloc(defs), loc_ret)
|
||||
})
|
||||
}
|
||||
ast::Expr::RecordBuilder(_) => {
|
||||
unreachable!("RecordBuilder should have been desugared by now")
|
||||
}
|
||||
ast::Expr::Backpassing(_, _, _) => {
|
||||
unreachable!("Backpassing should have been desugared by now")
|
||||
}
|
||||
|
@ -1356,6 +1359,22 @@ pub fn canonicalize_expr<'a>(
|
|||
|
||||
(RuntimeError(problem), Output::default())
|
||||
}
|
||||
ast::Expr::MultipleRecordBuilders(sub_expr) => {
|
||||
use roc_problem::can::RuntimeError::*;
|
||||
|
||||
let problem = MultipleRecordBuilders(sub_expr.region);
|
||||
env.problem(Problem::RuntimeError(problem.clone()));
|
||||
|
||||
(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,
|
||||
|
|
|
@ -7,7 +7,7 @@ use roc_module::called_via::BinOp::Pizza;
|
|||
use roc_module::called_via::{BinOp, CalledVia};
|
||||
use roc_module::ident::ModuleName;
|
||||
use roc_parse::ast::Expr::{self, *};
|
||||
use roc_parse::ast::{AssignedField, ValueDef, WhenBranch};
|
||||
use roc_parse::ast::{AssignedField, Collection, RecordBuilderField, ValueDef, WhenBranch};
|
||||
use roc_region::all::{Loc, Region};
|
||||
|
||||
// BinOp precedence logic adapted from Gluon by Markus Westerlind
|
||||
|
@ -137,6 +137,8 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
|
|||
| MalformedIdent(_, _)
|
||||
| MalformedClosure
|
||||
| PrecedenceConflict { .. }
|
||||
| MultipleRecordBuilders { .. }
|
||||
| UnappliedRecordBuilder { .. }
|
||||
| Tag(_)
|
||||
| OpaqueRef(_)
|
||||
| IngestedFile(_, _)
|
||||
|
@ -252,6 +254,10 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
|
|||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
@ -263,17 +269,60 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
|
|||
}
|
||||
Apply(loc_fn, loc_args, called_via) => {
|
||||
let mut desugared_args = Vec::with_capacity_in(loc_args.len(), arena);
|
||||
let mut builder_apply_exprs = None;
|
||||
|
||||
for loc_arg in loc_args.iter() {
|
||||
desugared_args.push(desugar_expr(arena, loc_arg));
|
||||
let mut current = loc_arg.value;
|
||||
let arg = loop {
|
||||
match current {
|
||||
RecordBuilder(fields) => {
|
||||
if builder_apply_exprs.is_some() {
|
||||
return arena.alloc(Loc {
|
||||
value: MultipleRecordBuilders(loc_expr),
|
||||
region: loc_expr.region,
|
||||
});
|
||||
}
|
||||
|
||||
let builder_arg = record_builder_arg(arena, loc_arg.region, fields);
|
||||
builder_apply_exprs = Some(builder_arg.apply_exprs);
|
||||
|
||||
break builder_arg.closure;
|
||||
}
|
||||
SpaceBefore(expr, _) | SpaceAfter(expr, _) | ParensAround(expr) => {
|
||||
current = *expr;
|
||||
}
|
||||
_ => break loc_arg,
|
||||
}
|
||||
};
|
||||
|
||||
desugared_args.push(desugar_expr(arena, arg));
|
||||
}
|
||||
|
||||
let desugared_args = desugared_args.into_bump_slice();
|
||||
|
||||
arena.alloc(Loc {
|
||||
let mut apply: &Loc<Expr> = arena.alloc(Loc {
|
||||
value: Apply(desugar_expr(arena, loc_fn), desugared_args, *called_via),
|
||||
region: loc_expr.region,
|
||||
})
|
||||
});
|
||||
|
||||
match builder_apply_exprs {
|
||||
None => {}
|
||||
|
||||
Some(apply_exprs) => {
|
||||
for expr in apply_exprs {
|
||||
let desugared_expr = desugar_expr(arena, expr);
|
||||
|
||||
let args = std::slice::from_ref(arena.alloc(apply));
|
||||
|
||||
apply = arena.alloc(Loc {
|
||||
value: Apply(desugared_expr, args, CalledVia::RecordBuilder),
|
||||
region: loc_expr.region,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply
|
||||
}
|
||||
When(loc_cond_expr, branches) => {
|
||||
let loc_desugared_cond = &*arena.alloc(desugar_expr(arena, loc_cond_expr));
|
||||
|
@ -430,6 +479,88 @@ fn desugar_field<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
struct RecordBuilderArg<'a> {
|
||||
closure: &'a Loc<Expr<'a>>,
|
||||
apply_exprs: Vec<'a, &'a Loc<Expr<'a>>>,
|
||||
}
|
||||
|
||||
fn record_builder_arg<'a>(
|
||||
arena: &'a Bump,
|
||||
region: Region,
|
||||
fields: Collection<'a, Loc<RecordBuilderField<'a>>>,
|
||||
) -> RecordBuilderArg<'a> {
|
||||
let mut record_fields = Vec::with_capacity_in(fields.len(), arena);
|
||||
let mut apply_exprs = Vec::with_capacity_in(fields.len(), arena);
|
||||
let mut apply_field_names = Vec::with_capacity_in(fields.len(), arena);
|
||||
|
||||
// Build the record that the closure will return and gather apply expressions
|
||||
|
||||
for field in fields.iter() {
|
||||
let mut current = field.value;
|
||||
|
||||
let new_field = loop {
|
||||
match current {
|
||||
RecordBuilderField::Value(label, spaces, expr) => {
|
||||
break AssignedField::RequiredValue(label, spaces, expr)
|
||||
}
|
||||
RecordBuilderField::ApplyValue(label, _spaces, expr) => {
|
||||
apply_field_names.push(label);
|
||||
apply_exprs.push(expr);
|
||||
|
||||
break AssignedField::LabelOnly(label);
|
||||
}
|
||||
RecordBuilderField::LabelOnly(label) => break AssignedField::LabelOnly(label),
|
||||
RecordBuilderField::SpaceBefore(sub_field, _) => {
|
||||
current = *sub_field;
|
||||
}
|
||||
RecordBuilderField::SpaceAfter(sub_field, _) => {
|
||||
current = *sub_field;
|
||||
}
|
||||
RecordBuilderField::Malformed(malformed) => {
|
||||
break AssignedField::Malformed(malformed)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
record_fields.push(Loc {
|
||||
value: new_field,
|
||||
region: field.region,
|
||||
});
|
||||
}
|
||||
|
||||
let record_fields = fields.replace_items(record_fields.into_bump_slice());
|
||||
|
||||
let mut body = arena.alloc(Loc {
|
||||
value: Record(record_fields),
|
||||
region,
|
||||
});
|
||||
|
||||
// Construct the builder's closure
|
||||
//
|
||||
// { x, y, z: 3 }
|
||||
// \y -> { x, y, z: 3 }
|
||||
// \x -> \y -> { x, y, z: 3 }
|
||||
|
||||
for name in apply_field_names.iter().rev() {
|
||||
let ident = roc_parse::ast::Pattern::Identifier(name.value);
|
||||
|
||||
let arg_pattern = arena.alloc(Loc {
|
||||
value: ident,
|
||||
region: name.region,
|
||||
});
|
||||
|
||||
body = arena.alloc(Loc {
|
||||
value: Closure(std::slice::from_ref(arg_pattern), body),
|
||||
region,
|
||||
});
|
||||
}
|
||||
|
||||
RecordBuilderArg {
|
||||
closure: body,
|
||||
apply_exprs,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move this desugaring to canonicalization, so we can use Symbols instead of strings
|
||||
#[inline(always)]
|
||||
fn binop_to_function(binop: BinOp) -> (&'static str, &'static str) {
|
||||
|
|
|
@ -14,8 +14,10 @@ mod helpers;
|
|||
mod test_can {
|
||||
use crate::helpers::{can_expr_with, test_home, CanExprOut};
|
||||
use bumpalo::Bump;
|
||||
use core::panic;
|
||||
use roc_can::expr::Expr::{self, *};
|
||||
use roc_can::expr::{ClosureData, IntValue, Recursive};
|
||||
use roc_module::symbol::Symbol;
|
||||
use roc_problem::can::{CycleEntry, FloatErrorKind, IntErrorKind, Problem, RuntimeError};
|
||||
use roc_region::all::{Position, Region};
|
||||
use std::{f64, i64};
|
||||
|
@ -655,6 +657,172 @@ mod test_can {
|
|||
));
|
||||
}
|
||||
|
||||
// RECORD BUILDERS
|
||||
#[test]
|
||||
fn record_builder_desugar() {
|
||||
let src = indoc!(
|
||||
r#"
|
||||
succeed = \_ -> crash "succeed"
|
||||
apply = \_ -> crash "get"
|
||||
|
||||
d = 3
|
||||
|
||||
succeed {
|
||||
a: 1,
|
||||
b <- apply "b",
|
||||
c <- apply "c",
|
||||
d
|
||||
}
|
||||
"#
|
||||
);
|
||||
let arena = Bump::new();
|
||||
let out = can_expr_with(&arena, test_home(), src);
|
||||
|
||||
assert_eq!(out.problems.len(), 0);
|
||||
|
||||
// Assert that we desugar to:
|
||||
//
|
||||
// (apply "c") ((apply "b") (succeed \b -> \c -> { a: 1, b, c, d }))
|
||||
|
||||
// (apply "c") ..
|
||||
let (apply_c, c_to_b) = simplify_curried_call(&out.loc_expr.value);
|
||||
assert_apply_call(apply_c, "c", &out.interns);
|
||||
|
||||
// (apply "b") ..
|
||||
let (apply_b, b_to_succeed) = simplify_curried_call(c_to_b);
|
||||
assert_apply_call(apply_b, "b", &out.interns);
|
||||
|
||||
// (succeed ..)
|
||||
let (succeed, b_closure) = simplify_curried_call(b_to_succeed);
|
||||
|
||||
match succeed {
|
||||
Var(sym, _) => assert_eq!(sym.as_str(&out.interns), "succeed"),
|
||||
_ => panic!("Not calling succeed: {:?}", succeed),
|
||||
}
|
||||
|
||||
// \b -> ..
|
||||
let (b_sym, c_closure) = simplify_builder_closure(b_closure);
|
||||
|
||||
// \c -> ..
|
||||
let (c_sym, c_body) = simplify_builder_closure(c_closure);
|
||||
|
||||
// { a: 1, b, c, d }
|
||||
match c_body {
|
||||
Record { fields, .. } => {
|
||||
match get_field_expr(fields, "a") {
|
||||
Num(_, num_str, _, _) => {
|
||||
assert_eq!(num_str.to_string(), "1");
|
||||
}
|
||||
expr => panic!("a is not a Num: {:?}", expr),
|
||||
}
|
||||
|
||||
assert_eq!(get_field_var_sym(fields, "b"), b_sym);
|
||||
assert_eq!(get_field_var_sym(fields, "c"), c_sym);
|
||||
assert_eq!(get_field_var_sym(fields, "d").as_str(&out.interns), "d");
|
||||
}
|
||||
_ => panic!("Closure body wasn't a Record: {:?}", c_body),
|
||||
}
|
||||
}
|
||||
|
||||
fn simplify_curried_call(expr: &Expr) -> (&Expr, &Expr) {
|
||||
match expr {
|
||||
LetNonRec(_, loc_expr) => simplify_curried_call(&loc_expr.value),
|
||||
Call(fun, args, _) => (&fun.1.value, &args[0].1.value),
|
||||
_ => panic!("Final Expr is not a Call: {:?}", expr),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_apply_call(expr: &Expr, expected: &str, interns: &roc_module::symbol::Interns) {
|
||||
match simplify_curried_call(expr) {
|
||||
(Var(sym, _), Str(val)) => {
|
||||
assert_eq!(sym.as_str(interns), "apply");
|
||||
assert_eq!(val.to_string(), expected);
|
||||
}
|
||||
call => panic!("Not a valid (get {}) call: {:?}", expected, call),
|
||||
};
|
||||
}
|
||||
|
||||
fn simplify_builder_closure(expr: &Expr) -> (Symbol, &Expr) {
|
||||
use roc_can::pattern::Pattern::*;
|
||||
|
||||
match expr {
|
||||
Closure(closure) => match &closure.arguments[0].2.value {
|
||||
Identifier(sym) => (*sym, &closure.loc_body.value),
|
||||
pattern => panic!("Not an identifier pattern: {:?}", pattern),
|
||||
},
|
||||
_ => panic!("Not a closure: {:?}", expr),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_field_expr<'a>(
|
||||
fields: &'a roc_collections::SendMap<roc_module::ident::Lowercase, roc_can::expr::Field>,
|
||||
name: &'a str,
|
||||
) -> &'a Expr {
|
||||
let ident = roc_module::ident::Lowercase::from(name);
|
||||
|
||||
&fields.get(&ident).unwrap().loc_expr.value
|
||||
}
|
||||
|
||||
fn get_field_var_sym(
|
||||
fields: &roc_collections::SendMap<roc_module::ident::Lowercase, roc_can::expr::Field>,
|
||||
name: &str,
|
||||
) -> roc_module::symbol::Symbol {
|
||||
match get_field_expr(fields, name) {
|
||||
Var(sym, _) => *sym,
|
||||
expr => panic!("Not a var: {:?}", expr),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_record_builders_error() {
|
||||
let src = indoc!(
|
||||
r#"
|
||||
succeed
|
||||
{ a <- apply "a" }
|
||||
{ b <- apply "b" }
|
||||
"#
|
||||
);
|
||||
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::MultipleRecordBuilders { .. })
|
||||
)));
|
||||
|
||||
assert!(matches!(
|
||||
loc_expr.value,
|
||||
Expr::RuntimeError(roc_problem::can::RuntimeError::MultipleRecordBuilders { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
|
|
@ -466,6 +466,7 @@ pub fn constrain_expr(
|
|||
let fn_reason = Reason::FnCall {
|
||||
name: opt_symbol,
|
||||
arity: loc_args.len() as u8,
|
||||
called_via: *called_via,
|
||||
};
|
||||
|
||||
let fn_con = constrain_expr(
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
};
|
||||
use roc_parse::ast::{
|
||||
AssignedField, Collection, Expr, ExtractSpaces, HasAbilities, HasAbility, HasClause, HasImpls,
|
||||
Tag, TypeAnnotation, TypeHeader,
|
||||
RecordBuilderField, Tag, TypeAnnotation, TypeHeader,
|
||||
};
|
||||
use roc_parse::ident::UppercaseIdent;
|
||||
use roc_region::all::Loc;
|
||||
|
@ -498,6 +498,109 @@ fn format_assigned_field_help<T>(
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Formattable for RecordBuilderField<'a> {
|
||||
fn is_multiline(&self) -> bool {
|
||||
is_multiline_record_builder_field_help(self)
|
||||
}
|
||||
|
||||
fn format_with_options(&self, buf: &mut Buf, _parens: Parens, newlines: Newlines, indent: u16) {
|
||||
// we abuse the `Newlines` type to decide between multiline or single-line layout
|
||||
format_record_builder_field_help(self, buf, indent, 0, newlines == Newlines::Yes);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_multiline_record_builder_field_help(afield: &RecordBuilderField<'_>) -> bool {
|
||||
use self::RecordBuilderField::*;
|
||||
|
||||
match afield {
|
||||
Value(_, spaces, ann) | ApplyValue(_, spaces, ann) => {
|
||||
!spaces.is_empty() || ann.value.is_multiline()
|
||||
}
|
||||
LabelOnly(_) => false,
|
||||
SpaceBefore(_, _) | SpaceAfter(_, _) => true,
|
||||
Malformed(text) => text.chars().any(|c| c == '\n'),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_record_builder_field_help(
|
||||
zelf: &RecordBuilderField,
|
||||
buf: &mut Buf,
|
||||
indent: u16,
|
||||
separator_spaces: usize,
|
||||
is_multiline: bool,
|
||||
) {
|
||||
use self::RecordBuilderField::*;
|
||||
|
||||
match zelf {
|
||||
Value(name, spaces, ann) => {
|
||||
if is_multiline {
|
||||
buf.newline();
|
||||
}
|
||||
|
||||
buf.indent(indent);
|
||||
buf.push_str(name.value);
|
||||
|
||||
if !spaces.is_empty() {
|
||||
fmt_spaces(buf, spaces.iter(), indent);
|
||||
}
|
||||
|
||||
buf.spaces(separator_spaces);
|
||||
buf.push(':');
|
||||
buf.spaces(1);
|
||||
ann.value.format(buf, indent);
|
||||
}
|
||||
ApplyValue(name, spaces, ann) => {
|
||||
if is_multiline {
|
||||
buf.newline();
|
||||
buf.indent(indent);
|
||||
}
|
||||
|
||||
buf.push_str(name.value);
|
||||
|
||||
if !spaces.is_empty() {
|
||||
fmt_spaces(buf, spaces.iter(), indent);
|
||||
}
|
||||
|
||||
buf.spaces(separator_spaces);
|
||||
buf.spaces(1);
|
||||
buf.push_str("<-");
|
||||
buf.spaces(1);
|
||||
ann.value.format(buf, indent);
|
||||
}
|
||||
LabelOnly(name) => {
|
||||
if is_multiline {
|
||||
buf.newline();
|
||||
buf.indent(indent);
|
||||
}
|
||||
|
||||
buf.push_str(name.value);
|
||||
}
|
||||
SpaceBefore(sub_field, spaces) => {
|
||||
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent);
|
||||
format_record_builder_field_help(
|
||||
sub_field,
|
||||
buf,
|
||||
indent,
|
||||
separator_spaces,
|
||||
is_multiline,
|
||||
);
|
||||
}
|
||||
SpaceAfter(sub_field, spaces) => {
|
||||
format_record_builder_field_help(
|
||||
sub_field,
|
||||
buf,
|
||||
indent,
|
||||
separator_spaces,
|
||||
is_multiline,
|
||||
);
|
||||
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent);
|
||||
}
|
||||
Malformed(raw) => {
|
||||
buf.push_str(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Formattable for Tag<'a> {
|
||||
fn is_multiline(&self) -> bool {
|
||||
use self::Tag::*;
|
||||
|
|
|
@ -9,7 +9,8 @@ use crate::spaces::{
|
|||
use crate::Buf;
|
||||
use roc_module::called_via::{self, BinOp};
|
||||
use roc_parse::ast::{
|
||||
AssignedField, Base, Collection, CommentOrNewline, Expr, ExtractSpaces, Pattern, WhenBranch,
|
||||
AssignedField, Base, Collection, CommentOrNewline, Expr, ExtractSpaces, Pattern,
|
||||
RecordBuilderField, WhenBranch,
|
||||
};
|
||||
use roc_parse::ast::{StrLiteral, StrSegment};
|
||||
use roc_parse::ident::Accessor;
|
||||
|
@ -77,7 +78,9 @@ impl<'a> Formattable for Expr<'a> {
|
|||
UnaryOp(loc_subexpr, _)
|
||||
| PrecedenceConflict(roc_parse::ast::PrecedenceConflict {
|
||||
expr: loc_subexpr, ..
|
||||
}) => loc_subexpr.is_multiline(),
|
||||
})
|
||||
| MultipleRecordBuilders(loc_subexpr)
|
||||
| UnappliedRecordBuilder(loc_subexpr) => loc_subexpr.is_multiline(),
|
||||
|
||||
ParensAround(subexpr) => subexpr.is_multiline(),
|
||||
|
||||
|
@ -100,6 +103,7 @@ impl<'a> Formattable for Expr<'a> {
|
|||
Record(fields) => is_collection_multiline(fields),
|
||||
Tuple(fields) => is_collection_multiline(fields),
|
||||
RecordUpdate { fields, .. } => is_collection_multiline(fields),
|
||||
RecordBuilder(fields) => is_collection_multiline(fields),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,10 +344,34 @@ impl<'a> Formattable for Expr<'a> {
|
|||
buf.push_str(string);
|
||||
}
|
||||
Record(fields) => {
|
||||
fmt_record(buf, None, *fields, indent);
|
||||
fmt_record_like(
|
||||
buf,
|
||||
None,
|
||||
*fields,
|
||||
indent,
|
||||
format_assigned_field_multiline,
|
||||
assigned_field_to_space_before,
|
||||
);
|
||||
}
|
||||
RecordUpdate { update, fields } => {
|
||||
fmt_record(buf, Some(*update), *fields, indent);
|
||||
fmt_record_like(
|
||||
buf,
|
||||
Some(*update),
|
||||
*fields,
|
||||
indent,
|
||||
format_assigned_field_multiline,
|
||||
assigned_field_to_space_before,
|
||||
);
|
||||
}
|
||||
RecordBuilder(fields) => {
|
||||
fmt_record_like(
|
||||
buf,
|
||||
None,
|
||||
*fields,
|
||||
indent,
|
||||
format_record_builder_field_multiline,
|
||||
record_builder_field_to_space_before,
|
||||
);
|
||||
}
|
||||
Closure(loc_patterns, loc_ret) => {
|
||||
fmt_closure(buf, loc_patterns, loc_ret, indent);
|
||||
|
@ -472,6 +500,8 @@ impl<'a> Formattable for Expr<'a> {
|
|||
}
|
||||
MalformedClosure => {}
|
||||
PrecedenceConflict { .. } => {}
|
||||
MultipleRecordBuilders { .. } => {}
|
||||
UnappliedRecordBuilder { .. } => {}
|
||||
IngestedFile(_, _) => {}
|
||||
}
|
||||
}
|
||||
|
@ -1301,12 +1331,18 @@ fn pattern_needs_parens_when_backpassing(pat: &Pattern) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
fn fmt_record<'a>(
|
||||
fn fmt_record_like<'a, Field, Format, ToSpaceBefore>(
|
||||
buf: &mut Buf,
|
||||
update: Option<&'a Loc<Expr<'a>>>,
|
||||
fields: Collection<'a, Loc<AssignedField<'a, Expr<'a>>>>,
|
||||
fields: Collection<'a, Loc<Field>>,
|
||||
indent: u16,
|
||||
) {
|
||||
format_field_multiline: Format,
|
||||
to_space_before: ToSpaceBefore,
|
||||
) where
|
||||
Field: Formattable,
|
||||
Format: Fn(&mut Buf, &Field, u16, &str),
|
||||
ToSpaceBefore: Fn(&'a Field) -> Option<(&'a Field, &'a [CommentOrNewline<'a>])>,
|
||||
{
|
||||
let loc_fields = fields.items;
|
||||
let final_comments = fields.final_comments();
|
||||
buf.indent(indent);
|
||||
|
@ -1342,7 +1378,7 @@ fn fmt_record<'a>(
|
|||
// In this case, we have to move the comma before the comment.
|
||||
|
||||
let is_first_item = index == 0;
|
||||
if let AssignedField::SpaceBefore(_sub_field, spaces) = &field.value {
|
||||
if let Some((_sub_field, spaces)) = to_space_before(&field.value) {
|
||||
let is_only_newlines = spaces.iter().all(|s| s.is_newline());
|
||||
if !is_first_item
|
||||
&& !is_only_newlines
|
||||
|
@ -1393,7 +1429,7 @@ fn fmt_record<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
fn format_field_multiline<T>(
|
||||
fn format_assigned_field_multiline<T>(
|
||||
buf: &mut Buf,
|
||||
field: &AssignedField<T>,
|
||||
indent: u16,
|
||||
|
@ -1449,7 +1485,7 @@ fn format_field_multiline<T>(
|
|||
// ```
|
||||
// we'd like to preserve this
|
||||
|
||||
format_field_multiline(buf, sub_field, indent, separator_prefix);
|
||||
format_assigned_field_multiline(buf, sub_field, indent, separator_prefix);
|
||||
}
|
||||
AssignedField::SpaceAfter(sub_field, spaces) => {
|
||||
// We have something like that:
|
||||
|
@ -1463,7 +1499,7 @@ fn format_field_multiline<T>(
|
|||
// # comment
|
||||
// otherfield
|
||||
// ```
|
||||
format_field_multiline(buf, sub_field, indent, separator_prefix);
|
||||
format_assigned_field_multiline(buf, sub_field, indent, separator_prefix);
|
||||
fmt_comments_only(buf, spaces.iter(), NewlineAt::Top, indent);
|
||||
}
|
||||
Malformed(raw) => {
|
||||
|
@ -1472,6 +1508,102 @@ fn format_field_multiline<T>(
|
|||
}
|
||||
}
|
||||
|
||||
fn assigned_field_to_space_before<'a, T>(
|
||||
field: &'a AssignedField<'a, T>,
|
||||
) -> Option<(&AssignedField<'a, T>, &'a [CommentOrNewline<'a>])> {
|
||||
match field {
|
||||
AssignedField::SpaceBefore(sub_field, spaces) => Some((sub_field, spaces)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_record_builder_field_multiline(
|
||||
buf: &mut Buf,
|
||||
field: &RecordBuilderField,
|
||||
indent: u16,
|
||||
separator_prefix: &str,
|
||||
) {
|
||||
use self::RecordBuilderField::*;
|
||||
match field {
|
||||
Value(name, spaces, ann) => {
|
||||
buf.newline();
|
||||
buf.indent(indent);
|
||||
buf.push_str(name.value);
|
||||
|
||||
if !spaces.is_empty() {
|
||||
fmt_spaces(buf, spaces.iter(), indent);
|
||||
buf.indent(indent);
|
||||
}
|
||||
|
||||
buf.push_str(separator_prefix);
|
||||
buf.push_str(":");
|
||||
buf.spaces(1);
|
||||
ann.value.format(buf, indent);
|
||||
buf.push(',');
|
||||
}
|
||||
ApplyValue(name, spaces, ann) => {
|
||||
buf.newline();
|
||||
buf.indent(indent);
|
||||
buf.push_str(name.value);
|
||||
|
||||
if !spaces.is_empty() {
|
||||
fmt_spaces(buf, spaces.iter(), indent);
|
||||
buf.indent(indent);
|
||||
}
|
||||
|
||||
buf.push_str(separator_prefix);
|
||||
buf.spaces(1);
|
||||
buf.push_str("<-");
|
||||
buf.spaces(1);
|
||||
ann.value.format(buf, indent);
|
||||
buf.push(',');
|
||||
}
|
||||
LabelOnly(name) => {
|
||||
buf.newline();
|
||||
buf.indent(indent);
|
||||
buf.push_str(name.value);
|
||||
buf.push(',');
|
||||
}
|
||||
SpaceBefore(sub_field, _spaces) => {
|
||||
// We have something like that:
|
||||
// ```
|
||||
// # comment
|
||||
// field,
|
||||
// ```
|
||||
// we'd like to preserve this
|
||||
|
||||
format_record_builder_field_multiline(buf, sub_field, indent, separator_prefix);
|
||||
}
|
||||
SpaceAfter(sub_field, spaces) => {
|
||||
// We have something like that:
|
||||
// ```
|
||||
// field # comment
|
||||
// , otherfield
|
||||
// ```
|
||||
// we'd like to transform it into:
|
||||
// ```
|
||||
// field,
|
||||
// # comment
|
||||
// otherfield
|
||||
// ```
|
||||
format_record_builder_field_multiline(buf, sub_field, indent, separator_prefix);
|
||||
fmt_comments_only(buf, spaces.iter(), NewlineAt::Top, indent);
|
||||
}
|
||||
Malformed(raw) => {
|
||||
buf.push_str(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_builder_field_to_space_before<'a>(
|
||||
field: &'a RecordBuilderField<'a>,
|
||||
) -> Option<(&RecordBuilderField<'a>, &'a [CommentOrNewline<'a>])> {
|
||||
match field {
|
||||
RecordBuilderField::SpaceBefore(sub_field, spaces) => Some((sub_field, spaces)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sub_expr_requests_parens(expr: &Expr<'_>) -> bool {
|
||||
match expr {
|
||||
Expr::BinOps(left_side, _) => {
|
||||
|
|
|
@ -4,8 +4,9 @@ use roc_module::called_via::{BinOp, UnaryOp};
|
|||
use roc_parse::{
|
||||
ast::{
|
||||
AbilityMember, AssignedField, Collection, CommentOrNewline, Defs, Expr, Has, HasAbilities,
|
||||
HasAbility, HasClause, HasImpls, Header, Module, Pattern, Spaced, Spaces, StrLiteral,
|
||||
StrSegment, Tag, TypeAnnotation, TypeDef, TypeHeader, ValueDef, WhenBranch,
|
||||
HasAbility, HasClause, HasImpls, Header, Module, Pattern, RecordBuilderField, Spaced,
|
||||
Spaces, StrLiteral, StrSegment, Tag, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
|
||||
WhenBranch,
|
||||
},
|
||||
header::{
|
||||
AppHeader, ExposedName, HostedHeader, ImportsEntry, InterfaceHeader, KeywordItem,
|
||||
|
@ -614,6 +615,29 @@ impl<'a, T: RemoveSpaces<'a> + Copy + std::fmt::Debug> RemoveSpaces<'a> for Assi
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> RemoveSpaces<'a> for RecordBuilderField<'a> {
|
||||
fn remove_spaces(&self, arena: &'a Bump) -> Self {
|
||||
match *self {
|
||||
RecordBuilderField::Value(a, _, c) => RecordBuilderField::Value(
|
||||
a.remove_spaces(arena),
|
||||
arena.alloc([]),
|
||||
arena.alloc(c.remove_spaces(arena)),
|
||||
),
|
||||
RecordBuilderField::ApplyValue(a, _, c) => RecordBuilderField::ApplyValue(
|
||||
a.remove_spaces(arena),
|
||||
arena.alloc([]),
|
||||
arena.alloc(c.remove_spaces(arena)),
|
||||
),
|
||||
RecordBuilderField::LabelOnly(a) => {
|
||||
RecordBuilderField::LabelOnly(a.remove_spaces(arena))
|
||||
}
|
||||
RecordBuilderField::Malformed(a) => RecordBuilderField::Malformed(a),
|
||||
RecordBuilderField::SpaceBefore(a, _) => a.remove_spaces(arena),
|
||||
RecordBuilderField::SpaceAfter(a, _) => a.remove_spaces(arena),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RemoveSpaces<'a> for StrLiteral<'a> {
|
||||
fn remove_spaces(&self, arena: &'a Bump) -> Self {
|
||||
match *self {
|
||||
|
@ -660,6 +684,7 @@ impl<'a> RemoveSpaces<'a> for Expr<'a> {
|
|||
fields: fields.remove_spaces(arena),
|
||||
},
|
||||
Expr::Record(a) => Expr::Record(a.remove_spaces(arena)),
|
||||
Expr::RecordBuilder(a) => Expr::RecordBuilder(a.remove_spaces(arena)),
|
||||
Expr::Tuple(a) => Expr::Tuple(a.remove_spaces(arena)),
|
||||
Expr::Var { module_name, ident } => Expr::Var { module_name, ident },
|
||||
Expr::Underscore(a) => Expr::Underscore(a),
|
||||
|
@ -722,6 +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(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),
|
||||
|
|
|
@ -88,6 +88,10 @@ pub enum CalledVia {
|
|||
/// This call is the result of desugaring string interpolation,
|
||||
/// e.g. "\(first) \(last)" is transformed into Str.concat (Str.concat first " ") last.
|
||||
StringInterpolation,
|
||||
|
||||
/// This call is the result of desugaring a Record Builder field.
|
||||
/// e.g. succeed { a <- get "a" } is transformed into (get "a") (succeed \a -> { a })
|
||||
RecordBuilder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
|
|
@ -264,6 +264,9 @@ pub enum Expr<'a> {
|
|||
|
||||
Tuple(Collection<'a, &'a Loc<Expr<'a>>>),
|
||||
|
||||
// Record Builders
|
||||
RecordBuilder(Collection<'a, Loc<RecordBuilderField<'a>>>),
|
||||
|
||||
// The name of a file to be ingested directly into a variable.
|
||||
IngestedFile(&'a Path, &'a Loc<TypeAnnotation<'a>>),
|
||||
|
||||
|
@ -324,6 +327,8 @@ pub enum Expr<'a> {
|
|||
// Both operators were non-associative, e.g. (True == False == False).
|
||||
// We should tell the author to disambiguate by grouping them with parens.
|
||||
PrecedenceConflict(&'a PrecedenceConflict<'a>),
|
||||
MultipleRecordBuilders(&'a Loc<Expr<'a>>),
|
||||
UnappliedRecordBuilder(&'a Loc<Expr<'a>>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
@ -684,6 +689,25 @@ pub enum AssignedField<'a, Val> {
|
|||
Malformed(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum RecordBuilderField<'a> {
|
||||
// A field with a value, e.g. `{ name: "blah" }`
|
||||
Value(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc<Expr<'a>>),
|
||||
|
||||
// A field with a function we can apply to build part of the record, e.g. `{ name <- apply getName }`
|
||||
ApplyValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc<Expr<'a>>),
|
||||
|
||||
// A label with no value, e.g. `{ name }` (this is sugar for { name: name })
|
||||
LabelOnly(Loc<&'a str>),
|
||||
|
||||
// We preserve this for the formatter; canonicalization ignores it.
|
||||
SpaceBefore(&'a RecordBuilderField<'a>, &'a [CommentOrNewline<'a>]),
|
||||
SpaceAfter(&'a RecordBuilderField<'a>, &'a [CommentOrNewline<'a>]),
|
||||
|
||||
/// A malformed assigned field, which will code gen to a runtime error
|
||||
Malformed(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CommentOrNewline<'a> {
|
||||
Newline,
|
||||
|
@ -1198,6 +1222,15 @@ impl<'a, Val> Spaceable<'a> for AssignedField<'a, Val> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Spaceable<'a> for RecordBuilderField<'a> {
|
||||
fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self {
|
||||
RecordBuilderField::SpaceBefore(self, spaces)
|
||||
}
|
||||
fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self {
|
||||
RecordBuilderField::SpaceAfter(self, spaces)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Spaceable<'a> for Tag<'a> {
|
||||
fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self {
|
||||
Tag::SpaceBefore(self, spaces)
|
||||
|
@ -1483,6 +1516,8 @@ impl<'a> Malformed for Expr<'a> {
|
|||
Record(items) => items.is_malformed(),
|
||||
Tuple(items) => items.is_malformed(),
|
||||
|
||||
RecordBuilder(items) => items.is_malformed(),
|
||||
|
||||
Closure(args, body) => args.iter().any(|arg| arg.is_malformed()) || body.is_malformed(),
|
||||
Defs(defs, body) => defs.is_malformed() || body.is_malformed(),
|
||||
Backpassing(args, call, body) => args.iter().any(|arg| arg.is_malformed()) || call.is_malformed() || body.is_malformed(),
|
||||
|
@ -1500,7 +1535,9 @@ impl<'a> Malformed for Expr<'a> {
|
|||
|
||||
MalformedIdent(_, _) |
|
||||
MalformedClosure |
|
||||
PrecedenceConflict(_) => true,
|
||||
PrecedenceConflict(_) |
|
||||
MultipleRecordBuilders(_) |
|
||||
UnappliedRecordBuilder(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1575,6 +1612,20 @@ impl<'a, T: Malformed> Malformed for AssignedField<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Malformed for RecordBuilderField<'a> {
|
||||
fn is_malformed(&self) -> bool {
|
||||
match self {
|
||||
RecordBuilderField::Value(_, _, expr) | RecordBuilderField::ApplyValue(_, _, expr) => {
|
||||
expr.is_malformed()
|
||||
}
|
||||
RecordBuilderField::LabelOnly(_) => false,
|
||||
RecordBuilderField::SpaceBefore(field, _)
|
||||
| RecordBuilderField::SpaceAfter(field, _) => field.is_malformed(),
|
||||
RecordBuilderField::Malformed(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Malformed for Pattern<'a> {
|
||||
fn is_malformed(&self) -> bool {
|
||||
use Pattern::*;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::ast::{
|
||||
AssignedField, Collection, CommentOrNewline, Defs, Expr, ExtractSpaces, Has, HasAbilities,
|
||||
Pattern, Spaceable, Spaces, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
|
||||
Pattern, RecordBuilderField, Spaceable, Spaces, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
|
||||
};
|
||||
use crate::blankspace::{
|
||||
space0_after_e, space0_around_e_no_after_indent_check, space0_around_ee, space0_before_e,
|
||||
|
@ -1876,7 +1876,10 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<
|
|||
pattern
|
||||
}
|
||||
|
||||
Expr::SpaceBefore(..) | Expr::SpaceAfter(..) | Expr::ParensAround(..) => unreachable!(),
|
||||
Expr::SpaceBefore(..)
|
||||
| Expr::SpaceAfter(..)
|
||||
| Expr::ParensAround(..)
|
||||
| Expr::RecordBuilder(..) => unreachable!(),
|
||||
|
||||
Expr::Record(fields) => {
|
||||
let patterns = fields.map_items_result(arena, |loc_assigned_field| {
|
||||
|
@ -1922,6 +1925,8 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<
|
|||
| Expr::Dbg(_, _)
|
||||
| Expr::MalformedClosure
|
||||
| Expr::PrecedenceConflict { .. }
|
||||
| Expr::MultipleRecordBuilders { .. }
|
||||
| Expr::UnappliedRecordBuilder { .. }
|
||||
| Expr::RecordUpdate { .. }
|
||||
| Expr::UnaryOp(_, _)
|
||||
| Expr::Crash => return Err(()),
|
||||
|
@ -2527,8 +2532,123 @@ fn list_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EList<'a>> {
|
|||
.trace("list_literal")
|
||||
}
|
||||
|
||||
pub fn record_value_field<'a>() -> impl Parser<'a, AssignedField<'a, Expr<'a>>, ERecord<'a>> {
|
||||
use AssignedField::*;
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum RecordField<'a> {
|
||||
RequiredValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc<Expr<'a>>),
|
||||
OptionalValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc<Expr<'a>>),
|
||||
LabelOnly(Loc<&'a str>),
|
||||
SpaceBefore(&'a RecordField<'a>, &'a [CommentOrNewline<'a>]),
|
||||
SpaceAfter(&'a RecordField<'a>, &'a [CommentOrNewline<'a>]),
|
||||
ApplyValue(Loc<&'a str>, &'a [CommentOrNewline<'a>], &'a Loc<Expr<'a>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FoundApplyValue;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FoundOptionalValue;
|
||||
|
||||
impl<'a> RecordField<'a> {
|
||||
fn is_apply_value(&self) -> bool {
|
||||
let mut current = self;
|
||||
|
||||
loop {
|
||||
match current {
|
||||
RecordField::ApplyValue(_, _, _) => break true,
|
||||
RecordField::SpaceBefore(field, _) | RecordField::SpaceAfter(field, _) => {
|
||||
current = *field;
|
||||
}
|
||||
_ => break false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_assigned_field(
|
||||
self,
|
||||
arena: &'a Bump,
|
||||
) -> Result<AssignedField<'a, Expr<'a>>, FoundApplyValue> {
|
||||
use AssignedField::*;
|
||||
|
||||
match self {
|
||||
RecordField::RequiredValue(loc_label, spaces, loc_expr) => {
|
||||
Ok(RequiredValue(loc_label, spaces, loc_expr))
|
||||
}
|
||||
|
||||
RecordField::OptionalValue(loc_label, spaces, loc_expr) => {
|
||||
Ok(OptionalValue(loc_label, spaces, loc_expr))
|
||||
}
|
||||
|
||||
RecordField::LabelOnly(loc_label) => Ok(LabelOnly(loc_label)),
|
||||
|
||||
RecordField::ApplyValue(_, _, _) => Err(FoundApplyValue),
|
||||
|
||||
RecordField::SpaceBefore(field, spaces) => {
|
||||
let assigned_field = field.to_assigned_field(arena)?;
|
||||
|
||||
Ok(SpaceBefore(arena.alloc(assigned_field), spaces))
|
||||
}
|
||||
|
||||
RecordField::SpaceAfter(field, spaces) => {
|
||||
let assigned_field = field.to_assigned_field(arena)?;
|
||||
|
||||
Ok(SpaceAfter(arena.alloc(assigned_field), spaces))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_builder_field(
|
||||
self,
|
||||
arena: &'a Bump,
|
||||
) -> Result<RecordBuilderField<'a>, FoundOptionalValue> {
|
||||
use RecordBuilderField::*;
|
||||
|
||||
match self {
|
||||
RecordField::RequiredValue(loc_label, spaces, loc_expr) => {
|
||||
Ok(Value(loc_label, spaces, loc_expr))
|
||||
}
|
||||
|
||||
RecordField::OptionalValue(_, _, _) => Err(FoundOptionalValue),
|
||||
|
||||
RecordField::LabelOnly(loc_label) => Ok(LabelOnly(loc_label)),
|
||||
|
||||
RecordField::ApplyValue(loc_label, spaces, loc_expr) => {
|
||||
Ok(ApplyValue(loc_label, spaces, loc_expr))
|
||||
}
|
||||
|
||||
RecordField::SpaceBefore(field, spaces) => {
|
||||
let builder_field = field.to_builder_field(arena)?;
|
||||
|
||||
Ok(SpaceBefore(arena.alloc(builder_field), spaces))
|
||||
}
|
||||
|
||||
RecordField::SpaceAfter(field, spaces) => {
|
||||
let builder_field = field.to_builder_field(arena)?;
|
||||
|
||||
Ok(SpaceAfter(arena.alloc(builder_field), spaces))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Spaceable<'a> for RecordField<'a> {
|
||||
fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self {
|
||||
RecordField::SpaceBefore(self, spaces)
|
||||
}
|
||||
fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self {
|
||||
RecordField::SpaceAfter(self, spaces)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_field<'a>() -> impl Parser<'a, RecordField<'a>, ERecord<'a>> {
|
||||
use RecordField::*;
|
||||
|
||||
enum AssignKind {
|
||||
Required,
|
||||
Optional,
|
||||
Apply,
|
||||
}
|
||||
|
||||
use AssignKind::*;
|
||||
|
||||
map_with_arena!(
|
||||
and!(
|
||||
|
@ -2536,9 +2656,10 @@ pub fn record_value_field<'a>() -> impl Parser<'a, AssignedField<'a, Expr<'a>>,
|
|||
and!(
|
||||
spaces(),
|
||||
optional(and!(
|
||||
either!(
|
||||
word1(b':', ERecord::Colon),
|
||||
word1(b'?', ERecord::QuestionMark)
|
||||
one_of!(
|
||||
map!(word1(b':', ERecord::Colon), |_| Required),
|
||||
map!(word1(b'?', ERecord::QuestionMark), |_| Optional),
|
||||
map!(word2(b'<', b'-', ERecord::Arrow), |_| Apply),
|
||||
),
|
||||
spaces_before(specialize_ref(ERecord::Expr, loc_expr(false)))
|
||||
))
|
||||
|
@ -2546,13 +2667,11 @@ pub fn record_value_field<'a>() -> impl Parser<'a, AssignedField<'a, Expr<'a>>,
|
|||
),
|
||||
|arena: &'a bumpalo::Bump, (loc_label, (spaces, opt_loc_val))| {
|
||||
match opt_loc_val {
|
||||
Some((Either::First(_), loc_val)) => {
|
||||
RequiredValue(loc_label, spaces, arena.alloc(loc_val))
|
||||
}
|
||||
Some((Required, loc_val)) => RequiredValue(loc_label, spaces, arena.alloc(loc_val)),
|
||||
|
||||
Some((Either::Second(_), loc_val)) => {
|
||||
OptionalValue(loc_label, spaces, arena.alloc(loc_val))
|
||||
}
|
||||
Some((Optional, loc_val)) => OptionalValue(loc_label, spaces, arena.alloc(loc_val)),
|
||||
|
||||
Some((Apply, loc_val)) => ApplyValue(loc_label, spaces, arena.alloc(loc_val)),
|
||||
|
||||
// If no value was provided, record it as a Var.
|
||||
// Canonicalize will know what to do with a Var later.
|
||||
|
@ -2577,7 +2696,7 @@ fn record_updateable_identifier<'a>() -> impl Parser<'a, Expr<'a>, ERecord<'a>>
|
|||
|
||||
struct RecordHelp<'a> {
|
||||
update: Option<Loc<Expr<'a>>>,
|
||||
fields: Collection<'a, Loc<AssignedField<'a, Expr<'a>>>>,
|
||||
fields: Collection<'a, Loc<RecordField<'a>>>,
|
||||
}
|
||||
|
||||
fn record_help<'a>() -> impl Parser<'a, RecordHelp<'a>, ERecord<'a>> {
|
||||
|
@ -2597,9 +2716,9 @@ fn record_help<'a>() -> impl Parser<'a, RecordHelp<'a>, ERecord<'a>> {
|
|||
word1(b'&', ERecord::Ampersand)
|
||||
))),
|
||||
fields: collection_inner!(
|
||||
loc!(record_value_field()),
|
||||
loc!(record_field()),
|
||||
word1(b',', ERecord::End),
|
||||
AssignedField::SpaceBefore
|
||||
RecordField::SpaceBefore
|
||||
),
|
||||
})),
|
||||
word1(b'}', ERecord::End)
|
||||
|
@ -2614,22 +2733,76 @@ fn record_literal_help<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> {
|
|||
record_field_access_chain()
|
||||
),
|
||||
move |arena, state, _, (record, accessors)| {
|
||||
// This is a record literal, not a destructure.
|
||||
let value = match record.update {
|
||||
Some(update) => Expr::RecordUpdate {
|
||||
update: &*arena.alloc(update),
|
||||
fields: record.fields,
|
||||
},
|
||||
None => Expr::Record(record.fields),
|
||||
let expr_result = match record.update {
|
||||
Some(update) => record_update_help(arena, update, record.fields),
|
||||
None => {
|
||||
let is_record_builder = record
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| field.value.is_apply_value());
|
||||
|
||||
if is_record_builder {
|
||||
record_builder_help(arena, record.fields)
|
||||
} else {
|
||||
let fields = record.fields.map_items(arena, |loc_field| {
|
||||
loc_field.map(|field| field.to_assigned_field(arena).unwrap())
|
||||
});
|
||||
|
||||
Ok(Expr::Record(fields))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let value = apply_expr_access_chain(arena, value, accessors);
|
||||
match expr_result {
|
||||
Ok(expr) => {
|
||||
let value = apply_expr_access_chain(arena, expr, accessors);
|
||||
|
||||
Ok((MadeProgress, value, state))
|
||||
Ok((MadeProgress, value, state))
|
||||
}
|
||||
Err(err) => Err((MadeProgress, err)),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn record_update_help<'a>(
|
||||
arena: &'a Bump,
|
||||
update: Loc<Expr<'a>>,
|
||||
fields: Collection<'a, Loc<RecordField<'a>>>,
|
||||
) -> Result<Expr<'a>, EExpr<'a>> {
|
||||
let result = fields.map_items_result(arena, |loc_field| {
|
||||
match loc_field.value.to_assigned_field(arena) {
|
||||
Ok(builder_field) => Ok(Loc {
|
||||
region: loc_field.region,
|
||||
value: builder_field,
|
||||
}),
|
||||
Err(FoundApplyValue) => Err(EExpr::RecordUpdateBuilder(loc_field.region)),
|
||||
}
|
||||
});
|
||||
|
||||
result.map(|fields| Expr::RecordUpdate {
|
||||
update: &*arena.alloc(update),
|
||||
fields,
|
||||
})
|
||||
}
|
||||
|
||||
fn record_builder_help<'a>(
|
||||
arena: &'a Bump,
|
||||
fields: Collection<'a, Loc<RecordField<'a>>>,
|
||||
) -> Result<Expr<'a>, EExpr<'a>> {
|
||||
let result = fields.map_items_result(arena, |loc_field| {
|
||||
match loc_field.value.to_builder_field(arena) {
|
||||
Ok(builder_field) => Ok(Loc {
|
||||
region: loc_field.region,
|
||||
value: builder_field,
|
||||
}),
|
||||
Err(FoundOptionalValue) => Err(EExpr::OptionalValueInRecordBuilder(loc_field.region)),
|
||||
}
|
||||
});
|
||||
|
||||
result.map(Expr::RecordBuilder)
|
||||
}
|
||||
|
||||
fn apply_expr_access_chain<'a>(
|
||||
arena: &'a Bump,
|
||||
value: Expr<'a>,
|
||||
|
|
|
@ -358,6 +358,8 @@ pub enum EExpr<'a> {
|
|||
|
||||
InParens(EInParens<'a>, Position),
|
||||
Record(ERecord<'a>, Position),
|
||||
OptionalValueInRecordBuilder(Region),
|
||||
RecordUpdateBuilder(Region),
|
||||
|
||||
// SingleQuote errors are folded into the EString
|
||||
Str(EString<'a>, Position),
|
||||
|
@ -410,6 +412,7 @@ pub enum ERecord<'a> {
|
|||
Field(Position),
|
||||
Colon(Position),
|
||||
QuestionMark(Position),
|
||||
Arrow(Position),
|
||||
Ampersand(Position),
|
||||
|
||||
// TODO remove
|
||||
|
@ -675,6 +678,7 @@ pub enum ETypeAbilityImpl<'a> {
|
|||
|
||||
Field(Position),
|
||||
Colon(Position),
|
||||
Arrow(Position),
|
||||
Optional(Position),
|
||||
Type(&'a EType<'a>, Position),
|
||||
|
||||
|
@ -695,6 +699,7 @@ impl<'a> From<ERecord<'a>> for ETypeAbilityImpl<'a> {
|
|||
ERecord::Open(p) => ETypeAbilityImpl::Open(p),
|
||||
ERecord::Field(p) => ETypeAbilityImpl::Field(p),
|
||||
ERecord::Colon(p) => ETypeAbilityImpl::Colon(p),
|
||||
ERecord::Arrow(p) => ETypeAbilityImpl::Arrow(p),
|
||||
ERecord::Space(s, p) => ETypeAbilityImpl::Space(s, p),
|
||||
ERecord::Updateable(p) => ETypeAbilityImpl::Updateable(p),
|
||||
ERecord::QuestionMark(p) => ETypeAbilityImpl::QuestionMark(p),
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use crate::ast::{
|
||||
AssignedField, CommentOrNewline, HasAbilities, HasAbility, HasClause, HasImpls, Pattern,
|
||||
AssignedField, CommentOrNewline, Expr, HasAbilities, HasAbility, HasClause, HasImpls, Pattern,
|
||||
Spaceable, Spaced, Tag, TypeAnnotation, TypeHeader,
|
||||
};
|
||||
use crate::blankspace::{
|
||||
space0_around_ee, space0_before_e, space0_before_optional_after, space0_e,
|
||||
};
|
||||
use crate::expr::record_value_field;
|
||||
use crate::expr::{record_field, FoundApplyValue};
|
||||
use crate::ident::{lowercase_ident, lowercase_ident_keyword_e};
|
||||
use crate::keyword;
|
||||
use crate::parser::{
|
||||
|
@ -537,7 +537,7 @@ fn parse_has_ability<'a>() -> impl Parser<'a, HasAbility<'a>, EType<'a>> {
|
|||
EType::TAbilityImpl,
|
||||
collection_trailing_sep_e!(
|
||||
word1(b'{', ETypeAbilityImpl::Open),
|
||||
specialize(|e: ERecord<'_>, _| e.into(), loc!(record_value_field())),
|
||||
specialize(|e: ERecord<'_>, _| e.into(), loc!(ability_impl_field())),
|
||||
word1(b',', ETypeAbilityImpl::End),
|
||||
word1(b'}', ETypeAbilityImpl::End),
|
||||
AssignedField::SpaceBefore
|
||||
|
@ -550,6 +550,15 @@ fn parse_has_ability<'a>() -> impl Parser<'a, HasAbility<'a>, EType<'a>> {
|
|||
}))
|
||||
}
|
||||
|
||||
fn ability_impl_field<'a>() -> impl Parser<'a, AssignedField<'a, Expr<'a>>, ERecord<'a>> {
|
||||
then(record_field(), move |arena, state, _, field| {
|
||||
match field.to_assigned_field(arena) {
|
||||
Ok(assigned_field) => Ok((MadeProgress, assigned_field, state)),
|
||||
Err(FoundApplyValue) => Err((MadeProgress, ERecord::Field(state.pos()))),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn expression<'a>(
|
||||
is_trailing_comma_valid: bool,
|
||||
stop_at_surface_has: bool,
|
||||
|
|
|
@ -361,6 +361,8 @@ impl Problem {
|
|||
| Problem::RuntimeError(RuntimeError::EmptySingleQuote(region))
|
||||
| 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)
|
||||
|
@ -588,6 +590,9 @@ pub enum RuntimeError {
|
|||
MultipleCharsInSingleQuote(Region),
|
||||
|
||||
DegenerateBranch(Region),
|
||||
|
||||
MultipleRecordBuilders(Region),
|
||||
UnappliedRecordBuilder(Region),
|
||||
}
|
||||
|
||||
impl RuntimeError {
|
||||
|
|
|
@ -1926,6 +1926,98 @@ mod test_fmt {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_builder() {
|
||||
expr_formats_same(indoc!(
|
||||
r#"
|
||||
{ a: 1, b <- get "b" |> batch, c <- get "c" |> batch, d }
|
||||
"#
|
||||
));
|
||||
|
||||
expr_formats_to(
|
||||
indoc!(
|
||||
r#"
|
||||
{ a: 1, b <- get "b" |> batch, c <- get "c" |> batch }
|
||||
"#
|
||||
),
|
||||
indoc!(
|
||||
r#"
|
||||
{ a: 1, b <- get "b" |> batch, c <- get "c" |> batch }
|
||||
"#
|
||||
),
|
||||
);
|
||||
|
||||
expr_formats_same(indoc!(
|
||||
r#"
|
||||
{
|
||||
a: 1,
|
||||
b <- get "b" |> batch,
|
||||
c <- get "c" |> batch,
|
||||
d,
|
||||
}
|
||||
"#
|
||||
));
|
||||
|
||||
expr_formats_to(
|
||||
indoc!(
|
||||
r#"
|
||||
{ a: 1, b <- get "b" |> batch,
|
||||
c <- get "c" |> batch, d }
|
||||
"#
|
||||
),
|
||||
indoc!(
|
||||
r#"
|
||||
{
|
||||
a: 1,
|
||||
b <- get "b" |> batch,
|
||||
c <- get "c" |> batch,
|
||||
d,
|
||||
}
|
||||
"#
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_record_builder_func_arg() {
|
||||
expr_formats_to(
|
||||
indoc!(
|
||||
r#"
|
||||
succeed { a: get "a" |> batch,
|
||||
b: get "b" |> batch,
|
||||
}
|
||||
"#
|
||||
),
|
||||
indoc!(
|
||||
r#"
|
||||
succeed {
|
||||
a: get "a" |> batch,
|
||||
b: get "b" |> batch,
|
||||
}
|
||||
"#
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_format_multiple_record_builders() {
|
||||
expr_formats_to(
|
||||
indoc!(
|
||||
r#"
|
||||
succeed { a <- get "a" }
|
||||
{ b <- get "b" }
|
||||
"#
|
||||
),
|
||||
indoc!(
|
||||
r#"
|
||||
succeed
|
||||
{ a <- get "a" }
|
||||
{ b <- get "b" }
|
||||
"#
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_comments_in_records() {
|
||||
expr_formats_same(indoc!(
|
||||
|
|
|
@ -3564,6 +3564,7 @@ pub enum Reason {
|
|||
FnCall {
|
||||
name: Option<Symbol>,
|
||||
arity: u8,
|
||||
called_via: CalledVia,
|
||||
},
|
||||
LowLevelOpArg {
|
||||
op: LowLevel,
|
||||
|
|
|
@ -2133,6 +2133,32 @@ fn pretty_runtime_error<'b>(
|
|||
|
||||
title = "DEGENERATE BRANCH";
|
||||
}
|
||||
RuntimeError::MultipleRecordBuilders(region) => {
|
||||
let tip = alloc
|
||||
.tip()
|
||||
.append(alloc.reflow("You can combine them or apply them separately."));
|
||||
|
||||
doc = alloc.stack([
|
||||
alloc.reflow("This function is applied to multiple record builders:"),
|
||||
alloc.region(lines.convert_region(region)),
|
||||
alloc.note("Functions can only take at most one record builder!"),
|
||||
tip,
|
||||
]);
|
||||
|
||||
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)
|
||||
|
|
|
@ -549,6 +549,46 @@ fn to_expr_report<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
EExpr::OptionalValueInRecordBuilder(region) => {
|
||||
let surroundings = Region::new(start, region.end());
|
||||
let region = lines.convert_region(*region);
|
||||
|
||||
let doc = alloc.stack([
|
||||
alloc.reflow(
|
||||
r"I am partway through parsing a record builder, and I found an optional field:",
|
||||
),
|
||||
alloc.region_with_subregion(lines.convert_region(surroundings), region),
|
||||
alloc.reflow("Optional fields can only appear when you destructure a record."),
|
||||
]);
|
||||
|
||||
Report {
|
||||
filename,
|
||||
doc,
|
||||
title: "BAD RECORD BUILDER".to_string(),
|
||||
severity: Severity::RuntimeError,
|
||||
}
|
||||
}
|
||||
|
||||
EExpr::RecordUpdateBuilder(region) => {
|
||||
let surroundings = Region::new(start, region.end());
|
||||
let region = lines.convert_region(*region);
|
||||
|
||||
let doc = alloc.stack([
|
||||
alloc.reflow(
|
||||
r"I am partway through parsing a record update, and I found a record builder field:",
|
||||
),
|
||||
alloc.region_with_subregion(lines.convert_region(surroundings), region),
|
||||
alloc.reflow("Record builders cannot be updated like records."),
|
||||
]);
|
||||
|
||||
Report {
|
||||
filename,
|
||||
doc,
|
||||
title: "BAD RECORD UPDATE".to_string(),
|
||||
severity: Severity::RuntimeError,
|
||||
}
|
||||
}
|
||||
|
||||
EExpr::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos),
|
||||
|
||||
&EExpr::Number(ENumber::End, pos) => {
|
||||
|
|
|
@ -1138,7 +1138,11 @@ fn to_expr_report<'b>(
|
|||
),
|
||||
}
|
||||
}
|
||||
Reason::FnCall { name, arity } => match describe_wanted_function(&found) {
|
||||
Reason::FnCall {
|
||||
name,
|
||||
arity,
|
||||
called_via,
|
||||
} => match describe_wanted_function(&found) {
|
||||
DescribedFunction::NotAFunction(tag) => {
|
||||
let this_value = match name {
|
||||
None => alloc.text("This value"),
|
||||
|
@ -1159,7 +1163,14 @@ fn to_expr_report<'b>(
|
|||
),
|
||||
]),
|
||||
alloc.region(lines.convert_region(expr_region)),
|
||||
alloc.reflow("I can't call an opaque type because I don't know what it is! Maybe you meant to unwrap it first?"),
|
||||
match called_via {
|
||||
CalledVia::RecordBuilder => {
|
||||
alloc.hint("Did you mean to apply it to a function first?")
|
||||
},
|
||||
_ => {
|
||||
alloc.reflow("I can't call an opaque type because I don't know what it is! Maybe you meant to unwrap it first?")
|
||||
}
|
||||
}
|
||||
]),
|
||||
Other => alloc.stack([
|
||||
alloc.concat([
|
||||
|
@ -1174,7 +1185,21 @@ fn to_expr_report<'b>(
|
|||
)),
|
||||
]),
|
||||
alloc.region(lines.convert_region(expr_region)),
|
||||
alloc.reflow("Are there any missing commas? Or missing parentheses?"),
|
||||
match called_via {
|
||||
CalledVia::RecordBuilder => {
|
||||
alloc.concat([
|
||||
alloc.tip(),
|
||||
alloc.reflow("Replace "),
|
||||
alloc.keyword("<-"),
|
||||
alloc.reflow(" with "),
|
||||
alloc.keyword(":"),
|
||||
alloc.reflow(" to assign the field directly.")
|
||||
])
|
||||
}
|
||||
_ => {
|
||||
alloc.reflow("Are there any missing commas? Or missing parentheses?")
|
||||
}
|
||||
}
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
@ -10177,6 +10177,166 @@ I recommend using camelCase. It's the standard style in Roc code!
|
|||
)
|
||||
);
|
||||
|
||||
// Record Builders
|
||||
|
||||
test_report!(
|
||||
optional_field_in_record_builder,
|
||||
indoc!(
|
||||
r#"
|
||||
{
|
||||
a <- apply "a",
|
||||
b,
|
||||
c ? "optional"
|
||||
}
|
||||
"#
|
||||
),
|
||||
@r###"
|
||||
── BAD RECORD BUILDER ────────── tmp/optional_field_in_record_builder/Test.roc ─
|
||||
|
||||
I am partway through parsing a record builder, and I found an optional
|
||||
field:
|
||||
|
||||
1│ app "test" provides [main] to "./platform"
|
||||
2│
|
||||
3│ main =
|
||||
4│ {
|
||||
5│ a <- apply "a",
|
||||
6│ b,
|
||||
7│ c ? "optional"
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Optional fields can only appear when you destructure a record.
|
||||
"###
|
||||
);
|
||||
|
||||
test_report!(
|
||||
record_update_builder,
|
||||
indoc!(
|
||||
r#"
|
||||
{ rec &
|
||||
a <- apply "a",
|
||||
b: 3
|
||||
}
|
||||
"#
|
||||
),
|
||||
@r###"
|
||||
── BAD RECORD UPDATE ────────────────────── tmp/record_update_builder/Test.roc ─
|
||||
|
||||
I am partway through parsing a record update, and I found a record
|
||||
builder field:
|
||||
|
||||
1│ app "test" provides [main] to "./platform"
|
||||
2│
|
||||
3│ main =
|
||||
4│ { rec &
|
||||
5│ a <- apply "a",
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Record builders cannot be updated like records.
|
||||
"###
|
||||
);
|
||||
|
||||
test_report!(
|
||||
multiple_record_builders,
|
||||
indoc!(
|
||||
r#"
|
||||
succeed
|
||||
{ a <- apply "a" }
|
||||
{ b <- apply "b" }
|
||||
"#
|
||||
),
|
||||
@r###"
|
||||
── MULTIPLE RECORD BUILDERS ────────────────────────────── /code/proj/Main.roc ─
|
||||
|
||||
This function is applied to multiple record builders:
|
||||
|
||||
4│> succeed
|
||||
5│> { a <- apply "a" }
|
||||
6│> { b <- apply "b" }
|
||||
|
||||
Note: Functions can only take at most one record builder!
|
||||
|
||||
Tip: You can combine them or apply them separately.
|
||||
|
||||
"###
|
||||
);
|
||||
|
||||
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!(
|
||||
record_builder_apply_non_function,
|
||||
indoc!(
|
||||
r#"
|
||||
succeed = \_ -> crash ""
|
||||
|
||||
succeed {
|
||||
a <- "a",
|
||||
}
|
||||
"#
|
||||
),
|
||||
@r###"
|
||||
── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─
|
||||
|
||||
This value is not a function, but it was given 1 argument:
|
||||
|
||||
7│ a <- "a",
|
||||
^^^
|
||||
|
||||
Tip: Replace `<-` with `:` to assign the field directly.
|
||||
"###
|
||||
);
|
||||
|
||||
// Skipping test because opaque types defined in the same module
|
||||
// do not fail with the special opaque type error
|
||||
//
|
||||
// test_report!(
|
||||
// record_builder_apply_opaque,
|
||||
// indoc!(
|
||||
// r#"
|
||||
// succeed = \_ -> crash ""
|
||||
|
||||
// Decode := {}
|
||||
|
||||
// get : Str -> Decode
|
||||
// get = \_ -> @Decode {}
|
||||
|
||||
// succeed {
|
||||
// a <- get "a",
|
||||
// # missing |> apply ^
|
||||
// }
|
||||
// "#
|
||||
// ),
|
||||
// @r###"
|
||||
// ── TOO MANY ARGS ───────────────────────────────────────── /code/proj/Main.roc ─
|
||||
|
||||
// This value is an opaque type, so it cannot be called with an argument:
|
||||
|
||||
// 12│ a <- get "a",
|
||||
// ^^^^^^^
|
||||
|
||||
// Hint: Did you mean to apply it to a function first?
|
||||
// "###
|
||||
// );
|
||||
|
||||
test_report!(
|
||||
destructure_assignment_introduces_no_variables_nested,
|
||||
indoc!(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue