Repository: cedar-policy/cedar
Pull request #1611 (2025-05-12 19:19:54)

Bump MSRV to 1.82
## Description of changes

## Issue #, if available

## Checklist for requesting a review

The change in this PR is (choose one, and delete the other options):

- [x] A change "invisible" to users (e.g., documentation, changes to "internal" crates like `cedar-policy-core`, `cedar-validator`, etc.)

I confirm that this PR (choose one, and delete the other options):

- [x] Updates the "Unreleased" section of the CHANGELOG with a description of my change (required for major/minor version bumps).

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.

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.


github-actions[bot]: 
# Coverage Report
Head Commit: ```ad0edf240d0d08a73c46058f3acda5b9be3050f9```

Base Commit: ```e0eadeae8cb9e5abdd21e5a35880d5ddefa759b3```

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

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

**Actual coverage:** 100.00%

**Status:** PASSED :white_check_mark:

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

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|

</details>

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

**Actual coverage:** 81.74%

**Status:** PASSED :white_check_mark:

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

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3111/4528 | 68.71% | --
cedar-policy-cli | :red_circle: | 571/972 | 58.74% | --
cedar-policy-core | :green_circle: | 11974/14209 | 84.27% | --
cedar-policy-formatter | :green_circle: | 914/1043 | 87.63% | --
cedar-policy-validator | :green_circle: | 8841/10307 | 85.78% | --
cedar-wasm | :red_circle: | 0/29 | 0.00% | --
</details>


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index f3a10ff5..a90c297a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,7 +22,7 @@ debug = "line-tables-only"  # this adds more debug symbols/info to the binary th
 # Keys that packages can inherit
 [workspace.package]
 # Check the minimum supported Rust version with `cargo install cargo-msrv && cargo msrv --min 1.X.0` where `X` is something lower than the version noted here (to confirm that versions lower than the one noted here _don't_ work)
-rust-version = "1.81"
+rust-version = "1.82"
 version = "4.4.0"
 homepage = "https://cedarpolicy.com"
 keywords = ["cedar", "authorization", "policy", "security"]
diff --git a/cedar-policy-core/src/ast/policy_set.rs b/cedar-policy-core/src/ast/policy_set.rs
index fc8e979b..c7d67b6a 100644
--- a/cedar-policy-core/src/ast/policy_set.rs
+++ b/cedar-policy-core/src/ast/policy_set.rs
@@ -20,6 +20,7 @@ use super::{
 };
 use itertools::Itertools;
 use miette::Diagnostic;
+use smol_str::format_smolstr;
 use std::collections::{hash_map::Entry, HashMap, HashSet};
 use std::{borrow::Borrow, sync::Arc};
 use thiserror::Error;
@@ -287,10 +288,11 @@ impl PolicySet {
         for (pid, ot) in other_contents {
             if let Some(tt) = this_contents.get(pid) {
                 if tt != ot {
-                    let mut new_pid = PolicyID::from_string(format!("policy{}", start_ind));
+                    let mut new_pid =
+                        PolicyID::from_smolstr(format_smolstr!("policy{}", start_ind));
                     *start_ind += 1;
                     while self.policy_id_is_bound(&new_pid) || other.policy_id_is_bound(&new_pid) {
-                        new_pid = PolicyID::from_string(format!("policy{}", start_ind));
+                        new_pid = PolicyID::from_smolstr(format_smolstr!("policy{}", start_ind));
                         *start_ind += 1;
                     }
                     renaming.insert(pid.clone(), new_pid);
diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs
index b0adada2..057ae417 100644
--- a/cedar-policy-core/src/ast/request.rs
+++ b/cedar-policy-core/src/ast/request.rs
@@ -22,7 +22,7 @@ use crate::extensions::Extensions;
 use crate::parser::Loc;
 use miette::Diagnostic;
 use serde::{Deserialize, Serialize};
-use smol_str::SmolStr;
+use smol_str::{SmolStr, ToSmolStr};
 use std::collections::{BTreeMap, HashMap};
 use std::sync::Arc;
 use thiserror::Error;
@@ -93,7 +93,7 @@ impl EntityUIDEntry {
                 Value::new(Arc::unwrap_or_clone(Arc::clone(euid)), loc.clone()).into()
             }
             EntityUIDEntry::Unknown { ty: None, loc } => {
-                Expr::unknown(Unknown::new_untyped(var.to_string()))
+                Expr::unknown(Unknown::new_untyped(var.to_smolstr()))
                     .with_maybe_source_loc(loc.clone())
                     .into()
             }
@@ -101,7 +101,7 @@ impl EntityUIDEntry {
                 ty: Some(known_type),
                 loc,
             } => Expr::unknown(Unknown::new_with_type(
-                var.to_string(),
+                var.to_smolstr(),
                 super::Type::Entity {
                     ty: known_type.clone(),
                 },
diff --git a/cedar-policy-core/src/entities.rs b/cedar-policy-core/src/entities.rs
index c774003b..11e93f6e 100644
--- a/cedar-policy-core/src/entities.rs
+++ b/cedar-policy-core/src/entities.rs
@@ -37,6 +37,8 @@ pub use json::{
 
 use conformance::EntitySchemaConformanceChecker;
 use err::*;
+#[cfg(feature = "partial-eval")]
+use smol_str::ToSmolStr;
 
 /// Represents an entity hierarchy, and allows looking up `Entity` objects by
 /// UID.
@@ -96,7 +98,7 @@ impl Entities {
                 Mode::Concrete => Dereference::NoSuchEntity,
                 #[cfg(feature = "partial-eval")]
                 Mode::Partial => Dereference::Residual(Expr::unknown(Unknown::new_with_type(
-                    format!("{uid}"),
+                    uid.to_smolstr(),
                     Type::Entity {
                         ty: uid.entity_type().clone(),
                     },
@@ -2134,7 +2136,7 @@ mod entities_tests {
     #[test]
     fn test_len() {
         let (e0, e1, e2, e3) = test_entities();
-        let v = vec![e0.clone(), e1.clone(), e2.clone(), e3.clone()];
+        let v = vec![e0, e1, e2, e3];
         let es = Entities::from_entities(
             v,
             None::<&NoEntitiesSchema>,
@@ -2301,10 +2303,10 @@ mod entities_tests {
         let fid = EntityUID::with_eid("F");
         let mut f = Entity::with_uid(fid.clone());
         f.add_parent(aid.clone());
-        f.add_parent(did.clone());
+        f.add_parent(did);
         f.add_parent(eid.clone());
-        d.add_parent(aid.clone());
-        d.add_parent(bid.clone());
+        d.add_parent(aid);
+        d.add_parent(bid);
         d.add_parent(cid.clone());
         e.add_parent(cid.clone());
 
@@ -2332,7 +2334,7 @@ mod entities_tests {
             updates,
             None::<&NoEntitiesSchema>,
             TCComputation::ComputeNow,
-            &Extensions::all_available(),
+            Extensions::all_available(),
         )
         .expect("Failed to remove entities");
         // Post-Update Hierarchy
@@ -2535,7 +2537,7 @@ mod schema_based_parsing_tests {
                     "work_ip",
                     "trust_score",
                 ]
-                .map(SmolStr::new)
+                .map(SmolStr::new_static)
                 .into_iter(),
             )
         }
@@ -3729,7 +3731,7 @@ mod schema_based_parsing_tests {
             fn required_attrs(&self) -> Box<dyn Iterator<Item = SmolStr>> {
                 Box::new(
                     ["isFullTime", "department", "manager"]
-                        .map(SmolStr::new)
+                        .map(SmolStr::new_static)
                         .into_iter(),
                 )
             }
diff --git a/cedar-policy-core/src/entities/conformance.rs b/cedar-policy-core/src/entities/conformance.rs
index 6ac66db1..2db7dfc6 100644
--- a/cedar-policy-core/src/entities/conformance.rs
+++ b/cedar-policy-core/src/entities/conformance.rs
@@ -102,7 +102,7 @@ impl<S: Schema> EntitySchemaConformanceChecker<'_, S> {
         // Ensure that all required attributes for `etype` are actually
         // included in `entity`
         for required_attr in schema_etype.required_attrs() {
-            if attrs.get(&required_attr).is_none() {
+            if !attrs.contains_key(&required_attr) {
                 return Err(EntitySchemaConformanceError::missing_entity_attr(
                     uid.clone(),
                     required_attr,
@@ -194,7 +194,7 @@ impl<S: Schema> EntitySchemaConformanceChecker<'_, S> {
                 }
             }
         }
-        for (_, val) in &tags {
+        for val in tags.values() {
             validate_euids_in_partial_value(self.schema, val)
                 .map_err(|e| EntitySchemaConformanceError::InvalidEnumEntity(e.into()))?;
         }
diff --git a/cedar-policy-core/src/entities/json/value.rs b/cedar-policy-core/src/entities/json/value.rs
index 678f184f..77b4a246 100644
--- a/cedar-policy-core/src/entities/json/value.rs
+++ b/cedar-policy-core/src/entities/json/value.rs
@@ -286,7 +286,7 @@ impl From<EntityUID> for TypeAndId {
     fn from(euid: EntityUID) -> TypeAndId {
         let (entity_type, eid) = euid.components();
         TypeAndId {
-            entity_type: entity_type.to_string().into(),
+            entity_type: entity_type.to_smolstr(),
             id: AsRef::<str>::as_ref(&eid).into(),
         }
     }
@@ -295,7 +295,7 @@ impl From<EntityUID> for TypeAndId {
 impl From<&EntityUID> for TypeAndId {
     fn from(euid: &EntityUID) -> TypeAndId {
         TypeAndId {
-            entity_type: euid.entity_type().to_string().into(),
+            entity_type: euid.entity_type().to_smolstr(),
             id: AsRef::<str>::as_ref(&euid.eid()).into(),
         }
     }
@@ -382,7 +382,7 @@ impl CedarValueJson {
                 #[allow(clippy::indexing_slicing)]
                 1 => Ok(Self::ExtnEscape {
                     __extn: FnAndArg {
-                        ext_fn: fn_name.to_string().into(),
+                        ext_fn: fn_name.to_smolstr(),
                         arg: Box::new(CedarValueJson::from_expr(
                             // assuming the invariant holds for `expr`, it must also hold here
                             BorrowedRestrictedExpr::new_unchecked(
diff --git a/cedar-policy-core/src/est/expr.rs b/cedar-policy-core/src/est/expr.rs
index 067f0ba6..950d2e5f 100644
--- a/cedar-policy-core/src/est/expr.rs
+++ b/cedar-policy-core/src/est/expr.rs
@@ -1129,7 +1129,7 @@ fn display_cedarvaluejson(
         } => {
             // search for the name and callstyle
             let style = Extensions::all_available().all_funcs().find_map(|f| {
-                if &f.name().to_string() == ext_fn {
+                if &f.name().to_smolstr() == ext_fn {
                     Some(f.style())
                 } else {
                     None
@@ -1439,7 +1439,7 @@ impl BoundedDisplay for ExtFuncCall {
         };
         // search for the name and callstyle
         let style = Extensions::all_available().all_funcs().find_map(|ext_fn| {
-            if &ext_fn.name().to_string() == fn_name {
+            if &ext_fn.name().to_smolstr() == fn_name {
                 Some(ext_fn.style())
             } else {
                 None
diff --git a/cedar-policy-core/src/est/scope_constraints.rs b/cedar-policy-core/src/est/scope_constraints.rs
index c999fe16..df4fb7fe 100644
--- a/cedar-policy-core/src/est/scope_constraints.rs
+++ b/cedar-policy-core/src/est/scope_constraints.rs
@@ -22,7 +22,7 @@ use crate::entities::json::{
 };
 use crate::parser::err::parse_errors;
 use serde::{Deserialize, Serialize};
-use smol_str::SmolStr;
+use smol_str::{SmolStr, ToSmolStr};
 use std::collections::{BTreeMap, HashMap};
 use std::sync::Arc;
 
@@ -672,7 +672,7 @@ impl From<ast::PrincipalOrResourceConstraint> for PrincipalConstraint {
             }
             ast::PrincipalOrResourceConstraint::IsIn(entity_type, euid) => {
                 PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
-                    entity_type: entity_type.to_string().into(),
+                    entity_type: entity_type.to_smolstr(),
                     in_entity: Some(match euid {
                         ast::EntityReference::EUID(e) => PrincipalOrResourceInConstraint::Entity {
                             entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
@@ -685,7 +685,7 @@ impl From<ast::PrincipalOrResourceConstraint> for PrincipalConstraint {
             }
             ast::PrincipalOrResourceConstraint::Is(entity_type) => {
                 PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
-                    entity_type: entity_type.to_string().into(),
+                    entity_type: entity_type.to_smolstr(),
                     in_entity: None,
                 })
             }
@@ -719,7 +719,7 @@ impl From<ast::PrincipalOrResourceConstraint> for ResourceConstraint {
             }
             ast::PrincipalOrResourceConstraint::IsIn(entity_type, euid) => {
                 ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
-                    entity_type: entity_type.to_string().into(),
+                    entity_type: entity_type.to_smolstr(),
                     in_entity: Some(match euid {
                         ast::EntityReference::EUID(e) => PrincipalOrResourceInConstraint::Entity {
                             entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
@@ -732,7 +732,7 @@ impl From<ast::PrincipalOrResourceConstraint> for ResourceConstraint {
             }
             ast::PrincipalOrResourceConstraint::Is(entity_type) => {
                 ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
-                    entity_type: entity_type.to_string().into(),
+                    entity_type: entity_type.to_smolstr(),
                     in_entity: None,
                 })
             }
diff --git a/cedar-policy-core/src/extensions/ipaddr.rs b/cedar-policy-core/src/extensions/ipaddr.rs
index fa0932f9..3a66cb19 100644
--- a/cedar-policy-core/src/extensions/ipaddr.rs
+++ b/cedar-policy-core/src/extensions/ipaddr.rs
@@ -176,7 +176,7 @@ impl std::str::FromStr for IPAddr {
     type Err = String;
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         // Return Err if the input is too long
-        if s.bytes().len() > IP_STR_REP_MAX_LEN as usize {
+        if s.len() > IP_STR_REP_MAX_LEN as usize {
             return Err(format!(
                 "error parsing IP address from string `{s}`: string length is too large"
             ));
diff --git a/cedar-policy-core/src/parser/cst_to_ast.rs b/cedar-policy-core/src/parser/cst_to_ast.rs
index c801799e..3aec81dc 100644
--- a/cedar-policy-core/src/parser/cst_to_ast.rs
+++ b/cedar-policy-core/src/parser/cst_to_ast.rs
@@ -50,7 +50,7 @@ use crate::fuzzy_match::fuzzy_search_limited;
 use itertools::{Either, Itertools};
 use nonempty::nonempty;
 use nonempty::NonEmpty;
-use smol_str::{SmolStr, ToSmolStr};
+use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 use std::cmp::Ordering;
 use std::collections::{BTreeMap, HashSet};
 use std::mem;
@@ -110,11 +110,12 @@ impl Node<Option<cst::Policies>> {
     ) -> Result<impl Iterator<Item = (ast::PolicyID, &Node<Option<cst::Policy>>)>> {
         let policies = self.try_as_inner()?;
 
-        Ok(policies
-            .0
-            .iter()
-            .enumerate()
-            .map(|(count, node)| (ast::PolicyID::from_string(format!("policy{count}")), node)))
+        Ok(policies.0.iter().enumerate().map(|(count, node)| {
+            (
+                ast::PolicyID::from_smolstr(format_smolstr!("policy{count}")),
+                node,
+            )
+        }))
     }
 
     /// convert `cst::Policies` to `ast::PolicySet`
@@ -673,7 +674,8 @@ impl Node<Option<cst::Ident>> {
             cst::Ident::Invalid(i) => Err(self
                 .to_ast_err(ToASTErrorKind::InvalidIdentifier(i.clone()))
                 .into()),
-            _ => Ok(ast::Id::new_unchecked(format!("{ident}"))),
+            cst::Ident::Ident(i) => Ok(ast::Id::new_unchecked(i.clone())),
+            _ => Ok(ast::Id::new_unchecked(ident.to_smolstr())),
         }
     }
 
@@ -689,7 +691,8 @@ impl Node<Option<cst::Ident>> {
             cst::Ident::Invalid(i) => Err(self
                 .to_ast_err(ToASTErrorKind::InvalidIdentifier(i.clone()))
                 .into()),
-            _ => Ok(ast::AnyId::new_unchecked(format!("{ident}"))),
+            cst::Ident::Ident(i) => Ok(ast::AnyId::new_unchecked(i.clone())),
+            _ => Ok(ast::AnyId::new_unchecked(ident.to_smolstr())),
         }
     }
 
@@ -2043,17 +2046,11 @@ impl Node<Option<cst::Name>> {
     // so it's fine to return an `Option` instead of a `Result`.
     fn maybe_to_var(&self) -> Option<ast::Var> {
         let name = self.as_inner()?;
-
-        let ident = match ParseErrors::transpose(name.path.iter().map(|id| id.to_valid_ident())) {
-            Ok(path) => {
-                if !path.is_empty() {
-                    // The path should be empty for a variable
-                    None
-                } else {
-                    name.name.as_inner()
-                }
-            }
-            Err(_) => None,
+        let ident = if name.path.is_empty() {
+            name.name.as_inner()
+        } else {
+            // The path should be empty for a variable
+            None
         }?;
 
         match ident {
diff --git a/cedar-policy-core/src/parser/err.rs b/cedar-policy-core/src/parser/err.rs
index e6b76ee5..e89fe093 100644
--- a/cedar-policy-core/src/parser/err.rs
+++ b/cedar-policy-core/src/parser/err.rs
@@ -857,15 +857,26 @@ impl ParseErrors {
     pub(crate) fn transpose<T>(
         i: impl IntoIterator<Item = Result<T, ParseErrors>>,
     ) -> Result<Vec<T>, Self> {
-        let mut errs = vec![];
-        let oks: Vec<_> = i
-            .into_iter()
-            .filter_map(|r| r.map_err(|e| errs.push(e)).ok())
-            .collect();
-        if let Some(combined_errs) = Self::flatten(errs) {
-            Err(combined_errs)
-        } else {
+        let iter = i.into_iter();
+        let (lower, upper) = iter.size_hint();
+        let capacity = upper.unwrap_or(lower);
+
+        let mut oks = Vec::with_capacity(capacity);
+        let mut errs = Vec::new();
+
+        for r in iter {
+            match r {
+                Ok(v) => oks.push(v),
+                Err(e) => errs.push(e),
+            }
+        }
+
+        if errs.is_empty() {
             Ok(oks)
+        } else {
+            // PANIC SAFETY: `errs` is not empty so `flatten` will return `Some(..)`
+            #[allow(clippy::unwrap_used)]
+            Err(Self::flatten(errs).unwrap())
         }
     }
 }
diff --git a/cedar-policy-core/src/parser/unescape.rs b/cedar-policy-core/src/parser/unescape.rs
index 972aceb6..3f472b91 100644
--- a/cedar-policy-core/src/parser/unescape.rs
+++ b/cedar-policy-core/src/parser/unescape.rs
@@ -19,13 +19,13 @@ use itertools::Itertools;
 use miette::Diagnostic;
 use nonempty::NonEmpty;
 use rustc_lexer::unescape::{unescape_str, EscapeError};
-use smol_str::SmolStr;
+use smol_str::{SmolStr, SmolStrBuilder};
 use std::ops::Range;
 use thiserror::Error;
 
 /// Unescape a string following Cedar's string escape rules
 pub fn to_unescaped_string(s: &str) -> Result<SmolStr, NonEmpty<UnescapeError>> {
-    let mut unescaped_str = String::new();
+    let mut unescaped_str = SmolStrBuilder::new();
     let mut errs = Vec::new();
     let mut callback = |range, r| match r {
         Ok(c) => unescaped_str.push(c),
@@ -42,7 +42,7 @@ pub fn to_unescaped_string(s: &str) -> Result<SmolStr, NonEmpty<UnescapeError>>
             tail: tails.iter().cloned().collect_vec(),
         })
     } else {
-        Ok(unescaped_str.into())
+        Ok(unescaped_str.finish())
     }
 }
 
diff --git a/cedar-policy-validator/src/deprecated_schema_compat/conversion.rs b/cedar-policy-validator/src/deprecated_schema_compat/conversion.rs
index 43b50476..5d64e229 100644
--- a/cedar-policy-validator/src/deprecated_schema_compat/conversion.rs
+++ b/cedar-policy-validator/src/deprecated_schema_compat/conversion.rs
@@ -214,11 +214,9 @@ impl TryFrom<compat::SchemaTypeVariant> for TypeVariant<RawName> {
                 additional_attributes,
             }),
             compat::SchemaTypeVariant::Entity { name } => Self::Entity {
-                name: RawName::from_normalized_str(&name)
-                    .map_err(|err| {
-                        serde::de::Error::custom(format!("invalid entity type `{name}`: {err}"))
-                    })?
-                    .into(),
+                name: RawName::from_normalized_str(&name).map_err(|err| {
+                    serde::de::Error::custom(format!("invalid entity type `{name}`: {err}"))
+                })?,
             },
             compat::SchemaTypeVariant::Extension { name } => Self::Extension {
                 name: UnreservedId::from_normalized_str(&name).map_err(|err| {
diff --git a/cedar-policy-validator/src/deprecated_schema_compat/json_schema.rs b/cedar-policy-validator/src/deprecated_schema_compat/json_schema.rs
index 28fc76da..d767838b 100644
--- a/cedar-policy-validator/src/deprecated_schema_compat/json_schema.rs
+++ b/cedar-policy-validator/src/deprecated_schema_compat/json_schema.rs
@@ -39,7 +39,7 @@ pub struct SchemaFragment(
     pub  HashMap<SmolStr, NamespaceDefinition>,
 );
 
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde_as]
 #[serde(deny_unknown_fields)]
 pub struct NamespaceDefinition {
@@ -56,7 +56,7 @@ pub struct NamespaceDefinition {
     pub actions: HashMap<SmolStr, ActionType>,
 }
 
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(deny_unknown_fields)]
 pub struct EntityType {
     // Key changed from `SmolStr` in 2.5.0 to `RawName` to avoid excess code duplication
@@ -67,7 +67,7 @@ pub struct EntityType {
     pub shape: AttributesOrContext,
 }
 
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(transparent)]
 pub struct AttributesOrContext(pub SchemaType);
 
@@ -80,7 +80,7 @@ impl Default for AttributesOrContext {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(deny_unknown_fields)]
 pub struct ActionType {
     #[serde(default)]
@@ -93,7 +93,7 @@ pub struct ActionType {
     pub member_of: Option<Vec<ActionEntityUID>>,
 }
 
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(deny_unknown_fields)]
 pub struct ApplySpec {
     // Key changed from `SmolStr` in 2.5.0 to `RawName` to avoid excess code duplication
@@ -108,7 +108,7 @@ pub struct ApplySpec {
     pub context: AttributesOrContext,
 }
 
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(deny_unknown_fields)]
 pub struct ActionEntityUID {
     pub id: SmolStr,
diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs
index 426effc6..73590545 100644
--- a/cedar-policy-validator/src/entity_manifest.rs
+++ b/cedar-policy-validator/src/entity_manifest.rs
@@ -757,7 +757,7 @@ when {
                         EntityRoot::Var(Var::Principal),
                         AccessTrie {
                             children: HashMap::from([(
-                                SmolStr::new("name"),
+                                SmolStr::new_static("name"),
                                 Box::new(AccessTrie {
                                     children: HashMap::new(),
                                     ancestors_trie: RootAccessTrie::new(),
diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs
index 66384aae..028c5d68 100644
--- a/cedar-policy-validator/src/typecheck.rs
+++ b/cedar-policy-validator/src/typecheck.rs
@@ -147,9 +147,9 @@ impl<'a> Typechecker<'a> {
         })
     }
 
-    fn single_env_typechecking<'b>(
+    fn single_env_typechecking(
         &self,
-        request_env: &RequestEnv<'b>,
+        request_env: &RequestEnv<'_>,
         policy_id: &PolicyID,
         expr: &Expr,
     ) -> PolicyCheck {
@@ -1041,7 +1041,7 @@ impl<'a> SingleEnvTypechecker<'a> {
                         }
                         // For `AnyEntity` we don't know anything about what
                         // entity type it could be, so we just return `Bool`.
-                        Some(Type::EntityOrRecord(EntityRecordKind::AnyEntity { .. })) => {
+                        Some(Type::EntityOrRecord(EntityRecordKind::AnyEntity)) => {
                             TypecheckAnswer::success(
                                 ExprBuilder::with_data(Some(Type::primitive_boolean()))
                                     .with_same_source_loc(e)
diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md
index 98162319..3668ccf1 100644
--- a/cedar-policy/CHANGELOG.md
+++ b/cedar-policy/CHANGELOG.md
@@ -20,6 +20,8 @@ Cedar Language Version: TBD
   and earlier. These functions will ignore unrecognized keys in some positions where they are currently an error,
   matching the behavior of earlier versions.  This is intended help some users migrate to the current 4.0 schema
   format. The new functions are deprecated and placed behind the `deprecated-schema-compat` feature. (#1600)
+- `Expression::new_duration`, `Expression::new_datetime`, `RestrictedExpression::new_duration`,
+   and `RestrictedExpression::new_datetime` (#1614)
 
 ### Changed
 
diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs
index 644fe002..6384a146 100644
--- a/cedar-policy/src/api.rs
+++ b/cedar-policy/src/api.rs
@@ -3826,6 +3826,28 @@ impl Expression {
             vec![src_expr],
         ))
     }
+
+    /// Create an expression representing a particular instant of time.
+    /// This function does not perform error checking on the source string,
+    /// it creates an expression that calls the `datetime` constructor.
+    pub fn new_datetime(src: impl AsRef<str>) -> Self {
+        let src_expr = ast::Expr::val(src.as_ref());
+        Self(ast::Expr::call_extension_fn(
+            datetime_extension_name(),
+            vec![src_expr],
+        ))
+    }
+
+    /// Create an expression representing a duration of time.
+    /// This function does not perform error checking on the source string,
+    /// it creates an expression that calls the `datetime` constructor.
+    pub fn new_duration(src: impl AsRef<str>) -> Self {
+        let src_expr = ast::Expr::val(src.as_ref());
+        Self(ast::Expr::call_extension_fn(
+            duration_extension_name(),
+            vec![src_expr],
+        ))
+    }
 }
 
 #[cfg(test)]
@@ -3933,6 +3955,28 @@ impl RestrictedExpression {
         ))
     }
 
+    /// Create an expression representing a particular instant of time.
+    /// This function does not perform error checking on the source string,
+    /// it creates an expression that calls the `datetime` constructor.
+    pub fn new_datetime(src: impl AsRef<str>) -> Self {
+        let src_expr = ast::RestrictedExpr::val(src.as_ref());
+        Self(ast::RestrictedExpr::call_extension_fn(
+            datetime_extension_name(),
+            [src_expr],
+        ))
+    }
+
+    /// Create an expression representing a duration of time.
+    /// This function does not perform error checking on the source string,
+    /// it creates an expression that calls the `datetime` constructor.
+    pub fn new_duration(src: impl AsRef<str>) -> Self {
+        let src_expr = ast::RestrictedExpr::val(src.as_ref());
+        Self(ast::RestrictedExpr::call_extension_fn(
+            duration_extension_name(),
+            [src_expr],
+        ))
+    }
+
     /// Create an unknown expression
     #[cfg(feature = "partial-eval")]
     pub fn new_unknown(name: impl AsRef<str>) -> Self {
@@ -3963,6 +4007,18 @@ fn ip_extension_name() -> ast::Name {
     ast::Name::unqualified_name("ip".parse().unwrap())
 }
 
+fn datetime_extension_name() -> ast::Name {
+    // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
+    #[allow(clippy::unwrap_used)]
+    ast::Name::unqualified_name("datetime".parse().unwrap())
+}
+
+fn duration_extension_name() -> ast::Name {
+    // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
+    #[allow(clippy::unwrap_used)]
+    ast::Name::unqualified_name("duration".parse().unwrap())
+}
+
 impl FromStr for RestrictedExpression {
     type Err = RestrictedExpressionParseError;
 
diff --git a/cedar-policy/src/api/deprecated_schema_compat.rs b/cedar-policy/src/api/deprecated_schema_compat.rs
index ef9896b0..70efed99 100644
--- a/cedar-policy/src/api/deprecated_schema_compat.rs
+++ b/cedar-policy/src/api/deprecated_schema_compat.rs
@@ -203,7 +203,7 @@ mod test_utils {
             &Report::new(Schema::from_json_value(in_context).unwrap_err()),
             &ExpectedErrorMessageBuilder::error(err).build(),
         );
-        let in_common = schema_with_common_type(ty.clone());
+        let in_common = schema_with_common_type(ty);
         Schema::from_deprecated_json_value(in_common.clone()).unwrap();
         expect_err(
             "",
@@ -217,13 +217,13 @@ mod test_utils {
     pub(crate) fn assert_type_json_ok_deprecated_and_standard(ty: serde_json::Value) {
         let in_entity_attr = schema_with_entity_attribute(ty.clone());
         Schema::from_deprecated_json_value(in_entity_attr.clone()).unwrap();
-        Schema::from_json_value(in_entity_attr.clone()).unwrap();
+        Schema::from_json_value(in_entity_attr).unwrap();
         let in_context = schema_with_context_attribute(ty.clone());
         Schema::from_deprecated_json_value(in_context.clone()).unwrap();
-        Schema::from_json_value(in_context.clone()).unwrap();
-        let in_common = schema_with_common_type(ty.clone());
+        Schema::from_json_value(in_context).unwrap();
+        let in_common = schema_with_common_type(ty);
         Schema::from_deprecated_json_value(in_common.clone()).unwrap();
-        Schema::from_json_value(in_common.clone()).unwrap();
+        Schema::from_json_value(in_common).unwrap();
     }
 
     #[track_caller]
@@ -237,7 +237,7 @@ mod test_utils {
         assert_schema_json_err_deprecated_and_standard(in_entity_attr, current_err, deprecated_err);
         let in_context = schema_with_context_attribute(ty.clone());
         assert_schema_json_err_deprecated_and_standard(in_context, current_err, deprecated_err);
-        let in_common = schema_with_common_type(ty.clone());
+        let in_common = schema_with_common_type(ty);
         assert_schema_json_err_deprecated_and_standard(in_common, current_err, deprecated_err);
     }
 
@@ -255,7 +255,7 @@ mod test_utils {
         );
         expect_err(
             "",
-            &Report::new(Schema::from_deprecated_json_value(schema.clone()).unwrap_err()),
+            &Report::new(Schema::from_deprecated_json_value(schema).unwrap_err()),
             deprecated_err,
         );
     }


Test patch:
diff --git a/cedar-policy/src/test/prop_test_policy_set.rs b/cedar-policy/src/test/prop_test_policy_set.rs
index ba41dfba..1cadbd01 100644
--- a/cedar-policy/src/test/prop_test_policy_set.rs
+++ b/cedar-policy/src/test/prop_test_policy_set.rs
@@ -107,7 +107,7 @@ impl PolicySetModel {
                     Entry::Vacant(_) => {
                         panic!("template to link map should have Vec for existing template")
                     }
-                };
+                }
                 assert!(!self.link_to_template_map.contains_key(policy_name));
                 self.link_to_template_map
                     .insert(policy_name.to_owned(), template_name.clone());
@@ -175,7 +175,7 @@ impl PolicySetModel {
                     Entry::Vacant(_) => {
                         panic!("template to link map should have Vec for existing template")
                     }
-                };
+                }
             }
             Self::remove_policy_name(&mut self.link_names, policy_id);
         }
@@ -272,7 +272,7 @@ fn string_to_policy_set_ops(s: &str) {
             }
             PolicySetOp::Link => my_policy_set.link(&format!("policy{policy_n}")),
             PolicySetOp::Unlink => my_policy_set.unlink(&format!("policy{policy_n}")),
-        };
+        }
 
         my_policy_set.check_equiv();
     }
diff --git a/cedar-policy/src/test/test.rs b/cedar-policy/src/test/test.rs
index 3f4f6134..6b79a071 100644
--- a/cedar-policy/src/test/test.rs
+++ b/cedar-policy/src/test/test.rs
@@ -2207,7 +2207,10 @@ action "g" appliesTo {
             r#"E::"""#.parse().unwrap(),
             std::iter::empty(),
             std::iter::empty(),
-            std::iter::once(("".to_owned(), RestrictedExpression::new_string("".into()))),
+            std::iter::once((
+                String::new(),
+                RestrictedExpression::new_string(String::new()),
+            )),
         )
         .unwrap();
         assert_matches!(Entities::from_entities([entity], Some(&schema)), Ok(_));
@@ -2215,7 +2218,7 @@ action "g" appliesTo {
             r#"E::"""#.parse().unwrap(),
             std::iter::empty(),
             std::iter::empty(),
-            std::iter::once(("".to_owned(), RestrictedExpression::new_long(42))),
+            std::iter::once((String::new(), RestrictedExpression::new_long(42))),
         )
         .unwrap();
         assert_matches!(
@@ -2247,7 +2250,7 @@ action "g" appliesTo {
             r#"E::"""#.parse().unwrap(),
             std::iter::empty(),
             std::iter::empty(),
-            std::iter::once(("".to_owned(), RestrictedExpression::new_long(42))),
+            std::iter::once((String::new(), RestrictedExpression::new_long(42))),
         )
         .unwrap();
         assert_matches!(
@@ -2292,7 +2295,7 @@ mod schema_based_parsing_tests {
     #[test]
     fn additional_json_attributes() {
         let (schema, _) = Schema::from_cedarschema_str(
-            r#"
+            r"
         entity A {
           d? : decimal,
           e? : B,
@@ -2303,7 +2306,7 @@ mod schema_based_parsing_tests {
           principal: A,
           resource: A,
         };
-        "#,
+        ",
         )
         .unwrap();
 
@@ -5401,8 +5404,12 @@ mod decimal_ip_constructors {
                 assert_eq!(fn_name, &("ip".parse().unwrap()));
                 assert_eq!(args.as_ref().len(), 1);
                 let arg = args.first().unwrap();
-                assert_matches!(arg.expr_kind(),
-                ast::ExprKind::Lit(ast::Literal::String(s)) => s.as_str() == "10.10.10.10");
+                assert_matches!(
+                    arg.expr_kind(),
+                    ast::ExprKind::Lit(ast::Literal::String(s)) => {
+                        assert_eq!(s.as_str(), "10.10.10.10");
+                    },
+                );
             }
         );
     }
@@ -5415,8 +5422,12 @@ mod decimal_ip_constructors {
                 assert_eq!(fn_name, &("decimal".parse().unwrap()));
                 assert_eq!(args.as_ref().len(), 1);
                 let arg = args.first().unwrap();
-                assert_matches!(arg.expr_kind(),
-                ast::ExprKind::Lit(ast::Literal::String(s)) => s.as_str() == "1234.1234");
+                assert_matches!(
+                    arg.expr_kind(),
+                    ast::ExprKind::Lit(ast::Literal::String(s)) => {
+                        assert_eq!(s.as_str(), "1234.1234");
+                    },
+                );
             }
         );
     }
@@ -5429,8 +5440,12 @@ mod decimal_ip_constructors {
                 assert_eq!(fn_name, &("decimal".parse().unwrap()));
                 assert_eq!(args.as_ref().len(), 1);
                 let arg = args.first().unwrap();
-                assert_matches!(arg.expr_kind(),
-                ast::ExprKind::Lit(ast::Literal::String(s)) => s.as_str() == "1234.1234");
+                assert_matches!(
+                    arg.expr_kind(),
+                    ast::ExprKind::Lit(ast::Literal::String(s)) => {
+                        assert_eq!(s.as_str(), "1234.1234");
+                    },
+                );
             }
         );
     }
@@ -5438,8 +5453,12 @@ mod decimal_ip_constructors {
     #[test]
     fn valid_decimal() {
         let decimal = Expression::new_decimal("1234.1234");
-        assert_matches!(evaluate_empty(&decimal),
-         Ok(EvalResult::ExtensionValue(s)) => s == "1234.1234");
+        assert_matches!(
+            evaluate_empty(&decimal),
+            Ok(EvalResult::ExtensionValue(s)) => {
+                assert_eq!(s, r#"decimal("1234.1234")"#);
+            },
+        );
     }
 
     #[test]
@@ -5451,6 +5470,120 @@ mod decimal_ip_constructors {
             }
         );
     }
+
+    #[test]
+    fn expr_datetime_constructor() {
+        let datetime = Expression::new_datetime("2025-05-14T17:18:00.000Z");
+        assert_matches!(datetime.into_inner().expr_kind(),
+            ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
+                assert_eq!(fn_name, &("datetime".parse().unwrap()));
+                assert_eq!(args.as_ref().len(), 1);
+                let arg = args.first().unwrap();
+                assert_matches!(
+                    arg.expr_kind(),
+                    ast::ExprKind::Lit(ast::Literal::String(s)) => {
+                        assert_eq!(s.as_str(), "2025-05-14T17:18:00.000Z");
+                    },
+                );
+            }
+        );
+    }
+
+    #[test]
+    fn rexpr_datetime_constructor() {
+        let datetime = RestrictedExpression::new_datetime("2025-05-14T17:18:00.000Z");
+        assert_matches!(datetime.into_inner().expr_kind(),
+            ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
+                assert_eq!(fn_name, &("datetime".parse().unwrap()));
+                assert_eq!(args.as_ref().len(), 1);
+                let arg = args.first().unwrap();
+                assert_matches!(
+                    arg.expr_kind(),
+                    ast::ExprKind::Lit(ast::Literal::String(s)) => {
+                        assert_eq!(s.as_str(), "2025-05-14T17:18:00.000Z");
+                    },
+                );
+            }
+        );
+    }
+
+    #[test]
+    fn valid_datetime() {
+        let datetime = Expression::new_datetime("2025-05-14T17:18:00.000Z");
+        assert_matches!(
+            evaluate_empty(&datetime),
+            Ok(EvalResult::ExtensionValue(s)) => {
+                assert_eq!(s, r#"datetime("2025-05-14T17:18:00.000Z")"#);
+            },
+        );
+    }
+
+    #[test]
+    fn invalid_datetime() {
+        let datetime = Expression::new_datetime("1/1/70");
+        assert_matches!(evaluate_empty(&datetime),
+            Err(EvaluationError::FailedExtensionFunctionExecution(e)) => {
+                assert_eq!(e.extension_name(), "datetime");
+            }
+        );
+    }
+
+    #[test]
+    fn expr_duration_constructor() {
+        let duration = Expression::new_duration("1d");
+        assert_matches!(duration.into_inner().expr_kind(),
+            ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
+                assert_eq!(fn_name, &("duration".parse().unwrap()));
+                assert_eq!(args.as_ref().len(), 1);
+                let arg = args.first().unwrap();
+                assert_matches!(
+                    arg.expr_kind(),
+                    ast::ExprKind::Lit(ast::Literal::String(s)) => {
+                        assert_eq!(s.as_str(), "1d");
+                    },
+                );
+            }
+        );
+    }
+
+    #[test]
+    fn rexpr_duration_constructor() {
+        let duration = RestrictedExpression::new_duration("2025-05-14T17:18:00.000Z");
+        assert_matches!(duration.into_inner().expr_kind(),
+            ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
+                assert_eq!(fn_name, &("duration".parse().unwrap()));
+                assert_eq!(args.as_ref().len(), 1);
+                let arg = args.first().unwrap();
+                assert_matches!(
+                    arg.expr_kind(),
+                    ast::ExprKind::Lit(ast::Literal::String(s)) => {
+                        assert_eq!(s.as_str(), "2025-05-14T17:18:00.000Z");
+                    },
+                );
+            }
+        );
+    }
+
+    #[test]
+    fn valid_duration() {
+        let duration = Expression::new_duration("1d");
+        assert_matches!(
+            evaluate_empty(&duration),
+            Ok(EvalResult::ExtensionValue(s)) => {
+                assert_eq!(s, r#"duration("1d")"#);
+            },
+        );
+    }
+
+    #[test]
+    fn invalid_duration() {
+        let duration = Expression::new_duration("twenty-four hours");
+        assert_matches!(evaluate_empty(&duration),
+            Err(EvaluationError::FailedExtensionFunctionExecution(e)) => {
+                assert_eq!(e.extension_name(), "duration");
+            }
+        );
+    }
 }
 
 mod into_iter_entities {
@@ -5736,7 +5869,7 @@ mod policy_set_est_tests {
             .is_err()
         {
             panic!("Should have exactly one");
-        };
+        }
     }
 
     #[test]