Repository: cedar-policy/cedar
Pull request #1626: Make location info optional
Author: login: adpaco-aws
Created: 2025-05-16T18:24:27Z | Merged: 2025-05-16T21:51:22Z

## Description of changes

Makes location information optional throughout most of the code base. Note that location information was already optional in some places. This change mainly focuses on the CST's `Loc`, but requires propagating the change to other structures and function/method calls like those where we expected a `miette::SourceSpan`. Whenever we can, we make our best to compute spans like we do in other parts of the code base.

## Checklist for requesting a review

The change in this PR is (choose one, and delete the other options):

- [ ] A breaking change requiring a major version bump to `cedar-policy` (e.g., changes to the signature of an existing API).
- [ ] A backwards-compatible change requiring a minor version bump to `cedar-policy` (e.g., addition of a new API).
- [ ] A bug fix or other functionality change requiring a patch to `cedar-policy`.
- [x] A change "invisible" to users (e.g., documentation, changes to "internal" crates like `cedar-policy-core`, `cedar-validator`, etc.)
- [ ] A change (breaking or otherwise) that only impacts unreleased or experimental code.

I confirm that this PR (choose one, and delete the other options):

- [ ] Updates the "Unreleased" section of the CHANGELOG with a description of my change (required for major/minor version bumps).
- [x] Does not update the CHANGELOG because my change does not significantly impact released code.

I confirm that [`cedar-spec`](https://github.com/cedar-policy/cedar-spec) (choose one, and delete the other options):

- [x] Does not require updates because my change does not impact the Cedar formal model or DRT infrastructure.
- [ ] Requires updates, and I have made / will make these updates myself. (Please include in your description a timeline or link to the relevant PR in `cedar-spec`, and how you have tested that your updates are correct.)
- [ ] Requires updates, but I do not plan to make them in the near future. (Make sure that your changes are hidden behind a feature flag to mark them as experimental.)
- [ ] I'm not sure how my change impacts `cedar-spec`. (Post your PR anyways, and we'll discuss in the comments.)

I confirm that [`docs.cedarpolicy.com`](https://docs.cedarpolicy.com/) (choose one, and delete the other options):

- [x] Does not require updates because my change does not impact the Cedar language specification.
- [ ] Requires updates, and I have made / will make these updates myself. (Please include in your description a timeline or link to the relevant PR in [`cedar-docs`](https://github.com/cedar-policy/cedar-docs). PRs should be targeted at a `staging-X.Y` branch, not `main`.)
- [ ] I'm not sure how my change impacts the documentation. (Post your PR anyways, and we'll discuss in the comments.)



Diff:
diff --git a/cedar-policy-core/src/ast/expr.rs b/cedar-policy-core/src/ast/expr.rs
index f9a3672026..54e720064d 100644
--- a/cedar-policy-core/src/ast/expr.rs
+++ b/cedar-policy-core/src/ast/expr.rs
@@ -480,7 +480,7 @@ impl<T> Expr<T> {
             ExprKind::Error { .. } => builder
                 .error(ParseErrors::singleton(ToASTError::new(
                     ToASTErrorKind::ASTErrorNode,
-                    Loc::new(0..1, "AST_ERROR_NODE".into()),
+                    Some(Loc::new(0..1, "AST_ERROR_NODE".into())),
                 )))
                 .unwrap(),
         }
diff --git a/cedar-policy-core/src/ast/name.rs b/cedar-policy-core/src/ast/name.rs
index 3e2eb66ea5..b59507f05e 100644
--- a/cedar-policy-core/src/ast/name.rs
+++ b/cedar-policy-core/src/ast/name.rs
@@ -547,7 +547,7 @@ impl Name {
                         src: s.to_string(),
                         normalized_src,
                     },
-                    Loc::new(diff_byte, s.into()),
+                    Some(Loc::new(diff_byte, s.into())),
                 )))
             }
         }
@@ -570,13 +570,10 @@ impl From<ReservedNameError> for ParseError {
     fn from(value: ReservedNameError) -> Self {
         ParseError::ToAST(ToASTError::new(
             value.clone().into(),
-            match &value.0.loc {
-                Some(loc) => loc.clone(),
-                None => {
-                    let name_str = value.0.to_string();
-                    Loc::new(0..(name_str.len()), name_str.into())
-                }
-            },
+            value.0.loc.clone().or_else(|| {
+                let name_str = value.0.to_string();
+                Some(Loc::new(0..(name_str.len()), name_str.into()))
+            }),
         ))
     }
 }
diff --git a/cedar-policy-core/src/ast/policy.rs b/cedar-policy-core/src/ast/policy.rs
index 81a83cc8d1..a7e1bd9cc4 100644
--- a/cedar-policy-core/src/ast/policy.rs
+++ b/cedar-policy-core/src/ast/policy.rs
@@ -71,7 +71,7 @@ cfg_tolerant_ast! {
             <ExprWithErrsBuilder as ExprBuilder>::new()
                 .error(ParseErrors::singleton(ToASTError::new(
                     ToASTErrorKind::ASTErrorNode,
-                    Loc::new(0..1, "ASTErrorNode".into()),
+                    Some(Loc::new(0..1, "ASTErrorNode".into())),
                 )))
                 .unwrap(),
         )
@@ -2380,7 +2380,7 @@ mod test {
         let expected_error = <ExprWithErrsBuilder as ExprBuilder>::new()
             .error(ParseErrors::singleton(ToASTError::new(
                 ToASTErrorKind::ASTErrorNode,
-                Loc::new(0..1, "ASTErrorNode".into()),
+                Some(Loc::new(0..1, "ASTErrorNode".into())),
             )))
             .unwrap();
 
diff --git a/cedar-policy-core/src/ast/restricted_expr.rs b/cedar-policy-core/src/ast/restricted_expr.rs
index 01bad3e8e0..2d3d85aae3 100644
--- a/cedar-policy-core/src/ast/restricted_expr.rs
+++ b/cedar-policy-core/src/ast/restricted_expr.rs
@@ -761,7 +761,7 @@ mod test {
                         }
                         .into()
                     ),
-                    Loc::new(0..32, Arc::from(str))
+                    Some(Loc::new(0..32, Arc::from(str)))
                 )))
             )),
         )
diff --git a/cedar-policy-core/src/error_macros.rs b/cedar-policy-core/src/error_macros.rs
index a641edacb1..30bc489dc3 100644
--- a/cedar-policy-core/src/error_macros.rs
+++ b/cedar-policy-core/src/error_macros.rs
@@ -14,24 +14,6 @@
  * limitations under the License.
  */
 
-/// Macro which implements the `.labels()` and `.source_code()` methods of
-/// `miette::Diagnostic` by using the parameter `$i` which must be the name
-/// of a field of type `Loc`
-#[macro_export]
-macro_rules! impl_diagnostic_from_source_loc_field {
-    ( $i:ident ) => {
-        fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-            Some(&self.$i.src as &dyn miette::SourceCode)
-        }
-
-        fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
-            Some(Box::new(std::iter::once(miette::LabeledSpan::underline(
-                self.$i.span,
-            ))) as _)
-        }
-    };
-}
-
 /// Macro which implements the `.labels()` and `.source_code()` methods of
 /// `miette::Diagnostic` by using the parameter `$i` which must be the name
 /// of a field of type `Option<Loc>`
@@ -52,31 +34,6 @@ macro_rules! impl_diagnostic_from_source_loc_opt_field {
     };
 }
 
-/// Macro which implements the `.labels()` and `.source_code()` methods of
-/// `miette::Diagnostic` by using the parameters `$i` and `$j` which must be
-/// names of fields of type `Loc`.
-/// Both locations will be underlined. It is assumed they have the same `src`.
-#[macro_export]
-macro_rules! impl_diagnostic_from_two_source_loc_fields {
-    ( $i:ident, $j:ident ) => {
-        fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-            // use the `src` from the first location and assume it is the same
-            // as the `src` from the second location
-            Some(&self.$i.src as &dyn miette::SourceCode)
-        }
-
-        fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
-            Some(Box::new(
-                [
-                    miette::LabeledSpan::underline(self.$i.span),
-                    miette::LabeledSpan::underline(self.$j.span),
-                ]
-                .into_iter(),
-            ) as _)
-        }
-    };
-}
-
 /// Macro which implements the `.labels()` and `.source_code()` methods of
 /// `miette::Diagnostic` by using the parameters `$i` and `$j` which must be the
 /// names of fields of type `Option<Loc>`.
diff --git a/cedar-policy-core/src/est.rs b/cedar-policy-core/src/est.rs
index c513692cfc..52be57aad8 100644
--- a/cedar-policy-core/src/est.rs
+++ b/cedar-policy-core/src/est.rs
@@ -172,7 +172,7 @@ impl TryFrom<cst::Policy> for Policy {
                 return Err(ParseErrors::singleton(ToASTError::new(
                     ToASTErrorKind::CSTErrorNode,
                     // Since we don't have a loc when doing this transformation, we create an arbitrary one
-                    Loc::new(0..1, "CSTErrorNode".into()),
+                    Some(Loc::new(0..1, "CSTErrorNode".into())),
                 )));
             }
         };
@@ -181,7 +181,7 @@ impl TryFrom<cst::Policy> for Policy {
         let maybe_annotations = policy.get_ast_annotations(|v, l| {
             Some(Annotation {
                 val: v?,
-                loc: Some(l.clone()),
+                loc: l.cloned(),
             })
         });
         let maybe_conditions = ParseErrors::transpose(policy.conds.into_iter().map(|node| {
diff --git a/cedar-policy-core/src/from_normalized_str.rs b/cedar-policy-core/src/from_normalized_str.rs
index 2cb654d900..76bd229f1f 100644
--- a/cedar-policy-core/src/from_normalized_str.rs
+++ b/cedar-policy-core/src/from_normalized_str.rs
@@ -56,7 +56,7 @@ pub trait FromNormalizedStr: FromStr<Err = ParseErrors> + Display {
                     src: s.to_string(),
                     normalized_src,
                 },
-                Loc::new(diff_byte, s.into()),
+                Some(Loc::new(diff_byte, s.into())),
             ))))
         }
     }
diff --git a/cedar-policy-core/src/parser.rs b/cedar-policy-core/src/parser.rs
index 75b0739f97..42f5784eec 100644
--- a/cedar-policy-core/src/parser.rs
+++ b/cedar-policy-core/src/parser.rs
@@ -73,8 +73,14 @@ pub fn parse_policyset_and_also_return_policy_text(
     // `PolicyId` present as a key in this map.
     let texts = cst
         .with_generated_policyids()
-        .expect("shouldn't be None since parse_policies() and to_policyset() didn't return Err")
-        .map(|(id, policy)| (id, &text[policy.loc.start()..policy.loc.end()]))
+        .expect("shouldn't be `None` since `parse_policies` and `to_policyset` didn't return `Err`")
+        .map(|(id, policy)| {
+            let loc = policy
+                .loc
+                .as_ref()
+                .expect("shouldn't be `None` since we parse with locations");
+            (id, &text[loc.start()..loc.end()])
+        })
         .collect::<HashMap<ast::PolicyID, &str>>();
     Ok((texts, pset))
 }
diff --git a/cedar-policy-core/src/parser/cst_to_ast.rs b/cedar-policy-core/src/parser/cst_to_ast.rs
index 7a9a39438d..9dd8761ddd 100644
--- a/cedar-policy-core/src/parser/cst_to_ast.rs
+++ b/cedar-policy-core/src/parser/cst_to_ast.rs
@@ -62,6 +62,8 @@ use std::sync::Arc;
 mod to_ref_or_refs;
 use to_ref_or_refs::OneOrMultipleRefs;
 
+const INVALID_SNIPPET: &str = "<invalid>";
+
 /// Type alias for convenience
 type Result<T> = std::result::Result<T, ParseErrors>;
 
@@ -265,7 +267,7 @@ impl Node<Option<cst::Policy>> {
             // The source parsed as a template, but not a static policy
             Ok(Err(ast::UnexpectedSlotError::FoundSlot(slot))) => Err(ToASTError::new(
                 ToASTErrorKind::expected_static_policy(slot.clone()),
-                slot.loc.unwrap_or_else(|| self.loc.clone()),
+                slot.loc.or_else(|| self.loc.clone()),
             )
             .into()),
             // The source failed to parse completely. If the parse errors include
@@ -277,7 +279,7 @@ impl Node<Option<cst::Policy>> {
                         ParseError::ToAST(err) => match err.kind() {
                             ToASTErrorKind::SlotsInConditionClause(inner) => Some(ToASTError::new(
                                 ToASTErrorKind::expected_static_policy(inner.slot.clone()),
-                                err.source_loc().clone(),
+                                err.source_loc().cloned(),
                             )),
                             _ => None,
                         },
@@ -312,7 +314,7 @@ impl Node<Option<cst::Policy>> {
 
         // convert annotations
         let maybe_annotations = policy.get_ast_annotations(|value, loc| {
-            ast::Annotation::with_optional_value(value, Some(loc.clone()))
+            ast::Annotation::with_optional_value(value, loc.cloned())
         });
 
         // convert scope
@@ -328,7 +330,7 @@ impl Node<Option<cst::Policy>> {
                         slot.clone(),
                         if is_when { "when" } else { "unless" },
                     ),
-                    slot.loc.unwrap_or_else(|| c.loc.clone()),
+                    slot.loc.or_else(|| c.loc.clone()),
                 )
                 .into()
             });
@@ -348,7 +350,7 @@ impl Node<Option<cst::Policy>> {
             action,
             resource,
             conds,
-            &self.loc,
+            self.loc.as_ref(),
         ))
     }
 
@@ -366,7 +368,7 @@ impl Node<Option<cst::Policy>> {
             // The source parsed as a template, but not a static policy
             Ok(Err(ast::UnexpectedSlotError::FoundSlot(slot))) => Err(ToASTError::new(
                 ToASTErrorKind::expected_static_policy(slot.clone()),
-                slot.loc.unwrap_or_else(|| self.loc.clone()),
+                slot.loc.or_else(|| self.loc.clone()),
             )
             .into()),
             // The source failed to parse completely. If the parse errors include
@@ -378,7 +380,7 @@ impl Node<Option<cst::Policy>> {
                         ParseError::ToAST(err) => match err.kind() {
                             ToASTErrorKind::SlotsInConditionClause(inner) => Some(ToASTError::new(
                                 ToASTErrorKind::expected_static_policy(inner.slot.clone()),
-                                err.source_loc().clone(),
+                                err.source_loc().cloned(),
                             )),
                             _ => None,
                         },
@@ -402,7 +404,7 @@ impl Node<Option<cst::Policy>> {
         let policy = match policy {
             cst::Policy::Policy(policy_impl) => policy_impl,
             cst::Policy::PolicyError => {
-                return Ok(ast::Template::error(id, Some(self.loc.clone())));
+                return Ok(ast::Template::error(id, self.loc.clone()));
             }
         };
         // convert effect
@@ -410,7 +412,7 @@ impl Node<Option<cst::Policy>> {
 
         // convert annotations
         let maybe_annotations = policy.get_ast_annotations(|value, loc| {
-            ast::Annotation::with_optional_value(value, Some(loc.clone()))
+            ast::Annotation::with_optional_value(value, loc.cloned())
         });
 
         // convert scope
@@ -425,7 +427,7 @@ impl Node<Option<cst::Policy>> {
                         slot.clone(),
                         if is_when { "when" } else { "unless" },
                     ),
-                    slot.loc.unwrap_or_else(|| c.loc.clone()),
+                    slot.loc.or_else(|| c.loc.clone()),
                 )
                 .into()
             });
@@ -445,7 +447,7 @@ impl Node<Option<cst::Policy>> {
             action,
             resource,
             conds,
-            &self.loc,
+            self.loc.as_ref(),
         ))
     }
 }
@@ -457,35 +459,50 @@ impl cst::PolicyImpl {
     ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
         // Tracks where the last variable in the scope ended. We'll point to
         // this position to indicate where to fill in vars if we're missing one.
-        let mut end_of_last_var = self.effect.loc.end();
+        let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
 
         let mut vars = self.variables.iter();
         let maybe_principal = if let Some(scope1) = vars.next() {
-            end_of_last_var = scope1.loc.end();
+            end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
             scope1.to_principal_constraint(TolerantAstSetting::NotTolerant)
         } else {
+            let effect_span = self
+                .effect
+                .loc
+                .as_ref()
+                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
             Err(ToASTError::new(
                 ToASTErrorKind::MissingScopeVariable(ast::Var::Principal),
-                self.effect.loc.span(end_of_last_var),
+                effect_span,
             )
             .into())
         };
         let maybe_action = if let Some(scope2) = vars.next() {
-            end_of_last_var = scope2.loc.end();
+            end_of_last_var = scope2.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
             scope2.to_action_constraint(TolerantAstSetting::NotTolerant)
         } else {
+            let effect_span = self
+                .effect
+                .loc
+                .as_ref()
+                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
             Err(ToASTError::new(
                 ToASTErrorKind::MissingScopeVariable(ast::Var::Action),
-                self.effect.loc.span(end_of_last_var),
+                effect_span,
             )
             .into())
         };
         let maybe_resource = if let Some(scope3) = vars.next() {
             scope3.to_resource_constraint(TolerantAstSetting::NotTolerant)
         } else {
+            let effect_span = self
+                .effect
+                .loc
+                .as_ref()
+                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
             Err(ToASTError::new(
                 ToASTErrorKind::MissingScopeVariable(ast::Var::Resource),
-                self.effect.loc.span(end_of_last_var),
+                effect_span,
             )
             .into())
         };
@@ -523,35 +540,50 @@ impl cst::PolicyImpl {
     ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
         // Tracks where the last variable in the scope ended. We'll point to
         // this position to indicate where to fill in vars if we're missing one.
-        let mut end_of_last_var = self.effect.loc.end();
+        let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
 
         let mut vars = self.variables.iter();
         let maybe_principal = if let Some(scope1) = vars.next() {
-            end_of_last_var = scope1.loc.end();
+            end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
             scope1.to_principal_constraint(TolerantAstSetting::Tolerant)
         } else {
+            let effect_span = self
+                .effect
+                .loc
+                .as_ref()
+                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
             Err(ToASTError::new(
                 ToASTErrorKind::MissingScopeVariable(ast::Var::Principal),
-                self.effect.loc.span(end_of_last_var),
+                effect_span,
             )
             .into())
         };
         let maybe_action = if let Some(scope2) = vars.next() {
-            end_of_last_var = scope2.loc.end();
+            end_of_last_var = scope2.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
             scope2.to_action_constraint(TolerantAstSetting::Tolerant)
         } else {
+            let effect_span = self
+                .effect
+                .loc
+                .as_ref()
+                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
             Err(ToASTError::new(
                 ToASTErrorKind::MissingScopeVariable(ast::Var::Action),
-                self.effect.loc.span(end_of_last_var),
+                effect_span,
             )
             .into())
         };
         let maybe_resource = if let Some(scope3) = vars.next() {
             scope3.to_resource_constraint(TolerantAstSetting::Tolerant)
         } else {
+            let effect_span = self
+                .effect
+                .loc
+                .as_ref()
+                .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
             Err(ToASTError::new(
                 ToASTErrorKind::MissingScopeVariable(ast::Var::Resource),
-                self.effect.loc.span(end_of_last_var),
+                effect_span,
             )
             .into())
         };
@@ -585,7 +617,7 @@ impl cst::PolicyImpl {
     /// Get annotations from the `cst::Policy`
     pub fn get_ast_annotations<T>(
         &self,
-        annotation_constructor: impl Fn(Option<SmolStr>, &Loc) -> T,
+        annotation_constructor: impl Fn(Option<SmolStr>, Option<&Loc>) -> T,
     ) -> Result<BTreeMap<ast::AnyId, T>> {
         let mut annotations = BTreeMap::new();
         let mut all_errs: Vec<ParseErrors> = vec![];
@@ -625,7 +657,7 @@ impl Node<Option<cst::Annotation>> {
     /// for the strings and does unescaping
     pub fn to_kv_pair<T>(
         &self,
-        annotation_constructor: impl Fn(Option<SmolStr>, &Loc) -> T,
+        annotation_constructor: impl Fn(Option<SmolStr>, Option<&Loc>) -> T,
     ) -> Result<(ast::AnyId, T)> {
         let anno = self.try_as_inner()?;
 
@@ -645,7 +677,7 @@ impl Node<Option<cst::Annotation>> {
             .transpose();
 
         let (k, v) = flatten_tuple_2(maybe_key, maybe_value)?;
-        Ok((k, annotation_constructor(v, &self.loc)))
+        Ok((k, annotation_constructor(v, self.loc.as_ref())))
     }
 }
 
@@ -741,9 +773,9 @@ impl ast::UnreservedId {
         &self,
         e: Build::Expr,
         args: Vec<Build::Expr>,
-        loc: &Loc,
+        loc: Option<&Loc>,
     ) -> Result<Build::Expr> {
-        let builder = Build::new().with_source_loc(loc);
+        let builder = Build::new().with_maybe_source_loc(loc);
         match self.as_ref() {
             "contains" => extract_single_argument(args.into_iter(), "contains", loc)
                 .map(|arg| builder.contains(e, arg)),
@@ -771,7 +803,7 @@ impl ast::UnreservedId {
                     if EXTENSION_STYLES.functions.contains(&unqual_name) {
                         Err(ToASTError::new(
                             ToASTErrorKind::MethodCallOnFunction(unqual_name.basename()),
-                            loc.clone(),
+                            loc.cloned(),
                         )
                         .into())
                     } else {
@@ -796,10 +828,10 @@ impl ast::UnreservedId {
                                     id: self.clone(),
                                     hint,
                                 },
-                                loc.clone(),
+                                loc.cloned(),
                             )
                             .into(),
-                            Some(loc),
+                            loc,
                         )
                     }
                 }
@@ -813,12 +845,12 @@ impl ast::UnreservedId {
 fn extract_single_argument<T>(
     args: impl ExactSizeIterator<Item = T>,
     fn_name: &'static str,
-    loc: &Loc,
+    loc: Option<&Loc>,
 ) -> Result<T> {
     args.exactly_one().map_err(|args| {
         ParseErrors::singleton(ToASTError::new(
             ToASTErrorKind::wrong_arity(fn_name, 1, args.len()),
-            loc.clone(),
+            loc.cloned(),
         ))
     })
 }
@@ -827,13 +859,13 @@ fn extract_single_argument<T>(
 fn require_zero_arguments<T>(
     args: &impl ExactSizeIterator<Item = T>,
     fn_name: &'static str,
-    loc: &Loc,
+    loc: Option<&Loc>,
 ) -> Result<()> {
     match args.len() {
         0 => Ok(()),
         n => Err(ParseErrors::singleton(ToASTError::new(
             ToASTErrorKind::wrong_arity(fn_name, 0, n),
-            loc.clone(),
+            loc.cloned(),
         ))),
     }
 }
@@ -920,7 +952,11 @@ impl Node<Option<cst::VariableDef>> {
                         Ok(et) => Ok(PrincipalOrResourceConstraint::IsIn(Arc::new(et), eref)),
                         Err(eos) => Err(eos.to_ast_err(ToASTErrorKind::InvalidIsType {
                             lhs: var.to_string(),
-                            rhs: eos.loc().snippet().unwrap_or("<invalid>").to_string(),
+                            rhs: eos
+                                .loc()
+                                .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
+                                .unwrap_or(INVALID_SNIPPET)
+                                .to_string(),
                         })),
                     }
                 }
@@ -937,7 +973,11 @@ impl Node<Option<cst::VariableDef>> {
                 Ok(et) => Ok(PrincipalOrResourceConstraint::Is(Arc::new(et))),
                 Err(eos) => Err(eos.to_ast_err(ToASTErrorKind::InvalidIsType {
                     lhs: var.to_string(),
-                    rhs: eos.loc().snippet().unwrap_or("<invalid>").to_string(),
+                    rhs: eos
+                        .loc()
+                        .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
+                        .unwrap_or(INVALID_SNIPPET)
+                        .to_string(),
                 })),
             }
         } else {
@@ -1067,7 +1107,7 @@ impl Node<Option<cst::Cond>> {
                 convert_expr_error_to_parse_error::<Build>(
                     self.to_ast_err(ToASTErrorKind::EmptyClause(Some(ident)))
                         .into(),
-                    Some(&self.loc),
+                    self.loc.as_ref(),
                 )
             }
         };
@@ -1076,7 +1116,10 @@ impl Node<Option<cst::Cond>> {
             if is_when {
                 (e, true)
             } else {
-                (Build::new().with_source_loc(&self.loc).not(e), false)
+                (
+                    Build::new().with_maybe_source_loc(self.loc.as_ref()).not(e),
+                    false,
+                )
             }
         })
     }
@@ -1128,57 +1171,59 @@ fn convert_expr_error_to_parse_error<Build: ExprBuilder>(
 #[derive(Debug)]
 pub(crate) enum ExprOrSpecial<'a, Expr> {
     /// Any expression except a variable, name, string literal, or boolean literal
-    Expr { expr: Expr, loc: Loc },
+    Expr { expr: Expr, loc: Option<Loc> },
     /// Variables, which act as expressions or names
-    Var { var: ast::Var, loc: Loc },
+    Var { var: ast::Var, loc: Option<Loc> },
     /// Name that isn't an expr and couldn't be converted to var
-    Name { name: ast::Name, loc: Loc },
+    Name { name: ast::Name, loc: Option<Loc> },
     /// String literal, not yet unescaped
     /// Must be processed with to_unescaped_string or to_pattern before inclusion in the AST
-    StrLit { lit: &'a SmolStr, loc: Loc },
+    StrLit { lit: &'a SmolStr, loc: Option<Loc> },
     /// A boolean literal
-    BoolLit { val: bool, loc: Loc },
+    BoolLit { val: bool, loc: Option<Loc> },
 }
 
 impl<Expr> ExprOrSpecial<'_, Expr>
 where
     Expr: std::fmt::Display,
 {
-    fn loc(&self) -> &Loc {
+    fn loc(&self) -> Option<&Loc> {
         match self {
-            Self::Expr { loc, .. } => loc,
-            Self::Var { loc, .. } => loc,
-            Self::Name { loc, .. } => loc,
-            Self::StrLit { loc, .. } => loc,
-            Self::BoolLit { loc, .. } => loc,
+            Self::Expr { loc, .. } => loc.as_ref(),
+            Self::Var { loc, .. } => loc.as_ref(),
+            Self::Name { loc, .. } => loc.as_ref(),
+            Self::StrLit { loc, .. } => loc.as_ref(),
+            Self::BoolLit { loc, .. } => loc.as_ref(),
         }
     }
 
     fn to_ast_err(&self, kind: impl Into<ToASTErrorKind>) -> ToASTError {
-        ToASTError::new(kind.into(), self.loc().clone())
+        ToASTError::new(kind.into(), self.loc().cloned())
     }
 
     fn into_expr<Build: ExprBuilder<Expr = Expr>>(self) -> Result<Expr> {
         match self {
             Self::Expr { expr, .. } => Ok(expr),
-            Self::Var { var, loc } => Ok(Build::new().with_source_loc(&loc).var(var)),
+            Self::Var { var, loc } => Ok(Build::new().with_maybe_source_loc(loc.as_ref()).var(var)),
             Self::Name { name, loc } => convert_expr_error_to_parse_error::<Build>(
                 ToASTError::new(
                     ToASTErrorKind::ArbitraryVariable(name.to_string().into()),
                     loc.clone(),
                 )
                 .into(),
-                Some(&loc),
+                loc.as_ref(),
             ),
             Self::StrLit { lit, loc } => {
                 match to_unescaped_string(lit) {
-                    Ok(s) => Ok(Build::new().with_source_loc(&loc).val(s)),
+                    Ok(s) => Ok(Build::new().with_maybe_source_loc(loc.as_ref()).val(s)),
                     Err(escape_errs) => Err(ParseErrors::new_from_nonempty(escape_errs.map(|e| {
                         ToASTError::new(ToASTErrorKind::Unescape(e), loc.clone()).into()
                     }))),
                 }
             }
-            Self::BoolLit { val, loc } => Ok(Build::new().with_source_loc(&loc).val(val)),
+            Self::BoolLit { val, loc } => {
+                Ok(Build::new().with_maybe_source_loc(loc.as_ref()).val(val))
+            }
         }
     }
 
@@ -1285,7 +1330,7 @@ impl Node<Option<cst::Expr>> {
             cst::Expr::ErrorExpr => {
                 let e = ToASTError::new(ToASTErrorKind::CSTErrorNode, self.loc.clone());
                 return Ok(ExprOrSpecial::Expr {
-                    expr: convert_expr_error_to_parse_error::<Build>(e.into(), Some(&self.loc))?,
+                    expr: convert_expr_error_to_parse_error::<Build>(e.into(), self.loc.as_ref())?,
                     loc: self.loc.clone(),
                 });
             }
@@ -1300,7 +1345,9 @@ impl Node<Option<cst::Expr>> {
 
                 let (i, t, e) = flatten_tuple_3(maybe_guard, maybe_then, maybe_else)?;
                 Ok(ExprOrSpecial::Expr {
-                    expr: Build::new().with_source_loc(&self.loc).ite(i, t, e),
+                    expr: Build::new()
+                        .with_maybe_source_loc(self.loc.as_ref())
+                        .ite(i, t, e),
                     loc: self.loc.clone(),
                 })
             }
@@ -1322,7 +1369,9 @@ impl Node<Option<cst::Or>> {
             Ok(first)
         } else {
             first.into_expr::<Build>().map(|first| ExprOrSpecial::Expr {
-                expr: Build::new().with_source_loc(&self.loc).or_nary(first, rest),
+                expr: Build::new()
+                    .with_maybe_source_loc(self.loc.as_ref())
+                    .or_nary(first, rest),
                 loc: self.loc.clone(),
             })
         }
@@ -1347,7 +1396,7 @@ impl Node<Option<cst::And>> {
         } else {
             first.into_expr::<Build>().map(|first| ExprOrSpecial::Expr {
                 expr: Build::new()
-                    .with_source_loc(&self.loc)
+                    .with_maybe_source_loc(self.loc.as_ref())
                     .and_nary(first, rest),
                 loc: self.loc.clone(),
             })
@@ -1396,7 +1445,7 @@ impl Node<Option<cst::Relation>> {
                 });
                 let (target, field) = flatten_tuple_2(maybe_target, maybe_field)?;
                 Ok(ExprOrSpecial::Expr {
-                    expr: construct_exprs_extended_has::<Build>(target, &field, &self.loc),
+                    expr: construct_exprs_extended_has::<Build>(target, &field, self.loc.as_ref()),
                     loc: self.loc.clone(),
                 })
             }
@@ -1406,7 +1455,7 @@ impl Node<Option<cst::Relation>> {
                 let (target, pattern) = flatten_tuple_2(maybe_target, maybe_pattern)?;
                 Ok(ExprOrSpecial::Expr {
                     expr: Build::new()
-                        .with_source_loc(&self.loc)
+                        .with_maybe_source_loc(self.loc.as_ref())
                         .like(target, pattern.into()),
                     loc: self.loc.clone(),
                 })
@@ -1426,7 +1475,11 @@ impl Node<Option<cst::Relation>> {
                                 .as_ref()
                                 .map(|expr| expr.to_string())
                                 .unwrap_or_else(|_| "..".to_string()),
-                            rhs: eos.loc().snippet().unwrap_or("<invalid>").to_string(),
+                            rhs: eos
+                                .loc()
+                                .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
+                                .unwrap_or(INVALID_SNIPPET)
+                                .to_string(),
                         })
                         .into()
                     });
@@ -1436,13 +1489,15 @@ impl Node<Option<cst::Relation>> {
                         let in_expr = in_entity.to_expr::<Build>()?;
                         Ok(ExprOrSpecial::Expr {
                             expr: Build::new()
-                                .with_source_loc(&self.loc)
+                                .with_maybe_source_loc(self.loc.as_ref())
                                 .is_in_entity_type(t, n, in_expr),
                             loc: self.loc.clone(),
                         })
                     }
                     None => Ok(ExprOrSpecial::Expr {
-                        expr: Build::new().with_source_loc(&self.loc).is_entity_type(t, n),
+                        expr: Build::new()
+                            .with_maybe_source_loc(self.loc.as_ref())
+                            .is_entity_type(t, n),
                         loc: self.loc.clone(),
                     }),
                 }
@@ -1576,7 +1631,7 @@ impl Node<Option<cst::Add>> {
             let first = first.into_expr::<Build>()?;
             Ok(ExprOrSpecial::Expr {
                 expr: Build::new()
-                    .with_source_loc(&self.loc)
+                    .with_maybe_source_loc(self.loc.as_ref())
                     .add_nary(first, rest),
                 loc: self.loc.clone(),
             })
@@ -1610,7 +1665,7 @@ impl Node<Option<cst::Mult>> {
             let first = first.into_expr::<Build>()?;
             Ok(ExprOrSpecial::Expr {
                 expr: Build::new()
-                    .with_source_loc(&self.loc)
+                    .with_maybe_source_loc(self.loc.as_ref())
                     .mul_nary(first, rest),
                 loc: self.loc.clone(),
             })
@@ -1634,7 +1689,9 @@ impl Node<Option<cst::Unary>> {
                     inner
                         .and_then(|e| e.into_expr::<Build>())
                         .map(|expr| ExprOrSpecial::Expr {
-                            expr: Build::new().with_source_loc(&self.loc).not(expr),
+                            expr: Build::new()
+                                .with_maybe_source_loc(self.loc.as_ref())
+                                .not(expr),
                             loc: self.loc.clone(),
                         })
                 })
@@ -1649,12 +1706,14 @@ impl Node<Option<cst::Unary>> {
                 let (last, rc) = if let Some(cst::Literal::Num(n)) = unary.item.to_lit() {
                     match n.cmp(&(i64::MAX as u64 + 1)) {
                         Ordering::Equal => (
-                            Ok(Build::new().with_source_loc(&unary.item.loc).val(i64::MIN)),
+                            Ok(Build::new()
+                                .with_maybe_source_loc(unary.item.loc.as_ref())
+                                .val(i64::MIN)),
                             c - 1,
                         ),
                         Ordering::Less => (
                             Ok(Build::new()
-                                .with_source_loc(&unary.item.loc)
+                                .with_maybe_source_loc(unary.item.loc.as_ref())
                                 .val(-(*n as i64))),
                             c - 1,
                         ),
@@ -1679,7 +1738,7 @@ impl Node<Option<cst::Unary>> {
                 // Fold the expression into a series of negation operations.
                 (0..rc)
                     .fold(last, |r, _| {
-                        r.map(|e| Build::new().with_source_loc(&self.loc).neg(e))
+                        r.map(|e| Build::new().with_maybe_source_loc(self.loc.as_ref()).neg(e))
                     })
                     .map(|expr| ExprOrSpecial::Expr {
                         expr,
@@ -1748,7 +1807,7 @@ impl Node<Option<cst::Member>> {
                 let args = std::mem::take(args);
                 // move the id out of the slice as well, to avoid cloning the internal string
                 let id = mem::replace(id, ast::UnreservedId::empty());
-                Ok((id.to_meth::<Build>(head, args, &self.loc)?, rest))
+                Ok((id.to_meth::<Build>(head, args, self.loc.as_ref())?, rest))
             }
 
             // field of arbitrary expr like `(principal.foo).bar`
@@ -1756,7 +1815,7 @@ impl Node<Option<cst::Member>> {
                 let id = mem::replace(id, ast::UnreservedId::empty());
                 Ok((
                     Build::new()
-                        .with_source_loc(&self.loc)
+                        .with_maybe_source_loc(self.loc.as_ref())
                         .get_attr(head, id.into_smolstr()),
                     rest,
                 ))
@@ -1766,7 +1825,9 @@ impl Node<Option<cst::Member>> {
             (Index(i), rest) => {
                 let i = mem::take(i);
                 Ok((
-                    Build::new().with_source_loc(&self.loc).get_attr(head, i),
+                    Build::new()
+                        .with_maybe_source_loc(self.loc.as_ref())
+                        .get_attr(head, i),
                     rest,
                 ))
             }
@@ -1822,9 +1883,11 @@ impl Node<Option<cst::Member>> {
                     let id = mem::replace(id, ast::UnreservedId::empty());
                     (
                         id.to_meth::<Build>(
-                            Build::new().with_source_loc(&var_loc).var(var),
+                            Build::new()
+                                .with_maybe_source_loc(var_loc.as_ref())
+                                .var(var),
                             args,
-                            &self.loc,
+                            self.loc.as_ref(),
                         )?,
                         rest,
                     )
@@ -1834,10 +1897,14 @@ impl Node<Option<cst::Member>> {
                 (Var { var, loc: var_loc }, [Field(i), rest @ ..]) => {
                     let id = mem::replace(i, ast::UnreservedId::empty());
                     (
-                        Build::new().with_source_loc(&self.loc).get_attr(
-                            Build::new().with_source_loc(&var_loc).var(var),
-                            id.into_smolstr(),
-                        ),
+                        Build::new()
+                            .with_maybe_source_loc(self.loc.as_ref())
+                            .get_attr(
+                                Build::new()
+                                    .with_maybe_source_loc(var_loc.as_ref())
+                                    .var(var),
+                                id.into_smolstr(),
+                            ),
                         rest,
                     )
                 }
@@ -1865,8 +1932,13 @@ impl Node<Option<cst::Member>> {
                     let i = mem::take(i);
                     (
                         Build::new()
-                            .with_source_loc(&self.loc)
-                            .get_attr(Build::new().with_source_loc(&var_loc).var(var), i),
+                            .with_maybe_source_loc(self.loc.as_ref())
+                            .get_attr(
+                                Build::new()
+                                    .with_maybe_source_loc(var_loc.as_ref())
+                                    .var(var),
+                                i,
+                            ),
                         rest,
                     )
                 }
@@ -1954,14 +2026,16 @@ impl Node<Option<cst::Primary>> {
             cst::Primary::EList(es) => {
                 let maybe_list = ParseErrors::transpose(es.iter().map(|e| e.to_expr::<Build>()));
                 maybe_list.map(|list| ExprOrSpecial::Expr {
-                    expr: Build::new().with_source_loc(&self.loc).set(list),
+                    expr: Build::new()
+                        .with_maybe_source_loc(self.loc.as_ref())
+                        .set(list),
                     loc: self.loc.clone(),
                 })
             }
             cst::Primary::RInits(is) => {
                 let rec = ParseErrors::transpose(is.iter().map(|i| i.to_init::<Build>()))?;
                 let expr = Build::new()
-                    .with_source_loc(&self.loc)
+                    .with_maybe_source_loc(self.loc.as_ref())
                     .record(rec)
                     .map_err(|e| {
                         Into::<ParseErrors>::into(ToASTError::new(e.into(), self.loc.clone()))
@@ -1990,7 +2064,9 @@ impl Node<Option<cst::Primary>> {
 impl Node<Option<cst::Slot>> {
     fn into_expr<Build: ExprBuilder>(self) -> Result<Build::Expr> {
         match self.try_as_inner()?.try_into() {
-            Ok(slot_id) => Ok(Build::new().with_source_loc(&self.loc).slot(slot_id)),
+            Ok(slot_id) => Ok(Build::new()
+                .with_maybe_source_loc(self.loc.as_ref())
+                .slot(slot_id)),
             Err(e) => Err(self.to_ast_err(e).into()),
         }
     }
@@ -2022,7 +2098,9 @@ impl Node<Option<cst::Name>> {
     fn to_type_constraint<Build: ExprBuilder>(&self) -> Result<Build::Expr> {
         match self.as_inner() {
             Some(_) => Err(self.to_ast_err(ToASTErrorKind::TypeConstraints).into()),
-            None => Ok(Build::new().with_source_loc(&self.loc).val(true)),
+            None => Ok(Build::new()
+                .with_maybe_source_loc(self.loc.as_ref())
+                .val(true)),
         }
     }
 
@@ -2079,7 +2157,7 @@ pub(crate) fn is_known_extension_func_str(s: &SmolStr) -> bool {
 
 impl ast::Name {
     /// Convert the `Name` into a `String` attribute, which fails if it had any namespaces
-    fn into_valid_attr(self, loc: Loc) -> Result<SmolStr> {
+    fn into_valid_attr(self, loc: Option<Loc>) -> Result<SmolStr> {
         if !self.0.path.is_empty() {
             Err(ToASTError::new(ToASTErrorKind::PathAsAttribute(self.to_string()), loc).into())
         } else {
@@ -2090,7 +2168,7 @@ impl ast::Name {
     fn into_func<Build: ExprBuilder>(
         self,
         args: Vec<Build::Expr>,
-        loc: Loc,
+        loc: Option<Loc>,
     ) -> Result<Build::Expr> {
         // error on standard methods
         if self.0.path.is_empty() {
@@ -2110,7 +2188,7 @@ impl ast::Name {
         }
         if EXTENSION_STYLES.functions.contains(&self) {
             Ok(Build::new()
-                .with_source_loc(&loc)
+                .with_maybe_source_loc(loc.as_ref())
                 .call_extension_fn(self, args))
         } else {
             fn suggest_function(name: &ast::Name, funs: &HashSet<&ast::Name>) -> Option<String> {
@@ -2149,7 +2227,7 @@ impl Node<Option<cst::Ref>> {
                 let (p, e) = flatten_tuple_2(maybe_path, maybe_eid)?;
                 Ok({
                     let loc = self.loc.clone();
-                    ast::EntityUID::from_components(p, ast::Eid::new(e), Some(loc))
+                    ast::EntityUID::from_components(p, ast::Eid::new(e), loc)
                 })
             }
             r @ cst::Ref::Ref { .. } => Err(self
@@ -2158,8 +2236,11 @@ impl Node<Option<cst::Ref>> {
         }
     }
     fn to_expr<Build: ExprBuilder>(&self) -> Result<Build::Expr> {
-        self.to_ref()
-            .map(|euid| Build::new().with_source_loc(&self.loc).val(euid))
+        self.to_ref().map(|euid| {
+            Build::new()
+                .with_maybe_source_loc(self.loc.as_ref())
+                .val(euid)
+        })
     }
 }
 
@@ -2178,7 +2259,7 @@ impl Node<Option<cst::Literal>> {
             }),
             cst::Literal::Num(n) => match Integer::try_from(*n) {
                 Ok(i) => Ok(ExprOrSpecial::Expr {
-                    expr: Build::new().with_source_loc(&self.loc).val(i),
+                    expr: Build::new().with_maybe_source_loc(self.loc.as_ref()).val(i),
                     loc: self.loc.clone(),
                 }),
                 Err(_) => Err(self
@@ -2218,12 +2299,12 @@ fn construct_template_policy(
     action: ast::ActionConstraint,
     resource: ast::ResourceConstraint,
     conds: Vec<ast::Expr>,
-    loc: &Loc,
+    loc: Option<&Loc>,
 ) -> ast::Template {
     let construct_template = |non_scope_constraint| {
         ast::Template::new(
             id,
-            Some(loc.clone()),
+            loc.cloned(),
             annotations,
             effect,
             principal,
@@ -2238,12 +2319,12 @@ fn construct_template_policy(
         // e.g., [c1, c2, c3,] --> ((c1 && c2) && c3)
         construct_template(
             ast::ExprBuilder::new()
-                .with_source_loc(loc)
+                .with_maybe_source_loc(loc)
                 .and_nary(first_expr, conds_iter),
         )
     } else {
         // use `true` to mark the absence of non-scope constraints
-        construct_template(ast::ExprBuilder::new().with_source_loc(loc).val(true))
+        construct_template(ast::ExprBuilder::new().with_maybe_source_loc(loc).val(true))
     }
 }
 fn construct_string_from_var(v: ast::Var) -> SmolStr {
@@ -2254,11 +2335,11 @@ fn construct_string_from_var(v: ast::Var) -> SmolStr {
         ast::Var::Context => "context".into(),
     }
 }
-fn construct_name(path: Vec<ast::Id>, id: ast::Id, loc: Loc) -> ast::InternalName {
+fn construct_name(path: Vec<ast::Id>, id: ast::Id, loc: Option<Loc>) -> ast::InternalName {
     ast::InternalName {
         id,
         path: Arc::new(path),
-        loc: Some(loc),
+        loc,
     }
 }
 
@@ -2266,9 +2347,9 @@ fn construct_expr_rel<Build: ExprBuilder>(
     f: Build::Expr,
     rel: cst::RelOp,
     s: Build::Expr,
-    loc: Loc,
+    loc: Option<Loc>,
 ) -> Result<Build::Expr> {
-    let builder = Build::new().with_source_loc(&loc);
+    let builder = Build::new().with_maybe_source_loc(loc.as_ref());
     match rel {
         cst::RelOp::Less => Ok(builder.less(f, s)),
         cst::RelOp::LessEq => Ok(builder.lesseq(f, s)),
@@ -2286,14 +2367,14 @@ fn construct_expr_rel<Build: ExprBuilder>(
 fn construct_exprs_extended_has<Build: ExprBuilder>(
     t: Build::Expr,
     attrs: &NonEmpty<SmolStr>,
-    loc: &Loc,
+    loc: Option<&Loc>,
 ) -> Build::Expr {
     let (first, rest) = attrs.split_first();
     let has_expr = Build::new()
-        .with_source_loc(loc)
+        .with_maybe_source_loc(loc)
         .has_attr(t.clone(), first.to_owned());
     let get_expr = Build::new()
-        .with_source_loc(loc)
+        .with_maybe_source_loc(loc)
         .get_attr(t, first.to_owned());
     // Foldl on the attribute list
     // It produces the following for `principal has contactInfo.address.zip`
@@ -2319,14 +2400,14 @@ fn construct_exprs_extended_has<Build: ExprBuilder>(
     rest.iter()
         .fold((has_expr, get_expr), |(has_expr, get_expr), attr| {
             (
-                Build::new().with_source_loc(loc).and(
+                Build::new().with_maybe_source_loc(loc).and(
                     has_expr,
                     Build::new()
-                        .with_source_loc(loc)
+                        .with_maybe_source_loc(loc)
                         .has_attr(get_expr.clone(), attr.to_owned()),
                 ),
                 Build::new()
-                    .with_source_loc(loc)
+                    .with_maybe_source_loc(loc)
                     .get_attr(get_expr, attr.to_owned()),
             )
         })
diff --git a/cedar-policy-core/src/parser/cst_to_ast/to_ref_or_refs.rs b/cedar-policy-core/src/parser/cst_to_ast/to_ref_or_refs.rs
index 9f165aa389..f9831a97a3 100644
--- a/cedar-policy-core/src/parser/cst_to_ast/to_ref_or_refs.rs
+++ b/cedar-policy-core/src/parser/cst_to_ast/to_ref_or_refs.rs
@@ -34,8 +34,8 @@ use crate::parser::{
 trait RefKind: Sized {
     fn err_str() -> &'static str;
     fn create_single_ref(e: EntityUID) -> Result<Self>;
-    fn create_multiple_refs(loc: &Loc) -> Result<fn(Vec<EntityUID>) -> Self>;
-    fn create_slot(loc: &Loc) -> Result<Self>;
+    fn create_multiple_refs(loc: Option<&Loc>) -> Result<fn(Vec<EntityUID>) -> Self>;
+    fn create_slot(loc: Option<&Loc>) -> Result<Self>;
     #[cfg(feature = "tolerant-ast")]
     fn error_node() -> Self;
 }
@@ -50,24 +50,24 @@ impl RefKind for SingleEntity {
         Ok(SingleEntity(e))
     }
 
-    fn create_multiple_refs(loc: &Loc) -> Result<fn(Vec<EntityUID>) -> Self> {
+    fn create_multiple_refs(loc: Option<&Loc>) -> Result<fn(Vec<EntityUID>) -> Self> {
         Err(ToASTError::new(
             ToASTErrorKind::wrong_entity_argument_one_expected(
                 err::parse_errors::Ref::Single,
                 err::parse_errors::Ref::Set,
             ),
-            loc.clone(),
+            loc.cloned(),
         )
         .into())
     }
 
-    fn create_slot(loc: &Loc) -> Result<Self> {
+    fn create_slot(loc: Option<&Loc>) -> Result<Self> {
         Err(ToASTError::new(
             ToASTErrorKind::wrong_entity_argument_one_expected(
                 err::parse_errors::Ref::Single,
                 err::parse_errors::Ref::Template,
             ),
-            loc.clone(),
+            loc.cloned(),
         )
         .into())
     }
@@ -82,22 +82,22 @@ impl RefKind for EntityReference {
         "an entity uid or matching template slot"
     }
 
-    fn create_slot(loc: &Loc) -> Result<Self> {
-        Ok(EntityReference::Slot(Some(loc.clone())))
+    fn create_slot(loc: Option<&Loc>) -> Result<Self> {
+        Ok(EntityReference::Slot(loc.cloned()))
     }
 
     fn create_single_ref(e: EntityUID) -> Result<Self> {
         Ok(EntityReference::euid(Arc::new(e)))
     }
 
-    fn create_multiple_refs(loc: &Loc) -> Result<fn(Vec<EntityUID>) -> Self> {
+    fn create_multiple_refs(loc: Option<&Loc>) -> Result<fn(Vec<EntityUID>) -> Self> {
         Err(ToASTError::new(
             ToASTErrorKind::wrong_entity_argument_two_expected(
                 err::parse_errors::Ref::Single,
                 err::parse_errors::Ref::Template,
                 err::parse_errors::Ref::Set,
             ),
-            loc.clone(),
+            loc.cloned(),
         )
         .into())
     }
@@ -119,14 +119,14 @@ impl RefKind for OneOrMultipleRefs {
         "an entity uid or set of entity uids"
     }
 
-    fn create_slot(loc: &Loc) -> Result<Self> {
+    fn create_slot(loc: Option<&Loc>) -> Result<Self> {
         Err(ToASTError::new(
             ToASTErrorKind::wrong_entity_argument_two_expected(
                 err::parse_errors::Ref::Single,
                 err::parse_errors::Ref::Set,
                 err::parse_errors::Ref::Template,
             ),
-            loc.clone(),
+            loc.cloned(),
         )
         .into())
     }
@@ -135,7 +135,7 @@ impl RefKind for OneOrMultipleRefs {
         Ok(OneOrMultipleRefs::Single(e))
     }
 
-    fn create_multiple_refs(_loc: &Loc) -> Result<fn(Vec<EntityUID>) -> Self> {
+    fn create_multiple_refs(_loc: Option<&Loc>) -> Result<fn(Vec<EntityUID>) -> Self> {
         fn create_multiple_refs(es: Vec<EntityUID>) -> OneOrMultipleRefs {
             OneOrMultipleRefs::Multiple(es)
         }
@@ -240,7 +240,7 @@ impl Node<Option<cst::Primary>> {
                 // it's the wrong slot. This avoids getting an error
                 // `found ?action instead of ?action` when `action` doesn't
                 // support slots.
-                let slot_ref = T::create_slot(&self.loc)?;
+                let slot_ref = T::create_slot(self.loc.as_ref())?;
                 let slot = s.try_as_inner()?;
                 if slot.matches(var) {
                     Ok(slot_ref)
@@ -297,7 +297,7 @@ impl Node<Option<cst::Primary>> {
             cst::Primary::EList(lst) => {
                 // Calling `create_multiple_refs` first so that we error
                 // immediately if we see a set when we don't expect one.
-                let create_multiple_refs = T::create_multiple_refs(&self.loc)?;
+                let create_multiple_refs = T::create_multiple_refs(self.loc.as_ref())?;
                 let v = match tolerant_setting {
                     TolerantAstSetting::NotTolerant => {
                         ParseErrors::transpose(lst.iter().map(|expr| expr.to_ref(var)))?
@@ -540,7 +540,7 @@ mod test {
     fn test_primary_rinits_node() -> Node<Option<cst::Primary>> {
         Node {
             node: Some(cst::Primary::RInits(vec![])),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 
@@ -548,9 +548,9 @@ mod test {
         Node {
             node: Some(cst::Primary::Expr(Node {
                 node: Some(cst::Expr::ErrorExpr),
-                loc: Loc::new(0..1, "This is a test".into()),
+                loc: Some(Loc::new(0..1, "This is a test".into())),
             })),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 
@@ -559,14 +559,14 @@ mod test {
             node: Some(cst::Primary::EList(vec![
                 Node {
                     node: Some(test_expr()),
-                    loc: Loc::new(0..1, "This is also a test".into()),
+                    loc: Some(Loc::new(0..1, "This is also a test".into())),
                 },
                 Node {
                     node: Some(test_expr()),
-                    loc: Loc::new(0..1, "This is also a test".into()),
+                    loc: Some(Loc::new(0..1, "This is also a test".into())),
                 },
             ])),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 
@@ -579,10 +579,10 @@ mod test {
                         item: test_primary_ref_node(),
                         access: vec![],
                     }),
-                    loc: Loc::new(0..1, "This is a test".into()),
+                    loc: Some(Loc::new(0..1, "This is a test".into())),
                 },
             }),
-            loc: Loc::new(0..1, "This is a test".into()),
+            loc: Some(Loc::new(0..1, "This is a test".into())),
         }
     }
 
@@ -592,7 +592,7 @@ mod test {
                 initial: test_unary_node(),
                 extended: vec![],
             }),
-            loc: Loc::new(0..1, "This is a test".into()),
+            loc: Some(Loc::new(0..1, "This is a test".into())),
         }
     }
 
@@ -602,7 +602,7 @@ mod test {
                 initial: test_mult_node(),
                 extended: vec![],
             }),
-            loc: Loc::new(0..1, "This is a test".into()),
+            loc: Some(Loc::new(0..1, "This is a test".into())),
         }
     }
 
@@ -612,7 +612,7 @@ mod test {
                 initial: test_add_node(),
                 extended: vec![],
             }),
-            loc: Loc::new(0..1, "This is a test".into()),
+            loc: Some(Loc::new(0..1, "This is a test".into())),
         }
     }
 
@@ -625,10 +625,10 @@ mod test {
                         initial: test_relation_node(),
                         extended: vec![],
                     }),
-                    loc: Loc::new(0..1, "This is a test".into()),
+                    loc: Some(Loc::new(0..1, "This is a test".into())),
                 },
             }),
-            loc: Loc::new(0..1, "This is a test".into()),
+            loc: Some(Loc::new(0..1, "This is a test".into())),
         })
     }
 
@@ -642,9 +642,9 @@ mod test {
         Node {
             node: Some(cst::Primary::Expr(Node {
                 node: Some(test_expr()),
-                loc: Loc::new(0..1, "This is a test".into()),
+                loc: Some(Loc::new(0..1, "This is a test".into())),
             })),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 
@@ -654,13 +654,13 @@ mod test {
                 node: Some(Name {
                     path: vec![],
                     name: Node {
-                        loc: Loc::new(0..1, "So much testing".into()),
+                        loc: Some(Loc::new(0..1, "So much testing".into())),
                         node: Some(cst::Ident::Ident("test".into())),
                     },
                 }),
-                loc: Loc::new(0..1, "This is a test".into()),
+                loc: Some(Loc::new(0..1, "This is a test".into())),
             })),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 
@@ -668,9 +668,9 @@ mod test {
         Node {
             node: Some(cst::Primary::Literal(Node {
                 node: Some(cst::Literal::True),
-                loc: Loc::new(0..1, "This is a test".into()),
+                loc: Some(Loc::new(0..1, "This is a test".into())),
             })),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 
@@ -678,9 +678,9 @@ mod test {
         Node {
             node: Some(cst::Primary::Slot(Node {
                 node: Some(cst::Slot::Principal),
-                loc: Loc::new(0..1, "This is a test".into()),
+                loc: Some(Loc::new(0..1, "This is a test".into())),
             })),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 
@@ -692,20 +692,20 @@ mod test {
                         node: Some(Name {
                             path: vec![],
                             name: Node {
-                                loc: Loc::new(0..1, "So much testing".into()),
+                                loc: Some(Loc::new(0..1, "So much testing".into())),
                                 node: Some(cst::Ident::Ident("test".into())),
                             },
                         }),
-                        loc: Loc::new(0..1, "This is a test".into()),
+                        loc: Some(Loc::new(0..1, "This is a test".into())),
                     },
                     eid: Node {
                         node: Some(cst::Str::String("test".into())),
-                        loc: Loc::new(0..1, "This is a test".into()),
+                        loc: Some(Loc::new(0..1, "This is a test".into())),
                     },
                 }),
-                loc: Loc::new(0..1, "This is a test".into()),
+                loc: Some(Loc::new(0..1, "This is a test".into())),
             })),
-            loc: Loc::new(0..1, "This is also a test".into()),
+            loc: Some(Loc::new(0..1, "This is also a test".into())),
         }
     }
 }
diff --git a/cedar-policy-core/src/parser/err.rs b/cedar-policy-core/src/parser/err.rs
index e89fe0938e..7359e7e70d 100644
--- a/cedar-policy-core/src/parser/err.rs
+++ b/cedar-policy-core/src/parser/err.rs
@@ -76,13 +76,13 @@ pub enum LiteralParseError {
 #[error("{kind}")]
 pub struct ToASTError {
     kind: ToASTErrorKind,
-    loc: Loc,
+    loc: Option<Loc>,
 }
 
 // Construct `labels` and `source_code` based on the `loc` in this
 // struct; and everything else forwarded directly to `kind`.
 impl Diagnostic for ToASTError {
-    impl_diagnostic_from_source_loc_field!(loc);
+    impl_diagnostic_from_source_loc_opt_field!(loc);
 
     fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
         self.kind.code()
@@ -107,7 +107,7 @@ impl Diagnostic for ToASTError {
 
 impl ToASTError {
     /// Construct a new `ToASTError`.
-    pub fn new(kind: ToASTErrorKind, loc: Loc) -> Self {
+    pub fn new(kind: ToASTErrorKind, loc: Option<Loc>) -> Self {
         Self { kind, loc }
     }
 
@@ -116,8 +116,8 @@ impl ToASTError {
         &self.kind
     }
 
-    pub(crate) fn source_loc(&self) -> &Loc {
-        &self.loc
+    pub(crate) fn source_loc(&self) -> Option<&Loc> {
+        self.loc.as_ref()
     }
 }
 
@@ -618,18 +618,20 @@ pub struct ToCSTError {
 
 impl ToCSTError {
     /// Extract a primary source span locating the error.
-    pub fn primary_source_span(&self) -> SourceSpan {
+    pub fn primary_source_span(&self) -> Option<SourceSpan> {
         match &self.err {
-            OwnedRawParseError::InvalidToken { location } => SourceSpan::from(*location),
-            OwnedRawParseError::UnrecognizedEof { location, .. } => SourceSpan::from(*location),
+            OwnedRawParseError::InvalidToken { location } => Some(SourceSpan::from(*location)),
+            OwnedRawParseError::UnrecognizedEof { location, .. } => {
+                Some(SourceSpan::from(*location))
+            }
             OwnedRawParseError::UnrecognizedToken {
                 token: (token_start, _, token_end),
                 ..
-            } => SourceSpan::from(*token_start..*token_end),
+            } => Some(SourceSpan::from(*token_start..*token_end)),
             OwnedRawParseError::ExtraToken {
                 token: (token_start, _, token_end),
-            } => SourceSpan::from(*token_start..*token_end),
-            OwnedRawParseError::User { error } => error.loc.span,
+            } => Some(SourceSpan::from(*token_start..*token_end)),
+            OwnedRawParseError::User { error } => error.loc.clone().map(|loc| loc.span),
         }
     }
 
@@ -669,19 +671,17 @@ impl Diagnostic for ToCSTError {
     }
 
     fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
-        let primary_source_span = self.primary_source_span();
+        let span = self.primary_source_span()?;
         let labeled_span = match &self.err {
-            OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(primary_source_span),
-            OwnedRawParseError::UnrecognizedEof { expected, .. } => LabeledSpan::new_with_span(
-                expected_to_string(expected, &POLICY_TOKEN_CONFIG),
-                primary_source_span,
-            ),
-            OwnedRawParseError::UnrecognizedToken { expected, .. } => LabeledSpan::new_with_span(
-                expected_to_string(expected, &POLICY_TOKEN_CONFIG),
-                primary_source_span,
-            ),
-            OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(primary_source_span),
-            OwnedRawParseError::User { .. } => LabeledSpan::underline(primary_source_span),
+            OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(span),
+            OwnedRawParseError::UnrecognizedEof { expected, .. } => {
+                LabeledSpan::new_with_span(expected_to_string(expected, &POLICY_TOKEN_CONFIG), span)
+            }
+            OwnedRawParseError::UnrecognizedToken { expected, .. } => {
+                LabeledSpan::new_with_span(expected_to_string(expected, &POLICY_TOKEN_CONFIG), span)
+            }
+            OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(span),
+            OwnedRawParseError::User { .. } => LabeledSpan::underline(span),
         };
         Some(Box::new(iter::once(labeled_span)))
     }
diff --git a/cedar-policy-core/src/parser/node.rs b/cedar-policy-core/src/parser/node.rs
index 364a2541f0..606ac6cc31 100644
--- a/cedar-policy-core/src/parser/node.rs
+++ b/cedar-policy-core/src/parser/node.rs
@@ -33,12 +33,20 @@ pub struct Node<T> {
     #[educe(PartialEq(ignore))]
     #[educe(PartialOrd(ignore))]
     #[educe(Hash(ignore))]
-    pub loc: Loc,
+    pub loc: Option<Loc>,
 }
 
 impl<T> Node<T> {
     /// Create a new Node with the given source location
     pub fn with_source_loc(node: T, loc: Loc) -> Self {
+        Node {
+            node,
+            loc: Some(loc),
+        }
+    }
+
+    /// Create a new Node with optional source location
+    pub fn with_maybe_source_loc(node: T, loc: Option<Loc>) -> Self {
         Node { node, loc }
     }
 
@@ -67,7 +75,7 @@ impl<T> Node<T> {
     }
 
     /// Consume the `Node`, yielding the node and attached source info.
-    pub fn into_inner(self) -> (T, Loc) {
+    pub fn into_inner(self) -> (T, Option<Loc>) {
         (self.node, self.loc)
     }
 
@@ -115,7 +123,7 @@ impl<T: std::error::Error> std::error::Error for Node<T> {
 
 // impl Diagnostic by taking `labels()` and `source_code()` from .loc and everything else from .node
 impl<T: Diagnostic> Diagnostic for Node<T> {
-    impl_diagnostic_from_source_loc_field!(loc);
+    impl_diagnostic_from_source_loc_opt_field!(loc);
 
     fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
         self.node.code()
@@ -161,16 +169,16 @@ impl<T> Node<Option<T>> {
     /// if no main data or if `f` returns `None`.
     pub fn apply<F, R>(&self, f: F) -> Option<R>
     where
-        F: FnOnce(&T, &Loc) -> Option<R>,
+        F: FnOnce(&T, Option<&Loc>) -> Option<R>,
     {
-        f(self.node.as_ref()?, &self.loc)
+        f(self.node.as_ref()?, self.loc.as_ref())
     }
 
     /// Apply the function `f` to the main data and `Loc`, consuming them.
     /// Returns `None` if no main data or if `f` returns `None`.
     pub fn into_apply<F, R>(self, f: F) -> Option<R>
     where
-        F: FnOnce(T, Loc) -> Option<R>,
+        F: FnOnce(T, Option<Loc>) -> Option<R>,
     {
         f(self.node?, self.loc)
     }
diff --git a/cedar-policy-formatter/src/pprint/doc.rs b/cedar-policy-formatter/src/pprint/doc.rs
index b43f92ca9e..70d9a4d4a8 100644
--- a/cedar-policy-formatter/src/pprint/doc.rs
+++ b/cedar-policy-formatter/src/pprint/doc.rs
@@ -38,7 +38,8 @@ impl Doc for Node<Option<VariableDef>> {
     fn to_doc<'src>(&self, context: &mut Context<'_, 'src>) -> Option<RcDoc<'src>> {
         let vd = self.as_inner()?;
 
-        let start_comment = get_comment_at_start(self.loc.span, &mut context.tokens)?;
+        let start_comment =
+            get_comment_at_start(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?;
         let var_doc = vd.variable.as_inner()?.to_doc(context)?;
 
         let is_doc = match &vd.entity_type {
@@ -46,13 +47,19 @@ impl Doc for Node<Option<VariableDef>> {
                 RcDoc::line()
                     .append(add_comment(
                         RcDoc::text("is"),
-                        get_comment_after_end(vd.variable.loc.span, &mut context.tokens)?,
+                        get_comment_after_end(
+                            vd.variable.loc.as_ref().map(|loc| loc.span),
+                            &mut context.tokens,
+                        )?,
                         RcDoc::nil(),
                     ))
                     .group()
                     .append(RcDoc::line().append(add_comment(
                         entity_type.to_doc(context)?,
-                        get_comment_at_start(entity_type.loc.span, &mut context.tokens)?,
+                        get_comment_at_start(
+                            entity_type.loc.as_ref().map(|loc| loc.span),
+                            &mut context.tokens,
+                        )?,
                         RcDoc::nil(),
                     )))
                     .nest(context.config.indent_width)
@@ -64,10 +71,14 @@ impl Doc for Node<Option<VariableDef>> {
         Some(match &vd.ineq {
             Some((op, rhs)) => {
                 let op_comment = match &vd.entity_type {
-                    Some(entity_type) => {
-                        get_comment_after_end(entity_type.loc.span, &mut context.tokens)?
-                    }
-                    None => get_comment_after_end(vd.variable.loc.span, &mut context.tokens)?,
+                    Some(entity_type) => get_comment_after_end(
+                        entity_type.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
+                    None => get_comment_after_end(
+                        vd.variable.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                 };
                 get_leading_comment_doc_from_str(start_comment.leading_comment()).append(
                     var_doc
@@ -95,16 +106,25 @@ impl Doc for Node<Option<VariableDef>> {
 impl Doc for Node<Option<Cond>> {
     fn to_doc<'src>(&self, context: &mut Context<'_, 'src>) -> Option<RcDoc<'src>> {
         let cond = self.as_inner()?;
-        let lb_comment = get_comment_after_end(cond.cond.loc.span, &mut context.tokens)?;
-        let rb_comment = get_comment_at_end(self.loc.span, &mut context.tokens)?;
-        let cond_comment = get_comment_at_start(cond.cond.loc.span, &mut context.tokens)?;
+        let lb_comment = get_comment_after_end(
+            cond.cond.loc.as_ref().map(|loc| loc.span),
+            &mut context.tokens,
+        )?;
+        let rb_comment =
+            get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?;
+        let cond_comment = get_comment_at_start(
+            cond.cond.loc.as_ref().map(|loc| loc.span),
+            &mut context.tokens,
+        )?;
 
         let rb_doc = add_comment(RcDoc::text("}"), rb_comment, RcDoc::nil());
         let cond_doc = cond.cond.to_doc(context)?;
         Some(match cond.expr.as_ref() {
             Some(expr) => {
-                let expr_leading_comment =
-                    get_leading_comment_at_start(expr.loc.span, &mut context.tokens)?;
+                let expr_leading_comment = get_leading_comment_at_start(
+                    expr.loc.as_ref().map(|loc| loc.span),
+                    &mut context.tokens,
+                )?;
                 let expr_doc = expr.to_doc(context)?;
                 get_leading_comment_doc_from_str(cond_comment.leading_comment()).append(
                     cond_doc
@@ -173,9 +193,18 @@ impl Doc for Node<Option<Expr>> {
                                 .nest(context.config.indent_width),
                         )
                     }
-                    let if_comment = get_comment_at_start(self.loc.span, &mut context.tokens)?;
-                    let then_comment = get_comment_after_end(c.loc.span, &mut context.tokens)?;
-                    let else_comment = get_comment_after_end(t.loc.span, &mut context.tokens)?;
+                    let if_comment = get_comment_at_start(
+                        self.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?;
+                    let then_comment = get_comment_after_end(
+                        c.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?;
+                    let else_comment = get_comment_after_end(
+                        t.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?;
                     Some(
                         pp_group("if", if_comment, c, context)
                             .append(RcDoc::line())
@@ -203,7 +232,8 @@ impl Doc for Node<Option<Or>> {
         let es: Vec<_> = std::iter::once(initial).chain(extended.iter()).collect();
         let mut d: RcDoc<'_> = RcDoc::nil();
         for e in es.iter().take(es.len() - 1) {
-            let op_comment = get_comment_after_end(e.loc.span, &mut context.tokens)?;
+            let op_comment =
+                get_comment_after_end(e.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?;
             d = d
                 .append(e.to_doc(context))
                 .append(RcDoc::space())
@@ -223,7 +253,8 @@ impl Doc for Node<Option<And>> {
         let es: Vec<_> = std::iter::once(initial).chain(extended.iter()).collect();
         let mut d: RcDoc<'_> = RcDoc::nil();
         for e in es.iter().take(es.len() - 1) {
-            let op_comment = get_comment_after_end(e.loc.span, &mut context.tokens)?;
+            let op_comment =
+                get_comment_after_end(e.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?;
             d = d
                 .append(e.to_doc(context))
                 .append(RcDoc::space())
@@ -249,7 +280,10 @@ impl Doc for Node<Option<Relation>> {
                                 .append(RcDoc::space())
                                 .append(add_comment(
                                     RcDoc::as_string(op),
-                                    get_comment_after_end(initial.loc.span, &mut context.tokens)?,
+                                    get_comment_after_end(
+                                        initial.loc.as_ref().map(|loc| loc.span),
+                                        &mut context.tokens,
+                                    )?,
                                     RcDoc::nil(),
                                 ))
                                 .append(RcDoc::space())
@@ -265,7 +299,10 @@ impl Doc for Node<Option<Relation>> {
                     .append(RcDoc::line())
                     .append(add_comment(
                         RcDoc::text("has"),
-                        get_comment_after_end(target.loc.span, &mut context.tokens)?,
+                        get_comment_after_end(
+                            target.loc.as_ref().map(|loc| loc.span),
+                            &mut context.tokens,
+                        )?,
                         RcDoc::nil(),
                     ))
                     .append(RcDoc::line())
@@ -278,7 +315,10 @@ impl Doc for Node<Option<Relation>> {
                     .append(RcDoc::line())
                     .append(add_comment(
                         RcDoc::text("like"),
-                        get_comment_after_end(target.loc.span, &mut context.tokens)?,
+                        get_comment_after_end(
+                            target.loc.as_ref().map(|loc| loc.span),
+                            &mut context.tokens,
+                        )?,
                         RcDoc::nil(),
                     ))
                     .append(RcDoc::line())
@@ -295,7 +335,10 @@ impl Doc for Node<Option<Relation>> {
                     .append(RcDoc::space())
                     .append(add_comment(
                         RcDoc::text("is"),
-                        get_comment_after_end(target.loc.span, &mut context.tokens)?,
+                        get_comment_after_end(
+                            target.loc.as_ref().map(|loc| loc.span),
+                            &mut context.tokens,
+                        )?,
                         RcDoc::nil(),
                     ))
                     .append(RcDoc::space())
@@ -310,7 +353,10 @@ impl Doc for Node<Option<Relation>> {
                             .append(RcDoc::line())
                             .append(add_comment(
                                 RcDoc::text("in"),
-                                get_comment_after_end(entity_type.loc.span, &mut context.tokens)?,
+                                get_comment_after_end(
+                                    entity_type.loc.as_ref().map(|loc| loc.span),
+                                    &mut context.tokens,
+                                )?,
                                 RcDoc::nil(),
                             ))
                             .append(RcDoc::space())
@@ -346,7 +392,10 @@ impl Doc for Node<Option<Add>> {
                             .append(RcDoc::space())
                             .append(add_comment(
                                 op.to_doc(context)?,
-                                get_comment_after_end(pair.1.loc.span, &mut context.tokens)?,
+                                get_comment_after_end(
+                                    pair.1.loc.as_ref().map(|loc| loc.span),
+                                    &mut context.tokens,
+                                )?,
                                 RcDoc::nil(),
                             ))
                             .append(RcDoc::line())
@@ -382,7 +431,10 @@ impl Doc for Node<Option<Mult>> {
                             .append(RcDoc::space())
                             .append(add_comment(
                                 op.to_doc(context)?,
-                                get_comment_after_end(pair.1.loc.span, &mut context.tokens)?,
+                                get_comment_after_end(
+                                    pair.1.loc.as_ref().map(|loc| loc.span),
+                                    &mut context.tokens,
+                                )?,
                                 RcDoc::nil(),
                             ))
                             .append(RcDoc::line())
@@ -403,10 +455,12 @@ impl Doc for Node<Option<Unary>> {
             match op {
                 NegOp::OverBang | NegOp::OverDash => None,
                 NegOp::Bang(n) | NegOp::Dash(n) => {
+                    let sloc = self.loc.as_ref()?;
+                    let eloc = e.item.loc.as_ref()?;
                     let comment = get_comment_in_range(
-                        (self.loc.start()..e.item.loc.start()).into(),
+                        Some((sloc.start()..eloc.start()).into()),
                         &mut context.tokens,
-                    );
+                    )?;
                     if comment.len() != n as usize {
                         return None;
                     }
@@ -464,7 +518,10 @@ impl Doc for Node<Option<RecInit>> {
                 .append(RcDoc::line_())
                 .append(add_comment(
                     RcDoc::text(":"),
-                    get_comment_after_end(e.0.loc.span, &mut context.tokens)?,
+                    get_comment_after_end(
+                        e.0.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 ))
                 .append(value_doc),
@@ -490,7 +547,10 @@ impl Doc for Node<Option<Name>> {
                             Some((
                                 d.append(add_comment(
                                     RcDoc::text("::"),
-                                    get_comment_after_end(e.loc.span, &mut context.tokens)?,
+                                    get_comment_after_end(
+                                        e.loc.as_ref().map(|loc| loc.span),
+                                        &mut context.tokens,
+                                    )?,
                                     RcDoc::nil(),
                                 ))
                                 .append(p.to_doc(context)?),
@@ -501,7 +561,10 @@ impl Doc for Node<Option<Name>> {
                     .0
                     .append(add_comment(
                         RcDoc::text("::"),
-                        get_comment_after_end(path.last()?.loc.span, &mut context.tokens)?,
+                        get_comment_after_end(
+                            path.last()?.loc.as_ref().map(|loc| loc.span),
+                            &mut context.tokens,
+                        )?,
                         RcDoc::nil(),
                     ))
                     .append(n.to_doc(context)),
@@ -518,7 +581,7 @@ impl Doc for Node<Option<Str>> {
         // on newlines, which may alter the string content.
         Some(add_comment(
             RcDoc::as_string(e),
-            get_comment_at_start(self.loc.span, &mut context.tokens)?,
+            get_comment_at_start(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
             RcDoc::nil(),
         ))
     }
@@ -531,7 +594,10 @@ impl Doc for Node<Option<Ref>> {
                 path.to_doc(context)?
                     .append(add_comment(
                         RcDoc::text("::"),
-                        get_comment_after_end(path.loc.span, &mut context.tokens)?,
+                        get_comment_after_end(
+                            path.loc.as_ref().map(|loc| loc.span),
+                            &mut context.tokens,
+                        )?,
                         RcDoc::nil(),
                     ))
                     .append(eid.to_doc(context)?),
@@ -545,7 +611,7 @@ impl Doc for Node<Option<Literal>> {
     fn to_doc<'src>(&self, context: &mut Context<'_, 'src>) -> Option<RcDoc<'src>> {
         Some(add_comment(
             RcDoc::as_string(self.as_inner()?),
-            get_comment_at_start(self.loc.span, &mut context.tokens)?,
+            get_comment_at_start(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
             RcDoc::nil(),
         ))
     }
@@ -555,7 +621,7 @@ impl Doc for Node<Option<Slot>> {
     fn to_doc<'src>(&self, context: &mut Context<'_, 'src>) -> Option<RcDoc<'src>> {
         Some(add_comment(
             RcDoc::as_string(self.as_inner()?),
-            get_comment_at_start(self.loc.span, &mut context.tokens)?,
+            get_comment_at_start(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
             RcDoc::nil(),
         ))
     }
@@ -571,7 +637,10 @@ impl Doc for Node<Option<Primary>> {
             Primary::Expr(e) => Some(
                 add_comment(
                     RcDoc::text("("),
-                    get_comment_at_start(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_start(
+                        self.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 )
                 .append(RcDoc::nil())
@@ -579,7 +648,7 @@ impl Doc for Node<Option<Primary>> {
                 .append(RcDoc::nil())
                 .append(add_comment(
                     RcDoc::text(")"),
-                    get_comment_at_end(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
                     RcDoc::nil(),
                 ))
                 .group(),
@@ -595,7 +664,10 @@ impl Doc for Node<Option<Primary>> {
                             Some((
                                 d.append(add_comment(
                                     RcDoc::text(","),
-                                    get_comment_after_end(e.loc.span, &mut context.tokens)?,
+                                    get_comment_after_end(
+                                        e.loc.as_ref().map(|loc| loc.span),
+                                        &mut context.tokens,
+                                    )?,
                                     RcDoc::nil(),
                                 ))
                                 .append(RcDoc::line())
@@ -607,12 +679,15 @@ impl Doc for Node<Option<Primary>> {
                 },
                 add_comment(
                     RcDoc::text("["),
-                    get_comment_at_start(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_start(
+                        self.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 ),
                 add_comment(
                     RcDoc::text("]"),
-                    get_comment_at_end(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
                     RcDoc::nil(),
                 ),
             )),
@@ -627,7 +702,10 @@ impl Doc for Node<Option<Primary>> {
                             Some((
                                 d.append(add_comment(
                                     RcDoc::text(","),
-                                    get_comment_after_end(e.loc.span, &mut context.tokens)?,
+                                    get_comment_after_end(
+                                        e.loc.as_ref().map(|loc| loc.span),
+                                        &mut context.tokens,
+                                    )?,
                                     RcDoc::nil(),
                                 ))
                                 .append(RcDoc::line())
@@ -639,12 +717,15 @@ impl Doc for Node<Option<Primary>> {
                 },
                 add_comment(
                     RcDoc::text("{"),
-                    get_comment_at_start(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_start(
+                        self.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 ),
                 add_comment(
                     RcDoc::text("}"),
-                    get_comment_at_end(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
                     RcDoc::nil(),
                 ),
             )),
@@ -660,7 +741,10 @@ impl Doc for Node<Option<MemAccess>> {
             MemAccess::Field(f) => Some(
                 add_comment(
                     RcDoc::text("."),
-                    get_comment_at_start(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_start(
+                        self.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 )
                 .append(f.to_doc(context)),
@@ -668,7 +752,10 @@ impl Doc for Node<Option<MemAccess>> {
             MemAccess::Call(args) => Some(
                 add_comment(
                     RcDoc::text("("),
-                    get_comment_at_start(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_start(
+                        self.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 )
                 .append(RcDoc::line_())
@@ -684,7 +771,10 @@ impl Doc for Node<Option<MemAccess>> {
                                 Some((
                                     d.append(add_comment(
                                         RcDoc::text(","),
-                                        get_comment_after_end(e.loc.span, &mut context.tokens)?,
+                                        get_comment_after_end(
+                                            e.loc.as_ref().map(|loc| loc.span),
+                                            &mut context.tokens,
+                                        )?,
                                         RcDoc::nil(),
                                     ))
                                     .append(RcDoc::line())
@@ -699,14 +789,17 @@ impl Doc for Node<Option<MemAccess>> {
                 .append(RcDoc::line_())
                 .append(add_comment(
                     RcDoc::text(")"),
-                    get_comment_at_end(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
                     RcDoc::nil(),
                 )),
             ),
             MemAccess::Index(idx) => Some(
                 add_comment(
                     RcDoc::text("["),
-                    get_comment_at_start(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_start(
+                        self.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 )
                 .append(RcDoc::line_())
@@ -714,7 +807,7 @@ impl Doc for Node<Option<MemAccess>> {
                 .append(RcDoc::line_())
                 .append(add_comment(
                     RcDoc::text("]"),
-                    get_comment_at_end(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
                     RcDoc::nil(),
                 )),
             ),
@@ -728,20 +821,23 @@ impl Doc for Node<Option<Annotation>> {
         let id_doc = annotation.key.to_doc(context);
         let at_doc = add_comment(
             RcDoc::text("@"),
-            get_comment_at_start(self.loc.span, &mut context.tokens)?,
+            get_comment_at_start(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
             RcDoc::nil(),
         );
         let val_doc = match annotation.value.as_ref() {
             Some(value) => {
                 let lp_doc = add_comment(
                     RcDoc::text("("),
-                    get_comment_after_end(annotation.key.loc.span, &mut context.tokens)?,
+                    get_comment_after_end(
+                        annotation.key.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::nil(),
                 );
                 let val_doc = value.to_doc(context);
                 let rp_doc = add_comment(
                     RcDoc::text(")"),
-                    get_comment_at_end(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
                     RcDoc::hardline(),
                 );
                 lp_doc.append(val_doc).append(rp_doc)
@@ -757,7 +853,7 @@ impl Doc for Node<Option<Ident>> {
     fn to_doc<'src>(&self, context: &mut Context<'_, 'src>) -> Option<RcDoc<'src>> {
         Some(add_comment(
             self.as_inner()?.to_doc(context)?,
-            get_comment_at_start(self.loc.span, &mut context.tokens)?,
+            get_comment_at_start(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
             RcDoc::nil(),
         ))
     }
@@ -776,8 +872,10 @@ impl Doc for Node<Option<Policy>> {
             policy.annotations.iter().map(|a| a.to_doc(context)),
             RcDoc::nil(),
         );
-        let eff_leading_comment =
-            get_leading_comment_at_start(policy.effect.loc.span, &mut context.tokens)?;
+        let eff_leading_comment = get_leading_comment_at_start(
+            policy.effect.loc.as_ref().map(|loc| loc.span),
+            &mut context.tokens,
+        )?;
         let eff_doc = policy.effect.to_doc(context)?;
         let vars = &policy.variables;
         let principal_doc = vars.first()?.to_doc(context)?;
@@ -793,13 +891,19 @@ impl Doc for Node<Option<Policy>> {
             principal_doc
                 .append(add_comment(
                     RcDoc::text(","),
-                    get_comment_after_end(vars.first()?.loc.span, &mut context.tokens)?,
+                    get_comment_after_end(
+                        vars.first()?.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::space(),
                 ))
                 .append(action_doc)
                 .append(add_comment(
                     RcDoc::text(","),
-                    get_comment_after_end(vars.get(1)?.loc.span, &mut context.tokens)?,
+                    get_comment_after_end(
+                        vars.get(1)?.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     RcDoc::space(),
                 ))
                 .append(resource_doc)
@@ -811,13 +915,19 @@ impl Doc for Node<Option<Policy>> {
                     principal_doc
                         .append(add_comment(
                             RcDoc::text(","),
-                            get_comment_after_end(vars.first()?.loc.span, &mut context.tokens)?,
+                            get_comment_after_end(
+                                vars.first()?.loc.as_ref().map(|loc| loc.span),
+                                &mut context.tokens,
+                            )?,
                             RcDoc::hardline(),
                         ))
                         .append(action_doc)
                         .append(add_comment(
                             RcDoc::text(","),
-                            get_comment_after_end(vars.get(1)?.loc.span, &mut context.tokens)?,
+                            get_comment_after_end(
+                                vars.get(1)?.loc.as_ref().map(|loc| loc.span),
+                                &mut context.tokens,
+                            )?,
                             RcDoc::hardline(),
                         ))
                         .append(resource_doc),
@@ -836,7 +946,10 @@ impl Doc for Node<Option<Policy>> {
                             .append(RcDoc::line())
                             .append(add_comment(
                                 RcDoc::text("("),
-                                get_comment_after_end(policy.effect.loc.span, &mut context.tokens)?,
+                                get_comment_after_end(
+                                    policy.effect.loc.as_ref().map(|loc| loc.span),
+                                    &mut context.tokens,
+                                )?,
                                 RcDoc::nil(),
                             ))
                             .group(),
@@ -845,7 +958,10 @@ impl Doc for Node<Option<Policy>> {
                 .append(vars_doc)
                 .append(add_comment(
                     RcDoc::text(")"),
-                    get_comment_after_end(vars.get(2)?.loc.span, &mut context.tokens)?,
+                    get_comment_after_end(
+                        vars.get(2)?.loc.as_ref().map(|loc| loc.span),
+                        &mut context.tokens,
+                    )?,
                     if conds.is_empty() {
                         RcDoc::nil()
                     } else {
@@ -855,7 +971,7 @@ impl Doc for Node<Option<Policy>> {
                 .append(cond_doc)
                 .append(add_comment(
                     RcDoc::text(";"),
-                    get_comment_at_end(self.loc.span, &mut context.tokens)?,
+                    get_comment_at_end(self.loc.as_ref().map(|loc| loc.span), &mut context.tokens)?,
                     RcDoc::nil(),
                 )),
         )
diff --git a/cedar-policy-formatter/src/pprint/utils.rs b/cedar-policy-formatter/src/pprint/utils.rs
index ad8e3d507a..12970cdaca 100644
--- a/cedar-policy-formatter/src/pprint/utils.rs
+++ b/cedar-policy-formatter/src/pprint/utils.rs
@@ -64,9 +64,10 @@ pub fn get_trailing_comment_doc_from_str<'src>(
 }
 
 fn get_token_at_start<'a, 'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &'a mut [WrappedToken<'src>],
 ) -> Option<&'a mut WrappedToken<'src>> {
+    let span = span?;
     tokens
         .as_mut()
         .iter_mut()
@@ -74,59 +75,64 @@ fn get_token_at_start<'a, 'src>(
 }
 
 pub fn get_comment_at_start<'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &mut [WrappedToken<'src>],
 ) -> Option<Comment<'src>> {
     Some(get_token_at_start(span, tokens)?.consume_comment())
 }
 
 pub fn get_leading_comment_at_start<'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &mut [WrappedToken<'src>],
 ) -> Option<Vec<&'src str>> {
     Some(get_token_at_start(span, tokens)?.consume_leading_comment())
 }
 
 fn get_token_after_end<'a, 'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &'a mut [WrappedToken<'src>],
 ) -> Option<&'a mut WrappedToken<'src>> {
+    let span = span?;
     let end = span.offset() + span.len();
     tokens.iter_mut().find_or_first(|t| t.span.start >= end)
 }
 
 fn get_token_at_end<'a, 'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &'a mut [WrappedToken<'src>],
 ) -> Option<&'a mut WrappedToken<'src>> {
+    let span = span?;
     let end = span.offset() + span.len();
     tokens.iter_mut().find(|t| t.span.end == end)
 }
 
 pub fn get_comment_at_end<'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &mut [WrappedToken<'src>],
 ) -> Option<Comment<'src>> {
     Some(get_token_at_end(span, tokens)?.consume_comment())
 }
 
 pub fn get_comment_after_end<'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &mut [WrappedToken<'src>],
 ) -> Option<Comment<'src>> {
     Some(get_token_after_end(span, tokens)?.consume_comment())
 }
 
 pub fn get_comment_in_range<'src>(
-    span: miette::SourceSpan,
+    span: Option<miette::SourceSpan>,
     tokens: &mut [WrappedToken<'src>],
-) -> Vec<Comment<'src>> {
-    tokens
-        .iter_mut()
-        .skip_while(|t| t.span.start < span.offset())
-        .take_while(|t| t.span.end <= span.offset() + span.len())
-        .map(|t| t.consume_comment())
-        .collect()
+) -> Option<Vec<Comment<'src>>> {
+    let span = span?;
+    Some(
+        tokens
+            .iter_mut()
+            .skip_while(|t| t.span.start < span.offset())
+            .take_while(|t| t.span.end <= span.offset() + span.len())
+            .map(|t| t.consume_comment())
+            .collect(),
+    )
 }
 
 /// Wrap an `RcDoc` with comments. If there is a leading comment, then this
diff --git a/cedar-policy-validator/src/cedar_schema/ast.rs b/cedar-policy-validator/src/cedar_schema/ast.rs
index 02811f7ce8..d6c1256437 100644
--- a/cedar-policy-validator/src/cedar_schema/ast.rs
+++ b/cedar-policy-validator/src/cedar_schema/ast.rs
@@ -57,8 +57,8 @@ pub fn deduplicate_annotations<T>(
         if let Some((old_key, _)) = unique_annotations.get_key_value(&key) {
             return Err(UserError::DuplicateAnnotations(
                 key.node,
-                Node::with_source_loc((), old_key.loc.clone()),
-                Node::with_source_loc((), key.loc),
+                Node::with_maybe_source_loc((), old_key.loc.clone()),
+                Node::with_maybe_source_loc((), key.loc),
             ));
         } else {
             unique_annotations.insert(key, value);
@@ -70,7 +70,7 @@ pub fn deduplicate_annotations<T>(
             .into_iter()
             .map(|(key, value)| {
                 let (val, loc) = match value {
-                    Some(n) => (Some(n.node), Some(n.loc)),
+                    Some(n) => (Some(n.node), n.loc),
                     None => (None, None),
                 };
                 (key.node, Annotation::with_optional_value(val, loc))
@@ -112,8 +112,8 @@ impl Path {
     }
 
     /// Source [`Loc`] of this [`Path`]
-    pub fn loc(&self) -> &Loc {
-        &self.0.loc
+    pub fn loc(&self) -> Option<&Loc> {
+        self.0.loc.as_ref()
     }
 
     /// Consume the [`Path`] and get an owned iterator over the elements. Most significant name first
@@ -123,7 +123,7 @@ impl Path {
         self.0
             .node
             .into_iter()
-            .map(move |x| Node::with_source_loc(x, loc.clone()))
+            .map(move |x| Node::with_maybe_source_loc(x, loc.clone()))
     }
 
     /// Get the base type name as well as the (potentially empty) prefix
@@ -139,11 +139,7 @@ impl Path {
 
 impl From<Path> for InternalName {
     fn from(value: Path) -> Self {
-        InternalName::new(
-            value.0.node.basename,
-            value.0.node.namespace,
-            Some(value.0.loc),
-        )
+        InternalName::new(value.0.node.basename, value.0.node.namespace, value.0.loc)
     }
 }
 
diff --git a/cedar-policy-validator/src/cedar_schema/err.rs b/cedar-policy-validator/src/cedar_schema/err.rs
index c2f3ce9629..cc93c9e090 100644
--- a/cedar-policy-validator/src/cedar_schema/err.rs
+++ b/cedar-policy-validator/src/cedar_schema/err.rs
@@ -24,8 +24,7 @@ use std::{
 
 use cedar_policy_core::{
     ast::AnyId,
-    impl_diagnostic_from_source_loc_field, impl_diagnostic_from_two_source_loc_fields,
-    impl_diagnostic_from_two_source_loc_opt_fields,
+    impl_diagnostic_from_source_loc_opt_field, impl_diagnostic_from_two_source_loc_opt_fields,
     parser::{
         err::{expected_to_string, ExpectedTokenConfig},
         unescape::UnescapeError,
@@ -55,13 +54,13 @@ pub enum UserError {
 
 impl UserError {
     // Extract a primary source span locating the error.
-    pub(crate) fn primary_source_span(&self) -> SourceSpan {
+    pub(crate) fn primary_source_span(&self) -> Option<SourceSpan> {
         match self {
-            Self::EmptyList(n) => n.loc.span,
-            Self::StringEscape(n) => n.loc.span,
-            Self::ReservedIdentifierUsed(n) => n.loc.span,
+            Self::EmptyList(n) => n.loc.as_ref().map(|loc| loc.span),
+            Self::StringEscape(n) => n.loc.as_ref().map(|loc| loc.span),
+            Self::ReservedIdentifierUsed(n) => n.loc.as_ref().map(|loc| loc.span),
             // use the first occurrence as the primary source span
-            Self::DuplicateAnnotations(_, n, _) => n.loc.span,
+            Self::DuplicateAnnotations(_, n, _) => n.loc.as_ref().map(|loc| loc.span),
         }
     }
 }
@@ -139,17 +138,19 @@ impl ParseError {
 
 impl ParseError {
     /// Extract a primary source span locating the error.
-    pub fn primary_source_span(&self) -> SourceSpan {
+    pub fn primary_source_span(&self) -> Option<SourceSpan> {
         match &self.err {
-            OwnedRawParseError::InvalidToken { location } => SourceSpan::from(*location),
-            OwnedRawParseError::UnrecognizedEof { location, .. } => SourceSpan::from(*location),
+            OwnedRawParseError::InvalidToken { location } => Some(SourceSpan::from(*location)),
+            OwnedRawParseError::UnrecognizedEof { location, .. } => {
+                Some(SourceSpan::from(*location))
+            }
             OwnedRawParseError::UnrecognizedToken {
                 token: (token_start, _, token_end),
                 ..
-            } => SourceSpan::from(*token_start..*token_end),
+            } => Some(SourceSpan::from(*token_start..*token_end)),
             OwnedRawParseError::ExtraToken {
                 token: (token_start, _, token_end),
-            } => SourceSpan::from(*token_start..*token_end),
+            } => Some(SourceSpan::from(*token_start..*token_end)),
             OwnedRawParseError::User { error } => error.primary_source_span(),
         }
     }
@@ -184,34 +185,43 @@ impl Diagnostic for ParseError {
     fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
         let primary_source_span = self.primary_source_span();
         match &self.err {
-            OwnedRawParseError::InvalidToken { .. } => Some(Box::new(std::iter::once(
-                LabeledSpan::underline(primary_source_span),
-            ))),
+            OwnedRawParseError::InvalidToken { .. } => primary_source_span
+                .map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
             OwnedRawParseError::UnrecognizedEof { expected, .. } => {
-                Some(Box::new(std::iter::once(LabeledSpan::new_with_span(
-                    expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
-                    primary_source_span,
-                ))))
+                primary_source_span.map(|span| {
+                    Box::new(std::iter::once(LabeledSpan::new_with_span(
+                        expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
+                        span,
+                    ))) as _
+                })
             }
             OwnedRawParseError::UnrecognizedToken { expected, .. } => {
-                Some(Box::new(std::iter::once(LabeledSpan::new_with_span(
-                    expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
-                    primary_source_span,
-                ))))
+                primary_source_span.map(|span| {
+                    Box::new(std::iter::once(LabeledSpan::new_with_span(
+                        expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
+                        span,
+                    ))) as _
+                })
             }
-            OwnedRawParseError::ExtraToken { .. } => Some(Box::new(std::iter::once(
-                LabeledSpan::underline(primary_source_span),
-            ))),
+            OwnedRawParseError::ExtraToken { .. } => primary_source_span
+                .map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
             OwnedRawParseError::User {
                 error: UserError::DuplicateAnnotations(_, n1, n2),
-            } => Some(Box::new(
-                std::iter::once(n1.loc.span)
-                    .chain(std::iter::once(n2.loc.span))
-                    .map(LabeledSpan::underline),
-            )),
-            OwnedRawParseError::User { .. } => Some(Box::new(std::iter::once(
-                LabeledSpan::underline(primary_source_span),
-            ))),
+            } => {
+                let spans: Vec<_> = [&n1.loc, &n2.loc]
+                    .into_iter()
+                    .filter_map(|opt_loc| opt_loc.as_ref())
+                    .map(|loc| LabeledSpan::underline(loc.span))
+                    .collect();
+
+                if spans.is_empty() {
+                    None
+                } else {
+                    Some(Box::new(spans.into_iter()))
+                }
+            }
+            OwnedRawParseError::User { .. } => primary_source_span
+                .map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
         }
     }
 }
@@ -439,7 +449,11 @@ pub enum ToJsonSchemaError {
 }
 
 impl ToJsonSchemaError {
-    pub(crate) fn duplicate_context(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
+    pub(crate) fn duplicate_context(
+        name: &impl ToSmolStr,
+        loc1: Option<Loc>,
+        loc2: Option<Loc>,
+    ) -> Self {
         Self::DuplicateContext(DuplicateContext {
             name: name.to_smolstr(),
             loc1,
@@ -447,7 +461,11 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn duplicate_decls(decl: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
+    pub(crate) fn duplicate_decls(
+        decl: &impl ToSmolStr,
+        loc1: Option<Loc>,
+        loc2: Option<Loc>,
+    ) -> Self {
         Self::DuplicateDeclarations(DuplicateDeclarations {
             decl: decl.to_smolstr(),
             loc1,
@@ -467,7 +485,11 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn duplicate_principal(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
+    pub(crate) fn duplicate_principal(
+        name: &impl ToSmolStr,
+        loc1: Option<Loc>,
+        loc2: Option<Loc>,
+    ) -> Self {
         Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
             name: name.to_smolstr(),
             kind: PR::Principal,
@@ -476,7 +498,11 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn duplicate_resource(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self {
+    pub(crate) fn duplicate_resource(
+        name: &impl ToSmolStr,
+        loc1: Option<Loc>,
+        loc2: Option<Loc>,
+    ) -> Self {
         Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
             name: name.to_smolstr(),
             kind: PR::Resource,
@@ -485,7 +511,7 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn no_principal(name: &impl ToSmolStr, name_loc: Loc) -> Self {
+    pub(crate) fn no_principal(name: &impl ToSmolStr, name_loc: Option<Loc>) -> Self {
         Self::NoPrincipalOrResource(NoPrincipalOrResource {
             kind: PR::Principal,
             name: name.to_smolstr(),
@@ -494,7 +520,7 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn no_resource(name: &impl ToSmolStr, name_loc: Loc) -> Self {
+    pub(crate) fn no_resource(name: &impl ToSmolStr, name_loc: Option<Loc>) -> Self {
         Self::NoPrincipalOrResource(NoPrincipalOrResource {
             kind: PR::Resource,
             name: name.to_smolstr(),
@@ -503,7 +529,11 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn empty_principal(name: &impl ToSmolStr, name_loc: Loc, loc: Loc) -> Self {
+    pub(crate) fn empty_principal(
+        name: &impl ToSmolStr,
+        name_loc: Option<Loc>,
+        loc: Option<Loc>,
+    ) -> Self {
         Self::NoPrincipalOrResource(NoPrincipalOrResource {
             kind: PR::Principal,
             name: name.to_smolstr(),
@@ -512,7 +542,11 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn empty_resource(name: &impl ToSmolStr, name_loc: Loc, loc: Loc) -> Self {
+    pub(crate) fn empty_resource(
+        name: &impl ToSmolStr,
+        name_loc: Option<Loc>,
+        loc: Option<Loc>,
+    ) -> Self {
         Self::NoPrincipalOrResource(NoPrincipalOrResource {
             kind: PR::Resource,
             name: name.to_smolstr(),
@@ -521,14 +555,14 @@ impl ToJsonSchemaError {
         })
     }
 
-    pub(crate) fn reserved_name(name: &impl ToSmolStr, loc: Loc) -> Self {
+    pub(crate) fn reserved_name(name: &impl ToSmolStr, loc: Option<Loc>) -> Self {
         Self::ReservedName(ReservedName {
             name: name.to_smolstr(),
             loc,
         })
     }
 
-    pub(crate) fn reserved_keyword(keyword: &impl ToSmolStr, loc: Loc) -> Self {
+    pub(crate) fn reserved_keyword(keyword: &impl ToSmolStr, loc: Option<Loc>) -> Self {
         Self::ReservedSchemaKeyword(ReservedSchemaKeyword {
             keyword: keyword.to_smolstr(),
             loc,
@@ -540,11 +574,11 @@ impl ToJsonSchemaError {
 #[error("this uses a reserved schema keyword: `{keyword}`")]
 pub struct ReservedSchemaKeyword {
     keyword: SmolStr,
-    loc: Loc,
+    loc: Option<Loc>,
 }
 
 impl Diagnostic for ReservedSchemaKeyword {
-    impl_diagnostic_from_source_loc_field!(loc);
+    impl_diagnostic_from_source_loc_opt_field!(loc);
 
     fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
         Some(Box::new("Keywords such as `entity`, `extension`, `set` and `record` cannot be used as common type names"))
@@ -555,11 +589,11 @@ impl Diagnostic for ReservedSchemaKeyword {
 #[error("use of the reserved `__cedar` namespace")]
 pub struct ReservedName {
     name: SmolStr,
-    loc: Loc,
+    loc: Option<Loc>,
 }
 
 impl Diagnostic for ReservedName {
-    impl_diagnostic_from_source_loc_field!(loc);
+    impl_diagnostic_from_source_loc_opt_field!(loc);
 
     fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
         Some(Box::new(
@@ -572,11 +606,11 @@ impl Diagnostic for ReservedName {
 #[error("unknown type name: `{name}`")]
 pub struct UnknownTypeName {
     name: SmolStr,
-    loc: Loc,
+    loc: Option<Loc>,
 }
 
 impl Diagnostic for UnknownTypeName {
-    impl_diagnostic_from_source_loc_field!(loc);
+    impl_diagnostic_from_source_loc_opt_field!(loc);
 
     fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
         let msg = format!(
@@ -592,12 +626,12 @@ impl Diagnostic for UnknownTypeName {
 pub struct DuplicatePrincipalOrResource {
     name: SmolStr,
     kind: PR,
-    loc1: Loc,
-    loc2: Loc,
+    loc1: Option<Loc>,
+    loc2: Option<Loc>,
 }
 
 impl Diagnostic for DuplicatePrincipalOrResource {
-    impl_diagnostic_from_two_source_loc_fields!(loc1, loc2);
+    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
 
     fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
         let msg = format!("Actions may only have a single {kind} declaration, but a {kind} declaration may specify a list of entity types like `{kind}: [X, Y, Z]`", kind=self.kind);
@@ -609,12 +643,12 @@ impl Diagnostic for DuplicatePrincipalOrResource {
 #[error("duplicate context declaration in action `{name}`")]
 pub struct DuplicateContext {
     name: SmolStr,
-    loc1: Loc,
-    loc2: Loc,
+    loc1: Option<Loc>,
+    loc2: Option<Loc>,
 }
 
 impl Diagnostic for DuplicateContext {
-    impl_diagnostic_from_two_source_loc_fields!(loc1, loc2);
+    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
 
     fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
         Some(Box::new(
@@ -626,12 +660,12 @@ impl Diagnostic for DuplicateContext {
 #[error("`{decl}` is declared twice")]
 pub struct DuplicateDeclarations {
     decl: SmolStr,
-    loc1: Loc,
-    loc2: Loc,
+    loc1: Option<Loc>,
+    loc2: Option<Loc>,
 }
 
 impl Diagnostic for DuplicateDeclarations {
-    impl_diagnostic_from_two_source_loc_fields!(loc1, loc2);
+    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Error)]
@@ -644,7 +678,7 @@ pub struct NoPrincipalOrResource {
     name: SmolStr,
     missing_or_empty: MissingOrEmpty,
     /// Loc of the action name
-    name_loc: Loc,
+    name_loc: Option<Loc>,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -654,7 +688,7 @@ enum MissingOrEmpty {
     /// The declaration was present but defined as `[]`
     Empty {
         /// `Loc` of the declaration
-        loc: Loc,
+        loc: Option<Loc>,
     },
 }
 
@@ -663,26 +697,31 @@ pub const NO_PR_HELP_MSG: &str =
 
 impl Diagnostic for NoPrincipalOrResource {
     fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-        Some(&self.name_loc.src as &dyn miette::SourceCode)
+        self.name_loc
+            .as_ref()
+            .map(|l| &l.src as &dyn miette::SourceCode)
     }
 
     fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
         match &self.missing_or_empty {
-            MissingOrEmpty::Missing => {
-                // just the action name
-                Some(Box::new(std::iter::once(miette::LabeledSpan::underline(
-                    self.name_loc.span,
-                ))))
-            }
+            MissingOrEmpty::Missing => self.name_loc.as_ref().map(|loc| {
+                Box::new(std::iter::once(miette::LabeledSpan::underline(loc.span))) as _
+            }),
             MissingOrEmpty::Empty { loc } => {
                 // also underline the bad declaration
-                let action_name = miette::LabeledSpan::new_with_span(
-                    Some("for this action".into()),
-                    self.name_loc.span,
-                );
-                let decl =
-                    miette::LabeledSpan::new_with_span(Some("must not be `[]`".into()), loc.span);
-                Some(Box::new([action_name, decl].into_iter()))
+                let action_name = self.name_loc.as_ref().map(|loc| {
+                    miette::LabeledSpan::new_with_span(Some("for this action".into()), loc.span)
+                });
+                let decl = loc.as_ref().map(|loc| {
+                    miette::LabeledSpan::new_with_span(Some("must not be `[]`".into()), loc.span)
+                });
+                let spans: Vec<_> = [action_name, decl].into_iter().flatten().collect();
+
+                if spans.is_empty() {
+                    None
+                } else {
+                    Some(Box::new(spans.into_iter()))
+                }
             }
         }
     }
@@ -707,7 +746,7 @@ impl Diagnostic for DuplicateNamespace {
 
 /// Error subtypes for [`SchemaWarning`]
 pub mod schema_warnings {
-    use cedar_policy_core::{impl_diagnostic_from_source_loc_field, parser::Loc};
+    use cedar_policy_core::{impl_diagnostic_from_source_loc_opt_field, parser::Loc};
     use miette::Diagnostic;
     use smol_str::SmolStr;
     use thiserror::Error;
@@ -721,11 +760,11 @@ pub mod schema_warnings {
     #[error("The name `{name}` shadows a builtin Cedar name. You'll have to refer to the builtin as `__cedar::{name}`.")]
     pub struct ShadowsBuiltinWarning {
         pub(crate) name: SmolStr,
-        pub(crate) loc: Loc,
+        pub(crate) loc: Option<Loc>,
     }
 
     impl Diagnostic for ShadowsBuiltinWarning {
-        impl_diagnostic_from_source_loc_field!(loc);
+        impl_diagnostic_from_source_loc_opt_field!(loc);
 
         fn severity(&self) -> Option<miette::Severity> {
             Some(miette::Severity::Warning)
@@ -741,24 +780,32 @@ pub mod schema_warnings {
     #[error("The common type name {name} shadows an entity name")]
     pub struct ShadowsEntityWarning {
         pub(crate) name: SmolStr,
-        pub(crate) entity_loc: Loc,
-        pub(crate) common_loc: Loc,
+        pub(crate) entity_loc: Option<Loc>,
+        pub(crate) common_loc: Option<Loc>,
     }
 
     impl Diagnostic for ShadowsEntityWarning {
         fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
-            Some(Box::new(
-                std::iter::once(&self.entity_loc)
-                    .chain(std::iter::once(&self.common_loc))
-                    .map(miette::LabeledSpan::underline),
-            ))
+            let spans: Vec<_> = [&self.entity_loc, &self.common_loc]
+                .into_iter()
+                .filter_map(|loc| loc.as_ref())
+                .map(miette::LabeledSpan::underline)
+                .collect();
+
+            if spans.is_empty() {
+                None
+            } else {
+                Some(Box::new(spans.into_iter()))
+            }
         }
 
         fn source_code(&self) -> Option<&dyn miette::SourceCode> {
             // just have to pick one; we assume `entity_loc` and `common_loc`
             // have the same source code.
             // if that isn't true we'll have a confusing underline.
-            Some(&self.entity_loc.src as _)
+            self.entity_loc
+                .as_ref()
+                .map(|loc| &loc.src as &dyn miette::SourceCode)
         }
 
         fn severity(&self) -> Option<miette::Severity> {
diff --git a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop
index 49406367ef..f4fd4f15ac 100644
--- a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop
+++ b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop
@@ -203,7 +203,7 @@ AppDecls: Node<NonEmpty<Node<AppDecl>>> = {
 // Type := PRIMTYPE | Path | SetType | RecType
 pub Type: Node<SType> = {
     <p:Path>
-        => { let loc = p.loc().clone(); Node::with_source_loc(SType::Ident(p), loc) },
+        => { let loc = p.loc().cloned(); Node::with_maybe_source_loc(SType::Ident(p), loc) },
     <l:@L> SET "<" <t:Type> ">" <r:@R>
         => Node::with_source_loc(SType::Set(Box::new(t)), Loc::new(l..r, Arc::clone(src))),
     <l:@L> "{" <ds:AttrDecls?> "}" <r:@R>
@@ -228,8 +228,8 @@ Comma<E>: Vec<E> = {
 
 // IDENT := ['_''a'-'z''A'-'Z']['_''a'-'z''A'-'Z''0'-'9']* - RESERVED
 Ident: Node<Id> = {
-    <id: AnyIdent> =>? Id::from_str(id.node.as_ref()).map(|i| Node::with_source_loc(i, id.loc.clone())).map_err(|err : cedar_policy_core::parser::err::ParseErrors| ParseError::User {
-        error: UserError::ReservedIdentifierUsed(Node::with_source_loc(id.node.to_smolstr(), id.loc.clone()))
+    <id: AnyIdent> =>? Id::from_str(id.node.as_ref()).map(|i| Node::with_maybe_source_loc(i, id.loc.clone())).map_err(|err : cedar_policy_core::parser::err::ParseErrors| ParseError::User {
+        error: UserError::ReservedIdentifierUsed(Node::with_maybe_source_loc(id.node.to_smolstr(), id.loc.clone()))
     }),
 }
 
diff --git a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
index 1f265cd926..d8d9c184cd 100644
--- a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
+++ b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs
@@ -112,7 +112,7 @@ pub fn cedar_type_to_json_type(ty: Node<Type>) -> json_schema::Type<RawName> {
     };
     json_schema::Type::Type {
         ty: variant,
-        loc: Some(ty.loc),
+        loc: ty.loc,
     }
 }
 
@@ -163,7 +163,7 @@ fn convert_namespace(
         .map(|p| {
             let internal_name = RawName::from(p.clone()).qualify_with(None); // namespace names are always written already-fully-qualified in the Cedar schema syntax
             Name::try_from(internal_name)
-                .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), p.loc().clone()))
+                .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), p.loc().cloned()))
         })
         .transpose()?;
     let def = namespace.try_into()?;
@@ -203,7 +203,7 @@ impl TryFrom<Annotated<Namespace>> for json_schema::NamespaceDefinition<RawName>
                     CommonType {
                         ty: cedar_type_to_json_type(decl.data.node.def),
                         annotations: decl.annotations.into(),
-                        loc: Some(decl.data.loc),
+                        loc: decl.data.loc,
                     },
                 ))
             })
@@ -231,7 +231,7 @@ fn convert_action_decl(
     } = a.data.node;
     // Create the internal type from the 'applies_to' clause and 'member_of'
     let applies_to = app_decls
-        .map(|decls| convert_app_decls(&names.first().node, &names.first().loc, decls))
+        .map(|decls| convert_app_decls(&names.first().node, names.first().loc.as_ref(), decls))
         .transpose()?
         .unwrap_or_else(|| json_schema::ApplySpec {
             resource_types: vec![],
@@ -246,9 +246,9 @@ fn convert_action_decl(
             applies_to: Some(applies_to.clone()),
             member_of: member_of.clone(),
             annotations: a.annotations.clone().into(),
-            loc: Some(a.data.loc.clone()),
+            loc: a.data.loc.clone(),
             #[cfg(feature = "extended-schema")]
-            defn_loc: Some(name.loc),
+            defn_loc: name.loc,
         };
         (name.node, ty)
     }))
@@ -264,7 +264,7 @@ fn convert_qual_name(qn: Node<QualName>) -> json_schema::ActionEntityUID<RawName
 /// * `name_loc` - The location of that first name
 fn convert_app_decls(
     name: &SmolStr,
-    name_loc: &Loc,
+    name_loc: Option<&Loc>,
     decls: Node<NonEmpty<Node<AppDecl>>>,
 ) -> Result<json_schema::ApplySpec<RawName>, ToJsonSchemaErrors> {
     // Split AppDecl's into context/principal/resource decls
@@ -288,7 +288,7 @@ fn convert_app_decls(
                     .into());
                 }
                 None => {
-                    context = Some(Node::with_source_loc(
+                    context = Some(Node::with_maybe_source_loc(
                         convert_context_decl(context_decl),
                         loc,
                     ));
@@ -316,12 +316,15 @@ fn convert_app_decls(
                 }
                 None => match entity_tys {
                     None => {
-                        return Err(
-                            ToJsonSchemaError::empty_principal(name, name_loc.clone(), loc).into(),
+                        return Err(ToJsonSchemaError::empty_principal(
+                            name,
+                            name_loc.cloned(),
+                            loc,
                         )
+                        .into())
                     }
                     Some(entity_tys) => {
-                        principal_types = Some(Node::with_source_loc(
+                        principal_types = Some(Node::with_maybe_source_loc(
                             entity_tys.iter().map(|n| n.clone().into()).collect(),
                             loc,
                         ))
@@ -347,11 +350,11 @@ fn convert_app_decls(
                 None => match entity_tys {
                     None => {
                         return Err(
-                            ToJsonSchemaError::empty_resource(name, name_loc.clone(), loc).into(),
+                            ToJsonSchemaError::empty_resource(name, name_loc.cloned(), loc).into(),
                         )
                     }
                     Some(entity_tys) => {
-                        resource_types = Some(Node::with_source_loc(
+                        resource_types = Some(Node::with_maybe_source_loc(
                             entity_tys.iter().map(|n| n.clone().into()).collect(),
                             loc,
                         ))
@@ -363,10 +366,10 @@ fn convert_app_decls(
     Ok(json_schema::ApplySpec {
         resource_types: resource_types
             .map(|node| node.node)
-            .ok_or_else(|| ToJsonSchemaError::no_resource(&name, name_loc.clone()))?,
+            .ok_or_else(|| ToJsonSchemaError::no_resource(&name, name_loc.cloned()))?,
         principal_types: principal_types
             .map(|node| node.node)
-            .ok_or_else(|| ToJsonSchemaError::no_principal(&name, name_loc.clone()))?,
+            .ok_or_else(|| ToJsonSchemaError::no_principal(&name, name_loc.cloned()))?,
         context: context.map(|c| c.node).unwrap_or_default(),
     })
 }
@@ -401,7 +404,7 @@ fn convert_entity_decl(
             }
         },
         annotations: e.annotations.into(),
-        loc: Some(e.data.loc.clone()),
+        loc: e.data.loc,
     };
 
     // Then map over all of the bound names
@@ -423,7 +426,7 @@ fn convert_attr_decls(
             attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
             additional_attributes: false,
         }),
-        loc: Some(attrs.loc),
+        loc: attrs.loc,
     })
 }
 
@@ -433,7 +436,7 @@ fn convert_context_decl(
 ) -> json_schema::AttributesOrContext<RawName> {
     json_schema::AttributesOrContext(match decl {
         Either::Left(p) => json_schema::Type::CommonTypeRef {
-            loc: Some(p.loc().clone()),
+            loc: p.loc().cloned(),
             type_name: p.into(),
         },
         Either::Right(attrs) => json_schema::Type::Type {
@@ -441,7 +444,7 @@ fn convert_context_decl(
                 attributes: attrs.node.into_iter().map(convert_attr_decl).collect(),
                 additional_attributes: false,
             }),
-            loc: Some(attrs.loc),
+            loc: attrs.loc,
         },
     })
 }
@@ -457,7 +460,7 @@ fn convert_attr_decl(
             required: attr.node.data.required,
             annotations: attr.node.annotations.into(),
             #[cfg(feature = "extended-schema")]
-            loc: Some(attr.loc),
+            loc: attr.loc,
         },
     )
 }
@@ -505,7 +508,7 @@ impl NamespaceRecord {
             .map(|n| {
                 let internal_name = RawName::from(n.clone()).qualify_with(None); // namespace names are already fully-qualified
                 Name::try_from(internal_name)
-                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), n.loc().clone()))
+                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), n.loc().cloned()))
             })
             .transpose()?;
         let (entities, actions, types) = partition_decls(&namespace.decls);
@@ -533,7 +536,7 @@ impl NamespaceRecord {
         let record = NamespaceRecord {
             entities,
             common_types,
-            loc: namespace.name.as_ref().map(|n| n.loc().clone()),
+            loc: namespace.name.as_ref().and_then(|n| n.loc()).cloned(),
         };
 
         Ok((ns, record))


Reviews:
user: login: cdisselkoen
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T19:46:14Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:00:54Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:19:21Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:21:31Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:22:38Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:24:46Z
user: login: chaluli
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:31:13Z
user: login: cdisselkoen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:33:46Z
user: login: cdisselkoen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:34:21Z
user: login: john-h-kastner-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:44:53Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T20:58:13Z
user: login: john-h-kastner-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T21:04:51Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T21:05:19Z
user: login: shaobo-he-aws
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T21:16:37Z
user: login: adpaco-aws
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T21:37:23Z
user: login: john-h-kastner-aws
body: Looks good then. We'll address the opened issue before releasing fast parsing 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T21:43:44Z

Review comments:
diff_hunk: @@ -570,11 +570,11 @@ impl From<ReservedNameError> for ParseError {
     fn from(value: ReservedNameError) -> Self {
         ParseError::ToAST(ToASTError::new(
             value.clone().into(),
-            match &value.0.loc {
-                Some(loc) => loc.clone(),
+            match value.0.loc {
+                loc @ Some(_) => loc,
                 None => {
                     let name_str = value.0.to_string();
-                    Loc::new(0..(name_str.len()), name_str.into())
+                    Some(Loc::new(0..(name_str.len()), name_str.into()))
                 }
path: cedar-policy-core/src/ast/name.rs
user: login: cdisselkoen
body: if you want, instead of `match` this could be `value.0.loc.or_else(|| ...)`
created_at: 2025-05-16T19:35:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,13 +21,14 @@
 macro_rules! impl_diagnostic_from_source_loc_field {
     ( $i:ident ) => {
         fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-            Some(&self.$i.src as &dyn miette::SourceCode)
+            self.$i.as_ref().map(|l| &l.src as &dyn miette::SourceCode)
path: cedar-policy-core/src/error_macros.rs
user: login: cdisselkoen
body: shouldn't we just use the `impl_diagnostic_from_source_loc_opt_field` macro (below) instead of changing this one?

If we do change this one, let's update the comment to indicate `$i` should be type `Option<Loc>`
created_at: 2025-05-16T19:36:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -425,7 +427,7 @@ impl Node<Option<cst::Policy>> {
                         slot.clone(),
                         if is_when { "when" } else { "unless" },
                     ),
-                    slot.loc.unwrap_or_else(|| c.loc.clone()),
+                    slot.loc.or_else(|| self.loc.clone()),
path: cedar-policy-core/src/parser/cst_to_ast.rs
user: login: cdisselkoen
body: intentional change from `c.loc` to `self.loc`?
created_at: 2025-05-16T19:39:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -523,35 +540,50 @@ impl cst::PolicyImpl {
     ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
         // Tracks where the last variable in the scope ended. We'll point to
         // this position to indicate where to fill in vars if we're missing one.
-        let mut end_of_last_var = self.effect.loc.end();
+        let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
 
         let mut vars = self.variables.iter();
         let maybe_principal = if let Some(scope1) = vars.next() {
-            end_of_last_var = scope1.loc.end();
-            scope1.to_principal_constraint(TolerantAstSetting::Tolerant)
+            end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
+            scope1.to_principal_constraint(TolerantAstSetting::NotTolerant)
path: cedar-policy-core/src/parser/cst_to_ast.rs
user: login: cdisselkoen
body: curious why the change from `Tolerant` to `NotTolerant` (also a few places below)
created_at: 2025-05-16T19:40:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -523,35 +540,50 @@ impl cst::PolicyImpl {
     ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
         // Tracks where the last variable in the scope ended. We'll point to
         // this position to indicate where to fill in vars if we're missing one.
-        let mut end_of_last_var = self.effect.loc.end();
+        let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
 
         let mut vars = self.variables.iter();
         let maybe_principal = if let Some(scope1) = vars.next() {
-            end_of_last_var = scope1.loc.end();
-            scope1.to_principal_constraint(TolerantAstSetting::Tolerant)
+            end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
+            scope1.to_principal_constraint(TolerantAstSetting::NotTolerant)
path: cedar-policy-core/src/parser/cst_to_ast.rs
user: login: adpaco-aws
body: This was a copy-paste mistake 😢 
created_at: 2025-05-16T20:00:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -425,7 +427,7 @@ impl Node<Option<cst::Policy>> {
                         slot.clone(),
                         if is_when { "when" } else { "unless" },
                     ),
-                    slot.loc.unwrap_or_else(|| c.loc.clone()),
+                    slot.loc.or_else(|| self.loc.clone()),
path: cedar-policy-core/src/parser/cst_to_ast.rs
user: login: adpaco-aws
body: Not really, good catch.
created_at: 2025-05-16T20:19:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -570,11 +570,11 @@ impl From<ReservedNameError> for ParseError {
     fn from(value: ReservedNameError) -> Self {
         ParseError::ToAST(ToASTError::new(
             value.clone().into(),
-            match &value.0.loc {
-                Some(loc) => loc.clone(),
+            match value.0.loc {
+                loc @ Some(_) => loc,
                 None => {
                     let name_str = value.0.to_string();
-                    Loc::new(0..(name_str.len()), name_str.into())
+                    Some(Loc::new(0..(name_str.len()), name_str.into()))
                 }
path: cedar-policy-core/src/ast/name.rs
user: login: adpaco-aws
body: I did this then decided against it because then you need to compute `name_str` twice (since you're in the closure) or pre-compute it. Open to suggestions if you know how to make it clean.
created_at: 2025-05-16T20:21:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,13 +21,14 @@
 macro_rules! impl_diagnostic_from_source_loc_field {
     ( $i:ident ) => {
         fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-            Some(&self.$i.src as &dyn miette::SourceCode)
+            self.$i.as_ref().map(|l| &l.src as &dyn miette::SourceCode)
path: cedar-policy-core/src/error_macros.rs
user: login: adpaco-aws
body: You're absolutely right. I didn't stop to read this code that much.
created_at: 2025-05-16T20:22:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,13 +21,14 @@
 macro_rules! impl_diagnostic_from_source_loc_field {
     ( $i:ident ) => {
         fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-            Some(&self.$i.src as &dyn miette::SourceCode)
+            self.$i.as_ref().map(|l| &l.src as &dyn miette::SourceCode)
path: cedar-policy-core/src/error_macros.rs
user: login: adpaco-aws
body: That said, I'm wondering if we should keep it. We're not using it anymore, but it's annotated with `#[macro_export]` so I'm guessing that prevents the unused warning?
created_at: 2025-05-16T20:24:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -570,11 +570,11 @@ impl From<ReservedNameError> for ParseError {
     fn from(value: ReservedNameError) -> Self {
         ParseError::ToAST(ToASTError::new(
             value.clone().into(),
-            match &value.0.loc {
-                Some(loc) => loc.clone(),
+            match value.0.loc {
+                loc @ Some(_) => loc,
                 None => {
                     let name_str = value.0.to_string();
-                    Loc::new(0..(name_str.len()), name_str.into())
+                    Some(Loc::new(0..(name_str.len()), name_str.into()))
                 }
path: cedar-policy-core/src/ast/name.rs
user: login: cdisselkoen
body: ```
value.0.loc.or_else(|| {
  let name_str = value.0.to_string();
  Some(Loc::new(0..(name_str.len()), name_str.into()))
});
```
created_at: 2025-05-16T20:33:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,13 +21,14 @@
 macro_rules! impl_diagnostic_from_source_loc_field {
     ( $i:ident ) => {
         fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-            Some(&self.$i.src as &dyn miette::SourceCode)
+            self.$i.as_ref().map(|l| &l.src as &dyn miette::SourceCode)
path: cedar-policy-core/src/error_macros.rs
user: login: cdisselkoen
body: Yeah. Is it used in any other crates we control?
created_at: 2025-05-16T20:34:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,8 +73,14 @@ pub fn parse_policyset_and_also_return_policy_text(
     // `PolicyId` present as a key in this map.
     let texts = cst
         .with_generated_policyids()
-        .expect("shouldn't be None since parse_policies() and to_policyset() didn't return Err")
-        .map(|(id, policy)| (id, &text[policy.loc.start()..policy.loc.end()]))
+        .expect("shouldn't be `None` since `parse_policies` and `to_policyset` didn't return `Err`")
+        .map(|(id, policy)| {
+            if let Some(loc) = &policy.loc {
+                (id, &text[loc.start()..loc.end()])
+            } else {
+                (id, "<undefined>")
path: cedar-policy-core/src/parser.rs
user: login: john-h-kastner-aws
body: Could this mess up the policy `Display` impl in `cedar-policy`? I think this text might be stored and returned as the lossless representation of polices parsed from text. 
created_at: 2025-05-16T20:41:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,8 +73,14 @@ pub fn parse_policyset_and_also_return_policy_text(
     // `PolicyId` present as a key in this map.
     let texts = cst
         .with_generated_policyids()
-        .expect("shouldn't be None since parse_policies() and to_policyset() didn't return Err")
-        .map(|(id, policy)| (id, &text[policy.loc.start()..policy.loc.end()]))
+        .expect("shouldn't be `None` since `parse_policies` and `to_policyset` didn't return `Err`")
+        .map(|(id, policy)| {
+            if let Some(loc) = &policy.loc {
+                (id, &text[loc.start()..loc.end()])
+            } else {
+                (id, "<undefined>")
path: cedar-policy-core/src/parser.rs
user: login: adpaco-aws
body: Sorry, I don't see how this and the `Display` impl are related... the impl relies on `LosslessPolicy` which I don't see using this API. But in general, the alternative here is to throw an error if a location is not found. Would you prefer that?
created_at: 2025-05-16T20:58:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,8 +73,14 @@ pub fn parse_policyset_and_also_return_policy_text(
     // `PolicyId` present as a key in this map.
     let texts = cst
         .with_generated_policyids()
-        .expect("shouldn't be None since parse_policies() and to_policyset() didn't return Err")
-        .map(|(id, policy)| (id, &text[policy.loc.start()..policy.loc.end()]))
+        .expect("shouldn't be `None` since `parse_policies` and `to_policyset` didn't return `Err`")
+        .map(|(id, policy)| {
+            if let Some(loc) = &policy.loc {
+                (id, &text[loc.start()..loc.end()])
+            } else {
+                (id, "<undefined>")
path: cedar-policy-core/src/parser.rs
user: login: john-h-kastner-aws
body: This bit. Looks like if `loc` is `None` the `LosslessPolicy` will contain `<undefined>`

https://github.com/cedar-policy/cedar/blob/40be99c42cd6e102515b28db6bfb4c0b1dfc9421/cedar-policy/src/api.rs#L2234-L2243

I think what we'll need is to add a `None` variant to `LosslessPolicy` for the fast parsing path. So, you won't get lossless roundtripping if you used fast parsing. 
created_at: 2025-05-16T21:04:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,13 +21,14 @@
 macro_rules! impl_diagnostic_from_source_loc_field {
     ( $i:ident ) => {
         fn source_code(&self) -> Option<&dyn miette::SourceCode> {
-            Some(&self.$i.src as &dyn miette::SourceCode)
+            self.$i.as_ref().map(|l| &l.src as &dyn miette::SourceCode)
path: cedar-policy-core/src/error_macros.rs
user: login: adpaco-aws
body: I can't find them anywhere in this repo nor in cedar-spec, so I'm removing it. Also, I was doing the same thing with the other macro... rewriting the non-optional. So I'm removing both non-optional, and using the optional versions only.
created_at: 2025-05-16T21:05:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,8 +73,14 @@ pub fn parse_policyset_and_also_return_policy_text(
     // `PolicyId` present as a key in this map.
     let texts = cst
         .with_generated_policyids()
-        .expect("shouldn't be None since parse_policies() and to_policyset() didn't return Err")
-        .map(|(id, policy)| (id, &text[policy.loc.start()..policy.loc.end()]))
+        .expect("shouldn't be `None` since `parse_policies` and `to_policyset` didn't return `Err`")
+        .map(|(id, policy)| {
+            if let Some(loc) = &policy.loc {
+                (id, &text[loc.start()..loc.end()])
+            } else {
+                (id, "<undefined>")
path: cedar-policy-core/src/parser.rs
user: login: adpaco-aws
body: Opened #1627 for this, we'll expect locations here for now.
created_at: 2025-05-16T21:37:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-16T20:26:53Z
body: 
# Coverage Report
Head Commit: ```0f4d66922a32e472b0a8c646c094adca8d8825b1```

Base Commit: ```40be99c42cd6e102515b28db6bfb4c0b1dfc9421```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/15076618950/artifacts/3142111277)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 90.45%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/expr.rs | :red_circle: | 0/1 | 0.00% | 483
cedar-policy-core/src/ast/name.rs | :yellow_circle: | 3/4 | 75.00% | 577
cedar-policy-core/src/ast/policy.rs | :green_circle: | 1/1 | 100.00% | 
cedar-policy-core/src/error_macros.rs | :green_circle: | 9/10 | 90.00% | 76
cedar-policy-core/src/est.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-core/src/from_normalized_str.rs | :green_circle: | 1/1 | 100.00% | 
cedar-policy-core/src/parser.rs | :green_circle: | 5/6 | 83.33% | 81
cedar-policy-core/src/parser/cst_to_ast.rs | :green_circle: | 173/179 | 96.65% | 371, 383, 430, 2101-2103
cedar-policy-core/src/parser/cst_to_ast/to_ref_or_refs.rs | :green_circle: | 13/13 | 100.00% | 
cedar-policy-core/src/parser/err.rs | :green_circle: | 16/18 | 88.89% | 633, 683
cedar-policy-core/src/parser/node.rs | :red_circle: | 7/10 | 70.00% | 172, 174, 181
cedar-policy-formatter/src/pprint/doc.rs | :green_circle: | 172/176 | 97.73% | 774-777
cedar-policy-formatter/src/pprint/utils.rs | :green_circle: | 21/21 | 100.00% | 
cedar-policy-validator/src/cedar_schema/ast.rs | :green_circle: | 7/7 | 100.00% | 
cedar-policy-validator/src/cedar_schema/err.rs | :red_circle: | 66/102 | 64.71% | 58, 60-62, 64, 144, 154, 189-190, 207-208, 211-219, 221, 224-225, 722, 790-797, 799, 807-809
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 25/25 | 100.00% | 
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.82%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3149/4566 | 68.97% | --
cedar-policy-cli | :red_circle: | 571/972 | 58.74% | --
cedar-policy-core | :green_circle: | 12046/14297 | 84.26% | --
cedar-policy-formatter | :green_circle: | 1034/1166 | 88.68% | --
cedar-policy-validator | :green_circle: | 8872/10347 | 85.74% | --
cedar-wasm | :red_circle: | 0/29 | 0.00% | --
</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T21:22:06Z
body: 
# Coverage Report
Head Commit: ```bc8cd15f77ab1a5dce204076cb3c7597fe46a374```

Base Commit: ```40be99c42cd6e102515b28db6bfb4c0b1dfc9421```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/15077453917/artifacts/3142392676)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 90.30%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/expr.rs | :red_circle: | 0/1 | 0.00% | 483
cedar-policy-core/src/ast/name.rs | :red_circle: | 3/5 | 60.00% | 574-575
cedar-policy-core/src/ast/policy.rs | :green_circle: | 1/1 | 100.00% | 
cedar-policy-core/src/est.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-core/src/from_normalized_str.rs | :green_circle: | 1/1 | 100.00% | 
cedar-policy-core/src/parser.rs | :green_circle: | 5/6 | 83.33% | 81
cedar-policy-core/src/parser/cst_to_ast.rs | :green_circle: | 173/179 | 96.65% | 371, 383, 430, 2101-2103
cedar-policy-core/src/parser/cst_to_ast/to_ref_or_refs.rs | :green_circle: | 13/13 | 100.00% | 
cedar-policy-core/src/parser/err.rs | :green_circle: | 16/18 | 88.89% | 633, 683
cedar-policy-core/src/parser/node.rs | :red_circle: | 7/10 | 70.00% | 172, 174, 181
cedar-policy-formatter/src/pprint/doc.rs | :green_circle: | 172/176 | 97.73% | 774-777
cedar-policy-formatter/src/pprint/utils.rs | :green_circle: | 21/21 | 100.00% | 
cedar-policy-validator/src/cedar_schema/ast.rs | :green_circle: | 7/7 | 100.00% | 
cedar-policy-validator/src/cedar_schema/err.rs | :red_circle: | 66/102 | 64.71% | 57, 59-61, 63, 143, 153, 188-189, 206-207, 210-218, 220, 223-224, 721, 789-796, 798, 806-808
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 25/25 | 100.00% | 
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.88%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3149/4566 | 68.97% | --
cedar-policy-cli | :red_circle: | 571/972 | 58.74% | --
cedar-policy-core | :green_circle: | 12052/14282 | 84.39% | --
cedar-policy-formatter | :green_circle: | 1034/1166 | 88.68% | --
cedar-policy-validator | :green_circle: | 8872/10347 | 85.74% | --
cedar-wasm | :red_circle: | 0/29 | 0.00% | --
</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T21:46:54Z
body: 
# Coverage Report
Head Commit: ```3adf02902d4a53c3715a9b8162d3c1e40ac55b61```

Base Commit: ```40be99c42cd6e102515b28db6bfb4c0b1dfc9421```

[Download the full coverage report.](https://github.com/cedar-policy/cedar/actions/runs/15077821709/artifacts/3142496480)

## Coverage of Added or Modified Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 90.51%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/ast/expr.rs | :red_circle: | 0/1 | 0.00% | 483
cedar-policy-core/src/ast/name.rs | :red_circle: | 3/5 | 60.00% | 574-575
cedar-policy-core/src/ast/policy.rs | :green_circle: | 1/1 | 100.00% | 
cedar-policy-core/src/est.rs | :green_circle: | 2/2 | 100.00% | 
cedar-policy-core/src/from_normalized_str.rs | :green_circle: | 1/1 | 100.00% | 
cedar-policy-core/src/parser.rs | :green_circle: | 8/8 | 100.00% | 
cedar-policy-core/src/parser/cst_to_ast.rs | :green_circle: | 173/179 | 96.65% | 371, 383, 430, 2101-2103
cedar-policy-core/src/parser/cst_to_ast/to_ref_or_refs.rs | :green_circle: | 13/13 | 100.00% | 
cedar-policy-core/src/parser/err.rs | :green_circle: | 16/18 | 88.89% | 633, 683
cedar-policy-core/src/parser/node.rs | :red_circle: | 7/10 | 70.00% | 172, 174, 181
cedar-policy-formatter/src/pprint/doc.rs | :green_circle: | 172/176 | 97.73% | 774-777
cedar-policy-formatter/src/pprint/utils.rs | :green_circle: | 21/21 | 100.00% | 
cedar-policy-validator/src/cedar_schema/ast.rs | :green_circle: | 7/7 | 100.00% | 
cedar-policy-validator/src/cedar_schema/err.rs | :red_circle: | 66/102 | 64.71% | 57, 59-61, 63, 143, 153, 188-189, 206-207, 210-218, 220, 223-224, 721, 789-796, 798, 806-808
cedar-policy-validator/src/cedar_schema/to_json_schema.rs | :green_circle: | 25/25 | 100.00% | 
</details>

## Coverage of All Lines of Rust Code
**Required coverage:** 80.00%

**Actual coverage:** 81.88%

**Status:** PASSED :white_check_mark:

<details>
<summary><b>Details</b></summary>

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3149/4566 | 68.97% | --
cedar-policy-cli | :red_circle: | 571/972 | 58.74% | --
cedar-policy-core | :green_circle: | 12055/14284 | 84.40% | --
cedar-policy-formatter | :green_circle: | 1034/1166 | 88.68% | --
cedar-policy-validator | :green_circle: | 8872/10347 | 85.74% | --
cedar-wasm | :red_circle: | 0/29 | 0.00% | --
</details>

author_association: NONE