Repository: cedar-policy/cedar
Pull request #1644 (2025-05-30 21:41:47)

Expose raw parsing APIs for policies
## Description of changes

Expose `parse_raw` APIs for `PolicySet`, `Policy` and `Template`.

Internally, these call new `parse*_raw` counterparts in `cedar-policy-core` that end up calling `parse_collect_errors`. The main difference there is that we're now making use of the additional `is_fast` flag which we pass to the grammar parsing function. We also rename the feature to `raw-parsing` which is more accurate.

The PR includes some tests for the new APIs, both in `cedar-policy` and `cedar-policy-core`. Feel free to ask for additional testing if you they're insufficient.

## 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).
- [x] 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`.
- [ ] 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.)


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

Base Commit: ```48022d87ca922ccaebc0465d5461435fb3e95715```

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

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

**Actual coverage:** 94.59%

**Status:** PASSED :white_check_mark:

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

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/parser.rs | :green_circle: | 14/14 | 100.00% | 
cedar-policy-core/src/parser/loc.rs | :yellow_circle: | 6/8 | 75.00% | 166, 170
cedar-policy-core/src/parser/text_to_cst.rs | :green_circle: | 15/15 | 100.00% | 
</details>

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

**Actual coverage:** 81.91%

**Status:** PASSED :white_check_mark:

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

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3219/4647 | 69.27% | 69.27%
cedar-policy-cli | :red_circle: | 571/972 | 58.74% | 58.74%
cedar-policy-core | :green_circle: | 12139/14388 | 84.37% | 84.36%
cedar-policy-formatter | :green_circle: | 1074/1206 | 89.05% | 89.05%
cedar-policy-validator | :green_circle: | 8864/10336 | 85.76% | 85.76%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

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

Base Commit: ```48022d87ca922ccaebc0465d5461435fb3e95715```

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

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

**Actual coverage:** 94.59%

**Status:** PASSED :white_check_mark:

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

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/parser.rs | :green_circle: | 14/14 | 100.00% | 
cedar-policy-core/src/parser/loc.rs | :yellow_circle: | 6/8 | 75.00% | 166, 170
cedar-policy-core/src/parser/text_to_cst.rs | :green_circle: | 15/15 | 100.00% | 
</details>

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

**Actual coverage:** 81.91%

**Status:** PASSED :white_check_mark:

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

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3219/4647 | 69.27% | 69.27%
cedar-policy-cli | :red_circle: | 571/972 | 58.74% | 58.74%
cedar-policy-core | :green_circle: | 12139/14388 | 84.37% | 84.36%
cedar-policy-formatter | :green_circle: | 1074/1206 | 89.05% | 89.05%
cedar-policy-validator | :green_circle: | 8864/10336 | 85.76% | 85.76%
cedar-wasm | :red_circle: | 0/29 | 0.00% | 0.00%
</details>

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

Base Commit: ```f85ca109594dc9a41778fde827e86a4471811d97```

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

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

**Actual coverage:** 94.59%

**Status:** PASSED :white_check_mark:

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

| File | Status | Covered | Coverage | Missed Lines |
|:-----|:------:|:-------:|---------:|:-------------|
cedar-policy-core/src/parser.rs | :green_circle: | 14/14 | 100.00% | 
cedar-policy-core/src/parser/loc.rs | :yellow_circle: | 6/8 | 75.00% | 166, 170
cedar-policy-core/src/parser/text_to_cst.rs | :green_circle: | 15/15 | 100.00% | 
</details>

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

**Actual coverage:** 81.92%

**Status:** PASSED :white_check_mark:

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

| Package | Status | Covered | Coverage | Base Coverage |
|:--------|:------:|:-------:|---------:|--------------:|
cedar-policy | :red_circle: | 3223/4654 | 69.25% | --
cedar-policy-cli | :red_circle: | 571/972 | 58.74% | --
cedar-policy-core | :green_circle: | 21009/24728 | 84.96% | --
cedar-policy-formatter | :green_circle: | 1074/1206 | 89.05% | --
cedar-wasm | :red_circle: | 0/29 | 0.00% | --
</details>


Patch:
diff --git a/cedar-policy-core/Cargo.toml b/cedar-policy-core/Cargo.toml
index f5ae2b22..48c819e0 100644
--- a/cedar-policy-core/Cargo.toml
+++ b/cedar-policy-core/Cargo.toml
@@ -64,8 +64,8 @@ wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]
 experimental = ["tolerant-ast", "extended-schema", "entity-manifest", "partial-validate", "partial-eval"]
 extended-schema = []
 
-# Feature for fast parsing
-fast-parsing = []
+# Feature for raw parsing
+raw-parsing = []
 
 [build-dependencies]
 lalrpop = "0.22.2"
diff --git a/cedar-policy-core/src/parser.rs b/cedar-policy-core/src/parser.rs
index 24b9429e..bdf925bf 100644
--- a/cedar-policy-core/src/parser.rs
+++ b/cedar-policy-core/src/parser.rs
@@ -54,6 +54,13 @@ pub fn parse_policyset(text: &str) -> Result<ast::PolicySet, err::ParseErrors> {
     cst.to_policyset()
 }
 
+/// Like [`parse_policyset`], but without retaining source information.
+#[cfg(feature = "raw-parsing")]
+pub fn parse_policyset_raw(text: &str) -> Result<ast::PolicySet, err::ParseErrors> {
+    let cst = text_to_cst::parse_policies_raw(text)?;
+    cst.to_policyset()
+}
+
 /// Like `parse_policyset()`, but also returns the (lossless) original text of
 /// each individual policy.
 /// INVARIANT: The `PolicyId` of every `Policy` and `Template` returned by the
@@ -146,15 +153,19 @@ pub fn parse_template(
     let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
     let cst = text_to_cst::parse_policy(text)?;
     let template = cst.to_template(id)?;
-    if template.slots().count() == 0 {
-        Err(err::ToASTError::new(
-            err::ToASTErrorKind::expected_template(),
-            cst.loc.into_maybe_loc(),
-        )
-        .into())
-    } else {
-        Ok(template)
-    }
+    validate_template_has_slots(template, cst)
+}
+
+/// Like [`parse_template`], but without retaining source information.
+#[cfg(feature = "raw-parsing")]
+pub fn parse_template_raw(
+    id: Option<ast::PolicyID>,
+    text: &str,
+) -> Result<ast::Template, err::ParseErrors> {
+    let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
+    let cst = text_to_cst::parse_policy_raw(text)?;
+    let template = cst.to_template(id)?;
+    validate_template_has_slots(template, cst)
 }
 
 /// Main function for parsing a (static) policy.
@@ -170,6 +181,17 @@ pub fn parse_policy(
     cst.to_policy(id)
 }
 
+/// Like [`parse_policy`], but without retaining source information.
+#[cfg(feature = "raw-parsing")]
+pub fn parse_policy_raw(
+    id: Option<ast::PolicyID>,
+    text: &str,
+) -> Result<ast::StaticPolicy, err::ParseErrors> {
+    let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
+    let cst = text_to_cst::parse_policy_raw(text)?;
+    cst.to_policy(id)
+}
+
 /// Like `parse_policy()`, but also returns the (lossless) EST -- that is, the
 /// EST of the original policy without any of the lossy transforms involved in
 /// converting to AST.
@@ -280,6 +302,23 @@ pub(crate) fn parse_anyid(id: &str) -> Result<ast::AnyId, err::ParseErrors> {
     cst.to_any_ident()
 }
 
+/// Check that a template contains slots. Return the template if it does, or an
+/// error otherwise.
+fn validate_template_has_slots(
+    template: ast::Template,
+    cst: Node<Option<cst::Policy>>,
+) -> Result<ast::Template, err::ParseErrors> {
+    if template.slots().count() == 0 {
+        Err(err::ToASTError::new(
+            err::ToASTErrorKind::expected_template(),
+            cst.loc.into_maybe_loc(),
+        )
+        .into())
+    } else {
+        Ok(template)
+    }
+}
+
 /// Utilities used in tests in this file (and maybe other files in this crate)
 #[cfg(test)]
 // PANIC SAFETY unit tests
diff --git a/cedar-policy-core/src/parser/loc.rs b/cedar-policy-core/src/parser/loc.rs
index 465d7710..34e9551a 100644
--- a/cedar-policy-core/src/parser/loc.rs
+++ b/cedar-policy-core/src/parser/loc.rs
@@ -17,7 +17,7 @@
 use std::sync::Arc;
 
 /// Represents an optional `Loc`.
-#[cfg(not(feature = "fast-parsing"))]
+#[cfg(not(feature = "raw-parsing"))]
 pub type MaybeLoc = Option<Loc>;
 
 /// Represents an optional `Loc`.
@@ -29,9 +29,9 @@ pub type MaybeLoc = Option<Loc>;
 /// - `Some` case: Slightly slower due to heap allocation and indirect access
 /// - `None` case: More memory efficient as no space is reserved for the location data
 ///
-/// When the `fast-parsing` feature is enabled, we use this type and avoid
+/// When the `raw-parsing` feature is enabled, we use this type and avoid
 /// storing locations during parsing, maximizing the parsing performance.
-#[cfg(feature = "fast-parsing")]
+#[cfg(feature = "raw-parsing")]
 pub type MaybeLoc = Option<Box<Loc>>;
 
 /// Represents a source location: index/range, and a reference to the source
@@ -121,11 +121,11 @@ pub trait IntoMaybeLoc {
 impl IntoMaybeLoc for Loc {
     #[inline]
     fn into_maybe_loc(self) -> MaybeLoc {
-        #[cfg(not(feature = "fast-parsing"))]
+        #[cfg(not(feature = "raw-parsing"))]
         {
             Some(self)
         }
-        #[cfg(feature = "fast-parsing")]
+        #[cfg(feature = "raw-parsing")]
         {
             Some(Box::new(self))
         }
@@ -135,11 +135,11 @@ impl IntoMaybeLoc for Loc {
 impl IntoMaybeLoc for Option<Loc> {
     #[inline]
     fn into_maybe_loc(self) -> MaybeLoc {
-        #[cfg(not(feature = "fast-parsing"))]
+        #[cfg(not(feature = "raw-parsing"))]
         {
             self
         }
-        #[cfg(feature = "fast-parsing")]
+        #[cfg(feature = "raw-parsing")]
         {
             self.map(|loc| Box::new(loc))
         }
@@ -149,11 +149,11 @@ impl IntoMaybeLoc for Option<Loc> {
 impl IntoMaybeLoc for Option<&Loc> {
     #[inline]
     fn into_maybe_loc(self) -> MaybeLoc {
-        #[cfg(not(feature = "fast-parsing"))]
+        #[cfg(not(feature = "raw-parsing"))]
         {
             self.cloned()
         }
-        #[cfg(feature = "fast-parsing")]
+        #[cfg(feature = "raw-parsing")]
         {
             self.map(|loc| Box::new(loc.clone()))
         }
@@ -163,11 +163,11 @@ impl IntoMaybeLoc for Option<&Loc> {
 impl IntoMaybeLoc for Option<Box<Loc>> {
     #[inline]
     fn into_maybe_loc(self) -> MaybeLoc {
-        #[cfg(not(feature = "fast-parsing"))]
+        #[cfg(not(feature = "raw-parsing"))]
         {
             self.map(|loc| *loc)
         }
-        #[cfg(feature = "fast-parsing")]
+        #[cfg(feature = "raw-parsing")]
         {
             self
         }
diff --git a/cedar-policy-core/src/parser/macros.rs b/cedar-policy-core/src/parser/macros.rs
index 62aa8cd2..77909712 100644
--- a/cedar-policy-core/src/parser/macros.rs
+++ b/cedar-policy-core/src/parser/macros.rs
@@ -22,11 +22,11 @@ macro_rules! maybe_loc {
         if $flag {
             None
         } else {
-            #[cfg(feature = "fast-parsing")]
+            #[cfg(feature = "raw-parsing")]
             {
                 Some(Box::new($loc))
             }
-            #[cfg(not(feature = "fast-parsing"))]
+            #[cfg(not(feature = "raw-parsing"))]
             {
                 Some($loc)
             }
diff --git a/cedar-policy-core/src/parser/node.rs b/cedar-policy-core/src/parser/node.rs
index bf708e25..95bdb0e8 100644
--- a/cedar-policy-core/src/parser/node.rs
+++ b/cedar-policy-core/src/parser/node.rs
@@ -38,7 +38,7 @@ pub struct Node<T> {
 
 impl<T> Node<T> {
     /// Create a new Node with the given (boxed) source location
-    #[cfg(feature = "fast-parsing")]
+    #[cfg(feature = "raw-parsing")]
     pub fn with_source_loc(node: T, loc: Box<Loc>) -> Self {
         Node {
             node,
@@ -47,7 +47,7 @@ impl<T> Node<T> {
     }
 
     /// Create a new Node with the given source location
-    #[cfg(not(feature = "fast-parsing"))]
+    #[cfg(not(feature = "raw-parsing"))]
     pub fn with_source_loc(node: T, loc: Loc) -> Self {
         Node {
             node,
diff --git a/cedar-policy-core/src/parser/text_to_cst.rs b/cedar-policy-core/src/parser/text_to_cst.rs
index 20a9f9e4..e84ca031 100644
--- a/cedar-policy-core/src/parser/text_to_cst.rs
+++ b/cedar-policy-core/src/parser/text_to_cst.rs
@@ -46,10 +46,11 @@ fn parse_collect_errors<'a, P, T>(
         bool,
         &'a str,
     ) -> Result<T, err::RawParseError<'a>>,
+    is_fast: bool,
     text: &'a str,
 ) -> Result<T, err::ParseErrors> {
     let mut errs = Vec::new();
-    let result = parse(parser, &mut errs, &Arc::from(text), false, text);
+    let result = parse(parser, &mut errs, &Arc::from(text), is_fast, text);
 
     let errors = errs
         .into_iter()
@@ -117,37 +118,59 @@ lazy_static::lazy_static! {
 
 /// Create CST for multiple policies from text
 pub fn parse_policies(text: &str) -> Result<Node<Option<cst::Policies>>, err::ParseErrors> {
-    parse_collect_errors(&*POLICIES_PARSER, grammar::PoliciesParser::parse, text)
+    parse_collect_errors(
+        &*POLICIES_PARSER,
+        grammar::PoliciesParser::parse,
+        false,
+        text,
+    )
 }
 
 /// Create CST for one policy statement from text
 pub fn parse_policy(text: &str) -> Result<Node<Option<cst::Policy>>, err::ParseErrors> {
-    parse_collect_errors(&*POLICY_PARSER, grammar::PolicyParser::parse, text)
+    parse_collect_errors(&*POLICY_PARSER, grammar::PolicyParser::parse, false, text)
 }
 
 /// Create CST for one Expression from text
 pub fn parse_expr(text: &str) -> Result<Node<Option<cst::Expr>>, err::ParseErrors> {
-    parse_collect_errors(&*EXPR_PARSER, grammar::ExprParser::parse, text)
+    parse_collect_errors(&*EXPR_PARSER, grammar::ExprParser::parse, false, text)
 }
 
 /// Create CST for one Entity Ref (i.e., UID) from text
 pub fn parse_ref(text: &str) -> Result<Node<Option<cst::Ref>>, err::ParseErrors> {
-    parse_collect_errors(&*REF_PARSER, grammar::RefParser::parse, text)
+    parse_collect_errors(&*REF_PARSER, grammar::RefParser::parse, false, text)
 }
 
 /// Create CST for one Primary value from text
 pub fn parse_primary(text: &str) -> Result<Node<Option<cst::Primary>>, err::ParseErrors> {
-    parse_collect_errors(&*PRIMARY_PARSER, grammar::PrimaryParser::parse, text)
+    parse_collect_errors(&*PRIMARY_PARSER, grammar::PrimaryParser::parse, false, text)
 }
 
 /// Parse text as a Name, or fail if it does not parse as a Name
 pub fn parse_name(text: &str) -> Result<Node<Option<cst::Name>>, err::ParseErrors> {
-    parse_collect_errors(&*NAME_PARSER, grammar::NameParser::parse, text)
+    parse_collect_errors(&*NAME_PARSER, grammar::NameParser::parse, false, text)
 }
 
 /// Parse text as an identifier, or fail if it does not parse as an identifier
 pub fn parse_ident(text: &str) -> Result<Node<Option<cst::Ident>>, err::ParseErrors> {
-    parse_collect_errors(&*IDENT_PARSER, grammar::IdentParser::parse, text)
+    parse_collect_errors(&*IDENT_PARSER, grammar::IdentParser::parse, false, text)
+}
+
+/// Create CST for multiple policies from text, but without retaining source information
+#[cfg(feature = "raw-parsing")]
+pub fn parse_policies_raw(text: &str) -> Result<Node<Option<cst::Policies>>, err::ParseErrors> {
+    parse_collect_errors(
+        &*POLICIES_PARSER,
+        grammar::PoliciesParser::parse,
+        true,
+        text,
+    )
+}
+
+/// Create CST for one policy statement from text, but without retaining source information
+#[cfg(feature = "raw-parsing")]
+pub fn parse_policy_raw(text: &str) -> Result<Node<Option<cst::Policy>>, err::ParseErrors> {
+    parse_collect_errors(&*POLICY_PARSER, grammar::PolicyParser::parse, true, text)
 }
 
 /// Create CST for one policy statement from text - allows CST error nodes on certain parse failures
@@ -169,6 +192,7 @@ pub fn parse_policies_tolerant(
 pub fn parse_expr_tolerant(text: &str) -> Result<Node<Option<cst::Expr>>, err::ParseErrors> {
     parse_collect_errors_tolerant(&*EXPR_PARSER, grammar::ExprParser::parse, text)
 }
+
 // PANIC SAFETY unit test code
 #[allow(clippy::panic)]
 // PANIC SAFETY unit test code
@@ -1423,6 +1447,280 @@ mod tests {
         );
     }
 
+    #[cfg(feature = "raw-parsing")]
+    mod raw_parsing {
+        use super::*;
+
+        #[track_caller]
+        fn assert_parse_raw_succeeds<T>(
+            parse: impl FnOnce(&str) -> Result<Node<Option<T>>, err::ParseErrors>,
+            text: &str,
+        ) {
+            let cst_node = parse(text)
+                .unwrap_or_else(|errs| panic!("failed to parse:\n{:?}", miette::Report::new(errs)));
+            assert!(cst_node.loc.is_none());
+        }
+
+        #[test]
+        fn comments_has() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{ principal //comment p
+                    has //comment has
+                    age //comment
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_like() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{ principal //comment p
+                    like //comment like
+
+                    age //comment
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_and() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{ 1 //comment p
+                    &&  //comment &&
+                        //comment &&
+                    "hello" //comment
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_or() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{ 1 //comment 1
+                        //  comment 1
+                    ||  //comment ||
+                        //comments ||
+                    "hello" //comment
+                            //comment hello
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_add() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{ 1 //comment 1
+                            //comment 1_2
+                    + //comment +
+                    //comment +
+                    2 //comment 2
+                        //comment 2
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_paren() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{
+                    ( //comment 1
+                        ( //comment 2
+                    1
+                        ) //comment 3
+                    ) //comment 4
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_set() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{
+                    [ // comment 1
+                    "hello" //comment 2
+                    , // comment 3
+                    // comment 3-2
+                    1 //comment 4
+                        //comment 5
+                    ]  //comment 5-0
+
+                    .  //comment 5-1
+
+                    contains //comment 5-2
+
+                    ( //comment 6
+
+                    "a"  //comment 7
+
+                    ) //comment 20
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_if() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{
+                    ( //comment open outer
+                    ( //comment open inner
+                    if //comment if
+                    1             //comment
+                    < //comment <
+                    2 //commment 2
+                    then // comment then
+                    "hello" //comment hello
+                    else  //comment else
+                        1 //comment 1
+                        ) //comment close inner
+                        ) //comment close outer
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_member_access() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal, action,resource)
+                    when{ principal. //comment .
+                    age // comment age
+                    };
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_principal() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    permit(principal //comment 1
+                    ==
+                    User::"alice" //comment 3
+                    ,  //comment 4
+                    action,resource);
+                "#,
+            );
+        }
+
+        #[test]
+        fn comments_annotation() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                //comment policy
+                // comment policy 2
+                @anno("good annotation")  // comments after annotation
+                // comments after annotation 2
+                        permit(principal //comment 1
+                        ==
+                        User::"alice" //comment 3
+                        ,  //comment 4
+                        action,resource);
+                    "#,
+            );
+        }
+
+        #[test]
+        fn comments_policy() {
+            // single line comments (`// ...`) are valid anywhere
+            assert_parse_raw_succeeds(
+                parse_policy_raw,
+                r#"
+                    //comment policy 1
+                    //comment policy 2
+                    permit( //comment 3
+                       // comment 4
+                    principal //comment principal
+                    == //comment == 1
+                       //comment == 2
+                    User::"alice" //comment alice
+                    , //comment comma 1
+                                //comment comma 2
+                    action //comment action 1
+                    //comment action 2
+                    , //comment comma action
+                    resource // comment resource
+                    )
+                    //comment 5
+                    //comment 6
+                    ;
+                "#,
+            );
+            //multi-line comments (`/* ... */`) are not allowed
+            let src = r#" /* multi-line
+                comment */
+                    permit(principal, action, resource)
+                    when{
+                        one.two
+                    };
+                "#;
+            assert_parse_fails(parse_policy_raw, src);
+        }
+
+        #[test]
+        fn multiple_policies() {
+            assert_parse_raw_succeeds(
+                parse_policies_raw,
+                r#"
+                    permit(
+                        principal in Group::"jane_friends",  // Policy c1
+                        action in [PhotoOp::"view", PhotoOp::"comment"],
+                        resource in Album::"jane_trips",
+                        context:Group
+                    );
+                    forbid(principal, action, resource)           // Policy c2
+                    when   { "private" in resource.tags }  // resource.tags is a set of strings
+                    unless { resource in user.account };
+                    "#,
+            );
+        }
+    }
+
     #[test]
     #[cfg(feature = "tolerant-ast")]
     fn policies_tolerant_success() {
diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md
index 20f85244..94085eab 100644
--- a/cedar-policy/CHANGELOG.md
+++ b/cedar-policy/CHANGELOG.md
@@ -24,8 +24,10 @@ Cedar Language Version: TBD
    and `RestrictedExpression::new_datetime` (#1614)
 - Added a function to be able to split a policy set parsed from a single string into its component static
   policies and templates. The relevant function is `policy_set_text_to_parts` in the `ffi` module (#1629).
-- Added the `fast-parsing` feature flag to enable future optimizations by allowing parsing without
-  storing source code information.
+- Added the `raw-parsing` feature flag which provides alternative parsing APIs (`parse_raw`) for
+  `PolicySet`, `Policy`, and `Template`. These APIs don't retain source code information, trading detailed error
+  reporting for reduced memory usage and faster parsing. The feature is only intended for use with raw parsing
+  APIs, as regular parsing performance is degraded when the `raw-parsing` feature is enabled.
 
 ### Changed
 
diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml
index be60028d..3b8c9b37 100644
--- a/cedar-policy/Cargo.toml
+++ b/cedar-policy/Cargo.toml
@@ -65,8 +65,8 @@ extended-schema = ["cedar-policy-core/extended-schema"]
 # release.
 deprecated-schema-compat = []
 
-# Feature for fast parsing
-fast-parsing = ["cedar-policy-core/fast-parsing"]
+# Feature for raw parsing
+raw-parsing = ["cedar-policy-core/raw-parsing"]
 
 [lib]
 # cdylib required for wasm
diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs
index 6c9daf33..c2dadf2b 100644
--- a/cedar-policy/src/api.rs
+++ b/cedar-policy/src/api.rs
@@ -2800,6 +2800,52 @@ impl PolicySet {
             }
         }
     }
+
+    /// Attempt to parse a [`PolicySet`] from source, without retaining source information.
+    ///
+    /// Policy ids will default to "policy*" with numbers from 0.
+    /// If you load more policies, do not use the default id, or there will be conflicts.
+    ///
+    /// See [`Policy`] for more.
+    ///
+    /// Similar to [`PolicySet::from_str`], but does not retain the original source
+    /// code or its locations. This allows for faster parsing and reduced memory
+    /// usage, but limits the ability to provide detailed error messages.
+    ///
+    /// Only available with the "raw-parsing" feature.
+    #[cfg(feature = "raw-parsing")]
+    pub fn parse_raw(policies: &str) -> Option<Self> {
+        let pset = parser::parse_policyset_raw(policies).ok()?;
+        let policies = pset
+            .policies()
+            .map(|p| {
+                (
+                    PolicyId::new(p.id().clone()),
+                    Policy {
+                        lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
+                        ast: p.clone(),
+                    },
+                )
+            })
+            .collect();
+        let templates = pset
+            .templates()
+            .map(|t| {
+                (
+                    PolicyId::new(t.id().clone()),
+                    Template {
+                        lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
+                        ast: t.clone(),
+                    },
+                )
+            })
+            .collect();
+        Some(Self {
+            ast: pset,
+            policies,
+            templates,
+        })
+    }
 }
 
 impl std::fmt::Display for PolicySet {
@@ -2994,6 +3040,26 @@ impl Template {
         })
     }
 
+    /// Attempt to parse a [`Template`] from source, without retaining source information.
+    /// Returns `None` if the input is a static policy (i.e., has no slots).
+    /// If `id` is Some, then the resulting template will have that `id`.
+    /// If the `id` is None, the parser will use the default "policy0".
+    /// The behavior around None may change in the future.
+    ///
+    /// Similar to [`Template::parse`], but does not retain the original source
+    /// code or its locations. This allows for faster parsing and reduced memory
+    /// usage, but limits the ability to provide detailed error messages.
+    ///
+    /// Only available with the "raw-parsing" feature.
+    #[cfg(feature = "raw-parsing")]
+    pub fn parse_raw(id: Option<PolicyId>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
+        let ast = parser::parse_template_raw(id.map(Into::into), src.as_ref())?;
+        Ok(Self {
+            ast,
+            lossless: LosslessPolicy::policy_or_template_text(Some(src.as_ref())),
+        })
+    }
+
     /// Get the `PolicyId` of this `Template`
     pub fn id(&self) -> &PolicyId {
         PolicyId::ref_cast(self.ast.id())
@@ -3687,6 +3753,30 @@ impl Policy {
         }
     }
 
+    /// Attempt to parse a [`Policy`] from source, without retaining source information.
+    /// If `id` is Some, the policy will be given that Policy Id.
+    /// If `id` is None, then "policy0" will be used.
+    /// The behavior around None may change in the future.
+    ///
+    /// This can fail if the policy fails to parse.
+    /// It can also fail if a template was passed in, as this function only accepts static
+    /// policies.
+    ///
+    /// Similar to [`Policy::parse`], but does not retain the original source
+    /// code or its locations. This allows for faster parsing and reduced memory
+    /// usage, but limits the ability to provide detailed error messages.
+    ///
+    /// Only available with the "raw-parsing" feature.
+    #[cfg(feature = "raw-parsing")]
+    pub fn parse_raw(id: Option<PolicyId>, policy_src: impl AsRef<str>) -> Option<Self> {
+        let inline_ast = parser::parse_policy_raw(id.map(Into::into), policy_src.as_ref()).ok()?;
+        let (_, ast) = ast::Template::link_static_policy(inline_ast);
+        Some(Self {
+            ast,
+            lossless: LosslessPolicy::policy_or_template_text(Some(policy_src.as_ref())),
+        })
+    }
+
     /// Get all the unknown entities from the policy
     #[doc = include_str!("../experimental_warning.md")]
     #[cfg(feature = "partial-eval")]


Test patch:
diff --git a/cedar-policy/src/test/test.rs b/cedar-policy/src/test/test.rs
index a159b792..eaea7207 100644
--- a/cedar-policy/src/test/test.rs
+++ b/cedar-policy/src/test/test.rs
@@ -8036,3 +8036,38 @@ mod test_entities_api {
         assert!(entities.is_ancestor_of(&e2_uid, &e1_uid));
     }
 }
+
+#[cfg(feature = "raw-parsing")]
+mod raw_parsing {
+    use crate::{Policy, PolicyId, PolicySet, Template};
+
+    #[test]
+    fn policyset_raw_parsing() {
+        const PSET_TEXT: &str = r#"
+            forbid(principal,action,resource)
+            when{ context has suspicion };
+
+            permit(
+                principal == Account::"jane",
+                action,
+                resource == Album::"jane_vacation"
+            );
+        "#;
+        let pset = PolicySet::parse_raw(PSET_TEXT).unwrap();
+        assert_eq!(pset.num_of_policies(), 2);
+    }
+
+    #[test]
+    fn policy_raw_parsing() {
+        const STATIC_POLICY_TEXT: &str = "permit(principal,action,resource);";
+        Policy::parse_raw(Some(PolicyId::new("policy0")), STATIC_POLICY_TEXT)
+            .expect("Failed to parse");
+    }
+
+    #[test]
+    fn template_raw_parsing() {
+        const TEMPLATE_TEXT: &str = "permit(principal == ?principal,action,resource);";
+        Template::parse_raw(Some(PolicyId::new("template0")), TEMPLATE_TEXT)
+            .expect("Failed to parse");
+    }
+}