Repository: cedar-policy/cedar
Pull request #1470 (2025-02-12 22:19:38)

 Allow error expressions in AST and handle empty When error
## Description of changes

This change allows us to parse AST's with expressions that are Errors. This will be useful in cases where we might want to parse an incomplete or invalid Cedar policy without generating an error. An example of where this might be useful would be in implementing auto-complete as part of an LSP or IDE plugin.

This particular change allows us to parse the following invalid policy 

`permit(principal, action, resource) when {};`

by calling the following:

```
 text_to_cst::parse_policy(text)
        .expect("failed parser")
        .to_policy_with_errors(ast::PolicyID::from_string("id"))
        .unwrap_or_else(|errs| {
            panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
        })
```

## Issue #, if available

## 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):

- [ x ] Updates the "Unreleased" section of the CHANGELOG with a description of my change (required for major/minor version bumps).
- [] 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.)



Patch:
diff --git a/cedar-policy-core/Cargo.toml b/cedar-policy-core/Cargo.toml
index ee448c75..362ee975 100644
--- a/cedar-policy-core/Cargo.toml
+++ b/cedar-policy-core/Cargo.toml
@@ -56,6 +56,7 @@ test-util = []
 
 # Experimental features.
 partial-eval = []
+tolerant-ast = []
 wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]
 
 [build-dependencies]
diff --git a/cedar-policy-core/src/ast.rs b/cedar-policy-core/src/ast.rs
index b5226efd..1b22dd1b 100644
--- a/cedar-policy-core/src/ast.rs
+++ b/cedar-policy-core/src/ast.rs
@@ -17,6 +17,8 @@
 //! This module contains the AST datatypes.
 
 mod expr;
+#[cfg(feature = "tolerant-ast")]
+pub(crate) mod expr_allows_errors;
 pub use expr::*;
 mod entity;
 pub use entity::*;
diff --git a/cedar-policy-core/src/ast/expr.rs b/cedar-policy-core/src/ast/expr.rs
index 818a0595..63326dc9 100644
--- a/cedar-policy-core/src/ast/expr.rs
+++ b/cedar-policy-core/src/ast/expr.rs
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#[cfg(feature = "tolerant-ast")]
+use super::expr_allows_errors::AstExprErrorKind;
 use crate::{
     ast::*,
     expr_builder::{self, ExprBuilder as _},
@@ -155,6 +157,12 @@ pub enum ExprKind<T = ()> {
     Set(Arc<Vec<Expr<T>>>),
     /// Anonymous record (whose elements may be arbitrary expressions)
     Record(Arc<BTreeMap<SmolStr, Expr<T>>>),
+    #[cfg(feature = "tolerant-ast")]
+    /// Error expression - allows us to continue parsing even when we have errors
+    Error {
+        /// Type of error that led to the failure
+        error_kind: AstExprErrorKind,
+    },
 }
 
 impl From<Value> for Expr {
@@ -191,7 +199,7 @@ impl From<PartialValue> for Expr {
 }
 
 impl<T> Expr<T> {
-    fn new(expr_kind: ExprKind<T>, source_loc: Option<Loc>, data: T) -> Self {
+    pub(crate) fn new(expr_kind: ExprKind<T>, source_loc: Option<Loc>, data: T) -> Self {
         Self {
             expr_kind,
             source_loc,
@@ -385,6 +393,8 @@ impl<T> Expr<T> {
             ExprKind::Is { .. } => Some(Type::Bool),
             ExprKind::Set(_) => Some(Type::Set),
             ExprKind::Record(_) => Some(Type::Record),
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { .. } => None,
         }
     }
 }
@@ -717,6 +727,8 @@ impl Expr {
                 expr.substitute_general::<T>(definitions)?,
                 entity_type.clone(),
             )),
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { .. } => Ok(self.clone()),
         }
     }
 }
@@ -857,6 +869,9 @@ impl<T: Default + Clone> expr_builder::ExprBuilder for ExprBuilder<T> {
 
     type Data = T;
 
+    #[cfg(feature = "tolerant-ast")]
+    type ErrorType = ParseErrors;
+
     fn loc(&self) -> Option<&Loc> {
         self.source_loc.as_ref()
     }
@@ -1168,6 +1183,12 @@ impl<T: Default + Clone> expr_builder::ExprBuilder for ExprBuilder<T> {
             entity_type,
         })
     }
+
+    /// Don't support AST Error nodes - return the error right back
+    #[cfg(feature = "tolerant-ast")]
+    fn error(self, parse_errors: ParseErrors) -> Result<Self::Expr, Self::ErrorType> {
+        Err(parse_errors)
+    }
 }
 
 impl<T> ExprBuilder<T> {
@@ -1482,6 +1503,8 @@ impl<T> Expr<T> {
                 expr.hash_shape(state);
                 entity_type.hash(state);
             }
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { error_kind, .. } => error_kind.hash(state),
         }
     }
 }
diff --git a/cedar-policy-core/src/ast/expr_allows_errors.rs b/cedar-policy-core/src/ast/expr_allows_errors.rs
new file mode 100644
index 00000000..be65751f
--- /dev/null
+++ b/cedar-policy-core/src/ast/expr_allows_errors.rs
@@ -0,0 +1,462 @@
+use crate::{
+    ast::*,
+    expr_builder::{self},
+    parser::{
+        err::{ParseErrors, ToASTErrorKind},
+        Loc,
+    },
+};
+use serde::{Deserialize, Serialize};
+use smol_str::SmolStr;
+use std::{
+    collections::{btree_map, BTreeMap},
+    sync::Arc,
+};
+use thiserror::Error;
+
+#[derive(Error, Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq)]
+pub enum AstExprErrorKind {
+    #[error("Invalid expression node: {0}")]
+    InvalidExpr(String),
+}
+
+impl From<ToASTErrorKind> for AstExprErrorKind {
+    fn from(value: ToASTErrorKind) -> Self {
+        AstExprErrorKind::InvalidExpr(value.to_string())
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct ExprWithErrsBuilder<T = ()> {
+    source_loc: Option<Loc>,
+    data: T,
+}
+
+impl<T: Default + Clone> expr_builder::ExprBuilder for ExprWithErrsBuilder<T> {
+    type Expr = Expr<T>;
+
+    type Data = T;
+
+    #[cfg(feature = "tolerant-ast")]
+    type ErrorType = Infallible;
+
+    fn loc(&self) -> Option<&Loc> {
+        self.source_loc.as_ref()
+    }
+
+    fn data(&self) -> &Self::Data {
+        &self.data
+    }
+
+    fn with_data(data: T) -> Self {
+        Self {
+            source_loc: None,
+            data,
+        }
+    }
+
+    fn with_maybe_source_loc(mut self, maybe_source_loc: Option<&Loc>) -> Self {
+        self.source_loc = maybe_source_loc.cloned();
+        self
+    }
+
+    /// Create an `Expr` that's just a single `Literal`.
+    ///
+    /// Note that you can pass this a `Literal`, an `Integer`, a `String`, etc.
+    fn val(self, v: impl Into<Literal>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::Lit(v.into()))
+    }
+
+    /// Create an `Unknown` `Expr`
+    fn unknown(self, u: Unknown) -> Expr<T> {
+        self.with_expr_kind(ExprKind::Unknown(u))
+    }
+
+    /// Create an `Expr` that's just this literal `Var`
+    fn var(self, v: Var) -> Expr<T> {
+        self.with_expr_kind(ExprKind::Var(v))
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    fn error(self, parse_errors: ParseErrors) -> Result<Expr<T>, Self::ErrorType> {
+        Ok(self.with_expr_kind(ExprKind::Error {
+            error_kind: AstExprErrorKind::InvalidExpr(parse_errors.to_string()),
+        }))
+    }
+
+    /// Create an `Expr` that's just this `SlotId`
+    fn slot(self, s: SlotId) -> Expr<T> {
+        self.with_expr_kind(ExprKind::Slot(s))
+    }
+
+    /// Create a ternary (if-then-else) `Expr`.
+    ///
+    /// `test_expr` must evaluate to a Bool type
+    fn ite(self, test_expr: Expr<T>, then_expr: Expr<T>, else_expr: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::If {
+            test_expr: Arc::new(test_expr),
+            then_expr: Arc::new(then_expr),
+            else_expr: Arc::new(else_expr),
+        })
+    }
+
+    /// Create a 'not' expression. `e` must evaluate to Bool type
+    fn not(self, e: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::UnaryApp {
+            op: UnaryOp::Not,
+            arg: Arc::new(e),
+        })
+    }
+
+    /// Create a '==' expression
+    fn is_eq(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::Eq,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create an 'and' expression. Arguments must evaluate to Bool type
+    fn and(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(match (&e1.expr_kind(), &e2.expr_kind()) {
+            (ExprKind::Lit(Literal::Bool(b1)), ExprKind::Lit(Literal::Bool(b2))) => {
+                ExprKind::Lit(Literal::Bool(*b1 && *b2))
+            }
+            _ => ExprKind::And {
+                left: Arc::new(e1),
+                right: Arc::new(e2),
+            },
+        })
+    }
+
+    /// Create an 'or' expression. Arguments must evaluate to Bool type
+    fn or(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(match (&e1.expr_kind(), &e2.expr_kind()) {
+            (ExprKind::Lit(Literal::Bool(b1)), ExprKind::Lit(Literal::Bool(b2))) => {
+                ExprKind::Lit(Literal::Bool(*b1 || *b2))
+            }
+
+            _ => ExprKind::Or {
+                left: Arc::new(e1),
+                right: Arc::new(e2),
+            },
+        })
+    }
+
+    /// Create a '<' expression. Arguments must evaluate to Long type
+    fn less(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::Less,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create a '<=' expression. Arguments must evaluate to Long type
+    fn lesseq(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::LessEq,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create an 'add' expression. Arguments must evaluate to Long type
+    fn add(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::Add,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create a 'sub' expression. Arguments must evaluate to Long type
+    fn sub(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::Sub,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create a 'mul' expression. Arguments must evaluate to Long type
+    fn mul(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::Mul,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create a 'neg' expression. `e` must evaluate to Long type.
+    fn neg(self, e: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::UnaryApp {
+            op: UnaryOp::Neg,
+            arg: Arc::new(e),
+        })
+    }
+
+    /// Create an 'in' expression. First argument must evaluate to Entity type.
+    /// Second argument must evaluate to either Entity type or Set type where
+    /// all set elements have Entity type.
+    fn is_in(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::In,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create a 'contains' expression.
+    /// First argument must have Set type.
+    fn contains(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::Contains,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create a 'contains_all' expression. Arguments must evaluate to Set type
+    fn contains_all(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::ContainsAll,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create an 'contains_any' expression. Arguments must evaluate to Set type
+    fn contains_any(self, e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::ContainsAny,
+            arg1: Arc::new(e1),
+            arg2: Arc::new(e2),
+        })
+    }
+
+    /// Create an 'is_empty' expression. Argument must evaluate to Set type
+    fn is_empty(self, expr: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::UnaryApp {
+            op: UnaryOp::IsEmpty,
+            arg: Arc::new(expr),
+        })
+    }
+
+    /// Create a 'getTag' expression.
+    /// `expr` must evaluate to Entity type, `tag` must evaluate to String type.
+    fn get_tag(self, expr: Expr<T>, tag: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::GetTag,
+            arg1: Arc::new(expr),
+            arg2: Arc::new(tag),
+        })
+    }
+
+    /// Create a 'hasTag' expression.
+    /// `expr` must evaluate to Entity type, `tag` must evaluate to String type.
+    fn has_tag(self, expr: Expr<T>, tag: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: BinaryOp::HasTag,
+            arg1: Arc::new(expr),
+            arg2: Arc::new(tag),
+        })
+    }
+
+    /// Create an `Expr` which evaluates to a Set of the given `Expr`s
+    fn set(self, exprs: impl IntoIterator<Item = Expr<T>>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::Set(Arc::new(exprs.into_iter().collect())))
+    }
+
+    /// Create an `Expr` which evaluates to a Record with the given (key, value) pairs.
+    fn record(
+        self,
+        pairs: impl IntoIterator<Item = (SmolStr, Expr<T>)>,
+    ) -> Result<Expr<T>, ExpressionConstructionError> {
+        let mut map = BTreeMap::new();
+        for (k, v) in pairs {
+            match map.entry(k) {
+                btree_map::Entry::Occupied(oentry) => {
+                    return Err(expression_construction_errors::DuplicateKeyError {
+                        key: oentry.key().clone(),
+                        context: "in record literal",
+                    }
+                    .into());
+                }
+                btree_map::Entry::Vacant(ventry) => {
+                    ventry.insert(v);
+                }
+            }
+        }
+        Ok(self.with_expr_kind(ExprKind::Record(Arc::new(map))))
+    }
+
+    /// Create an `Expr` which calls the extension function with the given
+    /// `Name` on `args`
+    fn call_extension_fn(self, fn_name: Name, args: impl IntoIterator<Item = Expr<T>>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::ExtensionFunctionApp {
+            fn_name,
+            args: Arc::new(args.into_iter().collect()),
+        })
+    }
+
+    /// Create an application `Expr` which applies the given built-in unary
+    /// operator to the given `arg`
+    fn unary_app(self, op: impl Into<UnaryOp>, arg: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::UnaryApp {
+            op: op.into(),
+            arg: Arc::new(arg),
+        })
+    }
+
+    /// Create an application `Expr` which applies the given built-in binary
+    /// operator to `arg1` and `arg2`
+    fn binary_app(self, op: impl Into<BinaryOp>, arg1: Expr<T>, arg2: Expr<T>) -> Expr<T> {
+        self.with_expr_kind(ExprKind::BinaryApp {
+            op: op.into(),
+            arg1: Arc::new(arg1),
+            arg2: Arc::new(arg2),
+        })
+    }
+
+    /// Create an `Expr` which gets a given attribute of a given `Entity` or record.
+    ///
+    /// `expr` must evaluate to either Entity or Record type
+    fn get_attr(self, expr: Expr<T>, attr: SmolStr) -> Expr<T> {
+        self.with_expr_kind(ExprKind::GetAttr {
+            expr: Arc::new(expr),
+            attr,
+        })
+    }
+
+    /// Create an `Expr` which tests for the existence of a given
+    /// attribute on a given `Entity` or record.
+    ///
+    /// `expr` must evaluate to either Entity or Record type
+    fn has_attr(self, expr: Expr<T>, attr: SmolStr) -> Expr<T> {
+        self.with_expr_kind(ExprKind::HasAttr {
+            expr: Arc::new(expr),
+            attr,
+        })
+    }
+
+    /// Create a 'like' expression.
+    ///
+    /// `expr` must evaluate to a String type
+    fn like(self, expr: Expr<T>, pattern: Pattern) -> Expr<T> {
+        self.with_expr_kind(ExprKind::Like {
+            expr: Arc::new(expr),
+            pattern,
+        })
+    }
+
+    /// Create an 'is' expression.
+    fn is_entity_type(self, expr: Expr<T>, entity_type: EntityType) -> Expr<T> {
+        self.with_expr_kind(ExprKind::Is {
+            expr: Arc::new(expr),
+            entity_type,
+        })
+    }
+
+    fn new() -> Self
+    where
+        Self: Sized,
+    {
+        Self::with_data(Self::Data::default())
+    }
+
+    fn with_source_loc(self, l: &Loc) -> Self
+    where
+        Self: Sized,
+    {
+        self.with_maybe_source_loc(Some(l))
+    }
+
+    fn is_in_entity_type(
+        self,
+        e1: Self::Expr,
+        entity_type: EntityType,
+        e2: Self::Expr,
+    ) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        self.clone().and(
+            self.clone().is_entity_type(e1.clone(), entity_type),
+            self.is_in(e1, e2),
+        )
+    }
+
+    fn noteq(self, e1: Self::Expr, e2: Self::Expr) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        self.clone().not(self.is_eq(e1, e2))
+    }
+
+    fn greater(self, e1: Self::Expr, e2: Self::Expr) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        // e1 > e2 is defined as !(e1 <= e2)
+        self.clone().not(self.lesseq(e1, e2))
+    }
+
+    fn greatereq(self, e1: Self::Expr, e2: Self::Expr) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        // e1 >= e2 is defined as !(e1 < e2)
+        self.clone().not(self.less(e1, e2))
+    }
+
+    fn and_nary(self, first: Self::Expr, others: impl IntoIterator<Item = Self::Expr>) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        others
+            .into_iter()
+            .fold(first, |acc, next| self.clone().and(acc, next))
+    }
+
+    fn or_nary(self, first: Self::Expr, others: impl IntoIterator<Item = Self::Expr>) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        others
+            .into_iter()
+            .fold(first, |acc, next| self.clone().or(acc, next))
+    }
+
+    fn add_nary(
+        self,
+        first: Self::Expr,
+        other: impl IntoIterator<Item = (crate::parser::cst::AddOp, Self::Expr)>,
+    ) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        other.into_iter().fold(first, |acc, (op, next)| match op {
+            crate::parser::cst::AddOp::Plus => self.clone().add(acc, next),
+            crate::parser::cst::AddOp::Minus => self.clone().sub(acc, next),
+        })
+    }
+
+    fn mul_nary(self, first: Self::Expr, other: impl IntoIterator<Item = Self::Expr>) -> Self::Expr
+    where
+        Self: Sized,
+    {
+        other
+            .into_iter()
+            .fold(first, |acc, next| self.clone().mul(acc, next))
+    }
+}
+
+impl<T> ExprWithErrsBuilder<T> {
+    /// Construct an `Expr` containing the `data` and `source_loc` in this
+    /// `ExprBuilder` and the given `ExprKind`.
+    pub fn with_expr_kind(self, expr_kind: ExprKind<T>) -> Expr<T> {
+        Expr::new(expr_kind, self.source_loc, self.data)
+    }
+}
diff --git a/cedar-policy-core/src/ast/expr_iterator.rs b/cedar-policy-core/src/ast/expr_iterator.rs
index 749ead5d..6751e491 100644
--- a/cedar-policy-core/src/ast/expr_iterator.rs
+++ b/cedar-policy-core/src/ast/expr_iterator.rs
@@ -82,6 +82,8 @@ impl<'a, T> Iterator for ExprIterator<'a, T> {
             ExprKind::Record(map) => {
                 self.expression_stack.extend(map.values());
             }
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { .. } => (),
         }
         Some(next_expr)
     }
diff --git a/cedar-policy-core/src/ast/restricted_expr.rs b/cedar-policy-core/src/ast/restricted_expr.rs
index 9976b3d2..497489cf 100644
--- a/cedar-policy-core/src/ast/restricted_expr.rs
+++ b/cedar-policy-core/src/ast/restricted_expr.rs
@@ -529,6 +529,8 @@ fn is_restricted(expr: &Expr) -> Result<(), RestrictedExpressionError> {
         ExprKind::ExtensionFunctionApp { args, .. } => args.iter().try_for_each(is_restricted),
         ExprKind::Set(exprs) => exprs.iter().try_for_each(is_restricted),
         ExprKind::Record(map) => map.values().try_for_each(is_restricted),
+        #[cfg(feature = "tolerant-ast")]
+        ExprKind::Error { .. } => Ok(()),
     }
 }
 
diff --git a/cedar-policy-core/src/ast/value.rs b/cedar-policy-core/src/ast/value.rs
index 825f41f5..87af87a6 100644
--- a/cedar-policy-core/src/ast/value.rs
+++ b/cedar-policy-core/src/ast/value.rs
@@ -359,6 +359,8 @@ impl TryFrom<Expr> for ValueKind {
                 .map(|(k, v)| Value::try_from(v.clone()).map(|v| (k.clone(), v)))
                 .collect::<Result<BTreeMap<SmolStr, Value>, _>>()
                 .map(|m| Self::Record(Arc::new(m))),
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { .. } => Err(NotValue::NotValue { loc }),
         }
     }
 }
diff --git a/cedar-policy-core/src/entities/json/err.rs b/cedar-policy-core/src/entities/json/err.rs
index f3d3dd8c..b649322a 100644
--- a/cedar-policy-core/src/entities/json/err.rs
+++ b/cedar-policy-core/src/entities/json/err.rs
@@ -120,6 +120,11 @@ pub enum JsonDeserializationError {
     #[error(transparent)]
     #[diagnostic(transparent)]
     TypeMismatch(TypeMismatch),
+    /// When trying to deserialize an AST with an error in it - this should fail
+    #[cfg(feature = "tolerant-ast")]
+    #[error("Unable to deserialize an AST Error node")]
+    #[diagnostic(help("AST error node indicates that the expression has failed to parse"))]
+    ASTErrorNode,
     /// Raised when a JsonValue contains the no longer supported `__expr` escape
     #[error("{0}, the `__expr` escape is no longer supported")]
     #[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))]
diff --git a/cedar-policy-core/src/est/err.rs b/cedar-policy-core/src/est/err.rs
index c31927ba..2a6af4ff 100644
--- a/cedar-policy-core/src/est/err.rs
+++ b/cedar-policy-core/src/est/err.rs
@@ -80,6 +80,10 @@ pub enum FromJsonError {
     #[error(transparent)]
     #[diagnostic(transparent)]
     InvalidActionType(#[from] parse_errors::InvalidActionType),
+    /// Returned when we have an error node in an AST - this is not supported
+    #[cfg(feature = "tolerant-ast")]
+    #[error("AST error node")]
+    ASTErrorNode,
 }
 
 /// Errors arising while converting a policy set from its JSON representation (aka EST) into an AST
diff --git a/cedar-policy-core/src/est/expr.rs b/cedar-policy-core/src/est/expr.rs
index a19e5aef..254b4e18 100644
--- a/cedar-policy-core/src/est/expr.rs
+++ b/cedar-policy-core/src/est/expr.rs
@@ -15,6 +15,10 @@
  */
 
 use super::FromJsonError;
+#[cfg(feature = "tolerant-ast")]
+use crate::ast::expr_allows_errors::AstExprErrorKind;
+#[cfg(feature = "tolerant-ast")]
+use crate::ast::Infallible;
 use crate::ast::{self, BoundedDisplay, EntityUID};
 use crate::entities::json::{
     err::EscapeKind, err::JsonDeserializationError, err::JsonDeserializationErrorContext,
@@ -25,6 +29,8 @@ use crate::extensions::Extensions;
 use crate::jsonvalue::JsonValueWithNoDuplicateKeys;
 use crate::parser::cst_to_ast;
 use crate::parser::err::ParseErrors;
+#[cfg(feature = "tolerant-ast")]
+use crate::parser::err::{ToASTError, ToASTErrorKind};
 use crate::parser::Node;
 use crate::parser::{cst, Loc};
 use itertools::Itertools;
@@ -383,6 +389,9 @@ pub enum ExprNoExt {
         #[cfg_attr(feature = "wasm", tsify(type = "Record<string, Expr>"))]
         BTreeMap<SmolStr, Expr>,
     ),
+    /// AST Error node - this represents a parsing error in a partially generated AST
+    #[cfg(feature = "tolerant-ast")]
+    Error(AstExprErrorKind),
 }
 
 /// Serde JSON structure for an extension function call in the EST format
@@ -413,6 +422,8 @@ impl ExprBuilder for Builder {
     type Expr = Expr;
 
     type Data = ();
+    #[cfg(feature = "tolerant-ast")]
+    type ErrorType = Infallible;
 
     fn with_data(_data: Self::Data) -> Self {
         Self
@@ -690,6 +701,13 @@ impl ExprBuilder for Builder {
             call: HashMap::from([(fn_name.to_smolstr(), args.into_iter().collect())]),
         })
     }
+
+    #[cfg(feature = "tolerant-ast")]
+    fn error(self, parse_errors: ParseErrors) -> Result<Self::Expr, Self::ErrorType> {
+        Ok(Expr::ExprNoExt(ExprNoExt::Error(
+            AstExprErrorKind::InvalidExpr(parse_errors.to_string()),
+        )))
+    }
 }
 
 impl Expr {
@@ -853,6 +871,8 @@ impl Expr {
                     }
                     Ok(Expr::ExprNoExt(ExprNoExt::Record(new_m)))
                 }
+                #[cfg(feature = "tolerant-ast")]
+                ExprNoExt::Error(_) => Err(JsonDeserializationError::ASTErrorNode),
             },
             Expr::ExtFuncCall(e_fn_call) => {
                 let mut new_m = HashMap::new();
@@ -1046,6 +1066,8 @@ impl Expr {
                     }),
                 }
             }
+            #[cfg(feature = "tolerant-ast")]
+            Expr::ExprNoExt(ExprNoExt::Error(_)) => Err(FromJsonError::ASTErrorNode),
         }
     }
 }
@@ -1113,6 +1135,15 @@ impl<T: Clone> From<ast::Expr<T>> for Expr {
                         .map(|(k, v)| (k, v.into())),
                 )
                 .unwrap(),
+            #[cfg(feature = "tolerant-ast")]
+            // PANIC SAFETY: error type is Infallible so can never happen
+            #[allow(clippy::unwrap_used)]
+            ast::ExprKind::Error { .. } => Builder::new()
+                .error(ParseErrors::singleton(ToASTError::new(
+                    ToASTErrorKind::ErrorNode,
+                    Loc::new(0..1, "AST_ERROR_NODE".into()),
+                )))
+                .unwrap(),
         }
     }
 }
@@ -1470,6 +1501,11 @@ impl BoundedDisplay for ExprNoExt {
                     }
                 }
             }
+            #[cfg(feature = "tolerant-ast")]
+            ExprNoExt::Error(e) => {
+                write!(f, "{e}")?;
+                Ok(())
+            }
         }
     }
 }
@@ -1558,6 +1594,13 @@ fn maybe_with_parens(
             BoundedDisplay::fmt(expr, f, n)?;
             write!(f, ")")?;
             Ok(())
+        },
+        #[cfg(feature = "tolerant-ast")]
+        Expr::ExprNoExt(ExprNoExt::Error { .. }) => {
+            write!(f, "(")?;
+            BoundedDisplay::fmt(expr, f, n)?;
+            write!(f, ")")?;
+            Ok(())
         }
     }
 }
diff --git a/cedar-policy-core/src/evaluator.rs b/cedar-policy-core/src/evaluator.rs
index a5866f86..ea395c2c 100644
--- a/cedar-policy-core/src/evaluator.rs
+++ b/cedar-policy-core/src/evaluator.rs
@@ -26,6 +26,8 @@ use std::collections::HashMap;
 use std::sync::Arc;
 
 mod err;
+#[cfg(feature = "tolerant-ast")]
+use crate::evaluator::EvaluationError::ASTErrorExpr;
 pub use err::evaluation_errors;
 pub use err::EvaluationError;
 pub(crate) use err::*;
@@ -760,6 +762,10 @@ impl<'e> Evaluator<'e> {
                     }
                 }
             }
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { .. } => Err(ASTErrorExpr(ASTErrorExprError {
+                source_loc: loc.cloned(),
+            })),
         }
     }
 
diff --git a/cedar-policy-core/src/evaluator/err.rs b/cedar-policy-core/src/evaluator/err.rs
index 36ae899e..20c10597 100644
--- a/cedar-policy-core/src/evaluator/err.rs
+++ b/cedar-policy-core/src/evaluator/err.rs
@@ -89,6 +89,12 @@ pub enum EvaluationError {
     #[diagnostic(transparent)]
     NonValue(#[from] evaluation_errors::NonValueError),
 
+    /// Trying to evaluate an expression AST node that gets generated when parsing fails
+    #[cfg(feature = "tolerant-ast")]
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    ASTErrorExpr(#[from] evaluation_errors::ASTErrorExprError),
+
     /// Maximum recursion limit reached for expression evaluation
     #[error(transparent)]
     #[diagnostic(transparent)]
@@ -110,6 +116,8 @@ impl EvaluationError {
             Self::FailedExtensionFunctionExecution(e) => e.source_loc.as_ref(),
             Self::NonValue(e) => e.source_loc.as_ref(),
             Self::RecursionLimit(e) => e.source_loc.as_ref(),
+            #[cfg(feature = "tolerant-ast")]
+            Self::ASTErrorExpr(e) => e.source_loc.as_ref(),
         }
     }
 
@@ -157,6 +165,10 @@ impl EvaluationError {
             Self::RecursionLimit(_) => {
                 Self::RecursionLimit(evaluation_errors::RecursionLimitError { source_loc })
             }
+            #[cfg(feature = "tolerant-ast")]
+            Self::ASTErrorExpr(_) => {
+                Self::ASTErrorExpr(evaluation_errors::ASTErrorExprError { source_loc })
+            }
         }
     }
 
@@ -695,6 +707,28 @@ pub mod evaluation_errors {
         }
     }
 
+    /// Represents an AST node that failed to parse - cannot be evaluated
+    //
+    // CAUTION: this type is publicly exported in `cedar-policy`.
+    // Don't make fields `pub`, don't make breaking changes, and use caution
+    // when adding public methods.
+    #[cfg(feature = "tolerant-ast")]
+    #[derive(Debug, PartialEq, Eq, Clone, Error)]
+    #[error("the expression contains an error")]
+    pub struct ASTErrorExprError {
+        /// Source location
+        pub(crate) source_loc: Option<Loc>,
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    impl Diagnostic for ASTErrorExprError {
+        impl_diagnostic_from_source_loc_opt_field!(source_loc);
+
+        fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+            Some(Box::new("Represents an AST node that failed to parse"))
+        }
+    }
+
     /// Maximum recursion limit reached for expression evaluation
     //
     // CAUTION: this type is publicly exported in `cedar-policy`.
diff --git a/cedar-policy-core/src/expr_builder.rs b/cedar-policy-core/src/expr_builder.rs
index cd66dd38..ee8cf65b 100644
--- a/cedar-policy-core/src/expr_builder.rs
+++ b/cedar-policy-core/src/expr_builder.rs
@@ -27,6 +27,9 @@ use crate::{
     parser::{cst, Loc},
 };
 
+#[cfg(feature = "tolerant-ast")]
+use crate::parser::err::ParseErrors;
+
 /// Defines a generic interface for building different expression data
 /// structures.
 #[allow(clippy::wrong_self_convention)]
@@ -38,6 +41,13 @@ pub trait ExprBuilder: Clone {
     /// can be `()` if no data is stored.
     type Data: Default;
 
+    /// Type for what error we return if we cannot construct an error node
+    ///
+    ///  By default we fail on errors and this should be a ParseErrors
+    ///  But when we run with error parsing enabled, can be Infallible
+    #[cfg(feature = "tolerant-ast")]
+    type ErrorType;
+
     /// Construct a new expression builder for an expression that will not carry any data.
     fn new() -> Self
     where
@@ -46,6 +56,10 @@ pub trait ExprBuilder: Clone {
         Self::with_data(Self::Data::default())
     }
 
+    /// Build an expression that failed to parse - can optionally include subexpressions that parsed successfully
+    #[cfg(feature = "tolerant-ast")]
+    fn error(self, parse_errors: ParseErrors) -> Result<Self::Expr, Self::ErrorType>;
+
     /// Build an expression storing this information
     fn with_data(data: Self::Data) -> Self;
 
diff --git a/cedar-policy-core/src/parser.rs b/cedar-policy-core/src/parser.rs
index 1ca4b2ba..7373fe0b 100644
--- a/cedar-policy-core/src/parser.rs
+++ b/cedar-policy-core/src/parser.rs
@@ -110,7 +110,7 @@ pub fn parse_policy_or_template(
 ) -> Result<ast::Template, err::ParseErrors> {
     let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
     let cst = text_to_cst::parse_policy(text)?;
-    cst.to_policy_template(id)
+    cst.to_template(id)
 }
 
 /// Like `parse_policy_or_template()`, but also returns the (lossless) EST -- that
@@ -122,7 +122,7 @@ pub fn parse_policy_or_template_to_est_and_ast(
 ) -> Result<(est::Policy, ast::Template), err::ParseErrors> {
     let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
     let cst = text_to_cst::parse_policy(text)?;
-    let ast = cst.to_policy_template(id)?;
+    let ast = cst.to_template(id)?;
     let est = cst.try_into_inner()?.try_into()?;
     Ok((est, ast))
 }
@@ -137,7 +137,7 @@ pub fn parse_template(
 ) -> Result<ast::Template, err::ParseErrors> {
     let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
     let cst = text_to_cst::parse_policy(text)?;
-    let template = cst.to_policy_template(id)?;
+    let template = cst.to_template(id)?;
     if template.slots().count() == 0 {
         Err(err::ToASTError::new(err::ToASTErrorKind::expected_template(), cst.loc).into())
     } else {
diff --git a/cedar-policy-core/src/parser/cst_to_ast.rs b/cedar-policy-core/src/parser/cst_to_ast.rs
index cc55f145..9b95e48a 100644
--- a/cedar-policy-core/src/parser/cst_to_ast.rs
+++ b/cedar-policy-core/src/parser/cst_to_ast.rs
@@ -39,6 +39,8 @@ use super::loc::Loc;
 use super::node::Node;
 use super::unescape::{to_pattern, to_unescaped_string};
 use super::util::{flatten_tuple_2, flatten_tuple_3, flatten_tuple_4};
+#[cfg(feature = "tolerant-ast")]
+use crate::ast::expr_allows_errors::ExprWithErrsBuilder;
 use crate::ast::{
     self, ActionConstraint, CallStyle, Integer, PatternElem, PolicySetError, PrincipalConstraint,
     PrincipalOrResourceConstraint, ResourceConstraint, UnreservedId,
@@ -135,8 +137,8 @@ impl Node<Option<cst::Policies>> {
                         };
                     }
                 }
-                Ok(Either::Left(inline_policy)) => {
-                    if let Err(e) = pset.add_static(inline_policy) {
+                Ok(Either::Left(static_policy)) => {
+                    if let Err(e) = pset.add_static(static_policy) {
                         match e {
                             PolicySetError::Occupied { id } => all_errs.push(
                                 self.to_ast_err(ToASTErrorKind::DuplicatePolicyId(id))
@@ -161,7 +163,13 @@ impl Node<Option<cst::Policies>> {
 }
 
 impl Node<Option<cst::Policy>> {
-    /// Convert `cst::Policy` to an AST `InlinePolicy` or `Template`
+    /// Convert `cst::Policy` to `ast::Template`. Works for static policies as
+    /// well, which will become templates with 0 slots
+    pub fn to_template(&self, id: ast::PolicyID) -> Result<ast::Template> {
+        self.to_policy_template(id)
+    }
+
+    /// Convert `cst::Policy` to an AST `StaticPolicy` or `Template`
     pub fn to_policy_or_template(
         &self,
         id: ast::PolicyID,
@@ -177,7 +185,7 @@ impl Node<Option<cst::Policy>> {
         }
     }
 
-    /// Convert `cst::Policy` to an AST `InlinePolicy`. (Will fail if the CST is for a template)
+    /// Convert `cst::Policy` to an AST `StaticPolicy`. (Will fail if the CST is for a template)
     pub fn to_policy(&self, id: ast::PolicyID) -> Result<ast::StaticPolicy> {
         let maybe_template = self.to_policy_template(id);
         let maybe_policy = maybe_template.map(ast::StaticPolicy::try_from);
@@ -212,7 +220,7 @@ impl Node<Option<cst::Policy>> {
         }
     }
 
-    /// Convert `cst::Policy` to `ast::Template`. Works for inline policies as
+    /// Convert `cst::Policy` to `ast::Template`. Works for static policies as
     /// well, which will become templates with 0 slots
     pub fn to_policy_template(&self, id: ast::PolicyID) -> Result<ast::Template> {
         let policy = self.try_as_inner()?;
@@ -260,6 +268,98 @@ impl Node<Option<cst::Policy>> {
             &self.loc,
         ))
     }
+
+    /// Convert `cst::Policy` to an AST `StaticPolicy`. (Will fail if the CST is for a template)
+    /// NOTE: This function allows partial parsing and can produce AST Error nodes
+    /// These cannot be evaluated
+    /// Should ONLY be used to examine a partially constructed AST from invalid Cedar
+    #[cfg(feature = "tolerant-ast")]
+    pub fn to_policy_with_errors(&self, id: ast::PolicyID) -> Result<ast::StaticPolicy> {
+        let maybe_template = self.to_policy_template_with_errors(id);
+        let maybe_policy = maybe_template.map(ast::StaticPolicy::try_from);
+        match maybe_policy {
+            // Successfully parsed a static policy
+            Ok(Ok(p)) => Ok(p),
+            // 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()),
+            )
+            .into()),
+            // The source failed to parse completely. If the parse errors include
+            // `SlotsInConditionClause` also add an `ExpectedStaticPolicy` error.
+            Err(mut errs) => {
+                let new_errs = errs
+                    .iter()
+                    .filter_map(|err| match err {
+                        ParseError::ToAST(err) => match err.kind() {
+                            ToASTErrorKind::SlotsInConditionClause(inner) => Some(ToASTError::new(
+                                ToASTErrorKind::expected_static_policy(inner.slot.clone()),
+                                err.source_loc().clone(),
+                            )),
+                            _ => None,
+                        },
+                        _ => None,
+                    })
+                    .collect::<Vec<_>>();
+                errs.extend(new_errs);
+                Err(errs)
+            }
+        }
+    }
+
+    /// Convert `cst::Policy` to `ast::Template`. Works for static policies as
+    /// well, which will become templates with 0 slots
+    /// NOTE: This function allows partial parsing and can produce AST Error nodes
+    /// These cannot be evaluated
+    /// Should ONLY be used to examine a partially constructed AST from invalid Cedar
+    #[cfg(feature = "tolerant-ast")]
+    pub fn to_policy_template_with_errors(&self, id: ast::PolicyID) -> Result<ast::Template> {
+        let policy = self.try_as_inner()?;
+
+        // convert effect
+        let maybe_effect = policy.effect.to_effect();
+
+        // convert annotations
+        let maybe_annotations = policy.get_ast_annotations(|value, loc| {
+            ast::Annotation::with_optional_value(value, Some(loc.clone()))
+        });
+
+        // convert scope
+        let maybe_scope = policy.extract_scope();
+
+        // convert conditions
+        let maybe_conds = ParseErrors::transpose(policy.conds.iter().map(|c| {
+            let (e, is_when) = c.to_expr::<ExprWithErrsBuilder<()>>()?;
+            let slot_errs = e.slots().map(|slot| {
+                ToASTError::new(
+                    ToASTErrorKind::slots_in_condition_clause(
+                        slot.clone(),
+                        if is_when { "when" } else { "unless" },
+                    ),
+                    slot.loc.unwrap_or_else(|| c.loc.clone()),
+                )
+                .into()
+            });
+            match ParseErrors::from_iter(slot_errs) {
+                Some(errs) => Err(errs),
+                None => Ok(e),
+            }
+        }));
+
+        let (effect, annotations, (principal, action, resource), conds) =
+            flatten_tuple_4(maybe_effect, maybe_annotations, maybe_scope, maybe_conds)?;
+        Ok(construct_template_policy(
+            id,
+            annotations.into(),
+            effect,
+            principal,
+            action,
+            resource,
+            conds,
+            &self.loc,
+        ))
+    }
 }
 
 impl cst::Policy {
@@ -745,7 +845,6 @@ impl Node<Option<cst::Cond>> {
     /// for information only.)
     fn to_expr<Build: ExprBuilder>(&self) -> Result<(Build::Expr, bool)> {
         let cond = self.try_as_inner()?;
-
         let is_when = cond.cond.to_cond_is_when()?;
 
         let maybe_expr = match &cond.expr {
@@ -764,9 +863,10 @@ impl Node<Option<cst::Cond>> {
                         }
                     }
                 };
-                Err(self
-                    .to_ast_err(ToASTErrorKind::EmptyClause(Some(ident)))
-                    .into())
+                convert_expr_error_to_parse_error::<Build>(
+                    self.to_ast_err(ToASTErrorKind::EmptyClause(Some(ident)))
+                        .into(),
+                )
             }
         };
 
@@ -794,6 +894,25 @@ impl Node<Option<cst::Str>> {
     }
 }
 
+#[cfg(feature = "tolerant-ast")]
+fn build_ast_error_node_if_possible<Build: ExprBuilder>(error: ParseErrors) -> Result<Build::Expr> {
+    let res = Build::new().error(error.clone());
+    match res {
+        Ok(r) => Ok(r),
+        Err(_) => Err(error),
+    }
+}
+
+/// Since ExprBuilder ErrorType can be Infallible or ParseErrors, if we get an error from building the node pass the ParseErrors along
+fn convert_expr_error_to_parse_error<Build: ExprBuilder>(
+    error: ParseErrors,
+) -> Result<Build::Expr> {
+    #[cfg(feature = "tolerant-ast")]
+    return build_ast_error_node_if_possible::<Build>(error);
+    #[allow(unreachable_code)]
+    Err(error)
+}
+
 /// Result type of conversion when we expect an Expr, Var, Name, or String.
 ///
 /// During conversion it is useful to keep track of expression that may be used
@@ -3195,7 +3314,7 @@ mod tests {
         for src in CORRECT_TEMPLATES {
             text_to_cst::parse_policy(src)
                 .expect("parse_error")
-                .to_policy_template(ast::PolicyID::from_string("i0"))
+                .to_template(ast::PolicyID::from_string("i0"))
                 .unwrap_or_else(|errs| {
                     panic!(
                         "Failed to create a policy template: {:?}",
@@ -5143,4 +5262,163 @@ mod tests {
             }
         );
     }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[track_caller]
+    fn assert_parse_policy_allows_errors(text: &str) -> ast::StaticPolicy {
+        text_to_cst::parse_policy(text)
+            .expect("failed parser")
+            .to_policy_with_errors(ast::PolicyID::from_string("id"))
+            .unwrap_or_else(|errs| {
+                panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
+            })
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[track_caller]
+    fn assert_parse_policy_allows_errors_fails(text: &str) -> ParseErrors {
+        let result = text_to_cst::parse_policy(text)
+            .expect("failed parser")
+            .to_policy_with_errors(ast::PolicyID::from_string("id"));
+        match result {
+            Ok(policy) => {
+                panic!("conversion to AST should have failed, but succeeded with:\n{policy}")
+            }
+            Err(errs) => errs,
+        }
+    }
+
+    // Test parsing AST that allows Error nodes
+    #[cfg(feature = "tolerant-ast")]
+    #[test]
+    fn parsing_with_errors_succeeds_with_empty_when() {
+        let src = r#"
+            permit(principal, action, resource) when {};
+        "#;
+        assert_parse_policy_allows_errors(src);
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[test]
+    fn show_policy1_errors_enabled() {
+        let src = r#"
+            permit(principal:p,action:a,resource:r)when{w}unless{u}advice{"doit"};
+        "#;
+        let errs = assert_parse_policy_allows_errors_fails(src);
+        expect_n_errors(src, &errs, 6);
+        expect_some_error_matches(
+            src,
+            &errs,
+            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
+                .help("try using `is` instead")
+                .exactly_one_underline("p")
+                .build(),
+        );
+        expect_some_error_matches(
+            src,
+            &errs,
+            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
+                .help("try using `is` instead")
+                .exactly_one_underline("a")
+                .build(),
+        );
+        expect_some_error_matches(
+            src,
+            &errs,
+            &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
+                .help("try using `is` instead")
+                .exactly_one_underline("r")
+                .build(),
+        );
+        expect_some_error_matches(
+            src,
+            &errs,
+            &ExpectedErrorMessageBuilder::error("invalid variable: w")
+                .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `w` in quotes to make a string?")
+                .exactly_one_underline("w")
+                .build(),
+        );
+        expect_some_error_matches(
+            src,
+            &errs,
+            &ExpectedErrorMessageBuilder::error("invalid variable: u")
+                .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `u` in quotes to make a string?")
+                .exactly_one_underline("u")
+                .build(),
+        );
+        expect_some_error_matches(
+            src,
+            &errs,
+            &ExpectedErrorMessageBuilder::error("invalid policy condition: advice")
+                .help("condition must be either `when` or `unless`")
+                .exactly_one_underline("advice")
+                .build(),
+        );
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[test]
+    fn show_policy2_errors_enabled() {
+        let src = r#"
+            permit(principal,action,resource)when{true};
+        "#;
+        assert_parse_policy_allows_errors(src);
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[test]
+    fn show_policy3_errors_enabled() {
+        let src = r#"
+            permit(principal in User::"jane",action,resource);
+        "#;
+        assert_parse_policy_allows_errors(src);
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[test]
+    fn show_policy4_errors_enabled() {
+        let src = r#"
+            forbid(principal in User::"jane",action,resource)unless{
+                context.group != "friends"
+            };
+        "#;
+        assert_parse_policy_allows_errors(src);
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[test]
+    fn single_annotation_errors_enabled() {
+        // common use-case
+        let policy = assert_parse_policy_allows_errors(
+            r#"
+            @anno("good annotation")permit(principal,action,resource);
+        "#,
+        );
+        assert_matches!(
+            policy.annotation(&ast::AnyId::new_unchecked("anno")),
+            Some(annotation) => assert_eq!(annotation.as_ref(), "good annotation")
+        );
+    }
+
+    #[cfg(feature = "tolerant-ast")]
+    #[test]
+    fn duplicate_annotations_error_errors_enabled() {
+        // duplication is error
+        let src = r#"
+            @anno("good annotation")
+            @anno2("good annotation")
+            @anno("oops, duplicate")
+            permit(principal,action,resource);
+        "#;
+        let errs = assert_parse_policy_allows_errors_fails(src);
+        // annotation duplication (anno)
+        expect_n_errors(src, &errs, 1);
+        expect_some_error_matches(
+            src,
+            &errs,
+            &ExpectedErrorMessageBuilder::error("duplicate annotation: @anno")
+                .exactly_one_underline("@anno(\"oops, duplicate\")")
+                .build(),
+        );
+    }
 }
diff --git a/cedar-policy-core/src/parser/err.rs b/cedar-policy-core/src/parser/err.rs
index ab96cf86..2e1c0f49 100644
--- a/cedar-policy-core/src/parser/err.rs
+++ b/cedar-policy-core/src/parser/err.rs
@@ -406,6 +406,10 @@ pub enum ToASTErrorKind {
     #[error("when `is` and `in` are used together, `is` must come first")]
     #[diagnostic(help("try `_ is _ in _`"))]
     InvertedIsIn,
+    /// Represents a parsing error in a partially generated AST
+    #[cfg(feature = "tolerant-ast")]
+    #[error("Trying to convert AST error node")]
+    ErrorNode,
 }
 
 fn invalid_is_help(lhs: &str, rhs: &str) -> String {
diff --git a/cedar-policy-validator/Cargo.toml b/cedar-policy-validator/Cargo.toml
index 5299ae5a..0b60e492 100644
--- a/cedar-policy-validator/Cargo.toml
+++ b/cedar-policy-validator/Cargo.toml
@@ -50,6 +50,7 @@ partial-validate = []
 level-validate = []
 wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]
 entity-manifest = []
+tolerant-ast = ["cedar-policy-core/tolerant-ast"]
 
 [dev-dependencies]
 similar-asserts = "1.6.1"
diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs
index 8add18ab..62272f09 100644
--- a/cedar-policy-validator/src/entity_manifest.rs
+++ b/cedar-policy-validator/src/entity_manifest.rs
@@ -666,6 +666,12 @@ fn entity_manifest_from_expr(
         ExprKind::HasAttr { expr, attr } => Ok(entity_manifest_from_expr(expr)?
             .get_or_has_attr(attr)
             .empty_paths()),
+        #[cfg(feature = "tolerant-ast")]
+        ExprKind::Error { .. } => Err(EntityManifestError::UnsupportedCedarFeature(
+            UnsupportedCedarFeatureError {
+                feature: "No support for AST error nodes".into(),
+            },
+        )),
     }
 }
 
diff --git a/cedar-policy-validator/src/level_validate.rs b/cedar-policy-validator/src/level_validate.rs
index e5c1e388..dfd4b4f3 100644
--- a/cedar-policy-validator/src/level_validate.rs
+++ b/cedar-policy-validator/src/level_validate.rs
@@ -16,6 +16,8 @@
 
 //! Implementation of level validation (RFC 76)
 
+use crate::validation_errors::InternalInvariantViolation;
+
 use super::*;
 use cedar_policy_core::ast::{BinaryOp, PolicyID};
 use typecheck::PolicyCheck;
@@ -65,7 +67,7 @@ impl Validator {
                 PolicyCheck::Success(e) | PolicyCheck::Irrelevant(_, e) => {
                     let res = Self::check_entity_deref_level_helper(&e, max_allowed_level, t.id());
                     if let Some(e) = res.1 {
-                        errs.push(ValidationError::EntityDerefLevelViolation(e))
+                        errs.push(e)
                     }
                 }
                 // PANIC SAFETY: We only validate the level after validation passed
@@ -77,8 +79,8 @@ impl Validator {
     }
 
     fn min(
-        v: impl IntoIterator<Item = (EntityDerefLevel, Option<EntityDerefLevelViolation>)>,
-    ) -> (EntityDerefLevel, Option<EntityDerefLevelViolation>) {
+        v: impl IntoIterator<Item = (EntityDerefLevel, Option<ValidationError>)>,
+    ) -> (EntityDerefLevel, Option<ValidationError>) {
         let p = v.into_iter().min_by(|(l1, _), (l2, _)| l1.cmp(l2));
         match p {
             Some(p) => p,
@@ -92,7 +94,7 @@ impl Validator {
         e: &cedar_policy_core::ast::Expr<Option<crate::types::Type>>,
         max_allowed_level: &EntityDerefLevel,
         policy_id: &PolicyID,
-    ) -> (EntityDerefLevel, Option<EntityDerefLevelViolation>) {
+    ) -> (EntityDerefLevel, Option<ValidationError>) {
         use crate::types::{EntityRecordKind, Type};
         use cedar_policy_core::ast::ExprKind;
         match e.expr_kind() {
@@ -138,12 +140,15 @@ impl Validator {
                 if new_level.level < 0 {
                     (
                         new_level,
-                        Some(EntityDerefLevelViolation {
-                            source_loc: e.source_loc().cloned(),
-                            policy_id: policy_id.clone(),
-                            actual_level: new_level,
-                            allowed_level: *max_allowed_level,
-                        }),
+                        Some(
+                            EntityDerefLevelViolation {
+                                source_loc: e.source_loc().cloned(),
+                                policy_id: policy_id.clone(),
+                                actual_level: new_level,
+                                allowed_level: *max_allowed_level,
+                            }
+                            .into(),
+                        ),
                     )
                 } else {
                     (new_level, None)
@@ -197,12 +202,15 @@ impl Validator {
                             if new_level.level < 0 {
                                 (
                                     new_level,
-                                    Some(EntityDerefLevelViolation {
-                                        source_loc: e.source_loc().cloned(),
-                                        policy_id: policy_id.clone(),
-                                        actual_level: new_level,
-                                        allowed_level: *max_allowed_level,
-                                    }),
+                                    Some(
+                                        EntityDerefLevelViolation {
+                                            source_loc: e.source_loc().cloned(),
+                                            policy_id: policy_id.clone(),
+                                            actual_level: new_level,
+                                            allowed_level: *max_allowed_level,
+                                        }
+                                        .into(),
+                                    ),
                                 )
                             } else {
                                 (new_level, None)
@@ -238,6 +246,16 @@ impl Validator {
                     .collect();
                 Self::min(v)
             }
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { .. } => (
+                EntityDerefLevel { level: 0 },
+                Some(ValidationError::InternalInvariantViolation(
+                    InternalInvariantViolation {
+                        source_loc: None,
+                        policy_id: policy_id.clone(),
+                    },
+                )),
+            ),
         }
     }
 }
diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs
index 33b51493..9972482b 100644
--- a/cedar-policy-validator/src/typecheck.rs
+++ b/cedar-policy-validator/src/typecheck.rs
@@ -1174,6 +1174,8 @@ impl<'a> SingleEnvTypechecker<'a> {
                     },
                 )
             }
+            #[cfg(feature = "tolerant-ast")]
+            ExprKind::Error { .. } => TypecheckAnswer::ErrorAstNode,
         }
     }
 
diff --git a/cedar-policy-validator/src/typecheck/typecheck_answer.rs b/cedar-policy-validator/src/typecheck/typecheck_answer.rs
index 02daf22b..bb854f6c 100644
--- a/cedar-policy-validator/src/typecheck/typecheck_answer.rs
+++ b/cedar-policy-validator/src/typecheck/typecheck_answer.rs
@@ -39,6 +39,10 @@ pub(crate) enum TypecheckAnswer<'a> {
 
     /// Recursion limit reached
     RecursionLimit,
+
+    /// Trying to typecheck an error node
+    #[cfg(feature = "tolerant-ast")]
+    ErrorAstNode,
 }
 
 impl<'a> TypecheckAnswer<'a> {
@@ -77,6 +81,8 @@ impl<'a> TypecheckAnswer<'a> {
             TypecheckAnswer::TypecheckSuccess { expr_type, .. } => Some(expr_type),
             TypecheckAnswer::TypecheckFail { expr_recovery_type } => Some(expr_recovery_type),
             TypecheckAnswer::RecursionLimit => None,
+            #[cfg(feature = "tolerant-ast")]
+            TypecheckAnswer::ErrorAstNode => None,
         }
         .and_then(|e| e.data().as_ref())
             == Some(ty)
@@ -87,6 +93,8 @@ impl<'a> TypecheckAnswer<'a> {
             TypecheckAnswer::TypecheckSuccess { expr_type, .. } => Some(expr_type),
             TypecheckAnswer::TypecheckFail { expr_recovery_type } => Some(expr_recovery_type),
             TypecheckAnswer::RecursionLimit => None,
+            #[cfg(feature = "tolerant-ast")]
+            TypecheckAnswer::ErrorAstNode => None,
         }
     }
 
@@ -96,6 +104,8 @@ impl<'a> TypecheckAnswer<'a> {
             TypecheckAnswer::TypecheckSuccess { .. } => true,
             TypecheckAnswer::TypecheckFail { .. } => false,
             TypecheckAnswer::RecursionLimit => false,
+            #[cfg(feature = "tolerant-ast")]
+            TypecheckAnswer::ErrorAstNode => false,
         }
     }
 
@@ -115,6 +125,8 @@ impl<'a> TypecheckAnswer<'a> {
             },
             TypecheckAnswer::TypecheckFail { .. } => self,
             TypecheckAnswer::RecursionLimit => self,
+            #[cfg(feature = "tolerant-ast")]
+            TypecheckAnswer::ErrorAstNode => self,
         }
     }
 
@@ -127,6 +139,8 @@ impl<'a> TypecheckAnswer<'a> {
             TypecheckAnswer::TypecheckSuccess { expr_type, .. } => TypecheckAnswer::fail(expr_type),
             TypecheckAnswer::TypecheckFail { .. } => self,
             TypecheckAnswer::RecursionLimit => self,
+            #[cfg(feature = "tolerant-ast")]
+            TypecheckAnswer::ErrorAstNode => self,
         }
     }
 
@@ -146,6 +160,8 @@ impl<'a> TypecheckAnswer<'a> {
                 f(expr_recovery_type, CapabilitySet::new()).into_fail()
             }
             TypecheckAnswer::RecursionLimit => self,
+            #[cfg(feature = "tolerant-ast")]
+            TypecheckAnswer::ErrorAstNode => self,
         }
     }
 
@@ -163,6 +179,8 @@ impl<'a> TypecheckAnswer<'a> {
         let mut unwrapped = Vec::new();
         let mut any_failed = false;
         let mut recusion_limit_reached = false;
+        #[cfg(feature = "tolerant-ast")]
+        let mut ast_has_errors = false;
         for ans in answers {
             any_failed |= !ans.typechecked();
             unwrapped.push(match ans {
@@ -177,9 +195,19 @@ impl<'a> TypecheckAnswer<'a> {
                     recusion_limit_reached = true;
                     break;
                 }
+                #[cfg(feature = "tolerant-ast")]
+                TypecheckAnswer::ErrorAstNode => {
+                    ast_has_errors = true;
+                    break;
+                }
             });
         }
 
+        #[cfg(feature = "tolerant-ast")]
+        if ast_has_errors {
+            return TypecheckAnswer::ErrorAstNode;
+        }
+
         let ans = f(unwrapped);
         if recusion_limit_reached {
             TypecheckAnswer::RecursionLimit
diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md
index 15367ccf..c211c7ac 100644
--- a/cedar-policy/CHANGELOG.md
+++ b/cedar-policy/CHANGELOG.md
@@ -22,6 +22,7 @@ Cedar Language Version: TBD
 - Added `Entities::remove_entities()` to remove `Entity`s from an `Entities` struct (resolving #701)
 - Added `PolicySet::merge_policyset()` to merge a `PolicySet` into another `PolicySet` struct (resolving #610)
 - Implemented [RFC 53 (enumerated entity types)](https://github.com/cedar-policy/rfcs/blob/main/text/0053-enum-entities.md)  (#1377)
+- Added the experimental feature `tolerant-ast` which allows certain errors to be propogated in AST expressions as an `ExprKind::Error` (#1470)
 
 ### Fixed
 
diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml
index 3099e251..97c02802 100644
--- a/cedar-policy/Cargo.toml
+++ b/cedar-policy/Cargo.toml
@@ -52,7 +52,7 @@ corpus-timing = []
 
 # Experimental features.
 # Enable all experimental features with `cargo build --features "experimental"`
-experimental = ["partial-eval", "permissive-validate", "partial-validate", "level-validate", "entity-manifest", "protobufs", "datetime"]
+experimental = ["partial-eval", "permissive-validate", "partial-validate", "level-validate", "entity-manifest", "protobufs", "datetime", "tolerant-ast"]
 entity-manifest = ["cedar-policy-validator/entity-manifest"]
 partial-eval = ["cedar-policy-core/partial-eval", "cedar-policy-validator/partial-eval"]
 permissive-validate = []
@@ -60,6 +60,7 @@ partial-validate = ["cedar-policy-validator/partial-validate"]
 protobufs = ["dep:prost", "dep:prost-build"]
 level-validate = ["cedar-policy-validator/level-validate"]
 wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]
+tolerant-ast = ["cedar-policy-core/tolerant-ast",  "cedar-policy-validator/tolerant-ast"]
 
 [lib]
 # cdylib required for wasm
diff --git a/cedar-policy/src/proto/ast.rs b/cedar-policy/src/proto/ast.rs
index c6048f2d..575eb649 100644
--- a/cedar-policy/src/proto/ast.rs
+++ b/cedar-policy/src/proto/ast.rs
@@ -466,7 +466,9 @@ impl From<&ast::Expr> for models::Expr {
                     .map(|(key, value)| (key.to_string(), models::Expr::from(value)))
                     .collect();
                 models::expr::expr_kind::Data::Record(models::expr::Record { items: precord })
-            }
+            },
+            #[cfg(feature="tolerant-ast")]
+            ast::ExprKind::Error { .. } => unimplemented!("Protobufs feature not compatible with ASTs that contain error nodes - this should never happen"),
         };
         Self {
             expr_kind: Some(Box::new(models::expr::ExprKind {


Test patch:
diff --git a/cedar-policy-validator/src/typecheck/test/type_annotation.rs b/cedar-policy-validator/src/typecheck/test/type_annotation.rs
index dd7f578d..c8648cdd 100644
--- a/cedar-policy-validator/src/typecheck/test/type_annotation.rs
+++ b/cedar-policy-validator/src/typecheck/test/type_annotation.rs
@@ -194,5 +194,9 @@ fn expr_typechecks_with_correct_annotation() {
         crate::typecheck::TypecheckAnswer::RecursionLimit => {
             panic!("Should not have hit recursion limit")
         }
+        #[cfg(feature = "tolerant-ast")]
+        crate::typecheck::TypecheckAnswer::ErrorAstNode => {
+            panic!("Should not type check an AST with an error node")
+        }
     }
 }