Repository: apollographql/apollo-rs
Pull request #713 (2023-10-20 19:46:31)

Check validity of `ast::Name` (and assorted breaking changes) 
Fixes https://github.com/apollographql/apollo-rs/issues/710

Featuring unsafe tagged pointers and Miri on CI

goto-bus-stop: I like it a lot overall and I'm not opposed to a bit of easily auditable unsafe. How much use would we expect the no-alloc static representation to have? Well-known names like "Query" or directives implemented by a server would use it but I'm wondering if that's enough allocations to be worth the complexity 😅  There is an alternative option to validate the `name!()` input at compile time but still copy it into an allocation at runtime right? That might be a footgun if you use it in a hot loop I guess.
SimonSapin: I’d expect almost all static `NodeStr`s to come from the `name!` macro. Yes, having `name!` still check validity at compile-time but allocate on the heap is also a possibility. And yes, only a small minority of all `Name`s involved e.g. when handling one request in the Router come from `name!`.

Even so, my opinion is that the complexity here is far from excessive: it’s entirely encapsulated in a ~300 lines modules (~100 of which are safe boilerplate impls). And verification by Miri gives some confidence in correctness. We routinely rely on Rowan and other libraries that also use unsafe internally (in specific modules, encapsulated with a safe abstraction.)

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 12dc1ec..77b1d16 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -57,6 +57,13 @@ workflows:
             parameters:
               platform: [linux, macos, windows]
               rust_channel: [stable, nightly]
+  miri:
+    jobs:
+      - miri:
+          name: Miri (rust nightly on linux, subset of tests)
+          matrix:
+            parameters:
+              platform: [linux]
   security-scans:
     jobs:
       - secops/gitleaks:
@@ -128,6 +135,23 @@ jobs:
           name: Run cargo test
           command: cargo test --all-features
 
+  miri:
+    parameters:
+      platform:
+        type: executor
+    executor: << parameters.platform >>
+    steps:
+      - checkout
+      - install_system_deps:
+          rust_channel: nightly
+          platform: << parameters.platform >>
+      - run:
+          name: Install Miri
+          command: rustup component add miri rust-src
+      - run:
+          name: Run a subset of tests under Miri
+          command: cargo miri test --test main -- node_str
+
 # The folowing are reusable command snippets can be referred to in any `steps`.
 # Commands we currently have: install_system_deps, install_rust_toolchain.
 commands:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index b5d0a1f..eefc6a1 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -40,6 +40,26 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - **Rename `Directives` and `Diagnostics` to `DirectiveList` and `DiagnosticList` - 
   [SimonSapin], [pull/732] fixing [issue/711].**
   The previous names were too similar to `Directive` and `Diagnostic` (singular).
+- **Rename `ComponentStr` to `ComponentName` - [SimonSapin], [pull/713]**
+  and its `node: NodeStr` field to `name: Name`.
+- **Assorted changed to GraphQL names - [SimonSapin], [pull/713] fixing [issue/710].**
+  - **Check validity of `ast::Name`.**
+    `NodeStr` is a smart string type with infallible conversion from `&str`.
+    `ast::Name` used to be a type alias for `NodeStr`, 
+    leaving the possibility of creating one invalid in GraphQL syntax.
+    Validation and serialization would not check this.
+    `Name` is now a wrapper type for `NodeStr`.
+    Its `new` constructor checks validity of the given string and returns a `Result`.
+    A new `name!` macro (see below) creates a `Name` with compile-time checking.
+  - **`OperationType::default_type_name` returns a `Name` instead of `&str`**
+  - **`Type::new_named("x")` is removed. Use `Type::Named(name!("x"))` instead.**
+  - **`ComponentStr` is renamed to `ComponentName`.**
+    It no longer has infallible conversions from `&str` or `String`.
+    Its `node` field is renamed to `name`;
+    the type of that field is changed from `NodeStr` to `Name`.
+  - **`NodeStr` no longer has a `to_component` method, only `Name` does**
+  - **Various function or method parameters changed from `impl Into<Name>` to `Name`,**
+    since there is no longer an infallible conversion from `&str`
 
 ## Features
 
@@ -66,10 +86,25 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
   * `executable::InlineFragment`
   * `executable::FragmentSpread`
   * `executable::FieldSet`
+- **Assorted changed to GraphQL names - [SimonSapin], [pull/713] fixing [issue/710].**
+  See also the BREAKING section above.
+  - **Add a `name!("example")` macro**,
+    to be imported with `use apollo_compiler::name;`.
+    It creates an `ast::Name` from a string literal, with a compile-time validity checking.
+    A `Name` created this way does not own allocated heap memory or a reference counter,
+    so cloning it is extremely cheap.
+  - **Add allocation-free `NodeStr::from_static`.**
+    This mostly exists to support the `name!` macro, but can also be used on its own:
+    ```rust
+    let s = apollo_compiler::NodeStr::from_static(&"example");
+    assert_eq!(s, "example");
+    ```
 
 [SimonSapin]: https://github.com/SimonSapin
 [issue/708]: https://github.com/apollographql/apollo-rs/issues/708
+[issue/710]: https://github.com/apollographql/apollo-rs/issues/710
 [issue/711]: https://github.com/apollographql/apollo-rs/issues/711
+[pull/713]: https://github.com/apollographql/apollo-rs/pull/713
 [pull/727]: https://github.com/apollographql/apollo-rs/pull/727
 [pull/728]: https://github.com/apollographql/apollo-rs/pull/728
 [pull/732]: https://github.com/apollographql/apollo-rs/pull/732
diff --git a/crates/apollo-compiler/Cargo.toml b/crates/apollo-compiler/Cargo.toml
index 6d13733..462e074 100644
--- a/crates/apollo-compiler/Cargo.toml
+++ b/crates/apollo-compiler/Cargo.toml
@@ -26,6 +26,9 @@ salsa = "0.16.1"
 serde = { version = "1.0", features = ["derive"] }
 thiserror = "1.0.31"
 triomphe = "0.1.9"
+# TODO: replace `sptr` with standard library methods when available:
+# https://github.com/rust-lang/rust/issues/95228
+sptr = "0.3.2"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 uuid = { version = "1.5", features = ["serde", "v4", "js"] }
diff --git a/crates/apollo-compiler/examples/rename.rs b/crates/apollo-compiler/examples/rename.rs
index f34f467..aa789d6 100644
--- a/crates/apollo-compiler/examples/rename.rs
+++ b/crates/apollo-compiler/examples/rename.rs
@@ -1,7 +1,7 @@
 //! This example shows how to rename a type definition
 
+use apollo_compiler::name;
 use apollo_compiler::schema::ExtendedType;
-use apollo_compiler::schema::Name;
 use apollo_compiler::Schema;
 
 #[cfg(not(test))]
@@ -21,7 +21,7 @@ fn renamed() -> Schema {
     let ExtendedType::Object(obj) = &mut type_def else {
         panic!("expected an object type")
     };
-    let new_name = Name::from("MyQuery");
+    let new_name = name!("MyQuery");
     obj.make_mut().name = new_name.clone();
 
     // 3. Insert back into the map using the new name as the key
@@ -36,7 +36,7 @@ fn renamed() -> Schema {
         .query
         .as_mut()
         .unwrap()
-        .node = new_name;
+        .name = new_name;
 
     schema.validate().unwrap();
     schema
diff --git a/crates/apollo-compiler/src/ast/from_cst.rs b/crates/apollo-compiler/src/ast/from_cst.rs
index bb43d8e..aaf5255 100644
--- a/crates/apollo-compiler/src/ast/from_cst.rs
+++ b/crates/apollo-compiler/src/ast/from_cst.rs
@@ -770,9 +770,10 @@ impl Convert for cst::Name {
     type Target = ast::Name;
 
     fn convert(&self, file_id: FileId) -> Option<Self::Target> {
-        Some(ast::Name::new_parsed(
-            self.text().as_str(),
-            NodeLocation::new(file_id, self.syntax()),
-        ))
+        let loc = NodeLocation::new(file_id, self.syntax());
+        let token = &self.syntax().first_token()?;
+        let str = token.text();
+        debug_assert!(ast::Name::check_syntax(str).is_ok());
+        Some(ast::Name(crate::NodeStr::new_parsed(str, loc)))
     }
 }
diff --git a/crates/apollo-compiler/src/ast/impls.rs b/crates/apollo-compiler/src/ast/impls.rs
index c766bfd..113bbd7 100644
--- a/crates/apollo-compiler/src/ast/impls.rs
+++ b/crates/apollo-compiler/src/ast/impls.rs
@@ -1,5 +1,8 @@
 use super::*;
+use crate::name;
 use crate::node::NodeLocation;
+use crate::schema::ComponentName;
+use crate::schema::ComponentOrigin;
 use crate::schema::SchemaBuilder;
 use crate::validation::DiagnosticList;
 use crate::ExecutableDocument;
@@ -473,11 +476,11 @@ impl OperationType {
     }
 
     /// Get the default name of the object type for this operation type
-    pub const fn default_type_name(self) -> &'static str {
+    pub const fn default_type_name(self) -> NamedType {
         match self {
-            OperationType::Query => "Query",
-            OperationType::Mutation => "Mutation",
-            OperationType::Subscription => "Subscription",
+            OperationType::Query => name!("Query"),
+            OperationType::Mutation => name!("Mutation"),
+            OperationType::Subscription => name!("Subscription"),
         }
     }
 
@@ -532,11 +535,6 @@ impl VariableDefinition {
 }
 
 impl Type {
-    /// Returns a new `Type::Named` with with a synthetic `Name` (not parsed from a source file)
-    pub fn new_named(name: &str) -> Self {
-        Type::Named(Name::new(name))
-    }
-
     /// Returns this type made non-null, if it isn’t already.
     pub fn non_null(self) -> Self {
         match self {
@@ -1115,6 +1113,136 @@ impl<N: Into<Name>, V: Into<Node<Value>>> From<(N, V)> for Node<Argument> {
     }
 }
 
+/// Create a [`Name`] from a string literal, checked for validity at compile time.
+///
+/// A `Name` created this way does not own allocated heap memory or a reference counter,
+/// so cloning it is extremely cheap.
+#[macro_export]
+macro_rules! name {
+    ($value: literal) => {{
+        const _: () = { assert!($crate::ast::Name::check_syntax($value).is_ok()) };
+        $crate::ast::Name::new_unchecked($crate::NodeStr::from_static(&$value))
+    }};
+}
+
+impl Name {
+    /// Creates a new `Name` if the given value is a valid GraphQL name.
+    pub fn new(value: &str) -> Result<Self, InvalidNameError> {
+        Self::check_syntax(value).map(|()| Self::new_unchecked(NodeStr::new(value)))
+    }
+
+    /// Creates a new `Name` without validity checking.
+    ///
+    /// Constructing an invalid name may cause invalid document serialization
+    /// but not memory-safety issues.
+    pub const fn new_unchecked(value: NodeStr) -> Self {
+        Self(value)
+    }
+
+    /// Checks whether the given string is a valid GraphQL name.
+    ///
+    /// <https://spec.graphql.org/October2021/#Name>
+    pub const fn check_syntax(value: &str) -> Result<(), InvalidNameError> {
+        let bytes = value.as_bytes();
+        let Some(&first) = bytes.first() else {
+            return Err(InvalidNameError {});
+        };
+        // `NameStart` and `NameContinue` are within the ASCII range,
+        // so converting from `u8` to `char` here does not affect the result:
+        if !Self::char_is_name_start(first as char) {
+            return Err(InvalidNameError {});
+        }
+        // TODO: iterator when available in const
+        let mut i = 1;
+        while i < bytes.len() {
+            if !Self::char_is_name_continue(bytes[i] as char) {
+                return Err(InvalidNameError {});
+            }
+            i += 1
+        }
+        Ok(())
+    }
+
+    /// <https://spec.graphql.org/October2021/#NameStart>
+    const fn char_is_name_start(c: char) -> bool {
+        matches!(c, 'a'..='z' | 'A'..='Z' | '_')
+    }
+
+    /// <https://spec.graphql.org/October2021/#NameContinue>
+    const fn char_is_name_continue(c: char) -> bool {
+        matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9')
+    }
+
+    pub fn to_component(&self, origin: ComponentOrigin) -> ComponentName {
+        ComponentName {
+            origin,
+            name: self.clone(),
+        }
+    }
+}
+
+impl std::ops::Deref for Name {
+    type Target = NodeStr;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl fmt::Display for Name {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.as_str().fmt(f)
+    }
+}
+
+impl fmt::Debug for Name {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.as_str().fmt(f)
+    }
+}
+
+impl AsRef<NodeStr> for Name {
+    fn as_ref(&self) -> &NodeStr {
+        &self.0
+    }
+}
+
+impl AsRef<str> for Name {
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl std::borrow::Borrow<str> for Name {
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl PartialEq<str> for Name {
+    fn eq(&self, other: &str) -> bool {
+        self.as_str() == other
+    }
+}
+
+impl PartialEq<&'_ str> for Name {
+    fn eq(&self, other: &&'_ str) -> bool {
+        self.as_str() == *other
+    }
+}
+
+impl PartialOrd<str> for Name {
+    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
+        self.as_str().partial_cmp(other)
+    }
+}
+
+impl PartialOrd<&'_ str> for Name {
+    fn partial_cmp(&self, other: &&'_ str) -> Option<std::cmp::Ordering> {
+        self.as_str().partial_cmp(*other)
+    }
+}
+
 impl<T: Extensible> TypeWithExtensions<T> {
     /// Iterate over elements of the base definition and its extensions.
     pub fn iter_all<'a, Item, DefIter, ExtIter>(
diff --git a/crates/apollo-compiler/src/ast/mod.rs b/crates/apollo-compiler/src/ast/mod.rs
index d4d7ef3..3a61e13 100644
--- a/crates/apollo-compiler/src/ast/mod.rs
+++ b/crates/apollo-compiler/src/ast/mod.rs
@@ -61,11 +61,16 @@ const _: () = {
 };
 
 /// A GraphQL identifier
-pub type Name = NodeStr;
+#[derive(Clone, Ord, Eq, PartialOrd, PartialEq, Hash)]
+pub struct Name(NodeStr);
 
 /// Refers to the name of a GraphQL type defined elsewhere
 pub type NamedType = Name;
 
+#[derive(Clone, Eq, PartialEq)]
+#[non_exhaustive]
+pub struct InvalidNameError {}
+
 #[derive(Clone, Eq, PartialEq, Hash)]
 pub enum Definition {
     OperationDefinition(Node<OperationDefinition>),
diff --git a/crates/apollo-compiler/src/executable/from_ast.rs b/crates/apollo-compiler/src/executable/from_ast.rs
index c4d7ffd..c911d16 100644
--- a/crates/apollo-compiler/src/executable/from_ast.rs
+++ b/crates/apollo-compiler/src/executable/from_ast.rs
@@ -1,4 +1,5 @@
 use super::*;
+use crate::name;
 
 pub(crate) struct BuildErrors {
     pub(crate) errors: Vec<BuildError>,
@@ -106,7 +107,7 @@ impl Operation {
             s.root_operation(ast.operation_type)?.clone()
         } else {
             // Hack for validate_standalone_excutable
-            ast.operation_type.default_type_name().into()
+            ast.operation_type.default_type_name().clone()
         };
         let mut selection_set = SelectionSet::new(ty);
         selection_set.extend_from_ast(schema, errors, &ast.selection_set);
@@ -165,7 +166,7 @@ impl SelectionSet {
                             description: None,
                             name: ast.name.clone(),
                             arguments: Vec::new(),
-                            ty: Type::new_named("UNKNOWN"),
+                            ty: Type::Named(name!("UNKNOWN")),
                             directives: Default::default(),
                         }))
                     };
diff --git a/crates/apollo-compiler/src/executable/mod.rs b/crates/apollo-compiler/src/executable/mod.rs
index f9a8398..55039ce 100644
--- a/crates/apollo-compiler/src/executable/mod.rs
+++ b/crates/apollo-compiler/src/executable/mod.rs
@@ -376,9 +376,9 @@ impl Fragment {
 
 impl SelectionSet {
     /// Create a new selection set
-    pub fn new(ty: impl Into<NamedType>) -> Self {
+    pub fn new(ty: NamedType) -> Self {
         Self {
-            ty: ty.into(),
+            ty,
             selections: Vec::new(),
         }
     }
@@ -399,18 +399,14 @@ impl SelectionSet {
     pub fn new_field<'schema>(
         &self,
         schema: &'schema Schema,
-        name: impl Into<Name>,
+        name: Name,
     ) -> Result<Field, schema::FieldLookupError<'schema>> {
-        let name = name.into();
         let definition = schema.type_field(&self.ty, &name)?.node.clone();
         Ok(Field::new(name, definition))
     }
 
     /// Create a new inline fragment to be added to this selection set with [`push`][Self::push]
-    pub fn new_inline_fragment(
-        &self,
-        opt_type_condition: Option<impl Into<NamedType>>,
-    ) -> InlineFragment {
+    pub fn new_inline_fragment(&self, opt_type_condition: Option<NamedType>) -> InlineFragment {
         if let Some(type_condition) = opt_type_condition {
             InlineFragment::with_type_condition(type_condition)
         } else {
@@ -419,7 +415,7 @@ impl SelectionSet {
     }
 
     /// Create a new fragment spread to be added to this selection set with [`push`][Self::push]
-    pub fn new_fragment_spread(&self, fragment_name: impl Into<Name>) -> FragmentSpread {
+    pub fn new_fragment_spread(&self, fragment_name: Name) -> FragmentSpread {
         FragmentSpread::new(fragment_name)
     }
 
@@ -511,24 +507,24 @@ impl Field {
     /// Create a new field with the given name and type.
     ///
     /// See [`SelectionSet::new_field`] too look up the type in a schema instead.
-    pub fn new(name: impl Into<Name>, definition: Node<schema::FieldDefinition>) -> Self {
+    pub fn new(name: Name, definition: Node<schema::FieldDefinition>) -> Self {
         let selection_set = SelectionSet::new(definition.ty.inner_named_type().clone());
         Field {
             definition,
             alias: None,
-            name: name.into(),
+            name,
             arguments: Vec::new(),
             directives: DirectiveList::new(),
             selection_set,
         }
     }
 
-    pub fn with_alias(mut self, alias: impl Into<Name>) -> Self {
-        self.alias = Some(alias.into());
+    pub fn with_alias(mut self, alias: Name) -> Self {
+        self.alias = Some(alias);
         self
     }
 
-    pub fn with_opt_alias(mut self, alias: Option<impl Into<Name>>) -> Self {
+    pub fn with_opt_alias(mut self, alias: Option<Name>) -> Self {
         self.alias = alias.map(Into::into);
         self
     }
@@ -546,7 +542,7 @@ impl Field {
         self
     }
 
-    pub fn with_argument(mut self, name: impl Into<Name>, value: impl Into<Node<Value>>) -> Self {
+    pub fn with_argument(mut self, name: Name, value: impl Into<Node<Value>>) -> Self {
         self.arguments.push((name, value).into());
         self
     }
@@ -590,8 +586,7 @@ impl Field {
 }
 
 impl InlineFragment {
-    pub fn with_type_condition(type_condition: impl Into<NamedType>) -> Self {
-        let type_condition = type_condition.into();
+    pub fn with_type_condition(type_condition: NamedType) -> Self {
         let selection_set = SelectionSet::new(type_condition.clone());
         Self {
             type_condition: Some(type_condition),
@@ -600,7 +595,7 @@ impl InlineFragment {
         }
     }
 
-    pub fn without_type_condition(parent_selection_set_type: impl Into<NamedType>) -> Self {
+    pub fn without_type_condition(parent_selection_set_type: NamedType) -> Self {
         Self {
             type_condition: None,
             directives: DirectiveList::new(),
@@ -638,9 +633,9 @@ impl InlineFragment {
 }
 
 impl FragmentSpread {
-    pub fn new(fragment_name: impl Into<Name>) -> Self {
+    pub fn new(fragment_name: Name) -> Self {
         Self {
-            fragment_name: fragment_name.into(),
+            fragment_name,
             directives: DirectiveList::new(),
         }
     }
@@ -674,7 +669,7 @@ impl FieldSet {
     /// Create a [`Parser`] to use different parser configuration.
     pub fn parse(
         schema: &Schema,
-        type_name: impl Into<NamedType>,
+        type_name: NamedType,
         source_text: impl Into<String>,
         path: impl AsRef<Path>,
     ) -> Self {
diff --git a/crates/apollo-compiler/src/node_str.rs b/crates/apollo-compiler/src/node_str.rs
index 1f5a48f..829d733 100644
--- a/crates/apollo-compiler/src/node_str.rs
+++ b/crates/apollo-compiler/src/node_str.rs
@@ -1,6 +1,11 @@
-use crate::schema::ComponentOrigin;
-use crate::schema::ComponentStr;
+#![allow(unstable_name_collisions)] // for `sptr::Strict`
+
 use crate::NodeLocation;
+use sptr::Strict;
+use std::marker::PhantomData;
+use std::mem::size_of;
+use std::mem::ManuallyDrop;
+use std::ptr::NonNull;
 use triomphe::ThinArc;
 
 /// Smart string type for names and string values in a GraphQL document
@@ -8,16 +13,67 @@ use triomphe::ThinArc;
 /// Like [`Node`][crate::Node] it is thread-safe, reference-counted,
 /// and carries an optional source location.
 /// It is a thin pointer to a single allocation, with a header followed by string data.
-#[derive(Clone)]
-pub struct NodeStr(ThinArc<Header, u8>);
+pub struct NodeStr {
+    /// A type-erased pointer for either `HeapRepr` or `StaticRepr`,
+    /// with a tag in the lowest bit: 1 for heap, 0 for static.
+    ptr: NonNull<()>,
+    phantom: PhantomData<UnpackedRepr>,
+}
 
 type Header = Option<NodeLocation>;
+type HeapRepr = ThinArc<Header, u8>;
+type StaticRepr = &'static &'static str;
+
+#[allow(unused)] // only used in PhantomData
+/// What we would use if it didn’t spend an extra 64 bits to store the 1-bit discriminant
+enum UnpackedRepr {
+    Heap(HeapRepr),
+    Static(StaticRepr),
+}
+
+const _: () = {
+    // Both `HeapRepr` and `StaticRepr` are pointers to sufficiently-aligned values,
+    // so the lowest address bit is always available to use as a tag
+    assert!(std::mem::align_of::<&'static str>() >= 2);
+    assert!(std::mem::align_of::<Header>() >= 2);
+
+    // Both pointers are non-null, leaving a niche to represent `None` without extra size
+    assert!(size_of::<Option<HeapRepr>>() == size_of::<usize>());
+    assert!(size_of::<Option<StaticRepr>>() == size_of::<usize>());
+    assert!(size_of::<Option<NodeStr>>() == size_of::<usize>());
+
+    // the `unsafe impl`s below are sound
+    const fn _assert_send<T: Send>() {}
+    const fn _assert_sync<T: Send>() {}
+    _assert_send::<HeapRepr>();
+    _assert_sync::<HeapRepr>();
+    _assert_send::<StaticRepr>();
+    _assert_sync::<StaticRepr>();
+};
+
+unsafe impl Send for NodeStr {}
+
+unsafe impl Sync for NodeStr {}
+
+const TAG_BITS: usize = 1_usize;
+
+fn address_has_tag(address: usize) -> bool {
+    (address & TAG_BITS) != 0
+}
+
+fn address_add_tag(address: usize) -> usize {
+    address | TAG_BITS
+}
+
+fn address_clear_tag(address: usize) -> usize {
+    address & !TAG_BITS
+}
 
 impl NodeStr {
     /// Create a new `NodeStr` parsed from the given source location
     #[inline]
     pub fn new_parsed(value: &str, location: NodeLocation) -> Self {
-        Self(ThinArc::from_header_and_slice(
+        Self::new_heap(ThinArc::from_header_and_slice(
             Some(location),
             value.as_bytes(),
         ))
@@ -26,31 +82,129 @@ impl NodeStr {
     /// Create a new `NodeStr` programatically, not parsed from a source file
     #[inline]
     pub fn new(value: &str) -> Self {
-        Self(ThinArc::from_header_and_slice(None, value.as_bytes()))
+        Self::new_heap(ThinArc::from_header_and_slice(None, value.as_bytes()))
     }
 
     #[inline]
-    pub fn to_component(&self, origin: ComponentOrigin) -> ComponentStr {
-        ComponentStr {
-            origin,
-            node: self.clone(),
+    fn new_heap(arc: HeapRepr) -> Self {
+        let ptr = ThinArc::into_raw(arc).cast_mut().cast::<()>();
+        let tagged_ptr = ptr.map_addr(|address| {
+            debug_assert!(!address_has_tag(address)); // checked statically with `align_of` above
+            address_add_tag(address)
+        });
+        Self {
+            // Safety: `ThinArc` is always non-null
+            ptr: unsafe { NonNull::new_unchecked(tagged_ptr) },
+            phantom: PhantomData,
+        }
+    }
+
+    /// Create a new `NodeStr` from a static string.
+    ///
+    /// `&str` is a wide pointer (length as pointer metadata stored next to the data pointer),
+    /// but we only have space for a thin pointer. So add another `&_` indirection.
+    ///
+    /// Example:
+    ///
+    /// ```
+    /// let s = apollo_compiler::NodeStr::from_static(&"example");
+    /// assert_eq!(s, "example");
+    /// ```
+    pub const fn from_static(str_ref: &'static &'static str) -> Self {
+        let ptr: *const &'static str = str_ref;
+        let ptr = ptr.cast_mut().cast();
+        // Safety: converted from `&_` which is non-null
+        let ptr = unsafe { NonNull::new_unchecked(ptr) };
+        Self {
+            ptr,
+            phantom: PhantomData,
+        }
+    }
+
+    #[inline]
+    fn as_heap(&self) -> Option<*const std::ffi::c_void> {
+        let ptr = self.ptr.as_ptr();
+        let address = ptr.addr();
+        let is_heap = address_has_tag(address);
+        is_heap.then(|| {
+            ptr.with_addr(address_clear_tag(address))
+                .cast_const()
+                .cast()
+        })
+    }
+
+    #[inline]
+    fn with_heap<R>(&self, f: impl FnOnce(Option<&HeapRepr>) -> R) -> R {
+        if let Some(heap_ptr) = self.as_heap() {
+            // Safety:
+            //
+            // * We’ve checked with the tag that this was created from `Self::new_heap`
+            // * This `from_raw` mirrors `into_raw` in `Self::new_heap`
+            //
+            // `from_raw` normally moves ownership away from the raw pointer,
+            // `ManuallyDrop` counteracts that.
+            let arc = ManuallyDrop::new(unsafe { ThinArc::from_raw(heap_ptr) });
+            f(Some(&arc))
+        } else {
+            f(None)
         }
     }
 
     #[inline]
     pub fn location(&self) -> Option<NodeLocation> {
-        self.0.header.header
+        self.with_heap(|maybe_heap| maybe_heap?.header.header)
     }
 
     #[inline]
     pub fn as_str(&self) -> &str {
-        unsafe { std::str::from_utf8_unchecked(&self.0.slice) }
+        self.with_heap(|maybe_heap| {
+            if let Some(heap) = maybe_heap {
+                // Safety: the bytes in `slice` were copied from an UTF-8 `&str`,
+                // and are immutable since.
+                let str = unsafe { std::str::from_utf8_unchecked(&heap.slice) };
+                // Safety: `heap` is a `&ThinArc` reference
+                // whose lifetime is limited to the stack frame of `with_heap`
+                // but that points to a `ThinArc` owned by `self`.
+                // Since `self` is immutable,
+                // the string slice owned by `ThinArc` lives as long as `self`
+                // and we can safely extend the lifetime of this borrow:
+                let raw: *const str = str;
+                unsafe { &*raw }
+            } else {
+                let ptr: *const &'static str = self.ptr.as_ptr().cast_const().cast();
+                // Safety: we just reversed the steps of `Self::_from_static`,
+                // which had started from a valid `&'static &'static str`
+                unsafe { *ptr }
+            }
+        })
     }
+}
 
-    /// Returns wether two `NodeStr`s point to the same allocation
-    #[inline]
-    pub fn ptr_eq(&self, other: &Self) -> bool {
-        self.0.as_ptr() == other.0.as_ptr()
+impl Clone for NodeStr {
+    fn clone(&self) -> Self {
+        self.with_heap(|maybe_heap| {
+            if let Some(heap) = maybe_heap {
+                Self::new_heap(ThinArc::clone(heap))
+            } else {
+                // `&'static &'static str` is `Copy`, just copy the pointer
+                Self { ..*self }
+            }
+        })
+    }
+}
+
+impl Drop for NodeStr {
+    fn drop(&mut self) {
+        if let Some(heap_ptr) = self.as_heap() {
+            // Safety:
+            //
+            // * We’ve checked with the tag that this was created from `Self::new_heap`
+            // * This `from_raw` mirrors `into_raw` in `Self::new_heap`
+            //
+            // `from_raw` moves ownership away from the raw pointer, which we want for drop.
+            let arc: HeapRepr = unsafe { ThinArc::from_raw(heap_ptr) };
+            drop(arc)
+        }
     }
 }
 
diff --git a/crates/apollo-compiler/src/parser.rs b/crates/apollo-compiler/src/parser.rs
index 0fe0576..7644a7a 100644
--- a/crates/apollo-compiler/src/parser.rs
+++ b/crates/apollo-compiler/src/parser.rs
@@ -205,7 +205,7 @@ impl Parser {
     pub fn parse_field_set(
         &mut self,
         schema: &Schema,
-        type_name: impl Into<ast::NamedType>,
+        type_name: ast::NamedType,
         source_text: impl Into<String>,
         path: impl AsRef<Path>,
     ) -> executable::FieldSet {
diff --git a/crates/apollo-compiler/src/schema/component.rs b/crates/apollo-compiler/src/schema/component.rs
index b39ee55..9aec965 100644
--- a/crates/apollo-compiler/src/schema/component.rs
+++ b/crates/apollo-compiler/src/schema/component.rs
@@ -1,6 +1,6 @@
+use crate::ast::Name;
 use crate::Node;
 use crate::NodeLocation;
-use crate::NodeStr;
 use std::hash;
 use std::ops::Deref;
 use std::ops::DerefMut;
@@ -123,89 +123,72 @@ impl<T> From<T> for Component<T> {
 
 /// A string component of a type or `schema`, for example the name of a union member type.
 ///
-/// Wraps a [`NodeStr`] and adds its origin: either a (`schema` or type) definition
+/// Wraps a [`Name`] and adds its origin: either a (`schema` or type) definition
 /// or a specific extension.
 ///
 /// Implements [`Deref`]
-/// so that methods and fields of `NodeStr` and [`str`] can be accessed directly.
+/// so that methods and fields of `Name` and [`str`] can be accessed directly.
 #[derive(Debug, Clone)]
-pub struct ComponentStr {
+pub struct ComponentName {
     pub origin: ComponentOrigin,
-    pub node: NodeStr,
+    pub name: Name,
 }
 
-impl ComponentStr {
-    /// Mark `value` as coming from a synthetic (no source location) definition (not an extension)
-    #[inline]
-    pub fn new(value: &str) -> Self {
-        Self {
-            origin: ComponentOrigin::Definition,
-            node: NodeStr::new(value),
-        }
+impl From<&Name> for ComponentName {
+    fn from(value: &Name) -> Self {
+        value.to_component(ComponentOrigin::Definition)
+    }
+}
+
+impl From<Name> for ComponentName {
+    fn from(value: Name) -> Self {
+        value.to_component(ComponentOrigin::Definition)
     }
 }
 
-impl hash::Hash for ComponentStr {
+impl hash::Hash for ComponentName {
     fn hash<H: hash::Hasher>(&self, state: &mut H) {
-        self.node.hash(state); // ignore `origin`
+        self.name.hash(state); // ignore `origin`
     }
 }
 
-impl Eq for ComponentStr {}
+impl Eq for ComponentName {}
 
-impl PartialEq for ComponentStr {
+impl PartialEq for ComponentName {
     fn eq(&self, other: &Self) -> bool {
-        self.node == other.node // ignore `origin`
+        self.name == other.name // ignore `origin`
     }
 }
 
-impl PartialEq<str> for ComponentStr {
+impl PartialEq<str> for ComponentName {
     fn eq(&self, other: &str) -> bool {
         self.as_str() == other
     }
 }
 
-impl Deref for ComponentStr {
-    type Target = NodeStr;
+impl Deref for ComponentName {
+    type Target = Name;
 
     #[inline]
     fn deref(&self) -> &Self::Target {
-        &self.node
+        &self.name
     }
 }
 
-impl std::borrow::Borrow<NodeStr> for ComponentStr {
-    fn borrow(&self) -> &NodeStr {
+impl std::borrow::Borrow<Name> for ComponentName {
+    fn borrow(&self) -> &Name {
         self
     }
 }
 
-impl std::borrow::Borrow<str> for ComponentStr {
+impl std::borrow::Borrow<str> for ComponentName {
     fn borrow(&self) -> &str {
         self
     }
 }
 
-impl AsRef<str> for ComponentStr {
+impl AsRef<str> for ComponentName {
     fn as_ref(&self) -> &str {
         self
     }
 }
-
-impl From<&'_ str> for ComponentStr {
-    fn from(value: &'_ str) -> Self {
-        Self::new(value)
-    }
-}
-
-impl From<&'_ String> for ComponentStr {
-    fn from(value: &'_ String) -> Self {
-        Self::new(value)
-    }
-}
-
-impl From<String> for ComponentStr {
-    fn from(value: String) -> Self {
-        Self::new(&value)
-    }
-}
diff --git a/crates/apollo-compiler/src/schema/from_ast.rs b/crates/apollo-compiler/src/schema/from_ast.rs
index 4a85467..dbde063 100644
--- a/crates/apollo-compiler/src/schema/from_ast.rs
+++ b/crates/apollo-compiler/src/schema/from_ast.rs
@@ -1,4 +1,5 @@
 use super::*;
+use crate::ast::OperationType;
 use indexmap::map::Entry;
 use std::sync::Arc;
 
@@ -267,27 +268,19 @@ impl SchemaBuilder {
             SchemaDefinitionStatus::Found => {}
             SchemaDefinitionStatus::NoneSoFar { orphan_extensions } => {
                 // This a macro rather than a closure to generate separate `static`s
+                let schema_def = schema.schema_definition.make_mut();
                 let mut has_implicit_root_operation = false;
-                macro_rules! default_root_operation {
-                    ($($operation_type: path: $root_operation: expr,)+) => {{
-                        $(
-                            let name = $operation_type.default_type_name();
-                            if let Some(ExtendedType::Object(_)) = schema.types.get(name) {
-                                static OBJECT_TYPE_NAME: OnceLock<ComponentStr> = OnceLock::new();
-                                $root_operation = Some(OBJECT_TYPE_NAME.get_or_init(|| {
-                                    Name::new(name).to_component(ComponentOrigin::Definition)
-                                }).clone());
-                                has_implicit_root_operation = true;
-                            }
-                        )+
-                    }};
+                for (operation_type, root_operation) in [
+                    (OperationType::Query, &mut schema_def.query),
+                    (OperationType::Mutation, &mut schema_def.mutation),
+                    (OperationType::Subscription, &mut schema_def.subscription),
+                ] {
+                    let name = operation_type.default_type_name();
+                    if schema.types.get(&name).is_some_and(|def| def.is_object()) {
+                        *root_operation = Some(name.into());
+                        has_implicit_root_operation = true
+                    }
                 }
-                let schema_def = schema.schema_definition.make_mut();
-                default_root_operation!(
-                    ast::OperationType::Query: schema_def.query,
-                    ast::OperationType::Mutation: schema_def.mutation,
-                    ast::OperationType::Subscription: schema_def.subscription,
-                );
 
                 let apply_schema_extensions =
                     // https://github.com/apollographql/apollo-rs/issues/682
@@ -339,7 +332,7 @@ impl SchemaBuilder {
 
 fn adopt_type_extensions(
     schema: &mut Schema,
-    type_name: &NodeStr,
+    type_name: &Name,
     extensions: &[ast::Definition],
 ) -> ExtendedType {
     macro_rules! extend {
@@ -456,14 +449,14 @@ impl SchemaDefinition {
         &mut self,
         errors: &mut Vec<BuildError>,
         origin: ComponentOrigin,
-        root_operations: &[Node<(ast::OperationType, Name)>],
+        root_operations: &[Node<(OperationType, Name)>],
     ) {
         for op in root_operations {
             let (operation_type, object_type_name) = &**op;
             let entry = match operation_type {
-                ast::OperationType::Query => &mut self.query,
-                ast::OperationType::Mutation => &mut self.mutation,
-                ast::OperationType::Subscription => &mut self.subscription,
+                OperationType::Query => &mut self.query,
+                OperationType::Mutation => &mut self.mutation,
+                OperationType::Subscription => &mut self.subscription,
             };
             match entry {
                 None => *entry = Some(object_type_name.to_component(origin.clone())),
@@ -532,7 +525,7 @@ impl ObjectType {
                 |prev, dup| {
                     errors.push(BuildError::DuplicateImplementsInterfaceInObject {
                         location: dup.location(),
-                        name_at_previous_location: prev.node.clone(),
+                        name_at_previous_location: prev.name.clone(),
                         type_name: definition.name.clone(),
                     })
                 },
@@ -585,7 +578,7 @@ impl ObjectType {
             |prev, dup| {
                 errors.push(BuildError::DuplicateImplementsInterfaceInObject {
                     location: dup.location(),
-                    name_at_previous_location: prev.node.clone(),
+                    name_at_previous_location: prev.name.clone(),
                     type_name: extension.name.clone(),
                 })
             },
@@ -624,7 +617,7 @@ impl InterfaceType {
                 |prev, dup| {
                     errors.push(BuildError::DuplicateImplementsInterfaceInInterface {
                         location: dup.location(),
-                        name_at_previous_location: prev.node.clone(),
+                        name_at_previous_location: prev.name.clone(),
                         type_name: definition.name.clone(),
                     })
                 },
@@ -677,7 +670,7 @@ impl InterfaceType {
             |prev, dup| {
                 errors.push(BuildError::DuplicateImplementsInterfaceInInterface {
                     location: dup.location(),
-                    name_at_previous_location: prev.node.clone(),
+                    name_at_previous_location: prev.name.clone(),
                     type_name: extension.name.clone(),
                 })
             },
@@ -721,7 +714,7 @@ impl UnionType {
                 |prev, dup| {
                     errors.push(BuildError::UnionMemberNameCollision {
                         location: dup.location(),
-                        name_at_previous_location: prev.node.clone(),
+                        name_at_previous_location: prev.name.clone(),
                         type_name: definition.name.clone(),
                     })
                 },
@@ -756,7 +749,7 @@ impl UnionType {
             |prev, dup| {
                 errors.push(BuildError::UnionMemberNameCollision {
                     location: dup.location(),
-                    name_at_previous_location: prev.node.clone(),
+                    name_at_previous_location: prev.name.clone(),
                     type_name: extension.name.clone(),
                 })
             },
@@ -927,9 +920,9 @@ fn collect_sticky<'a, V>(
 }
 
 fn extend_sticky_set(
-    set: &mut IndexSet<ComponentStr>,
-    iter: impl IntoIterator<Item = ComponentStr>,
-    mut duplicate: impl FnMut(&ComponentStr, ComponentStr),
+    set: &mut IndexSet<ComponentName>,
+    iter: impl IntoIterator<Item = ComponentName>,
+    mut duplicate: impl FnMut(&ComponentName, ComponentName),
 ) {
     for value in iter.into_iter() {
         match set.get(&value) {
@@ -941,9 +934,9 @@ fn extend_sticky_set(
     }
 }
 fn collect_sticky_set(
-    iter: impl IntoIterator<Item = ComponentStr>,
-    duplicate: impl FnMut(&ComponentStr, ComponentStr),
-) -> IndexSet<ComponentStr> {
+    iter: impl IntoIterator<Item = ComponentName>,
+    duplicate: impl FnMut(&ComponentName, ComponentName),
+) -> IndexSet<ComponentName> {
     let mut set = IndexSet::new();
     extend_sticky_set(&mut set, iter, duplicate);
     set
diff --git a/crates/apollo-compiler/src/schema/mod.rs b/crates/apollo-compiler/src/schema/mod.rs
index 8deee13..f1e45b2 100644
--- a/crates/apollo-compiler/src/schema/mod.rs
+++ b/crates/apollo-compiler/src/schema/mod.rs
@@ -18,12 +18,13 @@ mod from_ast;
 mod serialize;
 mod validation;
 
-pub use self::component::{Component, ComponentOrigin, ComponentStr, ExtensionId};
+pub use self::component::{Component, ComponentName, ComponentOrigin, ExtensionId};
 pub use self::from_ast::SchemaBuilder;
 pub use crate::ast::{
     Directive, DirectiveDefinition, DirectiveLocation, EnumValueDefinition, FieldDefinition,
     InputValueDefinition, Name, NamedType, Type, Value,
 };
+use crate::name;
 use crate::validation::DiagnosticList;
 
 /// High-level representation of a GraphQL schema
@@ -56,13 +57,13 @@ pub struct SchemaDefinition {
     pub directives: DirectiveList,
 
     /// Name of the object type for the `query` root operation
-    pub query: Option<ComponentStr>,
+    pub query: Option<ComponentName>,
 
     /// Name of the object type for the `mutation` root operation
-    pub mutation: Option<ComponentStr>,
+    pub mutation: Option<ComponentName>,
 
     /// Name of the object type for the `subscription` root operation
-    pub subscription: Option<ComponentStr>,
+    pub subscription: Option<ComponentName>,
 }
 
 #[derive(Clone, Eq, PartialEq, Hash, Default)]
@@ -92,7 +93,7 @@ pub struct ScalarType {
 pub struct ObjectType {
     pub description: Option<NodeStr>,
     pub name: Name,
-    pub implements_interfaces: IndexSet<ComponentStr>,
+    pub implements_interfaces: IndexSet<ComponentName>,
     pub directives: DirectiveList,
 
     /// Explicit field definitions.
@@ -106,7 +107,7 @@ pub struct ObjectType {
 pub struct InterfaceType {
     pub description: Option<NodeStr>,
     pub name: Name,
-    pub implements_interfaces: IndexSet<ComponentStr>,
+    pub implements_interfaces: IndexSet<ComponentName>,
 
     pub directives: DirectiveList,
 
@@ -126,7 +127,7 @@ pub struct UnionType {
     /// * Key: name of a member object type
     /// * Value: which union type extension defined this implementation,
     ///   or `None` for the union type definition.
-    pub members: IndexSet<ComponentStr>,
+    pub members: IndexSet<ComponentName>,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -391,7 +392,7 @@ impl Schema {
             ast::OperationType::Subscription => &self.schema_definition.subscription,
         }
         .as_ref()
-        .map(|component| &component.node)
+        .map(|component| &component.name)
     }
 
     /// Returns the definition of a type’s explicit field or meta-field.
@@ -439,7 +440,7 @@ impl Schema {
                 | ExtendedType::InputObject(_) => continue,
             };
             for interface in interfaces {
-                map.entry(interface.node.clone())
+                map.entry(interface.name.clone())
                     .or_default()
                     .insert(ty_name.clone());
             }
@@ -482,32 +483,32 @@ impl Schema {
                 // __typename: String!
                 Component::new(FieldDefinition {
                     description: None,
-                    name: Name::new("__typename"),
+                    name: name!("__typename"),
                     arguments: Vec::new(),
-                    ty: Type::new_named("String").non_null(),
+                    ty: Type::Named(name!("String")).non_null(),
                     directives: ast::DirectiveList::new(),
                 }),
                 // __schema: __Schema!
                 Component::new(FieldDefinition {
                     description: None,
-                    name: Name::new("__schema"),
+                    name: name!("__schema"),
                     arguments: Vec::new(),
-                    ty: Type::new_named("__Schema").non_null(),
+                    ty: Type::Named(name!("__Schema")).non_null(),
                     directives: ast::DirectiveList::new(),
                 }),
                 // __type(name: String!): __Type
                 Component::new(FieldDefinition {
                     description: None,
-                    name: Name::new("__type"),
+                    name: name!("__type"),
                     arguments: vec![InputValueDefinition {
                         description: None,
-                        name: Name::new("name"),
-                        ty: ast::Type::new_named("String").non_null().into(),
+                        name: name!("name"),
+                        ty: ast::Type::Named(name!("String")).non_null().into(),
                         default_value: None,
                         directives: ast::DirectiveList::new(),
                     }
                     .into()],
-                    ty: Type::new_named("__Type"),
+                    ty: Type::Named(name!("__Type")),
                     directives: ast::DirectiveList::new(),
                 }),
             ]
diff --git a/crates/apollo-compiler/src/schema/serialize.rs b/crates/apollo-compiler/src/schema/serialize.rs
index c7ed8b4..aa1e315 100644
--- a/crates/apollo-compiler/src/schema/serialize.rs
+++ b/crates/apollo-compiler/src/schema/serialize.rs
@@ -57,12 +57,11 @@ impl Node<SchemaDefinition> {
                 // If there were no explict `schema` definition,
                 // what implicit root operation would we get for this operation type?
                 let default_type_name = operation_type.default_type_name();
-                let implicit_root_operation: Option<&str> = types
-                    .get(default_type_name)
-                    .filter(|ty_def| ty_def.is_object())
-                    .map(|_ty_def| default_type_name);
+                let has_object = types
+                    .get(&default_type_name).is_some_and(|def| def.is_object());
+                let implicit_root_operation = has_object.then_some(&default_type_name);
                 // What we have
-                let actual_root_operation = root_operation.as_ref().map(|r| r.as_str());
+                let actual_root_operation = root_operation.as_ref().map(|r| &r.name);
                 // Only allow an implicit `schema` definition if they match
                 actual_root_operation == implicit_root_operation
             })
@@ -74,10 +73,10 @@ impl Node<SchemaDefinition> {
                 .into_iter()
                 .any(|op| op.is_some());
         let root_ops = |ext: Option<&ExtensionId>| -> Vec<Node<(OperationType, Name)>> {
-            let root_op = |op: &Option<ComponentStr>, ty| {
+            let root_op = |op: &Option<ComponentName>, ty| {
                 op.as_ref()
                     .filter(|name| name.origin.extension_id() == ext)
-                    .map(|name| (ty, name.node.clone()).into())
+                    .map(|name| (ty, name.name.clone()).into())
                     .into_iter()
             };
             root_op(&self.query, OperationType::Query)
@@ -305,11 +304,11 @@ fn components<'a, T: 'a>(
         .collect()
 }
 
-fn names(names: &IndexSet<ComponentStr>, ext: Option<&ExtensionId>) -> Vec<Name> {
+fn names(names: &IndexSet<ComponentName>, ext: Option<&ExtensionId>) -> Vec<Name> {
     names
         .iter()
         .filter(|component| component.origin.extension_id() == ext)
-        .map(|component| component.node.clone())
+        .map(|component| component.name.clone())
         .collect()
 }
 
diff --git a/crates/apollo-compiler/src/validation/directive.rs b/crates/apollo-compiler/src/validation/directive.rs
index dc41024..d222c93 100644
--- a/crates/apollo-compiler/src/validation/directive.rs
+++ b/crates/apollo-compiler/src/validation/directive.rs
@@ -262,7 +262,7 @@ pub(crate) fn validate_directives<'dir>(
                 let input_value = directive_definition
                     .arguments
                     .iter()
-                    .find(|val| val.name == *argument.name)
+                    .find(|val| val.name == argument.name)
                     .cloned();
 
                 // @b(a: true)
diff --git a/crates/apollo-compiler/src/validation/fragment.rs b/crates/apollo-compiler/src/validation/fragment.rs
index d1b4b03..b9d5654 100644
--- a/crates/apollo-compiler/src/validation/fragment.rs
+++ b/crates/apollo-compiler/src/validation/fragment.rs
@@ -43,7 +43,7 @@ pub(crate) fn get_possible_types<'a>(
         Some(schema::ExtendedType::Union(union_)) => union_
             .members
             .iter()
-            .map(|component| &component.node)
+            .map(|component| &component.name)
             .collect(),
         _ => Default::default(),
     }
diff --git a/crates/apollo-compiler/src/validation/interface.rs b/crates/apollo-compiler/src/validation/interface.rs
index e8d3edb..8ff35a9 100644
--- a/crates/apollo-compiler/src/validation/interface.rs
+++ b/crates/apollo-compiler/src/validation/interface.rs
@@ -176,7 +176,7 @@ pub(crate) fn validate_implements_interfaces(
         interface
             .implements_interfaces
             .iter()
-            .map(|component| &component.node)
+            .map(|component| &component.name)
             .zip(std::iter::repeat(name))
     });
     for (transitive_interface, via_interface) in transitive_interfaces {
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index f76f109..f1252ac 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -16,11 +16,11 @@ mod union_;
 mod value;
 mod variable;
 
+use crate::ast::Name;
 use crate::executable::BuildError as ExecutableBuildError;
 use crate::schema::BuildError as SchemaBuildError;
 use crate::FileId;
 use crate::NodeLocation;
-use crate::NodeStr;
 use crate::SourceFile;
 use crate::SourceMap;
 use indexmap::IndexSet;
@@ -484,11 +484,11 @@ impl ariadne::Span for NodeLocation {
 
 /// Track used names in a recursive function.
 struct RecursionStack {
-    seen: IndexSet<NodeStr>,
+    seen: IndexSet<Name>,
 }
 
 impl RecursionStack {
-    fn with_root(root: NodeStr) -> Self {
+    fn with_root(root: Name) -> Self {
         let mut seen = IndexSet::new();
         seen.insert(root);
         Self { seen }
@@ -505,10 +505,10 @@ impl RecursionStack {
 /// Pass the result of `guard.push(name)` to recursive calls. Use `guard.contains(name)` to check
 /// if the name was used somewhere up the call stack. When a guard is dropped, its name is removed
 /// from the list.
-struct RecursionGuard<'a>(&'a mut IndexSet<NodeStr>);
+struct RecursionGuard<'a>(&'a mut IndexSet<Name>);
 impl RecursionGuard<'_> {
     /// Mark that we saw a name.
-    fn push(&mut self, name: &NodeStr) -> RecursionGuard<'_> {
+    fn push(&mut self, name: &Name) -> RecursionGuard<'_> {
         debug_assert!(
             self.0.insert(name.clone()),
             "cannot push the same name twice to RecursionGuard, check contains() first"
@@ -516,11 +516,11 @@ impl RecursionGuard<'_> {
         RecursionGuard(self.0)
     }
     /// Check if we saw a name somewhere up the call stack.
-    fn contains(&self, name: &NodeStr) -> bool {
+    fn contains(&self, name: &Name) -> bool {
         self.0.iter().any(|seen| seen == name)
     }
     /// Return the name where we started.
-    fn first(&self) -> Option<&NodeStr> {
+    fn first(&self) -> Option<&Name> {
         self.0.first()
     }
 }
diff --git a/crates/apollo-compiler/src/validation/operation.rs b/crates/apollo-compiler/src/validation/operation.rs
index 09722d0..dfba217 100644
--- a/crates/apollo-compiler/src/validation/operation.rs
+++ b/crates/apollo-compiler/src/validation/operation.rs
@@ -1,7 +1,7 @@
 use crate::{
     ast,
     diagnostics::{ApolloDiagnostic, DiagnosticData, Label},
-    FileId, Node, ValidationDatabase,
+    name, FileId, Node, ValidationDatabase,
 };
 
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
@@ -29,7 +29,7 @@ pub(crate) fn validate_operation(
     let against_type = schema.root_operation(operation.operation_type);
 
     let named_fragments = db.ast_named_fragments(file_id);
-    let q = ast::NamedType::new("Query");
+    let q = name!("Query");
 
     if operation.operation_type == ast::OperationType::Subscription {
         let fields = super::selection::operation_fields(
diff --git a/crates/apollo-compiler/src/validation/validation_db.rs b/crates/apollo-compiler/src/validation/validation_db.rs
index 7013155..1beb942 100644
--- a/crates/apollo-compiler/src/validation/validation_db.rs
+++ b/crates/apollo-compiler/src/validation/validation_db.rs
@@ -1,7 +1,7 @@
 use crate::{
     ast,
     diagnostics::ApolloDiagnostic,
-    schema,
+    name, schema,
     validation::{
         self, directive, enum_, input_object, interface, object, operation, scalar, union_,
     },
@@ -244,15 +244,15 @@ fn ast_types(db: &dyn ValidationDatabase) -> Arc<ast::TypeSystem> {
                 directives: ast::DirectiveList::new(),
                 root_operations: {
                     let mut operations = Vec::with_capacity(3);
-                    let query_name = ast::Name::new("Query");
+                    let query_name = name!("Query");
                     if objects.contains_key(&query_name) {
                         operations.push((ast::OperationType::Query, query_name).into());
                     }
-                    let mutation_name = ast::Name::new("Mutation");
+                    let mutation_name = name!("Mutation");
                     if objects.contains_key(&mutation_name) {
                         operations.push((ast::OperationType::Mutation, mutation_name).into());
                     }
-                    let subscription_name = ast::Name::new("Subscription");
+                    let subscription_name = name!("Subscription");
                     if objects.contains_key(&subscription_name) {
                         operations
                             .push((ast::OperationType::Subscription, subscription_name).into());


Test patch:
diff --git a/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt b/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt
index 9be218a..2670a0b 100644
--- a/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt
+++ b/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt b/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt
index 208d31d..336973e 100644
--- a/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt
+++ b/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt b/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt
index f49558c..58671af 100644
--- a/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt
+++ b/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt
@@ -16,21 +16,21 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "customPetQuery",
+                name: "customPetQuery",
             },
         ),
         mutation: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "customPetMutation",
+                name: "customPetMutation",
             },
         ),
         subscription: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "customPetSubscription",
+                name: "customPetSubscription",
             },
         ),
     },
diff --git a/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt
index 68ab47e..0963f33 100644
--- a/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt
+++ b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt b/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt
index 1d51636..cc82977 100644
--- a/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt
+++ b/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt
index de06790..96a63e3 100644
--- a/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt
+++ b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "SearchQuery",
+                name: "SearchQuery",
             },
         ),
         mutation: None,
@@ -50,13 +50,13 @@ Schema {
                 name: "SearchResult",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Photo",
+                        name: "Photo",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Person",
+                        name: "Person",
                     },
                 },
             },
diff --git a/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt b/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt
index 902775b..b8b47b9 100644
--- a/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt
+++ b/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
@@ -49,9 +49,9 @@ Schema {
                 description: None,
                 name: "Query",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Node",
+                        name: "Node",
                     },
                 },
                 directives: [],
@@ -98,9 +98,9 @@ Schema {
                 description: None,
                 name: "Resource",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Node",
+                        name: "Node",
                     },
                 },
                 directives: [],
@@ -149,13 +149,13 @@ Schema {
                 description: None,
                 name: "Image",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Resource",
+                        name: "Resource",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Node",
+                        name: "Node",
                     },
                 },
                 directives: [],
diff --git a/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt
index 3daf215..a1cbac3 100644
--- a/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt
+++ b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt
index e8e22ed..5390419 100644
--- a/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt
+++ b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt b/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt
index b4d1db5..eef2724 100644
--- a/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt
+++ b/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt b/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt
index 0f4d372..cdfb032 100644
--- a/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt
+++ b/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt b/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt
index e9d98a2..8dfaba8 100644
--- a/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt
+++ b/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt b/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt
index e5a1703..2590fe6 100644
--- a/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt
+++ b/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0014_float_values.txt b/crates/apollo-compiler/test_data/ok/0014_float_values.txt
index ea79eb9..abed55b 100644
--- a/crates/apollo-compiler/test_data/ok/0014_float_values.txt
+++ b/crates/apollo-compiler/test_data/ok/0014_float_values.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0015_supergraph.txt b/crates/apollo-compiler/test_data/ok/0015_supergraph.txt
index 0994658..36afeae 100644
--- a/crates/apollo-compiler/test_data/ok/0015_supergraph.txt
+++ b/crates/apollo-compiler/test_data/ok/0015_supergraph.txt
@@ -45,15 +45,15 @@ Schema {
             },
         ],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Mutation",
+                name: "Mutation",
             },
         ),
         subscription: None,
@@ -245,13 +245,13 @@ Schema {
                 name: "AccountType",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "PasswordAccount",
+                        name: "PasswordAccount",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "SMSAccount",
+                        name: "SMSAccount",
                     },
                 },
             },
@@ -284,13 +284,13 @@ Schema {
                 name: "Body",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Image",
+                        name: "Image",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Text",
+                        name: "Text",
                     },
                 },
             },
@@ -300,9 +300,9 @@ Schema {
                 description: None,
                 name: "Book",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Product",
+                        name: "Product",
                     },
                 },
                 directives: [
@@ -781,13 +781,13 @@ Schema {
                 name: "Brand",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Ikea",
+                        name: "Ikea",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Amazon",
+                        name: "Amazon",
                     },
                 },
             },
@@ -797,9 +797,9 @@ Schema {
                 description: None,
                 name: "Car",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Vehicle",
+                        name: "Vehicle",
                     },
                 },
                 directives: [
@@ -1003,9 +1003,9 @@ Schema {
                 description: None,
                 name: "Furniture",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Product",
+                        name: "Product",
                     },
                 },
                 directives: [
@@ -1379,9 +1379,9 @@ Schema {
                 description: None,
                 name: "Image",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "NamedObject",
+                        name: "NamedObject",
                     },
                 },
                 directives: [],
@@ -1819,13 +1819,13 @@ Schema {
                 name: "MetadataOrError",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "KeyValue",
+                        name: "KeyValue",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Error",
+                        name: "Error",
                     },
                 },
             },
@@ -2242,9 +2242,9 @@ Schema {
                 description: None,
                 name: "ProductDetailsBook",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "ProductDetails",
+                        name: "ProductDetails",
                     },
                 },
                 directives: [],
@@ -2281,9 +2281,9 @@ Schema {
                 description: None,
                 name: "ProductDetailsFurniture",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "ProductDetails",
+                        name: "ProductDetails",
                     },
                 },
                 directives: [],
@@ -2950,9 +2950,9 @@ Schema {
                 description: None,
                 name: "Text",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "NamedObject",
+                        name: "NamedObject",
                     },
                 },
                 directives: [],
@@ -3024,13 +3024,13 @@ Schema {
                 name: "Thing",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Car",
+                        name: "Car",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Ikea",
+                        name: "Ikea",
                     },
                 },
             },
@@ -3558,9 +3558,9 @@ Schema {
                 description: None,
                 name: "Van",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Vehicle",
+                        name: "Vehicle",
                     },
                 },
                 directives: [
diff --git a/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt b/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt
index b0eac61..4db6e89 100644
--- a/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt
+++ b/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt b/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt
index 3cd39b2..3d7eaec 100644
--- a/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt
+++ b/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt b/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt
index 94967c5..6f55ef8 100644
--- a/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt
+++ b/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0019_extensions.txt b/crates/apollo-compiler/test_data/ok/0019_extensions.txt
index 377089f..f661af4 100644
--- a/crates/apollo-compiler/test_data/ok/0019_extensions.txt
+++ b/crates/apollo-compiler/test_data/ok/0019_extensions.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
@@ -77,9 +77,9 @@ Schema {
                 description: None,
                 name: "Object",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Intf",
+                        name: "Intf",
                     },
                 },
                 directives: [],
diff --git a/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt
index 7e17af7..98815ff 100644
--- a/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt
+++ b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt
index ad3ef13..c2539bc 100644
--- a/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt
+++ b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt
index fc65bb6..f5d96fd 100644
--- a/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt
+++ b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
@@ -135,9 +135,9 @@ Schema {
                 description: None,
                 name: "Dog",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Pet",
+                        name: "Pet",
                     },
                 },
                 directives: [],
@@ -239,9 +239,9 @@ Schema {
                 description: None,
                 name: "Cat",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Pet",
+                        name: "Pet",
                     },
                 },
                 directives: [],
diff --git a/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt b/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt
index f2e6ba4..a404e68 100644
--- a/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt
+++ b/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt b/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt
index dc14080..7a9fdce 100644
--- a/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt
+++ b/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt b/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt
index ea9d781..af5d04a 100644
--- a/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt
+++ b/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt b/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt
index 06cb680..b4a4c45 100644
--- a/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt
+++ b/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt b/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt
index cd3158a..532e5c5 100644
--- a/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt
+++ b/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
@@ -50,13 +50,13 @@ Schema {
                 name: "SearchResult",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Photo",
+                        name: "Photo",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Person",
+                        name: "Person",
                     },
                 },
             },
diff --git a/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt b/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt
index 8da7c6b..2e4fb2f 100644
--- a/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt
+++ b/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt b/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt
index 6cf5f5b..e3ee624 100644
--- a/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt
+++ b/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt b/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt
index bb0d128..fa04192 100644
--- a/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt
+++ b/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
@@ -71,9 +71,9 @@ Schema {
                 description: None,
                 name: "Dog",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Pet",
+                        name: "Pet",
                     },
                 },
                 directives: [],
@@ -110,9 +110,9 @@ Schema {
                 description: None,
                 name: "Cat",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Pet",
+                        name: "Pet",
                     },
                 },
                 directives: [],
@@ -150,13 +150,13 @@ Schema {
                 name: "CatOrDog",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Cat",
+                        name: "Cat",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Dog",
+                        name: "Dog",
                     },
                 },
             },
@@ -191,13 +191,13 @@ Schema {
                 name: "DogOrHuman",
                 directives: [],
                 members: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Dog",
+                        name: "Dog",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Human",
+                        name: "Human",
                     },
                 },
             },
@@ -229,9 +229,9 @@ Schema {
                 description: None,
                 name: "Resource",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Node",
+                        name: "Node",
                     },
                 },
                 directives: [],
@@ -268,13 +268,13 @@ Schema {
                 description: None,
                 name: "ConcreteResource",
                 implements_interfaces: {
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Resource",
+                        name: "Resource",
                     },
-                    ComponentStr {
+                    ComponentName {
                         origin: Definition,
-                        node: "Node",
+                        name: "Node",
                     },
                 },
                 directives: [],
diff --git a/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt b/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt
index 997bbe3..a8bd32a 100644
--- a/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt
+++ b/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt b/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt
index 1157e4c..f65b67c 100644
--- a/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt
+++ b/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt b/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt
index e833374..7de27cd 100644
--- a/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt
+++ b/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt
@@ -31,9 +31,9 @@ Schema {
             },
         ],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt b/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt
index 5067b60..1a41245 100644
--- a/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt
+++ b/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt b/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt
index 0c5a554..67e67fb 100644
--- a/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt
+++ b/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt
@@ -16,15 +16,15 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Mutation",
+                name: "Mutation",
             },
         ),
         subscription: None,
diff --git a/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt b/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt
index 1f8201a..07d9961 100644
--- a/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt
+++ b/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt
@@ -30,9 +30,9 @@ Schema {
             },
         ],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/test_data/ok/0038_argument_default.txt b/crates/apollo-compiler/test_data/ok/0038_argument_default.txt
index 39ee9f6..f198d3a 100644
--- a/crates/apollo-compiler/test_data/ok/0038_argument_default.txt
+++ b/crates/apollo-compiler/test_data/ok/0038_argument_default.txt
@@ -16,9 +16,9 @@ Schema {
         description: None,
         directives: [],
         query: Some(
-            ComponentStr {
+            ComponentName {
                 origin: Definition,
-                node: "Query",
+                name: "Query",
             },
         ),
         mutation: None,
diff --git a/crates/apollo-compiler/tests/field_set.rs b/crates/apollo-compiler/tests/field_set.rs
index d354645..857a558 100644
--- a/crates/apollo-compiler/tests/field_set.rs
+++ b/crates/apollo-compiler/tests/field_set.rs
@@ -1,4 +1,5 @@
 use apollo_compiler::executable::FieldSet;
+use apollo_compiler::name;
 use apollo_compiler::Schema;
 
 fn common_schema() -> Schema {
@@ -21,11 +22,11 @@ fn test_valid_field_sets() {
     let schema = common_schema();
 
     let input = "id";
-    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let field_set = FieldSet::parse(&schema, name!("Query"), input, "field_set.graphql");
     field_set.validate(&schema).unwrap();
 
     let input = "id organization { id }";
-    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let field_set = FieldSet::parse(&schema, name!("Query"), input, "field_set.graphql");
     field_set.validate(&schema).unwrap();
 }
 
@@ -34,7 +35,7 @@ fn test_invalid_field_sets() {
     let schema = common_schema();
 
     let input = "name";
-    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let field_set = FieldSet::parse(&schema, name!("Query"), input, "field_set.graphql");
     let errors = field_set
         .validate(&schema)
         .unwrap_err()
@@ -45,7 +46,7 @@ fn test_invalid_field_sets() {
     );
 
     let input = "id organization";
-    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let field_set = FieldSet::parse(&schema, name!("Query"), input, "field_set.graphql");
     let errors = field_set
         .validate(&schema)
         .unwrap_err()
@@ -60,7 +61,7 @@ fn test_invalid_field_sets() {
     );
 
     let input = "id(arg: true)";
-    let field_set = FieldSet::parse(&schema, "Query", input, "field_set.graphql");
+    let field_set = FieldSet::parse(&schema, name!("Query"), input, "field_set.graphql");
     let errors = field_set
         .validate(&schema)
         .unwrap_err()
diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs
index faa6c0d..d067ace 100644
--- a/crates/apollo-compiler/tests/main.rs
+++ b/crates/apollo-compiler/tests/main.rs
@@ -4,6 +4,7 @@ mod field_set;
 mod merge_schemas;
 /// Formerly in src/lib.rs
 mod misc;
+mod node_str;
 mod parser;
 mod schema;
 mod validation;
diff --git a/crates/apollo-compiler/tests/merge_schemas.rs b/crates/apollo-compiler/tests/merge_schemas.rs
index 4609cd9..2a6a80e 100644
--- a/crates/apollo-compiler/tests/merge_schemas.rs
+++ b/crates/apollo-compiler/tests/merge_schemas.rs
@@ -1,7 +1,7 @@
 use apollo_compiler::ast;
 use apollo_compiler::schema;
 use apollo_compiler::schema::Component;
-use apollo_compiler::schema::ComponentStr;
+use apollo_compiler::schema::ComponentName;
 use apollo_compiler::schema::ExtendedType;
 use apollo_compiler::Schema;
 use indexmap::IndexMap;
@@ -91,7 +91,7 @@ where
     Ok(())
 }
 
-fn merge_sets(merged: &mut IndexSet<ComponentStr>, new: &IndexSet<ComponentStr>) {
+fn merge_sets(merged: &mut IndexSet<ComponentName>, new: &IndexSet<ComponentName>) {
     for value in new {
         if !merged.contains(value) {
             merged.insert(value.clone());
diff --git a/crates/apollo-compiler/tests/misc.rs b/crates/apollo-compiler/tests/misc.rs
index 822aa58..230e9ad 100644
--- a/crates/apollo-compiler/tests/misc.rs
+++ b/crates/apollo-compiler/tests/misc.rs
@@ -1,4 +1,5 @@
 use apollo_compiler::executable::Selection;
+use apollo_compiler::name;
 use apollo_compiler::parse_mixed;
 use apollo_compiler::schema::ExtendedType;
 use apollo_compiler::ExecutableDocument;
@@ -181,7 +182,7 @@ union Union = Concrete
 
     assert_eq!(inline_fragments.len(), 1);
     let inline_fragment = inline_fragments.first().unwrap();
-    assert_eq!(inline_fragment.type_condition, Some("Concrete".into()));
+    assert_eq!(inline_fragment.type_condition, Some(name!("Concrete")));
 
     let inline_fragment_fields = inline_fragment.selection_set.fields();
     let inline_fragment_fields_types: HashMap<_, _> = inline_fragment_fields
@@ -202,7 +203,7 @@ union Union = Concrete
     let union_inline_fragment = union_inline_fragments.first().unwrap();
     assert_eq!(
         union_inline_fragment.type_condition,
-        Some("Concrete".into())
+        Some(name!("Concrete"))
     );
 
     let union_inline_fragment_fields = union_inline_fragment.selection_set.fields();
diff --git a/crates/apollo-compiler/tests/node_str.rs b/crates/apollo-compiler/tests/node_str.rs
new file mode 100644
index 0000000..3d49f7b
--- /dev/null
+++ b/crates/apollo-compiler/tests/node_str.rs
@@ -0,0 +1,12 @@
+use apollo_compiler::NodeStr;
+
+/// cargo +nightly miri test --test main -- node_str
+#[test]
+fn smoke_test() {
+    let heap = NodeStr::new(&123.to_string());
+    let static_ = NodeStr::from_static(&"123");
+    let heap_2 = heap.clone();
+    let static_2 = static_.clone();
+    assert_eq!(heap_2.as_str(), static_2.as_str());
+    assert_eq!(heap_2, static_2);
+}
diff --git a/crates/apollo-compiler/tests/snapshot_tests.rs b/crates/apollo-compiler/tests/snapshot_tests.rs
index d2f3d68..a310d23 100644
--- a/crates/apollo-compiler/tests/snapshot_tests.rs
+++ b/crates/apollo-compiler/tests/snapshot_tests.rs
@@ -8,6 +8,7 @@
 // Note: ALL #[test] functions must also have #[serial], to make FileId::reset
 
 use apollo_compiler::ast;
+use apollo_compiler::name;
 use apollo_compiler::schema;
 use apollo_compiler::DiagnosticList;
 use apollo_compiler::FileId;
@@ -253,19 +254,19 @@ fn project_root() -> PathBuf {
 fn test_invalid_synthetic_node() {
     let mut schema = Schema::new();
     schema.types.insert(
-        "Obj".into(),
+        name!("Obj"),
         schema::ObjectType {
             description: Default::default(),
-            name: "Obj".into(),
+            name: name!("Obj"),
             implements_interfaces: Default::default(),
             directives: Default::default(),
             fields: [(
-                "field".into(),
+                name!("field"),
                 schema::FieldDefinition {
                     description: Default::default(),
-                    name: "field".into(),
+                    name: name!("field"),
                     arguments: Default::default(),
-                    ty: schema::Type::new_named("UndefinedType"),
+                    ty: schema::Type::Named(name!("UndefinedType")),
                     directives: Default::default(),
                 }
                 .into(),
@@ -274,7 +275,7 @@ fn test_invalid_synthetic_node() {
         }
         .into(),
     );
-    schema.schema_definition.make_mut().query = Some("Obj".into());
+    schema.schema_definition.make_mut().query = Some(name!("Obj").into());
     let expected = expect_test::expect![[r#"
         Error: cannot find type `UndefinedType` in this document
     "#]];