cedar_policy_core/entities/json/
err.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::fmt::Display;
18
19use super::SchemaType;
20use crate::ast::{
21    BorrowedRestrictedExpr, EntityAttrEvaluationError, EntityUID, Expr, ExprKind, PolicyID,
22    RestrictedExpr, RestrictedExpressionError, Type,
23};
24use crate::entities::conformance::err::EntitySchemaConformanceError;
25use crate::entities::{Name, ReservedNameError};
26use crate::parser::err::ParseErrors;
27use either::Either;
28use itertools::Itertools;
29use miette::Diagnostic;
30use smol_str::SmolStr;
31use thiserror::Error;
32
33/// Escape kind
34#[derive(Debug)]
35pub enum EscapeKind {
36    /// Escape `__entity`
37    Entity,
38    /// Escape `__extn`
39    Extension,
40}
41
42impl Display for EscapeKind {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Entity => write!(f, "__entity"),
46            Self::Extension => write!(f, "__extn"),
47        }
48    }
49}
50
51/// Errors thrown during deserialization from JSON
52//
53// CAUTION: this type is publicly exported in `cedar-policy`.
54// Don't make fields `pub`, don't make breaking changes, and use caution
55// when adding public methods.
56#[derive(Debug, Diagnostic, Error)]
57#[non_exhaustive]
58pub enum JsonDeserializationError {
59    /// Error thrown by the `serde_json` crate
60    #[error(transparent)]
61    #[diagnostic(transparent)]
62    Serde(#[from] JsonError),
63    /// Contents of an escape failed to parse.
64    #[error(transparent)]
65    #[diagnostic(transparent)]
66    ParseEscape(ParseEscape),
67    /// Restricted expression error
68    #[error(transparent)]
69    #[diagnostic(transparent)]
70    RestrictedExpressionError(#[from] RestrictedExpressionError),
71    /// A field that needs to be a literal entity reference, was some other JSON value
72    #[error(transparent)]
73    #[diagnostic(transparent)]
74    ExpectedLiteralEntityRef(ExpectedLiteralEntityRef),
75    /// A field that needs to be an extension value, was some other JSON value
76    #[error(transparent)]
77    #[diagnostic(transparent)]
78    ExpectedExtnValue(ExpectedExtnValue),
79    /// Parents of actions should be actions, but this action has a non-action parent
80    #[error(transparent)]
81    #[diagnostic(transparent)]
82    ActionParentIsNotAction(ActionParentIsNotAction),
83    /// Schema-based parsing needed an implicit extension constructor, but no suitable
84    /// constructor was found
85    #[error(transparent)]
86    #[diagnostic(transparent)]
87    MissingImpliedConstructor(MissingImpliedConstructor),
88    /// The same key appears two or more times in a single record
89    #[error(transparent)]
90    #[diagnostic(transparent)]
91    DuplicateKey(DuplicateKey),
92    /// Error when evaluating an entity attribute
93    #[error(transparent)]
94    #[diagnostic(transparent)]
95    EntityAttributeEvaluation(#[from] EntityAttrEvaluationError),
96    /// During schema-based parsing, encountered an entity which does not
97    /// conform to the schema.
98    ///
99    /// This error contains the `Entity` analogues some of the other errors
100    /// listed below, among other things.
101    #[error(transparent)]
102    #[diagnostic(transparent)]
103    EntitySchemaConformance(EntitySchemaConformanceError),
104    /// During schema-based parsing, encountered this attribute on a record, but
105    /// that attribute shouldn't exist on that record
106    #[error(transparent)]
107    #[diagnostic(transparent)]
108    UnexpectedRecordAttr(UnexpectedRecordAttr),
109    /// During schema-based parsing, didn't encounter this attribute of a
110    /// record, but that attribute should have existed
111    #[error(transparent)]
112    #[diagnostic(transparent)]
113    MissingRequiredRecordAttr(MissingRequiredRecordAttr),
114    /// During schema-based parsing, found a different type than the schema indicated.
115    ///
116    /// (This is used in all cases except inside entity attributes; type mismatches in
117    /// entity attributes are reported as `Self::EntitySchemaConformance`. As of
118    /// this writing, that means this should only be used for schema-based
119    /// parsing of the `Context`.)
120    #[error(transparent)]
121    #[diagnostic(transparent)]
122    TypeMismatch(TypeMismatch),
123    /// When trying to deserialize an AST with an error in it - this should fail
124    #[cfg(feature = "tolerant-ast")]
125    #[error("Unable to deserialize an AST Error node")]
126    #[diagnostic(help("AST error node indicates that the expression has failed to parse"))]
127    ASTErrorNode,
128    /// Raised when a JsonValue contains the no longer supported `__expr` escape
129    #[error("{0}, the `__expr` escape is no longer supported")]
130    #[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))]
131    ExprTag(Box<JsonDeserializationErrorContext>),
132    /// Raised when the input JSON contains a `null`
133    #[error("{0}, found a `null`; JSON `null`s are not allowed in Cedar")]
134    Null(Box<JsonDeserializationErrorContext>),
135    /// Returned when a name contains `__cedar`
136    #[error(transparent)]
137    #[diagnostic(transparent)]
138    ReservedName(#[from] ReservedNameError),
139    /// Never returned as of 4.2.0 (entity tags are now stable), but this error
140    /// variant not removed because that would be a breaking change on this
141    /// publicly-exported type.
142    #[deprecated(
143        since = "4.2.0",
144        note = "entity-tags is now stable and fully supported, so this error never occurs"
145    )]
146    #[error("entity tags are not supported in this build; to use entity tags, you must enable the `entity-tags` experimental feature")]
147    UnsupportedEntityTags,
148}
149
150impl JsonDeserializationError {
151    pub(crate) fn parse_escape(
152        kind: EscapeKind,
153        value: impl Into<String>,
154        errs: ParseErrors,
155    ) -> Self {
156        Self::ParseEscape(ParseEscape {
157            kind,
158            value: value.into(),
159            errs,
160        })
161    }
162
163    pub(crate) fn expected_entity_ref(
164        ctx: JsonDeserializationErrorContext,
165        got: Either<serde_json::Value, Expr>,
166    ) -> Self {
167        Self::ExpectedLiteralEntityRef(ExpectedLiteralEntityRef {
168            ctx: Box::new(ctx),
169            got: Box::new(got),
170        })
171    }
172
173    pub(crate) fn expected_extn_value(
174        ctx: JsonDeserializationErrorContext,
175        got: Either<serde_json::Value, Expr>,
176    ) -> Self {
177        Self::ExpectedExtnValue(ExpectedExtnValue {
178            ctx: Box::new(ctx),
179            got: Box::new(got),
180        })
181    }
182
183    pub(crate) fn action_parent_is_not_action(uid: EntityUID, parent: EntityUID) -> Self {
184        Self::ActionParentIsNotAction(ActionParentIsNotAction { uid, parent })
185    }
186
187    pub(crate) fn missing_implied_constructor(
188        ctx: JsonDeserializationErrorContext,
189        return_type: SchemaType,
190    ) -> Self {
191        Self::MissingImpliedConstructor(MissingImpliedConstructor {
192            ctx: Box::new(ctx),
193            return_type: Box::new(return_type),
194        })
195    }
196
197    pub(crate) fn duplicate_key(
198        ctx: JsonDeserializationErrorContext,
199        key: impl Into<SmolStr>,
200    ) -> Self {
201        Self::DuplicateKey(DuplicateKey {
202            ctx: Box::new(ctx),
203            key: key.into(),
204        })
205    }
206
207    pub(crate) fn unexpected_record_attr(
208        ctx: JsonDeserializationErrorContext,
209        record_attr: impl Into<SmolStr>,
210    ) -> Self {
211        Self::UnexpectedRecordAttr(UnexpectedRecordAttr {
212            ctx: Box::new(ctx),
213            record_attr: record_attr.into(),
214        })
215    }
216
217    pub(crate) fn missing_required_record_attr(
218        ctx: JsonDeserializationErrorContext,
219        record_attr: impl Into<SmolStr>,
220    ) -> Self {
221        Self::MissingRequiredRecordAttr(MissingRequiredRecordAttr {
222            ctx: Box::new(ctx),
223            record_attr: record_attr.into(),
224        })
225    }
226
227    pub(crate) fn type_mismatch(
228        ctx: JsonDeserializationErrorContext,
229        err: TypeMismatchError,
230    ) -> Self {
231        Self::TypeMismatch(TypeMismatch {
232            ctx: Box::new(ctx),
233            err,
234        })
235    }
236}
237
238#[derive(Debug, Error, Diagnostic)]
239#[error("{ctx}, {err}")]
240/// General error for type mismatches
241pub struct TypeMismatch {
242    /// Context of this error, which will be something other than `EntityAttribute`.
243    /// (Type mismatches in entity attributes are reported as
244    /// `Self::EntitySchemaConformance`.)
245    ctx: Box<JsonDeserializationErrorContext>,
246    /// Underlying error
247    #[diagnostic(transparent)]
248    err: TypeMismatchError,
249}
250
251#[derive(Debug, Error, Diagnostic)]
252#[error("{}, expected the record to have an attribute `{}`, but it does not", .ctx, .record_attr)]
253/// Error type for a record missing a required attr
254pub struct MissingRequiredRecordAttr {
255    /// Context of this error
256    ctx: Box<JsonDeserializationErrorContext>,
257    /// Name of the (Record) attribute which was expected
258    record_attr: SmolStr,
259}
260
261#[derive(Debug, Diagnostic, Error)]
262#[error("{}, record attribute `{}` should not exist according to the schema", .ctx, .record_attr)]
263/// Error type for record attributes that should not exist
264pub struct UnexpectedRecordAttr {
265    /// Context of this error
266    ctx: Box<JsonDeserializationErrorContext>,
267    /// Name of the (Record) attribute which was unexpected
268    record_attr: SmolStr,
269}
270
271#[derive(Debug, Error, Diagnostic)]
272#[error("{}, duplicate key `{}` in record", .ctx, .key)]
273/// Error type for records having duplicate keys
274pub struct DuplicateKey {
275    /// Context of this error
276    ctx: Box<JsonDeserializationErrorContext>,
277    /// The key that appeared two or more times
278    key: SmolStr,
279}
280
281#[derive(Debug, Error, Diagnostic)]
282#[error("{}, missing extension constructor for {}", .ctx, .return_type)]
283#[diagnostic(help("expected a value of type {} because of the schema", .return_type))]
284/// Error type for missing extension constructors
285pub struct MissingImpliedConstructor {
286    /// Context of this error
287    ctx: Box<JsonDeserializationErrorContext>,
288    /// return type of the constructor we were looking for
289    return_type: Box<SchemaType>,
290}
291
292#[derive(Debug, Error, Diagnostic)]
293#[error("action `{}` has a non-action parent `{}`", .uid, .parent)]
294#[diagnostic(help("parents of actions need to have type `Action` themselves, perhaps namespaced"))]
295/// Error type for action  parents not having type `Action`
296pub struct ActionParentIsNotAction {
297    /// Action entity that had the invalid parent
298    uid: EntityUID,
299    /// Parent that is invalid
300    parent: EntityUID,
301}
302
303#[derive(Debug, Error, Diagnostic)]
304#[error("failed to parse escape `{kind}`: {value}, errors: {errs}")]
305#[diagnostic(help("{}", match .kind {
306        EscapeKind::Entity => r#"an __entity escape should have a value like `{ "type": "SomeType", "id": "SomeId" }`"#,
307        EscapeKind::Extension => r#"an __extn escape should have a value like `{ "fn": "SomeFn", "arg": "SomeArg" }`"#,
308    }))]
309/// Error type for incorrect escaping
310pub struct ParseEscape {
311    /// Escape kind
312    kind: EscapeKind,
313    /// Escape value at fault
314    value: String,
315    /// Parse errors
316    #[diagnostic(transparent)]
317    errs: ParseErrors,
318}
319
320#[derive(Debug, Error, Diagnostic)]
321#[error("{}, expected a literal entity reference, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
322#[diagnostic(help(
323    r#"literal entity references can be made with `{{ "type": "SomeType", "id": "SomeId" }}`"#
324))]
325/// Error type for getting any expression other than an entity reference
326pub struct ExpectedLiteralEntityRef {
327    /// Context of this error
328    ctx: Box<JsonDeserializationErrorContext>,
329    /// the expression we got instead
330    got: Box<Either<serde_json::Value, Expr>>,
331}
332
333#[derive(Debug, Error, Diagnostic)]
334#[error("{}, expected an extension value, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
335#[diagnostic(help(r#"extension values can be made with `{{ "fn": "SomeFn", "id": "SomeId" }}`"#))]
336/// Error type for getting any expression other than en extesion value
337pub struct ExpectedExtnValue {
338    /// Context of this error
339    ctx: Box<JsonDeserializationErrorContext>,
340    /// the expression we got instead
341    got: Box<Either<serde_json::Value, Expr>>,
342}
343
344#[derive(Debug, Error, Diagnostic)]
345#[error(transparent)]
346/// Wrapper type for errors from `serde_json`
347pub struct JsonError(#[from] serde_json::Error);
348
349impl From<serde_json::Error> for JsonDeserializationError {
350    fn from(value: serde_json::Error) -> Self {
351        Self::Serde(JsonError(value))
352    }
353}
354
355impl From<serde_json::Error> for JsonSerializationError {
356    fn from(value: serde_json::Error) -> Self {
357        Self::Serde(JsonError(value))
358    }
359}
360
361/// Errors thrown during serialization to JSON
362#[derive(Debug, Diagnostic, Error)]
363#[non_exhaustive]
364pub enum JsonSerializationError {
365    /// Error thrown by `serde_json`
366    #[error(transparent)]
367    #[diagnostic(transparent)]
368    Serde(#[from] JsonError),
369    /// Extension-function calls with 0 arguments are not currently supported in
370    /// our JSON format.
371    #[error(transparent)]
372    #[diagnostic(transparent)]
373    ExtnCall0Arguments(ExtnCall0Arguments),
374    /// Extension-function calls with 2 or more arguments are not currently
375    /// supported in our JSON format.
376    #[error(transparent)]
377    #[diagnostic(transparent)]
378    ExtnCall2OrMoreArguments(ExtnCall2OrMoreArguments),
379    /// Encountered a `Record` which can't be serialized to JSON because it
380    /// contains a key which is reserved as a JSON escape.
381    #[error(transparent)]
382    #[diagnostic(transparent)]
383    ReservedKey(ReservedKey),
384    /// Encountered an `ExprKind` which we didn't expect. Either a case is
385    /// missing in `CedarValueJson::from_expr()`, or an internal invariant was
386    /// violated and there is a non-restricted expression in `RestrictedExpr`
387    #[error(transparent)]
388    #[diagnostic(transparent)]
389    UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind),
390    /// Encountered a (partial-evaluation) residual which can't be encoded in
391    /// JSON
392    #[error(transparent)]
393    #[diagnostic(transparent)]
394    Residual(Residual),
395}
396
397impl JsonSerializationError {
398    pub(crate) fn call_0_args(func: Name) -> Self {
399        Self::ExtnCall0Arguments(ExtnCall0Arguments { func })
400    }
401
402    pub(crate) fn call_2_or_more_args(func: Name) -> Self {
403        Self::ExtnCall2OrMoreArguments(ExtnCall2OrMoreArguments { func })
404    }
405
406    pub(crate) fn reserved_key(key: impl Into<SmolStr>) -> Self {
407        Self::ReservedKey(ReservedKey { key: key.into() })
408    }
409
410    pub(crate) fn unexpected_restricted_expr_kind(kind: ExprKind) -> Self {
411        Self::UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind { kind })
412    }
413
414    pub(crate) fn residual(residual: Expr) -> Self {
415        Self::Residual(Residual { residual })
416    }
417}
418
419/// Error type for extension functions called w/ 0 arguments
420#[derive(Debug, Error, Diagnostic)]
421#[error("unsupported call to `{}` with 0 arguments", .func)]
422#[diagnostic(help(
423    "extension function calls with 0 arguments are not currently supported in our JSON format"
424))]
425pub struct ExtnCall0Arguments {
426    /// Name of the function which was called with 0 arguments
427    func: Name,
428}
429
430/// Error type for extension functions called w/ 2+ arguments
431#[derive(Debug, Error, Diagnostic)]
432#[error("unsupported call to `{}` with 2 or more arguments", .func)]
433#[diagnostic(help("extension function calls with 2 or more arguments are not currently supported in our JSON format"))]
434pub struct ExtnCall2OrMoreArguments {
435    /// Name of the function called w/ 2 or more arguments
436    func: Name,
437}
438
439/// Error type for using a reserved key in a record
440#[derive(Debug, Error, Diagnostic)]
441#[error("record uses reserved key `{}`", .key)]
442pub struct ReservedKey {
443    /// The reserved key used
444    key: SmolStr,
445}
446
447impl ReservedKey {
448    /// The reserved keyword used as a key
449    pub fn key(&self) -> impl AsRef<str> + '_ {
450        &self.key
451    }
452}
453
454/// Error type for a restricted expression containing a non-restricted expression
455#[derive(Debug, Error, Diagnostic)]
456#[error("unexpected restricted expression `{:?}`", .kind)]
457pub struct UnexpectedRestrictedExprKind {
458    /// The [`ExprKind`] we didn't expend to find
459    kind: ExprKind,
460}
461
462/// Error type for residuals that can't be serialized
463#[derive(Debug, Error, Diagnostic)]
464#[error("cannot encode residual as JSON: {}", .residual)]
465pub struct Residual {
466    /// The residual that can't be serialized
467    residual: Expr,
468}
469
470/// Gives information about the context of a JSON deserialization error (e.g.,
471/// where we were in the JSON document).
472#[derive(Debug, Clone)]
473pub enum JsonDeserializationErrorContext {
474    /// The error occurred while deserializing the attribute `attr` of an entity.
475    EntityAttribute {
476        /// Entity where the error occurred
477        uid: EntityUID,
478        /// Attribute where the error occurred
479        attr: SmolStr,
480    },
481    /// The error occurred while deserializing the tag `tag` of an entity.
482    EntityTag {
483        /// Entity where the error occurred
484        uid: EntityUID,
485        /// Tag where the error occurred
486        tag: SmolStr,
487    },
488    /// The error occurred while deserializing the `parents` field of an entity.
489    EntityParents {
490        /// Entity where the error occurred
491        uid: EntityUID,
492    },
493    /// The error occurred while deserializing the `uid` field of an entity.
494    EntityUid,
495    /// The error occurred while deserializing the `Context`.
496    Context,
497    /// The error occurred while deserializing a policy in JSON (EST) form.
498    Policy {
499        /// ID of the policy we were deserializing
500        id: PolicyID,
501    },
502    /// The error occured while deserializing a template link
503    TemplateLink,
504    /// The context was unknown, this shouldn't surface to users
505    Unknown,
506}
507
508/// Type mismatch error (in terms of `SchemaType`)
509#[derive(Debug, Diagnostic, Error)]
510#[error("type mismatch: value was expected to have type {expected}, but it {mismatch_reason}: `{}`",
511    display_restricted_expr(.actual_val.as_borrowed()),
512)]
513pub struct TypeMismatchError {
514    /// Type which was expected
515    expected: Box<SchemaType>,
516    /// Reason for the type mismatch
517    mismatch_reason: TypeMismatchReason,
518    /// Value which doesn't have the expected type
519    actual_val: Box<RestrictedExpr>,
520}
521
522#[derive(Debug, Error)]
523enum TypeMismatchReason {
524    /// We saw this dynamic type which is not compatible with the expected
525    /// schema type.
526    #[error("actually has type {0}")]
527    UnexpectedType(Type),
528    /// We saw a record type expression as expected, but it contains an
529    /// attribute we didn't expect.
530    #[error("contains an unexpected attribute `{0}`")]
531    UnexpectedAttr(SmolStr),
532    /// We saw a record type expression as expected, but it did not contain an
533    /// attribute we expected.
534    #[error("is missing the required attribute `{0}`")]
535    MissingRequiredAtr(SmolStr),
536    /// No further detail available.
537    #[error("does not")]
538    None,
539}
540
541impl TypeMismatchError {
542    pub(crate) fn type_mismatch(
543        expected: SchemaType,
544        actual_ty: Option<Type>,
545        actual_val: RestrictedExpr,
546    ) -> Self {
547        Self {
548            expected: Box::new(expected),
549            mismatch_reason: match actual_ty {
550                Some(ty) => TypeMismatchReason::UnexpectedType(ty),
551                None => TypeMismatchReason::None,
552            },
553            actual_val: Box::new(actual_val),
554        }
555    }
556
557    pub(crate) fn unexpected_attr(
558        expected: SchemaType,
559        unexpected_attr: SmolStr,
560        actual_val: RestrictedExpr,
561    ) -> Self {
562        Self {
563            expected: Box::new(expected),
564            mismatch_reason: TypeMismatchReason::UnexpectedAttr(unexpected_attr),
565            actual_val: Box::new(actual_val),
566        }
567    }
568
569    pub(crate) fn missing_required_attr(
570        expected: SchemaType,
571        missing_attr: SmolStr,
572        actual_val: RestrictedExpr,
573    ) -> Self {
574        Self {
575            expected: Box::new(expected),
576            mismatch_reason: TypeMismatchReason::MissingRequiredAtr(missing_attr),
577            actual_val: Box::new(actual_val),
578        }
579    }
580}
581
582impl std::fmt::Display for JsonDeserializationErrorContext {
583    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584        match self {
585            Self::EntityAttribute { uid, attr } => write!(f, "in attribute `{attr}` on `{uid}`"),
586            Self::EntityTag { uid, tag } => write!(f, "in tag `{tag}` on `{uid}`"),
587            Self::EntityParents { uid } => write!(f, "in parents field of `{uid}`"),
588            Self::EntityUid => write!(f, "in uid field of <unknown entity>"),
589            Self::Context => write!(f, "while parsing context"),
590            Self::Policy { id } => write!(f, "while parsing JSON policy `{id}`"),
591            Self::TemplateLink => write!(f, "while parsing a template link"),
592            Self::Unknown => write!(f, "parsing context was unknown, please file a bug report at https://github.com/cedar-policy/cedar so we can improve this error message"),
593        }
594    }
595}
596
597fn display_json_value(v: &Either<serde_json::Value, Expr>) -> String {
598    match v {
599        Either::Left(json) => display_value(json),
600        Either::Right(e) => e.to_string(),
601    }
602}
603
604/// Display a `serde_json::Value`, but sorting object attributes, so that the
605/// output is deterministic (important for tests that check equality of error
606/// messages).
607///
608/// Note that this doesn't sort array elements, because JSON arrays are ordered,
609/// so all JSON-handling functions naturally preserve order for arrays and thus
610/// provide a deterministic output.
611fn display_value(v: &serde_json::Value) -> String {
612    match v {
613        serde_json::Value::Array(contents) => {
614            format!("[{}]", contents.iter().map(display_value).join(", "))
615        }
616        serde_json::Value::Object(map) => {
617            let mut v: Vec<_> = map.iter().collect();
618            // We sort the keys here so that our error messages are consistent and defined
619            v.sort_by_key(|p| p.0);
620            let display_kv = |kv: &(&String, &serde_json::Value)| format!("\"{}\":{}", kv.0, kv.1);
621            format!("{{{}}}", v.iter().map(display_kv).join(","))
622        }
623        other => other.to_string(),
624    }
625}
626
627/// Display a `RestrictedExpr`, but sorting record attributes and set elements,
628/// so that the output is deterministic (important for tests that check equality
629/// of error messages).
630fn display_restricted_expr(expr: BorrowedRestrictedExpr<'_>) -> String {
631    match expr.expr_kind() {
632        ExprKind::Set(elements) => {
633            let restricted_exprs = elements.iter().map(BorrowedRestrictedExpr::new_unchecked); // since the RestrictedExpr invariant holds for the input, it holds for all set elements
634            format!(
635                "[{}]",
636                restricted_exprs
637                    .map(display_restricted_expr)
638                    .sorted_unstable()
639                    .join(", ")
640            )
641        }
642        ExprKind::Record(m) => {
643            format!(
644                "{{{}}}",
645                m.iter()
646                    .sorted_unstable_by_key(|(k, _)| SmolStr::clone(k))
647                    .map(|(k, v)| format!("\"{}\": {}", k.escape_debug(), v))
648                    .join(", ")
649            )
650        }
651        _ => format!("{expr}"), // all other cases: use the normal Display
652    }
653}