mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-03 19:58:18 +00:00
Merge pull request #7002 from smores56/record-update-shorthand
Implement `&foo` record updater syntax sugar
This commit is contained in:
commit
d66f581eeb
22 changed files with 303 additions and 41 deletions
|
@ -3,12 +3,14 @@
|
|||
use crate::def::Def;
|
||||
use crate::expr::Expr::{self, *};
|
||||
use crate::expr::{
|
||||
ClosureData, DeclarationTag, Declarations, FunctionDef, OpaqueWrapFunctionData, WhenBranch,
|
||||
ClosureData, DeclarationTag, Declarations, FunctionDef, OpaqueWrapFunctionData,
|
||||
StructAccessorData, WhenBranch,
|
||||
};
|
||||
use crate::pattern::{Pattern, RecordDestruct, TupleDestruct};
|
||||
|
||||
use roc_module::symbol::{Interns, ModuleId, Symbol};
|
||||
|
||||
use roc_types::types::IndexOrField;
|
||||
use ven_pretty::{text, Arena, DocAllocator, DocBuilder};
|
||||
|
||||
pub struct Ctx<'a> {
|
||||
|
@ -381,7 +383,10 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a,
|
|||
OpaqueWrapFunction(OpaqueWrapFunctionData { opaque_name, .. }) => {
|
||||
text!(f, "@{}", opaque_name.as_str(c.interns))
|
||||
}
|
||||
RecordAccessor(_) => todo!(),
|
||||
RecordAccessor(StructAccessorData { field, .. }) => match field {
|
||||
IndexOrField::Index(index) => text!(f, ".{}", index),
|
||||
IndexOrField::Field(name) => text!(f, ".{}", name),
|
||||
},
|
||||
RecordUpdate {
|
||||
symbol, updates, ..
|
||||
} => f
|
||||
|
|
|
@ -476,6 +476,60 @@ pub fn desugar_expr<'a>(
|
|||
},
|
||||
})
|
||||
}
|
||||
RecordUpdater(field_name) => {
|
||||
let region = loc_expr.region;
|
||||
|
||||
let closure_body = RecordUpdate {
|
||||
update: arena.alloc(Loc {
|
||||
region,
|
||||
value: Expr::Var {
|
||||
module_name: "",
|
||||
ident: "#record_updater_record",
|
||||
},
|
||||
}),
|
||||
fields: Collection::with_items(
|
||||
Vec::from_iter_in(
|
||||
[Loc::at(
|
||||
region,
|
||||
AssignedField::RequiredValue(
|
||||
Loc::at(region, field_name),
|
||||
&[],
|
||||
&*arena.alloc(Loc {
|
||||
region,
|
||||
value: Expr::Var {
|
||||
module_name: "",
|
||||
ident: "#record_updater_field",
|
||||
},
|
||||
}),
|
||||
),
|
||||
)],
|
||||
arena,
|
||||
)
|
||||
.into_bump_slice(),
|
||||
),
|
||||
};
|
||||
|
||||
arena.alloc(Loc {
|
||||
region,
|
||||
value: Closure(
|
||||
arena.alloc_slice_copy(&[
|
||||
Loc::at(
|
||||
region,
|
||||
Pattern::Identifier {
|
||||
ident: "#record_updater_record",
|
||||
},
|
||||
),
|
||||
Loc::at(
|
||||
region,
|
||||
Pattern::Identifier {
|
||||
ident: "#record_updater_field",
|
||||
},
|
||||
),
|
||||
]),
|
||||
arena.alloc(Loc::at(region, closure_body)),
|
||||
),
|
||||
})
|
||||
}
|
||||
Closure(loc_patterns, loc_ret) => arena.alloc(Loc {
|
||||
region: loc_expr.region,
|
||||
value: Closure(
|
||||
|
|
|
@ -1020,6 +1020,9 @@ pub fn canonicalize_expr<'a>(
|
|||
ast::Expr::Backpassing(_, _, _) => {
|
||||
internal_error!("Backpassing should have been desugared by now")
|
||||
}
|
||||
ast::Expr::RecordUpdater(_) => {
|
||||
internal_error!("Record updater should have been desugared by now")
|
||||
}
|
||||
ast::Expr::Closure(loc_arg_patterns, loc_body_expr) => {
|
||||
let (closure_data, output) =
|
||||
canonicalize_closure(env, var_store, scope, loc_arg_patterns, loc_body_expr, None);
|
||||
|
@ -2465,6 +2468,7 @@ pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool {
|
|||
| ast::Expr::Num(_)
|
||||
| ast::Expr::NonBase10Int { .. }
|
||||
| ast::Expr::AccessorFunction(_)
|
||||
| ast::Expr::RecordUpdater(_)
|
||||
| ast::Expr::Crash
|
||||
| ast::Expr::Underscore(_)
|
||||
| ast::Expr::MalformedIdent(_, _)
|
||||
|
|
|
@ -39,6 +39,7 @@ impl<'a> Formattable for Expr<'a> {
|
|||
| NonBase10Int { .. }
|
||||
| SingleQuote(_)
|
||||
| AccessorFunction(_)
|
||||
| RecordUpdater(_)
|
||||
| Var { .. }
|
||||
| Underscore { .. }
|
||||
| MalformedIdent(_, _)
|
||||
|
@ -510,6 +511,11 @@ impl<'a> Formattable for Expr<'a> {
|
|||
Accessor::TupleIndex(key) => buf.push_str(key),
|
||||
}
|
||||
}
|
||||
RecordUpdater(key) => {
|
||||
buf.indent(indent);
|
||||
buf.push('&');
|
||||
buf.push_str(key);
|
||||
}
|
||||
RecordAccess(expr, key) => {
|
||||
expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent);
|
||||
buf.push('.');
|
||||
|
|
|
@ -432,6 +432,9 @@ pub enum Expr<'a> {
|
|||
/// e.g. `.foo` or `.0`
|
||||
AccessorFunction(Accessor<'a>),
|
||||
|
||||
/// Update the value of a field in a record, e.g. `&foo`
|
||||
RecordUpdater(&'a str),
|
||||
|
||||
/// Look up exactly one field on a tuple, e.g. `(x, y).1`.
|
||||
TupleAccess(&'a Expr<'a>, &'a str),
|
||||
|
||||
|
@ -636,6 +639,7 @@ pub fn is_expr_suffixed(expr: &Expr) -> bool {
|
|||
Expr::SingleQuote(_) => false,
|
||||
Expr::RecordAccess(a, _) => is_expr_suffixed(a),
|
||||
Expr::AccessorFunction(_) => false,
|
||||
Expr::RecordUpdater(_) => false,
|
||||
Expr::TupleAccess(a, _) => is_expr_suffixed(a),
|
||||
Expr::List(items) => items.iter().any(|x| is_expr_suffixed(&x.value)),
|
||||
Expr::RecordUpdate { update, fields } => {
|
||||
|
@ -1024,6 +1028,7 @@ impl<'a, 'b> RecursiveValueDefIter<'a, 'b> {
|
|||
| Str(_)
|
||||
| SingleQuote(_)
|
||||
| AccessorFunction(_)
|
||||
| RecordUpdater(_)
|
||||
| Var { .. }
|
||||
| Underscore(_)
|
||||
| Crash
|
||||
|
@ -2487,6 +2492,7 @@ impl<'a> Malformed for Expr<'a> {
|
|||
Num(_) |
|
||||
NonBase10Int { .. } |
|
||||
AccessorFunction(_) |
|
||||
RecordUpdater(_) |
|
||||
Var { .. } |
|
||||
Underscore(_) |
|
||||
Tag(_) |
|
||||
|
|
|
@ -2181,6 +2181,7 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<
|
|||
| Expr::SingleFieldRecordBuilder(_)
|
||||
| Expr::OptionalFieldInRecordBuilder(_, _)
|
||||
| Expr::RecordUpdate { .. }
|
||||
| Expr::RecordUpdater(_)
|
||||
| Expr::UnaryOp(_, _)
|
||||
| Expr::TrySuffix { .. }
|
||||
| Expr::Crash
|
||||
|
@ -3251,7 +3252,7 @@ pub fn join_alias_to_body<'a>(
|
|||
/// 2. The beginning of a function call (e.g. `foo bar baz`)
|
||||
/// 3. The beginning of a definition (e.g. `foo =`)
|
||||
/// 4. The beginning of a type annotation (e.g. `foo :`)
|
||||
/// 5. A reserved keyword (e.g. `if ` or `case `), meaning we should do something else.
|
||||
/// 5. A reserved keyword (e.g. `if ` or `when `), meaning we should do something else.
|
||||
|
||||
fn assign_or_destructure_identifier<'a>() -> impl Parser<'a, Ident<'a>, EExpr<'a>> {
|
||||
parse_ident
|
||||
|
@ -3313,6 +3314,7 @@ fn ident_to_expr<'a>(arena: &'a Bump, src: Ident<'a>) -> Expr<'a> {
|
|||
answer
|
||||
}
|
||||
Ident::AccessorFunction(string) => Expr::AccessorFunction(string),
|
||||
Ident::RecordUpdaterFunction(string) => Expr::RecordUpdater(string),
|
||||
Ident::Malformed(string, problem) => Expr::MalformedIdent(string, problem),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,42 +46,12 @@ pub enum Ident<'a> {
|
|||
},
|
||||
/// `.foo { foo: 42 }` or `.1 (1, 2, 3)`
|
||||
AccessorFunction(Accessor<'a>),
|
||||
/// `&foo { foo: 42 } 3`
|
||||
RecordUpdaterFunction(&'a str),
|
||||
/// .Foo or foo. or something like foo.Bar
|
||||
Malformed(&'a str, BadIdent),
|
||||
}
|
||||
|
||||
impl<'a> Ident<'a> {
|
||||
pub fn len(&self) -> usize {
|
||||
use self::Ident::*;
|
||||
|
||||
match self {
|
||||
Tag(string) | OpaqueRef(string) => string.len(),
|
||||
Access {
|
||||
module_name, parts, ..
|
||||
} => {
|
||||
let mut len = if module_name.is_empty() {
|
||||
0
|
||||
} else {
|
||||
module_name.len() + 1
|
||||
// +1 for the dot
|
||||
};
|
||||
|
||||
for part in parts.iter() {
|
||||
len += part.len() + 1 // +1 for the dot
|
||||
}
|
||||
|
||||
len - 1
|
||||
}
|
||||
AccessorFunction(string) => string.len(),
|
||||
Malformed(string, _) => string.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// This could be:
|
||||
///
|
||||
/// * A record field, e.g. "email" in `.email` or in `email:`
|
||||
|
@ -272,6 +242,7 @@ pub enum BadIdent {
|
|||
WeirdDotAccess(Position),
|
||||
WeirdDotQualified(Position),
|
||||
StrayDot(Position),
|
||||
StrayAmpersand(Position),
|
||||
BadOpaqueRef(Position),
|
||||
QualifiedTupleAccessor(Position),
|
||||
}
|
||||
|
@ -416,6 +387,18 @@ fn chomp_accessor(buffer: &[u8], pos: Position) -> Result<Accessor, BadIdent> {
|
|||
}
|
||||
}
|
||||
|
||||
/// a `&foo` record updater function
|
||||
fn chomp_record_updater(buffer: &[u8], pos: Position) -> Result<&str, BadIdent> {
|
||||
// assumes the leading `&` has been chomped already
|
||||
match chomp_lowercase_part(buffer) {
|
||||
Ok(name) => Ok(name),
|
||||
Err(_) => {
|
||||
// we've already made progress with the initial `&`
|
||||
Err(BadIdent::StrayAmpersand(pos.bump_column(1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// a `@Token` opaque
|
||||
fn chomp_opaque_ref(buffer: &[u8], pos: Position) -> Result<&str, BadIdent> {
|
||||
// assumes the leading `@` has NOT been chomped already
|
||||
|
@ -458,6 +441,14 @@ fn chomp_identifier_chain<'a>(
|
|||
}
|
||||
Err(fail) => return Err((1, fail)),
|
||||
},
|
||||
'&' => match chomp_record_updater(&buffer[1..], pos) {
|
||||
Ok(updater) => {
|
||||
let bytes_parsed = 1 + updater.len();
|
||||
return Ok((bytes_parsed as u32, Ident::RecordUpdaterFunction(updater)));
|
||||
}
|
||||
// return 0 bytes consumed on failure to allow parsing &&
|
||||
Err(fail) => return Err((0, fail)),
|
||||
},
|
||||
'@' => match chomp_opaque_ref(buffer, pos) {
|
||||
Ok(tagname) => {
|
||||
let bytes_parsed = tagname.len();
|
||||
|
|
|
@ -721,6 +721,7 @@ impl<'a> Normalize<'a> for Expr<'a> {
|
|||
Expr::Str(a) => Expr::Str(a.normalize(arena)),
|
||||
Expr::RecordAccess(a, b) => Expr::RecordAccess(arena.alloc(a.normalize(arena)), b),
|
||||
Expr::AccessorFunction(a) => Expr::AccessorFunction(a),
|
||||
Expr::RecordUpdater(a) => Expr::RecordUpdater(a),
|
||||
Expr::TupleAccess(a, b) => Expr::TupleAccess(arena.alloc(a.normalize(arena)), b),
|
||||
Expr::TrySuffix { expr: a, target } => Expr::TrySuffix {
|
||||
expr: arena.alloc(a.normalize(arena)),
|
||||
|
@ -840,6 +841,7 @@ fn remove_spaces_bad_ident(ident: BadIdent) -> BadIdent {
|
|||
BadIdent::WeirdDotAccess(_) => BadIdent::WeirdDotAccess(Position::zero()),
|
||||
BadIdent::WeirdDotQualified(_) => BadIdent::WeirdDotQualified(Position::zero()),
|
||||
BadIdent::StrayDot(_) => BadIdent::StrayDot(Position::zero()),
|
||||
BadIdent::StrayAmpersand(_) => BadIdent::StrayAmpersand(Position::zero()),
|
||||
BadIdent::BadOpaqueRef(_) => BadIdent::BadOpaqueRef(Position::zero()),
|
||||
BadIdent::QualifiedTupleAccessor(_) => BadIdent::QualifiedTupleAccessor(Position::zero()),
|
||||
}
|
||||
|
@ -1227,6 +1229,7 @@ impl<'a> Normalize<'a> for EPattern<'a> {
|
|||
EPattern::IndentEnd(_) => EPattern::IndentEnd(Position::zero()),
|
||||
EPattern::AsIndentStart(_) => EPattern::AsIndentStart(Position::zero()),
|
||||
EPattern::AccessorFunction(_) => EPattern::AccessorFunction(Position::zero()),
|
||||
EPattern::RecordUpdaterFunction(_) => EPattern::RecordUpdaterFunction(Position::zero()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -605,6 +605,7 @@ pub enum EPattern<'a> {
|
|||
AsIndentStart(Position),
|
||||
|
||||
AccessorFunction(Position),
|
||||
RecordUpdaterFunction(Position),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
|
@ -436,6 +436,10 @@ fn loc_ident_pattern_help<'a>(
|
|||
MadeProgress,
|
||||
EPattern::AccessorFunction(loc_ident.region.start()),
|
||||
)),
|
||||
Ident::RecordUpdaterFunction(_string) => Err((
|
||||
MadeProgress,
|
||||
EPattern::RecordUpdaterFunction(loc_ident.region.start()),
|
||||
)),
|
||||
Ident::Malformed(malformed, problem) => {
|
||||
debug_assert!(!malformed.is_empty());
|
||||
|
||||
|
|
|
@ -885,6 +885,38 @@ fn update_single_element_record() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
|
||||
fn update_record_shorthand() {
|
||||
assert_evals_to!(
|
||||
indoc!(
|
||||
r"
|
||||
rec = { foo: 42, bar: 2.46f64 }
|
||||
|
||||
rec |> &foo (rec.foo + 1)
|
||||
"
|
||||
),
|
||||
(2.46, 43),
|
||||
(f64, i64)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))]
|
||||
fn update_single_element_record_shorthand() {
|
||||
assert_evals_to!(
|
||||
indoc!(
|
||||
r"
|
||||
rec = { foo: 42}
|
||||
|
||||
&foo rec (rec.foo + 1)
|
||||
"
|
||||
),
|
||||
43,
|
||||
i64
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
|
||||
fn booleans_in_record() {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
data =
|
||||
{ x: 5, y: 0 }
|
||||
|> &y 3
|
||||
|
||||
data
|
|
@ -0,0 +1,86 @@
|
|||
SpaceAfter(
|
||||
Defs(
|
||||
Defs {
|
||||
tags: [
|
||||
Index(2147483648),
|
||||
],
|
||||
regions: [
|
||||
@0-42,
|
||||
],
|
||||
space_before: [
|
||||
Slice(start = 0, length = 0),
|
||||
],
|
||||
space_after: [
|
||||
Slice(start = 0, length = 0),
|
||||
],
|
||||
spaces: [],
|
||||
type_defs: [],
|
||||
value_defs: [
|
||||
Body(
|
||||
@0-4 Identifier {
|
||||
ident: "data",
|
||||
},
|
||||
@11-42 SpaceBefore(
|
||||
BinOps(
|
||||
[
|
||||
(
|
||||
@11-25 SpaceAfter(
|
||||
Record(
|
||||
[
|
||||
@13-17 RequiredValue(
|
||||
@13-14 "x",
|
||||
[],
|
||||
@16-17 Num(
|
||||
"5",
|
||||
),
|
||||
),
|
||||
@19-23 RequiredValue(
|
||||
@19-20 "y",
|
||||
[],
|
||||
@22-23 Num(
|
||||
"0",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
[
|
||||
Newline,
|
||||
],
|
||||
),
|
||||
@34-36 Pizza,
|
||||
),
|
||||
],
|
||||
@37-42 Apply(
|
||||
@37-39 RecordUpdater(
|
||||
"y",
|
||||
),
|
||||
[
|
||||
@41-42 Num(
|
||||
"3",
|
||||
),
|
||||
],
|
||||
Space,
|
||||
),
|
||||
),
|
||||
[
|
||||
Newline,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
@44-48 SpaceBefore(
|
||||
Var {
|
||||
module_name: "",
|
||||
ident: "data",
|
||||
},
|
||||
[
|
||||
Newline,
|
||||
Newline,
|
||||
],
|
||||
),
|
||||
),
|
||||
[
|
||||
Newline,
|
||||
],
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
data =
|
||||
{ x: 5, y: 0 }
|
||||
|> &y 3
|
||||
|
||||
data
|
|
@ -0,0 +1 @@
|
|||
foo &bar 5
|
|
@ -0,0 +1,20 @@
|
|||
SpaceAfter(
|
||||
Apply(
|
||||
@0-3 Var {
|
||||
module_name: "",
|
||||
ident: "foo",
|
||||
},
|
||||
[
|
||||
@3-7 RecordUpdater(
|
||||
"bar",
|
||||
),
|
||||
@9-10 Num(
|
||||
"5",
|
||||
),
|
||||
],
|
||||
Space,
|
||||
),
|
||||
[
|
||||
Newline,
|
||||
],
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
foo&bar 5
|
|
@ -455,6 +455,8 @@ mod test_snapshots {
|
|||
pass/record_func_type_decl.expr,
|
||||
pass/record_type_with_function.expr,
|
||||
pass/record_update.expr,
|
||||
pass/record_updater_literal_apply.expr,
|
||||
pass/record_updater_var_apply.expr,
|
||||
pass/record_with_if.expr,
|
||||
pass/requires_type.header,
|
||||
pass/separate_defs.moduledefs,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue