Repository: martinohmann/hcl-rs
Pull request #267 (2023-07-13 17:43:49)

refactor(parser)!: switch to `hcl-edit` for parsing HCL
BREAKING CHANGES: The parser will now return an error if it encounters a duplicate attribute in a HCL body. Furthermore, the `Message` variant of the `Error` type was changed from `Message { msg: String, location: Option<Location> }` to `Message(String)` and the `Location` type was removed from the `error` module. Use the `location()` method of the error wrapped by the new `Error::Parse` variant to get access to the location where the parser failed.

The old `pest` parser was replaced by the `winnow`-based parser that is used by `hcl-edit`. There's only one HCL parser implementation that needs to be maintained now.

The `hcl-edit` parser is more correct and also over 100% faster than the old `pest`-based implementation.

As a nice side effect, this decreases the number of transitive dependencies for the `hcl-rs` crate as well.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 828c250..4ed1cfa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,15 +66,6 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
-[[package]]
-name = "block-buffer"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
-dependencies = [
- "generic-array",
-]
-
 [[package]]
 name = "bumpalo"
 version = "3.11.1"
@@ -152,15 +143,6 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
 
-[[package]]
-name = "cpufeatures"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "criterion"
 version = "0.5.1"
@@ -240,16 +222,6 @@ dependencies = [
  "cfg-if",
 ]
 
-[[package]]
-name = "crypto-common"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
-dependencies = [
- "generic-array",
- "typenum",
-]
-
 [[package]]
 name = "ctor"
 version = "0.1.26"
@@ -266,16 +238,6 @@ version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
 
-[[package]]
-name = "digest"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
-dependencies = [
- "block-buffer",
- "crypto-common",
-]
-
 [[package]]
 name = "either"
 version = "1.8.0"
@@ -315,16 +277,6 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
-[[package]]
-name = "generic-array"
-version = "0.14.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
-dependencies = [
- "typenum",
- "version_check",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.2.8"
@@ -390,8 +342,6 @@ dependencies = [
  "indexmap",
  "indoc",
  "itoa",
- "pest",
- "pest_derive",
  "pretty_assertions",
  "serde",
  "serde_json",
@@ -563,50 +513,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "pest"
-version = "2.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4"
-dependencies = [
- "thiserror",
- "ucd-trie",
-]
-
-[[package]]
-name = "pest_derive"
-version = "2.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603"
-dependencies = [
- "pest",
- "pest_generator",
-]
-
-[[package]]
-name = "pest_generator"
-version = "2.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "pest_meta"
-version = "2.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065"
-dependencies = [
- "once_cell",
- "pest",
- "sha1",
-]
-
 [[package]]
 name = "plotters"
 version = "0.3.4"
@@ -771,17 +677,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "sha1"
-version = "0.10.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
 [[package]]
 name = "smawk"
 version = "0.3.1"
@@ -830,26 +725,6 @@ dependencies = [
  "unicode-width",
 ]
 
-[[package]]
-name = "thiserror"
-version = "1.0.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "tinytemplate"
 version = "1.2.1"
@@ -860,18 +735,6 @@ dependencies = [
  "serde_json",
 ]
 
-[[package]]
-name = "typenum"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
-
-[[package]]
-name = "ucd-trie"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
-
 [[package]]
 name = "unicode-ident"
 version = "1.0.6"
diff --git a/crates/hcl-rs/Cargo.toml b/crates/hcl-rs/Cargo.toml
index 269f32c..1e656c3 100644
--- a/crates/hcl-rs/Cargo.toml
+++ b/crates/hcl-rs/Cargo.toml
@@ -31,16 +31,13 @@ path = "src/lib.rs"
 
 [features]
 default = []
-edit = ["dep:hcl-edit"]
-perf = ["hcl-edit?/perf", "hcl-primitives/perf"]
+perf = ["hcl-edit/perf", "hcl-primitives/perf"]
 
 [dependencies]
 indexmap = { version = "2.0.0", features = ["serde"] }
 itoa = "1.0.8"
-hcl-edit = { version = "0.7.0", path = "../hcl-edit", optional = true }
+hcl-edit = { version = "0.7.0", path = "../hcl-edit" }
 hcl-primitives = { version = "0.1.1", path = "../hcl-primitives", features = ["serde"] }
-pest = "2.5.2"
-pest_derive = "2.5.2"
 serde = { version = "1.0.156", features = ["derive"] }
 vecmap-rs = { version = "0.1.11", features = ["serde"] }
 
diff --git a/crates/hcl-rs/src/error.rs b/crates/hcl-rs/src/error.rs
index 8324fe1..c7b17c6 100644
--- a/crates/hcl-rs/src/error.rs
+++ b/crates/hcl-rs/src/error.rs
@@ -1,7 +1,6 @@
 //! The `Error` and `Result` types used by this crate.
+use crate::edit::parser;
 use crate::eval;
-use crate::parser::Rule;
-use pest::{error::LineColLocation, Span};
 use serde::{de, ser};
 use std::fmt::{self, Display};
 use std::io;
@@ -14,13 +13,8 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
 #[derive(Debug)]
 #[non_exhaustive]
 pub enum Error {
-    /// Represents a generic error message with optional location.
-    Message {
-        /// The error message.
-        msg: String,
-        /// An optional location context where the error happened in the input.
-        location: Option<Location>,
-    },
+    /// Represents a generic error message.
+    Message(String),
     /// Represents the error emitted when the `Deserializer` hits an unexpected end of input.
     Eof,
     /// Represents an error that resulted from invalid UTF8 input.
@@ -37,6 +31,8 @@ pub enum Error {
     InvalidIdentifier(String),
     /// Represents errors during expression evaluation.
     Eval(eval::Error),
+    /// Represents errors while parsing HCL.
+    Parse(parser::Error),
 }
 
 impl Error {
@@ -44,18 +40,7 @@ impl Error {
     where
         T: Display,
     {
-        Error::Message {
-            msg: msg.to_string(),
-            location: None,
-        }
-    }
-
-    /// Returns the `Location` in the input where the error happened, if available.
-    pub fn location(&self) -> Option<&Location> {
-        match self {
-            Error::Message { location, .. } => location.as_ref(),
-            _ => None,
-        }
+        Error::Message(msg.to_string())
     }
 }
 
@@ -63,20 +48,16 @@ impl Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             Error::Eof => write!(f, "unexpected end of input"),
-            Error::Io(err) => Display::fmt(err, f),
-            Error::Utf8(err) => Display::fmt(err, f),
-            Error::Message { msg, location } => match location {
-                Some(loc) => {
-                    write!(f, "{msg} in line {}, col {}", loc.line, loc.col)
-                }
-                None => write!(f, "{msg}"),
-            },
+            Error::Io(err) => write!(f, "{err}"),
+            Error::Utf8(err) => write!(f, "{err}"),
+            Error::Message(msg) => write!(f, "{msg}"),
             Error::InvalidEscape(c) => write!(f, "invalid escape sequence '\\{c}'"),
             Error::InvalidUnicodeCodePoint(u) => {
                 write!(f, "invalid unicode code point '\\u{u}'")
             }
             Error::InvalidIdentifier(ident) => write!(f, "invalid identifier `{ident}`"),
             Error::Eval(err) => write!(f, "eval error: {err}"),
+            Error::Parse(err) => write!(f, "{err}"),
         }
     }
 }
@@ -93,16 +74,9 @@ impl From<Utf8Error> for Error {
     }
 }
 
-impl From<pest::error::Error<Rule>> for Error {
-    fn from(err: pest::error::Error<Rule>) -> Self {
-        let (line, col) = match err.line_col {
-            LineColLocation::Pos((l, c)) | LineColLocation::Span((l, c), (_, _)) => (l, c),
-        };
-
-        Error::Message {
-            msg: err.to_string(),
-            location: Some(Location { line, col }),
-        }
+impl From<parser::Error> for Error {
+    fn from(err: parser::Error) -> Self {
+        Error::Parse(err)
     }
 }
 
@@ -125,19 +99,3 @@ impl de::Error for Error {
         Error::new(msg)
     }
 }
-
-/// One-based line and column at which the error was detected.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Location {
-    /// The one-based line number of the error.
-    pub line: usize,
-    /// The one-based column number of the error.
-    pub col: usize,
-}
-
-impl From<Span<'_>> for Location {
-    fn from(span: Span<'_>) -> Self {
-        let (line, col) = span.start_pos().line_col();
-        Location { line, col }
-    }
-}
diff --git a/crates/hcl-rs/src/expr/edit.rs b/crates/hcl-rs/src/expr/edit.rs
index cf14b27..958cb4b 100644
--- a/crates/hcl-rs/src/expr/edit.rs
+++ b/crates/hcl-rs/src/expr/edit.rs
@@ -193,10 +193,9 @@ impl From<template::StringTemplate> for TemplateExpr {
 
 impl From<template::HeredocTemplate> for Heredoc {
     fn from(value: template::HeredocTemplate) -> Self {
-        let strip = match value.indent() {
-            Some(0) | None => HeredocStripMode::None,
-            Some(_) => HeredocStripMode::Indent,
-        };
+        let strip = value
+            .indent()
+            .map_or(HeredocStripMode::None, |_| HeredocStripMode::Indent);
 
         Heredoc {
             delimiter: value.delimiter.into(),
diff --git a/crates/hcl-rs/src/expr/mod.rs b/crates/hcl-rs/src/expr/mod.rs
index 57492ad..723684c 100644
--- a/crates/hcl-rs/src/expr/mod.rs
+++ b/crates/hcl-rs/src/expr/mod.rs
@@ -5,7 +5,6 @@
 
 mod conditional;
 pub(crate) mod de;
-#[cfg(feature = "edit")]
 mod edit;
 mod for_expr;
 mod func_call;
diff --git a/crates/hcl-rs/src/lib.rs b/crates/hcl-rs/src/lib.rs
index e77bf0c..8a4565d 100644
--- a/crates/hcl-rs/src/lib.rs
+++ b/crates/hcl-rs/src/lib.rs
@@ -41,7 +41,6 @@ mod tests;
 mod util;
 pub mod value;
 
-#[cfg(feature = "edit")]
 pub use hcl_edit as edit;
 
 // Re-exported for convenience.
diff --git a/crates/hcl-rs/src/parser/expr.rs b/crates/hcl-rs/src/parser/expr.rs
deleted file mode 100644
index 9efa0f5..0000000
--- a/crates/hcl-rs/src/parser/expr.rs
+++ /dev/null
@@ -1,293 +0,0 @@
-use super::*;
-use crate::{
-    expr::{
-        BinaryOp, Conditional, Expression, ForExpr, FuncCall, FuncCallBuilder, Heredoc,
-        HeredocStripMode, Object, ObjectKey, Operation, TemplateExpr, Traversal, TraversalOperator,
-        UnaryOp, UnaryOperator, Variable,
-    },
-    util::dedent,
-};
-
-pub fn expression(pair: Pair<Rule>) -> Result<Expression> {
-    let pairs = pair.into_inner();
-    let (expr, pairs) = unary_op(pairs)?;
-    let (expr, pairs) = binary_op(expr, pairs)?;
-    conditional(expr, pairs)
-}
-
-fn unary_op(mut pairs: Pairs<Rule>) -> Result<(Expression, Pairs<Rule>)> {
-    let pair = pairs.next().unwrap();
-
-    let expr = match pair.as_rule() {
-        Rule::UnaryOperator => {
-            let operator = from_str(pair);
-            let expr = expr_term(pairs.next().unwrap())?;
-
-            match (operator, expr) {
-                (UnaryOperator::Neg, Expression::Number(num)) => Expression::Number(-num),
-                (operator, expr) => Expression::from(Operation::Unary(UnaryOp { operator, expr })),
-            }
-        }
-        _ => expr_term(pair)?,
-    };
-
-    Ok((expr, pairs))
-}
-
-fn binary_op(expr: Expression, mut pairs: Pairs<Rule>) -> Result<(Expression, Pairs<Rule>)> {
-    let expr = match pairs.peek() {
-        Some(pair) => match pair.as_rule() {
-            Rule::BinaryOperator => Expression::from(Operation::Binary(BinaryOp {
-                lhs_expr: expr,
-                operator: from_str(pairs.next().unwrap()),
-                rhs_expr: expression(pairs.next().unwrap())?,
-            })),
-            _ => expr,
-        },
-        None => expr,
-    };
-
-    Ok((expr, pairs))
-}
-
-fn conditional(expr: Expression, mut pairs: Pairs<Rule>) -> Result<Expression> {
-    let expr = match pairs.next() {
-        Some(pair) => Expression::from(Conditional {
-            cond_expr: expr,
-            true_expr: expression(pair)?,
-            false_expr: expression(pairs.next().unwrap())?,
-        }),
-        None => expr,
-    };
-
-    Ok(expr)
-}
-
-fn expressions(pair: Pair<Rule>) -> Result<Vec<Expression>> {
-    pair.into_inner().map(expression).collect()
-}
-
-fn expr_term(pair: Pair<Rule>) -> Result<Expression> {
-    let mut pairs = pair.into_inner();
-    let pair = pairs.next().unwrap();
-
-    let expr = match pair.as_rule() {
-        Rule::BooleanLit => Expression::Bool(from_str(pair)),
-        Rule::Float => {
-            Number::from_f64(from_str::<f64>(pair)).map_or(Expression::Null, Expression::Number)
-        }
-        Rule::Int => Expression::from(from_str::<i64>(pair)),
-        Rule::NullLit => Expression::Null,
-        Rule::StringLit => unescape_string(inner(pair)).map(Expression::String)?,
-        Rule::TemplateExpr => Expression::TemplateExpr(Box::new(template_expr(inner(pair)))),
-        Rule::Tuple => expressions(pair).map(Expression::Array)?,
-        Rule::Object => object(pair).map(Expression::Object)?,
-        Rule::Variable => Expression::Variable(Variable::from(ident(pair))),
-        Rule::FunctionCall => Expression::FuncCall(Box::new(func_call(pair)?)),
-        Rule::Parenthesis => Expression::Parenthesis(Box::new(expression(inner(pair))?)),
-        Rule::ForExpr => Expression::from(for_expr(inner(pair))?),
-        rule => unexpected_rule(rule),
-    };
-
-    traversal(expr, pairs)
-}
-
-fn traversal(expr: Expression, pairs: Pairs<Rule>) -> Result<Expression> {
-    let operators = pairs
-        .map(traversal_operator)
-        .collect::<Result<Vec<TraversalOperator>>>()?;
-
-    if operators.is_empty() {
-        Ok(expr)
-    } else {
-        Ok(Expression::from(Traversal { expr, operators }))
-    }
-}
-
-fn for_expr(pair: Pair<Rule>) -> Result<ForExpr> {
-    match pair.as_rule() {
-        Rule::ForTupleExpr => for_list_expr(pair),
-        Rule::ForObjectExpr => for_object_expr(pair),
-        rule => unexpected_rule(rule),
-    }
-}
-
-fn for_list_expr(pair: Pair<Rule>) -> Result<ForExpr> {
-    let mut pairs = pair.into_inner();
-    let intro = for_intro(pairs.next().unwrap())?;
-    let value_expr = expression(pairs.next().unwrap())?;
-    let cond_expr = match pairs.next() {
-        Some(pair) => Some(expression(inner(pair))?),
-        None => None,
-    };
-
-    Ok(ForExpr {
-        key_var: intro.key_var,
-        value_var: intro.value_var,
-        collection_expr: intro.collection_expr,
-        key_expr: None,
-        value_expr,
-        grouping: false,
-        cond_expr,
-    })
-}
-
-fn for_object_expr(pair: Pair<Rule>) -> Result<ForExpr> {
-    let mut pairs = pair.into_inner();
-    let intro = for_intro(pairs.next().unwrap())?;
-    let key_expr = expression(pairs.next().unwrap())?;
-    let value_expr = expression(pairs.next().unwrap())?;
-
-    let (grouping, cond_expr) = match (pairs.next(), pairs.next()) {
-        (Some(_), Some(pair)) => (true, Some(expression(inner(pair))?)),
-        (Some(pair), None) => match pair.as_rule() {
-            Rule::ValueGrouping => (true, None),
-            Rule::ForCond => (false, Some(expression(inner(pair))?)),
-            rule => unexpected_rule(rule),
-        },
-        (_, _) => (false, None),
-    };
-
-    Ok(ForExpr {
-        key_var: intro.key_var,
-        value_var: intro.value_var,
-        collection_expr: intro.collection_expr,
-        key_expr: Some(key_expr),
-        value_expr,
-        grouping,
-        cond_expr,
-    })
-}
-
-struct ForIntro {
-    key_var: Option<Identifier>,
-    value_var: Identifier,
-    collection_expr: Expression,
-}
-
-fn for_intro(pair: Pair<Rule>) -> Result<ForIntro> {
-    let mut pairs = pair.into_inner();
-    let mut value_var = Some(ident(pairs.next().unwrap()));
-    let mut expr = pairs.next().unwrap();
-
-    // If there are two identifiers, the first one is the key and the second one the value.
-    let key_var = match expr.as_rule() {
-        Rule::Identifier => {
-            let key = value_var.replace(ident(expr));
-            expr = pairs.next().unwrap();
-            key
-        }
-        _ => None,
-    };
-
-    Ok(ForIntro {
-        key_var,
-        value_var: value_var.take().unwrap(),
-        collection_expr: expression(expr)?,
-    })
-}
-
-fn func_call(pair: Pair<Rule>) -> Result<FuncCall> {
-    let mut pairs = pair.into_inner();
-    let builder = FuncCall::builder(ident(pairs.next().unwrap()));
-    let mut args = pairs.next().unwrap().into_inner();
-
-    args.try_fold(builder, |builder, pair| match pair.as_rule() {
-        Rule::ExpandFinal => Ok(builder.expand_final(true)),
-        _ => Ok(builder.arg(expression(pair)?)),
-    })
-    .map(FuncCallBuilder::build)
-}
-
-fn traversal_operator(pair: Pair<Rule>) -> Result<TraversalOperator> {
-    let operator = match pair.as_rule() {
-        Rule::AttrSplat => TraversalOperator::AttrSplat,
-        Rule::FullSplat => TraversalOperator::FullSplat,
-        Rule::GetAttr => TraversalOperator::GetAttr(ident(inner(pair))),
-        Rule::Index => {
-            let pair = inner(pair);
-
-            match pair.as_rule() {
-                Rule::LegacyIndex => TraversalOperator::LegacyIndex(from_str::<u64>(inner(pair))),
-                _ => TraversalOperator::Index(expression(pair)?),
-            }
-        }
-        rule => unexpected_rule(rule),
-    };
-
-    Ok(operator)
-}
-
-fn template_expr(pair: Pair<Rule>) -> TemplateExpr {
-    match pair.as_rule() {
-        Rule::QuotedStringTemplate => TemplateExpr::QuotedString(string(inner(pair))),
-        Rule::Heredoc => TemplateExpr::Heredoc(heredoc(pair)),
-        rule => unexpected_rule(rule),
-    }
-}
-
-fn object(pair: Pair<Rule>) -> Result<Object<ObjectKey, Expression>> {
-    ObjectIter::new(pair)
-        .map(|(k, v)| Ok((object_key(k)?, expression(v)?)))
-        .collect()
-}
-
-fn object_key(pair: Pair<Rule>) -> Result<ObjectKey> {
-    match pair.as_rule() {
-        Rule::Identifier => Ok(ObjectKey::Identifier(ident(pair))),
-        _ => expression(pair).map(ObjectKey::Expression),
-    }
-}
-
-fn heredoc(pair: Pair<Rule>) -> Heredoc {
-    let mut pairs = pair.into_inner();
-    let intro = pairs.next().unwrap();
-
-    let strip = match intro.as_rule() {
-        Rule::HeredocIntroNormal => HeredocStripMode::None,
-        Rule::HeredocIntroIndent => HeredocStripMode::Indent,
-        rule => unexpected_rule(rule),
-    };
-
-    let delimiter = ident(pairs.next().unwrap());
-
-    let template = pairs.next().unwrap();
-
-    let mut template = match strip {
-        HeredocStripMode::None => string(template),
-        HeredocStripMode::Indent => dedent(template.as_str()).to_string(),
-    };
-
-    // Append the trailing newline here. This is easier than doing this in the grammar.
-    template.push('\n');
-
-    Heredoc {
-        delimiter,
-        template,
-        strip,
-    }
-}
-
-struct ObjectIter<'a> {
-    inner: Pairs<'a, Rule>,
-}
-
-impl<'a> ObjectIter<'a> {
-    fn new(pair: Pair<'a, Rule>) -> Self {
-        ObjectIter {
-            inner: pair.into_inner(),
-        }
-    }
-}
-
-impl<'a> Iterator for ObjectIter<'a> {
-    type Item = (Pair<'a, Rule>, Pair<'a, Rule>);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        match (self.inner.next(), self.inner.next()) {
-            (Some(k), Some(v)) => Some((k, v)),
-            (Some(k), None) => panic!("missing value for key: {k}"),
-            (_, _) => None,
-        }
-    }
-}
diff --git a/crates/hcl-rs/src/parser/grammar/hcl.pest b/crates/hcl-rs/src/parser/grammar/hcl.pest
deleted file mode 100644
index 2a45aaf..0000000
--- a/crates/hcl-rs/src/parser/grammar/hcl.pest
+++ /dev/null
@@ -1,178 +0,0 @@
-// HCL spec (https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md)
-
-// The top-level scope of an HCL file
-Hcl = _{ SOI ~ Body ~ EOI }
-
-// Structural elements
-Body      = { (Attribute | Block)* }
-Attribute = { Identifier ~ "=" ~ Expression }
-Block     = { Identifier ~ (StringLit | Identifier)* ~ BlockBody }
-BlockBody = { "{" ~ Body ~ "}" }
-
-// Expressions
-Expression = {
-    UnaryOperator? ~
-    ExprTerm ~
-    (BinaryOperator ~ Expression)? ~
-    ("?" ~ Expression ~ ":" ~ Expression)?
-}
-ExprTerm = {
-    (Value | TemplateExpr | FunctionCall | Variable | ForExpr | Parenthesis) ~
-    (Splat | GetAttr | Index)*
-}
-Parenthesis = { "(" ~ Expression ~ ")" }
-
-// Values
-Value = _{ LiteralValue | CollectionValue }
-
-// Literal values
-LiteralValue = _{ StringLit | NumericLit | BooleanLit | NullLit }
-
-// Identifiers
-Identifier     = @{ IdentFirstChar ~ IdentChar* }
-IdentChar      = _{ Letter | Decimal | "-" | "_" }
-IdentFirstChar = _{ Letter | "_" }
-Letter         = _{ 'a'..'z' | 'A'..'Z' }
-
-// Booleans
-BooleanLit = @{ Boolean ~ !Identifier }
-Boolean    =  { "true" | "false" }
-
-// Null
-NullLit = @{ Null ~ !Identifier }
-Null    =  { "null" }
-
-// Numeric literals
-NumericLit = _{ Float | Int }
-Float      = @{ Decimal+ ~ (("." ~ Decimal+ ~ (ExpMark ~ Decimal+)?) | (ExpMark ~ Decimal+)) }
-Int        = @{ Decimal+ }
-ExpMark    =  { ("e" | "E") ~ ("+" | "-")? }
-Decimal    =  { '0'..'9' }
-
-// Collection values
-CollectionValue = _{ Tuple | Object }
-Tuple           =  { "[" ~ (Expression ~ ("," ~ Expression)* ~ ","?)? ~ "]" }
-Object          =  { "{" ~ (ObjectItem ~ (","? ~ ObjectItem)* ~ ","?)? ~ "}" }
-ObjectItem      = _{ ObjectItemIdent | ObjectItemExpr }
-ObjectItemIdent = _{ Identifier ~ ("=" | ":") ~ Expression }
-ObjectItemExpr  = _{ Expression ~ ("=" | ":") ~ Expression }
-
-// Template expressions
-TemplateExpr = { QuotedStringTemplate | Heredoc }
-
-// Heredoc templates
-Heredoc = ${
-    HeredocIntro ~ PUSH(Identifier) ~ NEWLINE ~
-    HeredocTemplate ~ NEWLINE ~
-    SpaceOrTab* ~ POP
-}
-HeredocIntro       = _{ HeredocIntroIndent | HeredocIntroNormal }
-HeredocIntroIndent =  { "<<-" }
-HeredocIntroNormal =  { "<<" }
-
-HeredocTemplate = ${
-    (HeredocLiteral | TemplateInterpolation | TemplateDirective)*
-}
-HeredocLiteral    = @{ HeredocStringPart+ }
-HeredocStringPart =  {
-    "$${"
-    | "%%{"
-    | !("${" | "%{" | (NEWLINE ~ SpaceOrTab* ~ PEEK)) ~ ANY
-}
-
-// Quoted string templates
-QuotedStringTemplate      = ${ "\"" ~ QuotedStringTemplateInner ~ "\"" }
-QuotedStringTemplateInner = ${
-    (QuotedStringTemplateLiteral | TemplateInterpolation | TemplateDirective)*
-}
-QuotedStringTemplateLiteral = @{ StringPart+ }
-
-// String literals
-StringLit  = ${ "\"" ~ String ~ "\"" }
-String     = @{ StringPart* }
-StringPart =  {
-    "$${"
-    | "%%{"
-    | !("\"" | "\\" | "${" | "%{") ~ ANY
-    | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t")
-    | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4})
-}
-
-// Functions and function calls
-FunctionCall = { Identifier ~ Arguments }
-Arguments    = { "(" ~ (Expression ~ ("," ~ Expression)* ~ ("," | ExpandFinal)?)? ~ ")" }
-ExpandFinal  = { "..." }
-
-// For expressions
-ForExpr       = { ForTupleExpr | ForObjectExpr }
-ForTupleExpr  = { "[" ~ ForIntro ~ Expression ~ ForCond? ~ "]" }
-ForObjectExpr = { "{" ~ ForIntro ~ Expression ~ "=>" ~ Expression ~ ValueGrouping? ~ ForCond? ~ "}" }
-ForIntro      = { "for" ~ Identifier ~ ("," ~ Identifier)? ~ "in" ~ Expression ~ ":" }
-ForCond       = { "if" ~ Expression }
-ValueGrouping = { "..." }
-
-// Variables
-Variable = @{ Identifier }
-
-// Index operator
-Index       =  { ("[" ~ Expression ~ "]") | LegacyIndex }
-LegacyIndex = ${ "." ~ Int }
-
-// Attribute access operator
-GetAttr = ${ "." ~ Identifier }
-
-// Splat operators
-Splat     = _{ (AttrSplat ~ GetAttr*) | (FullSplat ~ (GetAttr | Index)*) }
-AttrSplat =  { ".*" }
-FullSplat =  { "[*]" }
-
-// Unary and binary operators
-UnaryOperator      = { "-" | "!" }
-BinaryOperator     = { CompareOperator | ArithmeticOperator | LogicOperator }
-CompareOperator    = { "==" | "!=" | "<=" | ">=" | "<" | ">" }
-ArithmeticOperator = { "+" | "-" | "*" | "/" | "%" }
-LogicOperator      = { "&&" | "||" }
-
-// Comments
-COMMENT         = _{ InlineComment | BlockComment }
-InlineComment   = _{ ("#" | "//") ~ (!EoInlineComment ~ ANY)* }
-BlockComment    = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" }
-EoInlineComment = _{ NEWLINE | EOI }
-
-// Whitespace
-WHITESPACE = _{ SpaceOrTab | NEWLINE }
-SpaceOrTab = _{ " " | "\t" }
-
-// Top-level rule for the template sub-language
-HclTemplate = ${ SOI ~ Template ~ EOI }
-
-Template              = ${ (TemplateLiteral | TemplateInterpolation | TemplateDirective)* }
-TemplateLiteral       = @{ ("$${" | "%%{" | (!("${" | "%{") ~ ANY))+ }
-TemplateInterpolation = !{ TemplateIExprStart ~ Expression ~ TemplateExprEnd }
-TemplateDirective     =  { TemplateIf | TemplateFor }
-
-// Interpolation expression start
-TemplateIExprStart       = _{ TemplateIExprStartStrip | TemplateIExprStartNormal }
-TemplateIExprStartNormal =  { "${" }
-TemplateIExprStartStrip  =  { "${~" }
-
-// Directive expression start
-TemplateDExprStart       = _{ TemplateDExprStartStrip | TemplateDExprStartNormal }
-TemplateDExprStartNormal =  { "%{" }
-TemplateDExprStartStrip  =  { "%{~" }
-
-// Interpolation or directive expression end
-TemplateExprEnd       = _{ TemplateExprEndNormal | TemplateExprEndStrip }
-TemplateExprEndNormal =  { "}" }
-TemplateExprEndStrip  =  { "~}" }
-
-// If directive
-TemplateIf        = ${ TemplateIfExpr ~ Template ~ (TemplateElseExpr ~ Template)? ~ TemplateEndIfExpr }
-TemplateIfExpr    = !{ TemplateDExprStart ~ "if" ~ Expression ~ TemplateExprEnd }
-TemplateElseExpr  = !{ TemplateDExprStart ~ "else" ~ TemplateExprEnd }
-TemplateEndIfExpr = !{ TemplateDExprStart ~ "endif" ~ TemplateExprEnd }
-
-// For directive
-TemplateFor        = ${ TemplateForExpr ~ Template ~ TemplateEndForExpr }
-TemplateForExpr    = !{ TemplateDExprStart ~ "for" ~ Identifier ~ ("," ~ Identifier)? ~ "in" ~ Expression ~ TemplateExprEnd }
-TemplateEndForExpr = !{ TemplateDExprStart ~ "endfor" ~ TemplateExprEnd }
diff --git a/crates/hcl-rs/src/parser/mod.rs b/crates/hcl-rs/src/parser/mod.rs
index e95bde1..2a7f2ec 100644
--- a/crates/hcl-rs/src/parser/mod.rs
+++ b/crates/hcl-rs/src/parser/mod.rs
@@ -1,25 +1,7 @@
-mod expr;
-mod structure;
-mod template;
-#[cfg(test)]
-mod tests;
-
-use self::{expr::expression, structure::body, template::template};
-use crate::{
-    expr::Expression, structure::Body, template::Template, util::unescape, Identifier, Number,
-    Result,
-};
-use hcl_primitives::template::unescape_markers;
-use pest::{
-    iterators::{Pair, Pairs},
-    Parser as _,
-};
-use pest_derive::Parser;
-use std::str::FromStr;
-
-#[derive(Parser)]
-#[grammar = "parser/grammar/hcl.pest"]
-struct HclParser;
+use crate::edit;
+use crate::structure::Body;
+use crate::template::Template;
+use crate::Result;
 
 /// Parse a `hcl::Body` from a `&str`.
 ///
@@ -63,40 +45,11 @@ struct HclParser;
 ///
 /// This function fails with an error if the `input` cannot be parsed as HCL.
 pub fn parse(input: &str) -> Result<Body> {
-    let pair = HclParser::parse(Rule::Hcl, input)?.next().unwrap();
-    body(pair)
+    let body: edit::structure::Body = input.parse()?;
+    Ok(body.into())
 }
 
 pub fn parse_template(input: &str) -> Result<Template> {
-    let pair = HclParser::parse(Rule::HclTemplate, input)?.next().unwrap();
-    template(inner(pair))
-}
-
-fn string(pair: Pair<Rule>) -> String {
-    pair.as_str().to_owned()
-}
-
-fn unescape_string(pair: Pair<Rule>) -> Result<String> {
-    unescape(pair.as_str()).map(|c| unescape_markers(&c).to_string())
-}
-
-fn ident(pair: Pair<Rule>) -> Identifier {
-    Identifier::unchecked(pair.as_str())
-}
-
-fn from_str<T>(pair: Pair<Rule>) -> T
-where
-    T: FromStr,
-    <T as FromStr>::Err: std::fmt::Debug,
-{
-    pair.as_str().parse::<T>().unwrap()
-}
-
-fn inner(pair: Pair<Rule>) -> Pair<Rule> {
-    pair.into_inner().next().unwrap()
-}
-
-#[track_caller]
-fn unexpected_rule(rule: Rule) -> ! {
-    panic!("unexpected rule: {rule:?}")
+    let template: edit::template::Template = input.parse()?;
+    Ok(template.into())
 }
diff --git a/crates/hcl-rs/src/parser/structure.rs b/crates/hcl-rs/src/parser/structure.rs
deleted file mode 100644
index dda4165..0000000
--- a/crates/hcl-rs/src/parser/structure.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use super::*;
-use crate::structure::{Attribute, Block, BlockLabel, Body, Structure};
-
-pub fn body(pair: Pair<Rule>) -> Result<Body> {
-    pair.into_inner().map(structure).collect()
-}
-
-fn structure(pair: Pair<Rule>) -> Result<Structure> {
-    match pair.as_rule() {
-        Rule::Attribute => attribute(pair).map(Structure::Attribute),
-        Rule::Block => block(pair).map(Structure::Block),
-        rule => unexpected_rule(rule),
-    }
-}
-
-fn attribute(pair: Pair<Rule>) -> Result<Attribute> {
-    let mut pairs = pair.into_inner();
-
-    Ok(Attribute {
-        key: ident(pairs.next().unwrap()),
-        expr: expression(pairs.next().unwrap())?,
-    })
-}
-
-fn block(pair: Pair<Rule>) -> Result<Block> {
-    let mut pairs = pair.into_inner();
-    let identifier = ident(pairs.next().unwrap());
-    let (labels, body): (Vec<Pair<Rule>>, Vec<Pair<Rule>>) =
-        pairs.partition(|pair| pair.as_rule() != Rule::BlockBody);
-
-    Ok(Block {
-        identifier,
-        labels: labels.into_iter().map(block_label).collect::<Result<_>>()?,
-        body: block_body(body.into_iter().next().unwrap())?,
-    })
-}
-
-fn block_label(pair: Pair<Rule>) -> Result<BlockLabel> {
-    match pair.as_rule() {
-        Rule::Identifier => Ok(BlockLabel::Identifier(ident(pair))),
-        Rule::StringLit => unescape_string(inner(pair)).map(BlockLabel::String),
-        rule => unexpected_rule(rule),
-    }
-}
-
-fn block_body(pair: Pair<Rule>) -> Result<Body> {
-    match pair.as_rule() {
-        Rule::BlockBody => body(inner(pair)),
-        rule => unexpected_rule(rule),
-    }
-}
diff --git a/crates/hcl-rs/src/parser/template.rs b/crates/hcl-rs/src/parser/template.rs
deleted file mode 100644
index 7e5b796..0000000
--- a/crates/hcl-rs/src/parser/template.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-use super::*;
-use crate::template::{
-    Directive, Element, ForDirective, IfDirective, Interpolation, Strip, Template,
-};
-use hcl_primitives::template::unescape_markers;
-
-pub fn template(pair: Pair<Rule>) -> Result<Template> {
-    pair.into_inner().map(element).collect()
-}
-
-fn element(pair: Pair<Rule>) -> Result<Element> {
-    match pair.as_rule() {
-        Rule::TemplateLiteral => Ok(Element::Literal(unescape_markers(pair.as_str()).into())),
-        Rule::TemplateInterpolation => interpolation(pair).map(Element::Interpolation),
-        Rule::TemplateDirective => directive(inner(pair)).map(Element::Directive),
-        rule => unexpected_rule(rule),
-    }
-}
-
-fn interpolation(pair: Pair<Rule>) -> Result<Interpolation> {
-    let mut pairs = pair.into_inner();
-    let start = pairs.next().unwrap();
-    let expr = pairs.next().unwrap();
-    let end = pairs.next().unwrap();
-
-    Ok(Interpolation {
-        expr: expression(expr)?,
-        strip: strip_mode(start, end),
-    })
-}
-
-fn directive(pair: Pair<Rule>) -> Result<Directive> {
-    match pair.as_rule() {
-        Rule::TemplateIf => if_directive(pair).map(Directive::If),
-        Rule::TemplateFor => for_directive(pair).map(Directive::For),
-        rule => unexpected_rule(rule),
-    }
-}
-
-fn if_directive(pair: Pair<Rule>) -> Result<IfDirective> {
-    let mut pairs = pair.into_inner();
-    let if_expr = if_expr(pairs.next().unwrap())?;
-    let true_template = template(pairs.next().unwrap())?;
-    let mut expr = pairs.next().unwrap();
-
-    // Else branch is optional.
-    let (false_template, else_strip) = match expr.as_rule() {
-        Rule::TemplateElseExpr => {
-            let else_strip = else_expr_strip_mode(expr);
-            let false_template = template(pairs.next().unwrap())?;
-            expr = pairs.next().unwrap();
-            (Some(false_template), else_strip)
-        }
-        Rule::TemplateEndIfExpr => (None, Strip::default()),
-        rule => unexpected_rule(rule),
-    };
-
-    Ok(IfDirective {
-        cond_expr: if_expr.cond_expr,
-        true_template,
-        false_template,
-        if_strip: if_expr.if_strip,
-        else_strip,
-        endif_strip: end_expr_strip_mode(expr),
-    })
-}
-
-struct IfExpr {
-    cond_expr: Expression,
-    if_strip: Strip,
-}
-
-fn if_expr(pair: Pair<Rule>) -> Result<IfExpr> {
-    let mut pairs = pair.into_inner();
-    let start = pairs.next().unwrap();
-    let cond_expr = expression(pairs.next().unwrap())?;
-    let end = pairs.next().unwrap();
-
-    Ok(IfExpr {
-        cond_expr,
-        if_strip: strip_mode(start, end),
-    })
-}
-
-fn else_expr_strip_mode(pair: Pair<Rule>) -> Strip {
-    let mut pairs = pair.into_inner();
-    let start = pairs.next().unwrap();
-    let end = pairs.next().unwrap();
-    strip_mode(start, end)
-}
-
-fn for_directive(pair: Pair<Rule>) -> Result<ForDirective> {
-    let mut pairs = pair.into_inner();
-    let for_expr = for_expr(pairs.next().unwrap())?;
-    let template = template(pairs.next().unwrap())?;
-    let endfor_strip = end_expr_strip_mode(pairs.next().unwrap());
-
-    Ok(ForDirective {
-        key_var: for_expr.key_var,
-        value_var: for_expr.value_var,
-        collection_expr: for_expr.collection_expr,
-        template,
-        for_strip: for_expr.for_strip,
-        endfor_strip,
-    })
-}
-
-struct ForExpr {
-    key_var: Option<Identifier>,
-    value_var: Identifier,
-    collection_expr: Expression,
-    for_strip: Strip,
-}
-
-fn for_expr(pair: Pair<Rule>) -> Result<ForExpr> {
-    let mut pairs = pair.into_inner();
-    let start = pairs.next().unwrap();
-    let mut value_var = Some(ident(pairs.next().unwrap()));
-    let mut expr = pairs.next().unwrap();
-
-    // If there are two identifiers, the first one is the key and the second one the value.
-    let key_var = match expr.as_rule() {
-        Rule::Identifier => {
-            let key_var = value_var.replace(ident(expr));
-            expr = pairs.next().unwrap();
-            key_var
-        }
-        _ => None,
-    };
-
-    let end = pairs.next().unwrap();
-
-    Ok(ForExpr {
-        key_var,
-        value_var: value_var.take().unwrap(),
-        collection_expr: expression(expr)?,
-        for_strip: strip_mode(start, end),
-    })
-}
-
-fn end_expr_strip_mode(pair: Pair<Rule>) -> Strip {
-    let mut pairs = pair.into_inner();
-    let start = pairs.next().unwrap();
-    let end = pairs.next().unwrap();
-    strip_mode(start, end)
-}
-
-fn strip_mode(start: Pair<Rule>, end: Pair<Rule>) -> Strip {
-    let strip_start = match start.as_rule() {
-        Rule::TemplateIExprStartStrip | Rule::TemplateDExprStartStrip => true,
-        Rule::TemplateIExprStartNormal | Rule::TemplateDExprStartNormal => false,
-        rule => unexpected_rule(rule),
-    };
-
-    let strip_end = match end.as_rule() {
-        Rule::TemplateExprEndStrip => true,
-        Rule::TemplateExprEndNormal => false,
-        rule => unexpected_rule(rule),
-    };
-
-    Strip::from((strip_start, strip_end))
-}
diff --git a/crates/hcl-rs/src/structure/mod.rs b/crates/hcl-rs/src/structure/mod.rs
index 3d4230c..97beb4d 100644
--- a/crates/hcl-rs/src/structure/mod.rs
+++ b/crates/hcl-rs/src/structure/mod.rs
@@ -49,7 +49,6 @@ mod attribute;
 mod block;
 mod body;
 pub(crate) mod de;
-#[cfg(feature = "edit")]
 mod edit;
 pub mod iter;
 mod json_spec;
diff --git a/crates/hcl-rs/src/template/mod.rs b/crates/hcl-rs/src/template/mod.rs
index a8e0743..4e2d754 100644
--- a/crates/hcl-rs/src/template/mod.rs
+++ b/crates/hcl-rs/src/template/mod.rs
@@ -83,7 +83,6 @@
 //! The [`eval`][crate::eval] module provides evaluation capabilities for templates and
 //! expressions. See the [module-level documentation][crate::eval] for examples.
 
-#[cfg(feature = "edit")]
 mod edit;
 
 use crate::de::FromStrVisitor;
diff --git a/crates/hcl-rs/src/util.rs b/crates/hcl-rs/src/util.rs
index 7f95733..68dd0ce 100644
--- a/crates/hcl-rs/src/util.rs
+++ b/crates/hcl-rs/src/util.rs
@@ -2,54 +2,6 @@ use crate::{Error, Result};
 use std::borrow::Cow;
 use std::str::Chars;
 
-// String dedent implementation which does not distinguish between spaces, tabs or unicode
-// whitespace but simply treats all of them as "one unit of whitespace".
-//
-// This is how the original HCL spec seems to handle it based on the original specsuite although it
-// is not formally defined. E.g. ' ' (space) and '\u{2003}' (unicode "em-space") are treated as one
-// unit of whitespace even though the former is 1 byte and the latter is 3 bytes long.
-pub fn dedent(s: &str) -> Cow<str> {
-    if s.is_empty() {
-        return Cow::Borrowed(s);
-    }
-
-    let mut leading_ws = usize::MAX;
-    let mut non_empty_lines = 0;
-
-    // Find the minimum number of possible leading units of whitespace that can be be stripped off
-    // of each non-empty line.
-    for line in s.lines().filter(|line| !line.is_empty()) {
-        let line_leading_ws = line.chars().take_while(|ch| ch.is_whitespace()).count();
-
-        if line_leading_ws == 0 {
-            // Fast path: no dedent needed if we encounter a non-empty line which starts with a
-            // non-whitespace character.
-            return Cow::Borrowed(s);
-        }
-
-        leading_ws = leading_ws.min(line_leading_ws);
-        non_empty_lines += 1;
-    }
-
-    // Strip the determined amount of leading whitespace off of each line.
-    let mut dedented = String::with_capacity(s.len() - leading_ws * non_empty_lines);
-
-    for line in s.lines() {
-        if !line.is_empty() {
-            dedented.extend(line.chars().skip(leading_ws));
-        }
-
-        dedented.push('\n');
-    }
-
-    if dedented.ends_with('\n') && !s.ends_with('\n') {
-        let new_len = dedented.len() - 1;
-        dedented.truncate(new_len);
-    }
-
-    Cow::Owned(dedented)
-}
-
 /// Takes in a string with backslash escapes written out with literal backslash characters and
 /// converts it to a string with the proper escaped characters.
 ///


Test patch:
diff --git a/crates/hcl-rs/src/parser/tests.rs b/crates/hcl-rs/src/parser/tests.rs
deleted file mode 100644
index 2f18ff6..0000000
--- a/crates/hcl-rs/src/parser/tests.rs
+++ /dev/null
@@ -1,378 +0,0 @@
-use super::*;
-use pest::*;
-
-#[test]
-fn parse_identifier() {
-    parses_to! {
-        parser: HclParser,
-        input: "_an-id3nt1fieR",
-        rule: Rule::Identifier,
-        tokens: [
-            Identifier(0, 14)
-        ]
-    };
-}
-
-#[test]
-fn parse_string() {
-    parses_to! {
-        parser: HclParser,
-        input: "\"a string\"",
-        rule: Rule::StringLit,
-        tokens: [
-            StringLit(0, 10, [
-                String(1, 9)
-            ])
-        ]
-    };
-}
-
-#[test]
-fn parse_number() {
-    parses_to! {
-        parser: HclParser,
-        input: "12e+10",
-        rule: Rule::NumericLit,
-        tokens: [
-            Float(0, 6)
-        ]
-    };
-
-    parses_to! {
-        parser: HclParser,
-        input: "42",
-        rule: Rule::NumericLit,
-        tokens: [
-            Int(0, 2)
-        ]
-    };
-}
-
-#[test]
-fn parse_conditional() {
-    parses_to! {
-        parser: HclParser,
-        input: "var.enabled ? 1 : 0",
-        rule: Rule::Expression,
-        tokens: [
-            Expression(0, 19, [
-                ExprTerm(0, 11, [
-                    Variable(0, 3),
-                    GetAttr(3, 11, [
-                        Identifier(4, 11)
-                    ])
-                ]),
-                Expression(14, 16, [
-                    ExprTerm(14, 16, [
-                        Int(14, 15)
-                    ])
-                ]),
-                Expression(18, 19, [
-                    ExprTerm(18, 19, [
-                        Int(18, 19)
-                    ])
-                ])
-            ])
-        ]
-    };
-}
-
-#[test]
-fn parse_collections() {
-    parses_to! {
-        parser: HclParser,
-        input: r#"foo = ["bar", ["baz"]]"#,
-        rule: Rule::Attribute,
-        tokens: [
-            Attribute(0, 22, [
-                Identifier(0, 3),
-                Expression(6, 22, [
-                    ExprTerm(6, 22, [
-                        Tuple(6, 22, [
-                            Expression(7, 12, [
-                                ExprTerm(7, 12, [
-                                    StringLit(7, 12, [
-                                        String(8, 11)
-                                    ])
-                                ])
-                            ]),
-                            Expression(14, 21, [
-                                ExprTerm(14, 21, [
-                                    Tuple(14, 21, [
-                                        Expression(15, 20, [
-                                            ExprTerm(15, 20, [
-                                                StringLit(15, 20, [
-                                                    String(16, 19)
-                                                ])
-                                            ])
-                                        ])
-                                    ])
-                                ])
-                            ])
-                        ])
-                    ])
-                ])
-            ])
-        ]
-    };
-
-    parses_to! {
-        parser: HclParser,
-        input: r#"foo = {"bar" = "baz","qux" = ident }"#,
-        rule: Rule::Attribute,
-        tokens: [
-            Attribute(0, 36, [
-                Identifier(0, 3),
-                Expression(6, 36, [
-                    ExprTerm(6, 36, [
-                        Object(6, 36, [
-                            Expression(7, 13, [
-                                ExprTerm(7, 13, [
-                                    StringLit(7, 12, [
-                                        String(8, 11)
-                                    ])
-                                ])
-                            ]),
-                            Expression(15, 20, [
-                                ExprTerm(15, 20, [
-                                    StringLit(15, 20, [
-                                        String(16, 19)
-                                    ])
-                                ])
-                            ]),
-                            Expression(21, 27, [
-                                ExprTerm(21, 27, [
-                                    StringLit(21, 26, [
-                                        String(22, 25)
-                                    ])
-                                ])
-                            ]),
-                            Expression(29, 35, [
-                                ExprTerm(29, 35, [
-                                    Variable(29, 34)
-                                ])
-                            ])
-                        ])
-                    ])
-                ])
-            ])
-        ]
-    };
-}
-
-#[test]
-fn parse_template() {
-    parses_to! {
-        parser: HclParser,
-        input: "<<HEREDOC\n${foo}\n%{if asdf}qux%{endif}\nheredoc\nHEREDOC",
-        rule: Rule::ExprTerm,
-        tokens: [
-            ExprTerm(0, 54, [
-                TemplateExpr(0, 54, [
-                    Heredoc(0, 54, [
-                        HeredocIntroNormal(0, 2),
-                        Identifier(2, 9),
-                        HeredocTemplate(10, 46, [
-                            TemplateInterpolation(10, 16, [
-                                TemplateIExprStartNormal(10, 12),
-                                Expression(12, 15, [
-                                    ExprTerm(12, 15, [
-                                        Variable(12, 15)
-                                    ])
-                                ]),
-                                TemplateExprEndNormal(15, 16)
-                            ]),
-                            HeredocLiteral(16, 17),
-                            TemplateDirective(17, 38, [
-                                TemplateIf(17, 38, [
-                                    TemplateIfExpr(17, 27, [
-                                        TemplateDExprStartNormal(17, 19),
-                                        Expression(22, 26, [
-                                            ExprTerm(22, 26, [
-                                                Variable(22, 26)
-                                            ])
-                                        ]),
-                                        TemplateExprEndNormal(26, 27),
-                                    ]),
-                                    Template(27, 30, [
-                                        TemplateLiteral(27, 30)
-                                    ]),
-                                    TemplateEndIfExpr(30, 38, [
-                                        TemplateDExprStartNormal(30, 32),
-                                        TemplateExprEndNormal(37, 38),
-                                    ])
-                                ])
-                            ]),
-                            HeredocLiteral(38, 46)
-                        ])
-                    ])
-                ])
-            ])
-        ]
-    };
-
-    parses_to! {
-        parser: HclParser,
-        input: r#""foo ${bar} $${baz}, %{if cond ~} qux %{~ endif}""#,
-        rule: Rule::ExprTerm,
-        tokens: [
-            ExprTerm(0, 49, [
-                TemplateExpr(0, 49, [
-                    QuotedStringTemplate(0, 49, [
-                        QuotedStringTemplateInner(1, 48, [
-                            QuotedStringTemplateLiteral(1, 5),
-                            TemplateInterpolation(5, 11, [
-                                TemplateIExprStartNormal(5, 7),
-                                Expression(7, 10, [
-                                    ExprTerm(7, 10, [
-                                        Variable(7, 10)
-                                    ])
-                                ]),
-                                TemplateExprEndNormal(10, 11)
-                            ]),
-                            QuotedStringTemplateLiteral(11, 21),
-                            TemplateDirective(21, 48, [
-                                TemplateIf(21, 48, [
-                                    TemplateIfExpr(21, 33, [
-                                        TemplateDExprStartNormal(21, 23),
-                                        Expression(26, 31, [
-                                            ExprTerm(26, 31, [
-                                                Variable(26, 30)
-                                            ])
-                                        ]),
-                                        TemplateExprEndStrip(31, 33),
-                                    ]),
-                                    Template(33, 38, [
-                                        TemplateLiteral(33, 38)
-                                    ]),
-                                    TemplateEndIfExpr(38, 48, [
-                                        TemplateDExprStartStrip(38, 41),
-                                        TemplateExprEndNormal(47, 48),
-                                    ])
-                                ])
-                            ])
-                        ])
-                    ])
-                ])
-            ])
-        ]
-    };
-}
-
-#[test]
-fn parse_cond_in_interpolation() {
-    parses_to! {
-        parser: HclParser,
-        input: r#"name = "${var.l ? "us-east-1." : ""}""#,
-        rule: Rule::Attribute,
-        tokens: [
-            Attribute(0, 37, [
-                Identifier(0, 4),
-                Expression(7, 37, [
-                    ExprTerm(7, 37, [
-                        TemplateExpr(7, 37, [
-                            QuotedStringTemplate(7, 37, [
-                                QuotedStringTemplateInner(8, 36, [
-                                    TemplateInterpolation(8, 36, [
-                                        TemplateIExprStartNormal(8, 10),
-                                        Expression(10, 35, [
-                                            ExprTerm(10, 15, [
-                                                Variable(10, 13),
-                                                GetAttr(13, 15, [
-                                                    Identifier(14, 15)
-                                                ])
-                                            ]),
-                                            Expression(18, 31, [
-                                                ExprTerm(18, 31, [
-                                                    StringLit(18, 30, [
-                                                        String(19, 29)
-                                                    ])
-                                                ])
-                                            ]),
-                                            Expression(33, 35, [
-                                                ExprTerm(33, 35, [
-                                                    StringLit(33, 35, [
-                                                        String(34, 34)
-                                                    ])
-                                                ])
-                                            ])
-                                        ]),
-                                        TemplateExprEndNormal(35, 36)
-                                    ])
-                                ])
-                            ])
-                        ])
-                    ])
-                ])
-            ])
-        ]
-    };
-}
-
-#[test]
-fn parse_nested_function_call_with_splat() {
-    parses_to! {
-        parser: HclParser,
-        input: r#"element(concat(aws_kms_key.key-one.*.arn, aws_kms_key.key-two.*.arn), 0)"#,
-        rule: Rule::FunctionCall,
-        tokens: [
-            FunctionCall(0, 72, [
-                Identifier(0, 7),
-                Arguments(7, 72, [
-                    Expression(8, 68, [
-                        ExprTerm(8, 68, [
-                            FunctionCall(8, 68, [
-                                Identifier(8, 14),
-                                Arguments(14, 68, [
-                                    Expression(15, 40, [
-                                        ExprTerm(15, 40, [
-                                            Variable(15, 26),
-                                            GetAttr(26, 34, [
-                                                Identifier(27, 34)
-                                            ]),
-                                            AttrSplat(34, 36),
-                                            GetAttr(36, 40, [
-                                                Identifier(37, 40)
-                                            ]),
-                                        ])
-                                    ]),
-                                    Expression(42, 67, [
-                                        ExprTerm(42, 67, [
-                                            Variable(42, 53),
-                                            GetAttr(53, 61, [
-                                                Identifier(54, 61)
-                                            ]),
-                                            AttrSplat(61, 63),
-                                            GetAttr(63, 67, [
-                                                Identifier(64, 67)
-                                            ])
-                                        ])
-                                    ])
-                                ])
-                            ])
-                        ])
-                    ]),
-                    Expression(70, 71, [
-                        ExprTerm(70, 71, [
-                            Int(70, 71)
-                        ])
-                    ])
-                ])
-            ])
-        ]
-    };
-}
-
-#[test]
-fn parse_escaped_slash_in_string() {
-    parses_to! {
-        parser: HclParser,
-        input: r#""\\""#,
-        rule: Rule::StringLit,
-        tokens: [
-            StringLit(0, 4, [
-                String(1, 3),
-            ])
-        ]
-    };
-}
diff --git a/crates/hcl-rs/tests/de.rs b/crates/hcl-rs/tests/de.rs
index 80b432f..d7a20ef 100644
--- a/crates/hcl-rs/tests/de.rs
+++ b/crates/hcl-rs/tests/de.rs
@@ -137,7 +137,7 @@ fn duplicate_attribute() {
         foo = ["bar"]
         foo = ["baz"]
     "#;
-    assert_deserialize(input, hcl::value!({ foo = ["baz"] }));
+    assert!(hcl::from_str::<Value>(input).is_err());
 }
 
 #[test]
@@ -336,6 +336,8 @@ fn unescape_strings() {
         block "label\\with\\backslashes" {
           string_attr = "I \u2665 unicode"
 
+          template_attr = "I \u2665 ${unicode}\n"
+
           object_attr = {
             "key\nwith\nnewlines" = true
           }
@@ -352,6 +354,7 @@ fn unescape_strings() {
             Block::builder("block")
                 .add_label("label\\with\\backslashes")
                 .add_attribute(("string_attr", "I \u{2665} unicode"))
+                .add_attribute(("template_attr", TemplateExpr::from("I \u{2665} ${unicode}\n")))
                 .add_attribute((
                     "object_attr",
                     Expression::from_iter([("key\nwith\nnewlines", true)]),
diff --git a/crates/specsuite/tests/expressions/invalid-heredoc.t b/crates/specsuite/tests/expressions/invalid-heredoc.t
index 13d5377..8bb3940 100644
--- a/crates/specsuite/tests/expressions/invalid-heredoc.t
+++ b/crates/specsuite/tests/expressions/invalid-heredoc.t
@@ -1,4 +1,4 @@
 // EOF must be immediately followed by a newline
 diagnostics {
-  error = " --> 2:12\n  |\n2 |   source = <<-EOF # This is invalid\n  |            ^---\n  |\n  = expected Expression in line 2, col 12"
+  error = " --> HCL parse error in line 2, column 18\n  |\n2 |   source = <<-EOF # This is invalid\n  |                  ^---\n  |\n  = expected newline"
 }