Support '??' syntax for optional record fields

This commit is contained in:
Anthony Bullard 2025-01-04 14:26:20 -06:00
parent 3d4dd5b583
commit a24fe430b4
No known key found for this signature in database
21 changed files with 42 additions and 43 deletions

View file

@ -871,7 +871,7 @@ mapWithIndexHelp = \src, dest, func, index, length ->
## All of these options are compatible with the others. For example, you can use `At` or `After` ## All of these options are compatible with the others. For example, you can use `At` or `After`
## with `start` regardless of what `end` and `step` are set to. ## with `start` regardless of what `end` and `step` are set to.
range : _ range : _
range = \{ start, end, step ? 0 } -> range = \{ start, end, step ?? 0 } ->
{ calcNext, stepIsPositive } = { calcNext, stepIsPositive } =
if step == 0 then if step == 0 then
when T start end is when T start end is

View file

@ -618,8 +618,8 @@ isGte : Num a, Num a -> Bool
## ##
## If either argument is [*NaN*](Num.isNaN), returns `Bool.false` no matter what. (*NaN* ## If either argument is [*NaN*](Num.isNaN), returns `Bool.false` no matter what. (*NaN*
## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).) ## is [defined to be unordered](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN).)
isApproxEq : Frac a, Frac a, { rtol ? Frac a, atol ? Frac a } -> Bool isApproxEq : Frac a, Frac a, { rtol ?? Frac a, atol ?? Frac a } -> Bool
isApproxEq = \x, y, { rtol ? 0.00001, atol ? 0.00000001 } -> isApproxEq = \x, y, { rtol ?? 0.00001, atol ?? 0.00000001 } ->
eq = x <= y && x >= y eq = x <= y && x >= y
meetsTolerance = Num.absDiff x y <= Num.max atol (rtol * Num.max (Num.abs x) (Num.abs y)) meetsTolerance = Num.absDiff x y <= Num.max atol (rtol * Num.max (Num.abs x) (Num.abs y))
eq || meetsTolerance eq || meetsTolerance

View file

@ -386,7 +386,7 @@ impl<'a> Nodify<'a> for AssignedField<'a, TypeAnnotation<'a>> {
assigned_field_value_to_node(n.into_bump_str(), arena, sp, &value.value, ":", flags) assigned_field_value_to_node(n.into_bump_str(), arena, sp, &value.value, ":", flags)
} }
AssignedField::OptionalValue(name, sp, value) => { AssignedField::OptionalValue(name, sp, value) => {
assigned_field_value_to_node(name.value, arena, sp, &value.value, "?", flags) assigned_field_value_to_node(name.value, arena, sp, &value.value, "??", flags)
} }
AssignedField::LabelOnly(name) => NodeInfo { AssignedField::LabelOnly(name) => NodeInfo {
before: &[], before: &[],
@ -512,7 +512,7 @@ fn format_assigned_field_help<T>(
buf.spaces(separator_spaces); buf.spaces(separator_spaces);
buf.indent(indent); buf.indent(indent);
buf.push('?'); buf.push_str("??");
buf.spaces(1); buf.spaces(1);
ann.value.format(buf, indent); ann.value.format(buf, indent);
} }

View file

@ -701,16 +701,8 @@ fn fmt_apply(
if !expr.before.is_empty() { if !expr.before.is_empty() {
format_spaces(buf, expr.before, Newlines::Yes, indent); format_spaces(buf, expr.before, Newlines::Yes, indent);
} }
expr.item.format_with_options( expr.item
buf, .format_with_options(buf, Parens::InApply, Newlines::Yes, indent);
if use_commas_and_parens {
Parens::NotNeeded
} else {
Parens::InApply
},
Newlines::Yes,
indent,
);
if use_commas_and_parens { if use_commas_and_parens {
buf.push('('); buf.push('(');

View file

@ -268,7 +268,7 @@ fn fmt_pattern_only(
Pattern::OptionalField(name, loc_pattern) => { Pattern::OptionalField(name, loc_pattern) => {
buf.indent(indent); buf.indent(indent);
snakify_camel_ident(buf, name); snakify_camel_ident(buf, name);
buf.push_str(" ?"); buf.push_str(" ??");
buf.spaces(1); buf.spaces(1);
loc_pattern.format(buf, indent); loc_pattern.format(buf, indent);
} }

View file

@ -4538,8 +4538,8 @@ mod test_reporting {
4 f : { foo bar } 4 f : { foo bar }
^ ^
I was expecting to see a colon, question mark, comma or closing curly I was expecting to see a colon, two question marks (??), comma or
brace. closing curly brace.
" "
); );

View file

@ -2082,6 +2082,7 @@ pub fn merge_spaces<'a>(
/// If the given Expr would parse the same way as a valid Pattern, convert it. /// If the given Expr would parse the same way as a valid Pattern, convert it.
/// Example: (foo) could be either an Expr::Var("foo") or Pattern::Identifier("foo") /// Example: (foo) could be either an Expr::Var("foo") or Pattern::Identifier("foo")
fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<'a>, ()> { fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<'a>, ()> {
println!("expr_to_pattern_help");
let mut expr = expr.extract_spaces(); let mut expr = expr.extract_spaces();
while let Expr::ParensAround(loc_expr) = &expr.item { while let Expr::ParensAround(loc_expr) = &expr.item {
@ -3587,7 +3588,10 @@ pub fn record_field<'a>() -> impl Parser<'a, RecordField<'a>, ERecord<'a>> {
optional(either( optional(either(
and(byte(b':', ERecord::Colon), record_field_expr()), and(byte(b':', ERecord::Colon), record_field_expr()),
and( and(
byte(b'?', ERecord::QuestionMark), and(
byte(b'?', ERecord::QuestionMark),
optional(byte(b'?', ERecord::QuestionMark)),
),
spaces_before(specialize_err_ref(ERecord::Expr, loc_expr(true))), spaces_before(specialize_err_ref(ERecord::Expr, loc_expr(true))),
), ),
)), )),

View file

@ -5,7 +5,7 @@ use crate::blankspace::{space0_before_optional_after, space0_e, spaces, spaces_b
use crate::ident::{lowercase_ident, parse_ident, Accessor, Ident}; use crate::ident::{lowercase_ident, parse_ident, Accessor, Ident};
use crate::keyword; use crate::keyword;
use crate::parser::{ use crate::parser::{
self, backtrackable, byte, collection_trailing_sep_e, fail_when, loc, map, map_with_arena, self, and, backtrackable, byte, collection_trailing_sep_e, fail_when, loc, map, map_with_arena,
optional, skip_first, skip_second, specialize_err, specialize_err_ref, then, three_bytes, optional, skip_first, skip_second, specialize_err, specialize_err_ref, then, three_bytes,
two_bytes, zero_or_more, EPattern, PInParens, PList, PRecord, Parser, two_bytes, zero_or_more, EPattern, PInParens, PList, PRecord, Parser,
}; };
@ -551,7 +551,10 @@ fn record_pattern_field<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PRecord<'a>>
// (This is true in both literals and types.) // (This is true in both literals and types.)
let (_, opt_loc_val, state) = optional(either( let (_, opt_loc_val, state) = optional(either(
byte(b':', PRecord::Colon), byte(b':', PRecord::Colon),
byte(b'?', PRecord::Optional), and(
byte(b'?', PRecord::Optional),
optional(byte(b'?', PRecord::Optional)),
),
)) ))
.parse(arena, state, min_indent)?; .parse(arena, state, min_indent)?;

View file

@ -588,7 +588,10 @@ fn record_type_field<'a>() -> impl Parser<'a, AssignedField<'a, TypeAnnotation<'
// (This is true in both literals and types.) // (This is true in both literals and types.)
let (_, opt_loc_val, state) = optional(either( let (_, opt_loc_val, state) = optional(either(
byte(b':', ETypeRecord::Colon), byte(b':', ETypeRecord::Colon),
byte(b'?', ETypeRecord::Optional), and(
byte(b'?', ETypeRecord::Optional),
optional(byte(b'?', ETypeRecord::Optional)),
),
)) ))
.parse(arena, state, min_indent)?; .parse(arena, state, min_indent)?;

View file

@ -102,10 +102,7 @@ fn round_trip_once(input: Input<'_>, options: Options) -> Option<String> {
let arena = Bump::new(); let arena = Bump::new();
let actual = match input.parse_in(&arena) { let actual = match input.parse_in(&arena) {
Ok(a) => { Ok(a) => a,
println!("actual {a:#?}");
a
}
Err(e) => { Err(e) => {
if options.minimize_initial_parse_error { if options.minimize_initial_parse_error {
return Some(format!("Initial parse failed: {:?}", e.normalize(&arena))); return Some(format!("Initial parse failed: {:?}", e.normalize(&arena)));

View file

@ -1,3 +1,3 @@
\I { p ? Y \I { p ?? Y
Y } [] -> Y } [] ->
K # ( K # (

View file

@ -1,5 +1,5 @@
O O
{ p ? if { p ?? if
a a
then then
A A

View file

@ -1 +1 @@
module { x, y ? 0 } -> [menu] module { x, y ?? 0 } -> [menu]

View file

@ -1,5 +1,5 @@
{ {
l? l??
""" """
""", """,
} }

View file

@ -1,2 +1,2 @@
{ i ? Y } = p { i ?? Y } = p
Q Q

View file

@ -1,5 +1,5 @@
0 : { 0 : {
i i
? d, ?? d,
} }
O O

View file

@ -1,4 +1,4 @@
{ e ? f { e ?? f
4 } = f 4 } = f
e e
r r

View file

@ -1,8 +1,8 @@
M M
{ s ? s { s ?? s
{ J & { J &
} } } }
{ s ? s { s ?? s
{ J & { J &
} } : p } } : p
y y

View file

@ -2497,13 +2497,13 @@ mod test_fmt {
expr_formats_to( expr_formats_to(
indoc!( indoc!(
r" r"
f : { a ?Str } f : { a ??Str }
f" f"
), ),
indoc!( indoc!(
r" r"
f : { a ? Str } f : { a ?? Str }
f" f"
), ),
@ -2513,7 +2513,7 @@ mod test_fmt {
indoc!( indoc!(
r" r"
f : { f : {
a ?Str, a ??Str,
} }
f" f"
@ -2521,7 +2521,7 @@ mod test_fmt {
indoc!( indoc!(
r" r"
f : { f : {
a ? Str, a ?? Str,
} }
f" f"
@ -2743,7 +2743,7 @@ mod test_fmt {
r" r"
f : f :
{ {
someField ? Int * # comment 1 someField ?? Int * # comment 1
, ,
# comment 2 # comment 2
} }
@ -2753,7 +2753,7 @@ mod test_fmt {
indoc!( indoc!(
r" r"
f : { f : {
some_field ? Int *, # comment 1 some_field ?? Int *, # comment 1
# comment 2 # comment 2
} }

View file

@ -2889,7 +2889,7 @@ fn to_trecord_report<'a>(
alloc.region_with_subregion(lines.convert_region(surroundings), region, severity), alloc.region_with_subregion(lines.convert_region(surroundings), region, severity),
alloc.concat([ alloc.concat([
alloc.reflow( alloc.reflow(
r"I was expecting to see a colon, question mark, comma or closing curly brace.", r"I was expecting to see a colon, two question marks (??), comma or closing curly brace.",
), ),
]), ]),
]); ]);