Implement GN variables that are custom starlark values.

Note: You can't yet do anything with them.

Bug: 528225104
Change-Id: I520a86f5d017ea3faaab7e912715193a6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24802
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/src/gn/desc_builder.cc b/src/gn/desc_builder.cc
index 67d28af..2fe6e38 100644
--- a/src/gn/desc_builder.cc
+++ b/src/gn/desc_builder.cc
@@ -194,6 +194,8 @@
         return ToBaseValue(val.list_value());
       case Value::NONE:
         return base::Value();
+      case Value::STARLARK_VALUE:
+        return base::Value(val.ToString(false));
     }
     NOTREACHED();
     return base::Value();
diff --git a/src/gn/ffi/bridge.cc b/src/gn/ffi/bridge.cc
index f119648..a35ec04 100644
--- a/src/gn/ffi/bridge.cc
+++ b/src/gn/ffi/bridge.cc
@@ -737,6 +737,18 @@
 }
 #endif // CXXBRIDGE1_LAYOUT
 
+namespace detail {
+template <typename T, typename = void *>
+struct operator_new {
+  void *operator()(::std::size_t sz) { return ::operator new(sz); }
+};
+
+template <typename T>
+struct operator_new<T, decltype(T::operator new(sizeof(T)))> {
+  void *operator()(::std::size_t sz) { return T::operator new(sz); }
+};
+} // namespace detail
+
 template <typename T>
 union ManuallyDrop {
   T value;
@@ -744,6 +756,14 @@
   ~ManuallyDrop() {}
 };
 
+template <typename T>
+union MaybeUninit {
+  T value;
+  void *operator new(::std::size_t sz) { return detail::operator_new<T>{}(sz); }
+  MaybeUninit() {}
+  ~MaybeUninit() {}
+};
+
 namespace {
 template <bool> struct deleter_if {
   template <typename T> void operator()(T *) {}
@@ -777,6 +797,7 @@
 using Value = ::Value;
 using ParseNode = ::ParseNode;
 struct Session;
+struct OwnedFrozenValue;
 
 #ifndef CXXBRIDGE1_STRUCT_Any
 #define CXXBRIDGE1_STRUCT_Any
@@ -825,6 +846,7 @@
   String = 3,
   List = 4,
   Scope = 5,
+  StarlarkValue = 6,
 };
 #endif // CXXBRIDGE1_ENUM_ValueType
 
@@ -845,6 +867,23 @@
 };
 #endif // CXXBRIDGE1_STRUCT_Session
 
+#ifndef CXXBRIDGE1_STRUCT_OwnedFrozenValue
+#define CXXBRIDGE1_STRUCT_OwnedFrozenValue
+struct OwnedFrozenValue final : public ::rust::Opaque {
+  ::rust::Box<::OwnedFrozenValue> clone() const noexcept;
+  ::rust::String to_string() const noexcept;
+  bool eq(::OwnedFrozenValue const &other) const noexcept;
+  ~OwnedFrozenValue() = delete;
+
+private:
+  friend ::rust::layout;
+  struct layout {
+    static ::std::size_t size() noexcept;
+    static ::std::size_t align() noexcept;
+  };
+};
+#endif // CXXBRIDGE1_STRUCT_OwnedFrozenValue
+
 extern "C" {
 void cxxbridge1$196$free_vector_buffer(::Any *ptr) noexcept {
   void (*free_vector_buffer$)(::Any *) = ::free_vector_buffer;
@@ -1020,6 +1059,16 @@
   void (*SetValueScope$)(::Value &, ::ParseNodePtr, ::std::unique_ptr<::Scope>) = ::SetValueScope;
   SetValueScope$(val, ::std::move(*origin), ::std::unique_ptr<::Scope>(scope));
 }
+
+void cxxbridge1$196$SetValueStarlark(::Value &val, ::ParseNodePtr *origin, ::OwnedFrozenValue *starlark_val) noexcept {
+  void (*SetValueStarlark$)(::Value &, ::ParseNodePtr, ::rust::Box<::OwnedFrozenValue>) = ::SetValueStarlark;
+  SetValueStarlark$(val, ::std::move(*origin), ::rust::Box<::OwnedFrozenValue>::from_raw(starlark_val));
+}
+
+void cxxbridge1$196$Value$starlark_value(::Value const &self, ::OwnedFrozenValue const **return$) noexcept {
+  ::OwnedFrozenValue const &(::Value::*starlark_value$)() const = &::Value::starlark_value;
+  new (return$) ::OwnedFrozenValue const *(&(self.*starlark_value$)());
+}
 ::std::size_t cxxbridge1$196$Session$operator$sizeof() noexcept;
 ::std::size_t cxxbridge1$196$Session$operator$alignof() noexcept;
 
@@ -1028,6 +1077,14 @@
 ::Session *cxxbridge1$196$Session$new_for_testing() noexcept;
 
 void cxxbridge1$196$Session$load_values(::Session const &self, ::rust::Str label, ::rust::Str relative_to, ::rust::Slice<::rust::Str const> keys, ::Scope &scope, ::Settings const &settings, ::ParseNodePtr *origin, ::Err &err) noexcept;
+::std::size_t cxxbridge1$196$OwnedFrozenValue$operator$sizeof() noexcept;
+::std::size_t cxxbridge1$196$OwnedFrozenValue$operator$alignof() noexcept;
+
+::OwnedFrozenValue *cxxbridge1$196$OwnedFrozenValue$clone_cxx(::OwnedFrozenValue const &self) noexcept;
+
+void cxxbridge1$196$OwnedFrozenValue$to_string_cxx(::OwnedFrozenValue const &self, ::rust::String *return$) noexcept;
+
+bool cxxbridge1$196$OwnedFrozenValue$eq_cxx(::OwnedFrozenValue const &self, ::OwnedFrozenValue const &other) noexcept;
 } // extern "C"
 
 ::std::size_t Session::layout::size() noexcept {
@@ -1051,6 +1108,28 @@
   cxxbridge1$196$Session$load_values(*this, label, relative_to, keys, scope, settings, &origin$.value, err);
 }
 
+::std::size_t OwnedFrozenValue::layout::size() noexcept {
+  return cxxbridge1$196$OwnedFrozenValue$operator$sizeof();
+}
+
+::std::size_t OwnedFrozenValue::layout::align() noexcept {
+  return cxxbridge1$196$OwnedFrozenValue$operator$alignof();
+}
+
+::rust::Box<::OwnedFrozenValue> OwnedFrozenValue::clone() const noexcept {
+  return ::rust::Box<::OwnedFrozenValue>::from_raw(cxxbridge1$196$OwnedFrozenValue$clone_cxx(*this));
+}
+
+::rust::String OwnedFrozenValue::to_string() const noexcept {
+  ::rust::MaybeUninit<::rust::String> return$;
+  cxxbridge1$196$OwnedFrozenValue$to_string_cxx(*this, &return$.value);
+  return ::std::move(return$.value);
+}
+
+bool OwnedFrozenValue::eq(::OwnedFrozenValue const &other) const noexcept {
+  return cxxbridge1$196$OwnedFrozenValue$eq_cxx(*this, other);
+}
+
 extern "C" {
 static_assert(::rust::detail::is_complete<::std::remove_extent<::Err>::type>::value, "definition of `::Err` is required");
 static_assert(sizeof(::std::unique_ptr<::Err>) == sizeof(void *), "");
@@ -1128,6 +1207,10 @@
   ::rust::deleter_if<::rust::detail::is_complete<::Value>::value>{}(ptr);
 }
 
+::OwnedFrozenValue *cxxbridge1$box$OwnedFrozenValue$alloc() noexcept;
+void cxxbridge1$box$OwnedFrozenValue$dealloc(::OwnedFrozenValue *) noexcept;
+void cxxbridge1$box$OwnedFrozenValue$drop(::rust::Box<::OwnedFrozenValue> *ptr) noexcept;
+
 ::Session *cxxbridge1$box$Session$alloc() noexcept;
 void cxxbridge1$box$Session$dealloc(::Session *) noexcept;
 void cxxbridge1$box$Session$drop(::rust::Box<::Session> *ptr) noexcept;
@@ -1136,6 +1219,18 @@
 namespace rust {
 inline namespace cxxbridge1 {
 template <>
+::OwnedFrozenValue *Box<::OwnedFrozenValue>::allocation::alloc() noexcept {
+  return cxxbridge1$box$OwnedFrozenValue$alloc();
+}
+template <>
+void Box<::OwnedFrozenValue>::allocation::dealloc(::OwnedFrozenValue *ptr) noexcept {
+  cxxbridge1$box$OwnedFrozenValue$dealloc(ptr);
+}
+template <>
+void Box<::OwnedFrozenValue>::drop() noexcept {
+  cxxbridge1$box$OwnedFrozenValue$drop(this);
+}
+template <>
 ::Session *Box<::Session>::allocation::alloc() noexcept {
   return cxxbridge1$box$Session$alloc();
 }
diff --git a/src/gn/ffi/bridge.h b/src/gn/ffi/bridge.h
index 6435cb1..d49ba36 100644
--- a/src/gn/ffi/bridge.h
+++ b/src/gn/ffi/bridge.h
@@ -754,6 +754,7 @@
 using Value = ::Value;
 using ParseNode = ::ParseNode;
 struct Session;
+struct OwnedFrozenValue;
 
 #ifndef CXXBRIDGE1_STRUCT_Any
 #define CXXBRIDGE1_STRUCT_Any
@@ -802,6 +803,7 @@
   String = 3,
   List = 4,
   Scope = 5,
+  StarlarkValue = 6,
 };
 #endif // CXXBRIDGE1_ENUM_ValueType
 
@@ -821,3 +823,20 @@
   };
 };
 #endif // CXXBRIDGE1_STRUCT_Session
+
+#ifndef CXXBRIDGE1_STRUCT_OwnedFrozenValue
+#define CXXBRIDGE1_STRUCT_OwnedFrozenValue
+struct OwnedFrozenValue final : public ::rust::Opaque {
+  ::rust::Box<::OwnedFrozenValue> clone() const noexcept;
+  ::rust::String to_string() const noexcept;
+  bool eq(::OwnedFrozenValue const &other) const noexcept;
+  ~OwnedFrozenValue() = delete;
+
+private:
+  friend ::rust::layout;
+  struct layout {
+    static ::std::size_t size() noexcept;
+    static ::std::size_t align() noexcept;
+  };
+};
+#endif // CXXBRIDGE1_STRUCT_OwnedFrozenValue
diff --git a/src/gn/ffi/value.cc b/src/gn/ffi/value.cc
index 4904326..2f3813c 100644
--- a/src/gn/ffi/value.cc
+++ b/src/gn/ffi/value.cc
@@ -48,6 +48,12 @@
   new (&self) Value(origin.ptr, std::move(scope));
 }
 
+void SetValueStarlark(Value& self,
+                      ParseNodePtr origin,
+                      rust::Box<OwnedFrozenValue> starlark_val) {
+  new (&self) Value(origin.ptr, std::move(starlark_val));
+}
+
 SliceAny GetValueList(const Value& self) {
   return AsSlice(self.list_value());
 }
diff --git a/src/gn/ffi/value.h b/src/gn/ffi/value.h
index 16e2853..213eea9 100644
--- a/src/gn/ffi/value.h
+++ b/src/gn/ffi/value.h
@@ -13,6 +13,7 @@
 class Scope;
 struct ParseNodePtr;
 struct SliceAny;
+struct OwnedFrozenValue;
 
 enum class ValueType : uint8_t;
 
@@ -38,6 +39,9 @@
 void SetValueScope(Value& self,
                    ParseNodePtr origin,
                    std::unique_ptr<Scope> scope);
+void SetValueStarlark(Value& self,
+                      ParseNodePtr origin,
+                      rust::Box<OwnedFrozenValue> starlark_val);
 // Returns a "std::vector<Value>".
 //
 // Safety: Rust is required to convert this to a Slice<Value>.
diff --git a/src/gn/functions_unittest.cc b/src/gn/functions_unittest.cc
index 1b7da65..31418ee 100644
--- a/src/gn/functions_unittest.cc
+++ b/src/gn/functions_unittest.cc
@@ -747,22 +747,40 @@
     setup.scope()->set_source_dir(SourceDir("//"));
 
     std::string scl_content = R"scl(
-a = "hello"
+def my_rule_impl(ctx):
+  pass
+
+my_rule = rule(
+  implementation = my_rule_impl
+)
+hello = "hello"
 )scl";
     base::FilePath scl_path = temp_dir.GetPath().AppendASCII("rules.scl");
     ASSERT_EQ(
         static_cast<int>(scl_content.size()),
         base::WriteFile(scl_path, scl_content.c_str(), scl_content.size()));
 
-    TestParseInput input(R"gn(load("//:rules.scl", "a"))gn");
+    TestParseInput input(R"gn(
+load("//:rules.scl", "hello", "my_rule", "my_rule_impl")
+
+assert(my_rule == my_rule)
+assert(my_rule != my_rule_impl)
+assert("${my_rule}" == "<rule: my_rule>")
+copy_of_rule = my_rule
+assert(copy_of_rule == my_rule)
+)gn");
     ASSERT_SUCCESS(input);
     Err err;
     input.parsed()->Execute(setup.scope(), &err);
     ASSERT_FALSE(err.has_error()) << err.message();
 
-    const Value* val_a = setup.scope()->GetValue("a");
-    ASSERT_TRUE(val_a);
-    EXPECT_EQ(Value::STRING, val_a->type());
-    EXPECT_EQ("hello", val_a->string_value());
+    const Value* val_hello = setup.scope()->GetValue("hello");
+    ASSERT_TRUE(val_hello);
+    EXPECT_EQ(Value::STRING, val_hello->type());
+    EXPECT_EQ("hello", val_hello->string_value());
+
+    const Value* val_my_rule = setup.scope()->GetValue("my_rule");
+    ASSERT_TRUE(val_my_rule);
+    EXPECT_EQ(Value::STARLARK_VALUE, val_my_rule->type());
   }
 }
diff --git a/src/gn/operators.cc b/src/gn/operators.cc
index 93cbe72..2adb7b2 100644
--- a/src/gn/operators.cc
+++ b/src/gn/operators.cc
@@ -266,7 +266,8 @@
     case Value::BOOLEAN:
     case Value::INTEGER:  // Filter out the individual int/string.
     case Value::STRING:
-    case Value::SCOPE: {
+    case Value::SCOPE:
+    case Value::STARLARK_VALUE: {
       bool found_match = false;
       for (size_t i = 0; i < v.size(); /* nothing */) {
         if (v[i] == to_remove) {
diff --git a/src/gn/starlark/crates/ffi/src/bridge.rs b/src/gn/starlark/crates/ffi/src/bridge.rs
index 642dc76..3fb3d5b 100644
--- a/src/gn/starlark/crates/ffi/src/bridge.rs
+++ b/src/gn/starlark/crates/ffi/src/bridge.rs
@@ -14,6 +14,22 @@
 ///   rust.
 use crate::session::Session;
 
+pub struct OwnedFrozenValue(pub starlark::values::OwnedFrozenValue);
+
+impl OwnedFrozenValue {
+    pub fn clone_cxx(&self) -> Box<Self> {
+        Box::new(Self(self.0.clone()))
+    }
+
+    pub fn to_string_cxx(&self) -> String {
+        self.0.value().to_string()
+    }
+
+    pub fn eq_cxx(&self, other: &Self) -> bool {
+        self.0.value() == other.0.value()
+    }
+}
+
 #[cxx::bridge]
 // Allow let_underscore_drop because the cxx::bridge generated code has non-binding
 // lets on C++ types with destructors.
@@ -52,6 +68,7 @@
         String = 3,
         List = 4,
         Scope = 5,
+        StarlarkValue = 6,
     }
     unsafe extern "C++" {
         // include! simply tells cxxbridge to put the #include in the generated C++
@@ -186,6 +203,12 @@
             origin: ParseNodePtr,
             scope: UniquePtr<Scope>,
         );
+        pub(in crate::value) fn SetValueStarlark(
+            val: Pin<&mut Value>,
+            origin: ParseNodePtr,
+            starlark_val: Box<OwnedFrozenValue>,
+        );
+        pub(in crate::value) fn starlark_value(self: &Value) -> &OwnedFrozenValue;
     }
 
     extern "Rust" {
@@ -208,6 +231,14 @@
             origin: ParseNodePtr,
             err: Pin<&mut Err>,
         );
+
+        type OwnedFrozenValue;
+        #[rust_name = "clone_cxx"]
+        fn clone(self: &OwnedFrozenValue) -> Box<OwnedFrozenValue>;
+        #[rust_name = "to_string_cxx"]
+        fn to_string(self: &OwnedFrozenValue) -> String;
+        #[rust_name = "eq_cxx"]
+        fn eq(self: &OwnedFrozenValue, other: &OwnedFrozenValue) -> bool;
     }
 }
 
diff --git a/src/gn/starlark/crates/ffi/src/errors.rs b/src/gn/starlark/crates/ffi/src/errors.rs
index 649a918..d3b1255 100644
--- a/src/gn/starlark/crates/ffi/src/errors.rs
+++ b/src/gn/starlark/crates/ffi/src/errors.rs
@@ -9,6 +9,8 @@
 pub(crate) enum Error {
     #[error("Key '{0}' not found in module '{1}'")]
     KeyNotFound(String, Label),
+    #[error("Passing a custom starlark type to builtin GN target types is not supported: {0}")]
+    PassingNonFrozenStarlarkValueToGn(String),
     #[error("This is not allowed while executing a function loaded from a starlark file.")]
     RequiresBzlFile,
 }
diff --git a/src/gn/starlark/crates/ffi/src/lib.rs b/src/gn/starlark/crates/ffi/src/lib.rs
index 2edea21..e2a9823 100644
--- a/src/gn/starlark/crates/ffi/src/lib.rs
+++ b/src/gn/starlark/crates/ffi/src/lib.rs
@@ -32,7 +32,10 @@
 mod test_with_scope;
 mod value;
 
-pub use bridge::{Err, KeyValue, Label, OutputFile, Scope, Settings, SourceDir, Value, ValueType};
+pub use bridge::{
+    Err, KeyValue, Label, OutputFile, OwnedFrozenValue, Scope, Settings, SourceDir, Value,
+    ValueType,
+};
 pub use mutability::Immutable;
 pub use opaque::{NonOpaque, OpaqueSized};
 pub use scope::OwnedScope;
diff --git a/src/gn/starlark/crates/ffi/src/scope.rs b/src/gn/starlark/crates/ffi/src/scope.rs
index eb9dab0..d74f3a3 100644
--- a/src/gn/starlark/crates/ffi/src/scope.rs
+++ b/src/gn/starlark/crates/ffi/src/scope.rs
@@ -54,7 +54,10 @@
 pub struct OwnedScope(pub cxx::UniquePtr<Scope>);
 
 impl types::Scope for OwnedScope {
-    fn copy_with<'a, 'v>(&self, kv: impl Iterator<Item = (&'a str, StarlarkValue<'v>)>) -> Self {
+    fn copy_with<'a, 'v>(
+        &self,
+        kv: impl Iterator<Item = (&'a str, StarlarkValue<'v>)>,
+    ) -> starlark::Result<Self> {
         let parent = self.0.as_ref().unwrap();
         // Scope stores a map from string_view to value. Since we don't know the
         // lifetime of the string we were given, we must intern it in order to
@@ -67,10 +70,10 @@
         for (placeholder, val) in placeholders.as_slice_mut().iter_mut().zip(vals) {
             placeholder
                 .as_mut()
-                .assign(val, child_pin.settings(), Default::default());
+                .assign(val, None, child_pin.settings(), Default::default())?;
         }
 
-        Self(child_scope)
+        Ok(Self(child_scope))
     }
 
     fn get<'v>(&self, key: &str, heap: &Heap<'v>) -> Option<StarlarkValue<'v>> {
@@ -103,8 +106,9 @@
             let val_int = heap.alloc(42);
             let val_str = heap.alloc("hello");
 
-            let grandchild =
-                owned_scope.copy_with(vec![("foo", val_int), ("bar", val_str)].into_iter());
+            let grandchild = owned_scope
+                .copy_with(vec![("foo", val_int), ("bar", val_str)].into_iter())
+                .unwrap();
 
             assert_eq!(grandchild.get("foo", &heap).unwrap().unpack_i32(), Some(42));
             assert_eq!(
diff --git a/src/gn/starlark/crates/ffi/src/session.rs b/src/gn/starlark/crates/ffi/src/session.rs
index a69d2b1..ae97f24 100644
--- a/src/gn/starlark/crates/ffi/src/session.rs
+++ b/src/gn/starlark/crates/ffi/src/session.rs
@@ -98,7 +98,9 @@
                     .get(key)
                     .map_err(|_| Error::KeyNotFound(key.to_string(), label.clone()))?;
                 let mut cxx_value = crate::bridge::SetValue(scope.as_mut(), key, origin);
-                cxx_value.as_mut().assign(value.value(), settings, origin);
+                cxx_value
+                    .as_mut()
+                    .assign(value.value(), Some(value.owner()), settings, origin)?;
             }
             Ok(())
         })());
diff --git a/src/gn/starlark/crates/ffi/src/value.rs b/src/gn/starlark/crates/ffi/src/value.rs
index 6ef19f0..7c308de 100644
--- a/src/gn/starlark/crates/ffi/src/value.rs
+++ b/src/gn/starlark/crates/ffi/src/value.rs
@@ -4,10 +4,12 @@
 
 use std::pin::Pin;
 
-use starlark::values::{list::ListRef, structs::StructRef};
+use starlark::values::{list::ListRef, structs::StructRef, OwnedFrozenValue};
 
 use crate::{
+    bridge,
     bridge::{SliceAny, Value, ValueType},
+    errors::Error,
     Immutable, Scope, Settings, Slice,
 };
 
@@ -34,6 +36,13 @@
                 let scope = unsafe { &*scope_ptr };
                 heap.alloc(starlark::values::structs::AllocStruct(scope.get_kv(heap)))
             },
+            ValueType::StarlarkValue => {
+                let rust_val = self.starlark_value();
+                heap.add_reference(rust_val.0.owner());
+                // Safety: This is safe when combined with the above line, which ensures it will
+                // not get GC'd.
+                starlark::values::Value::new_frozen(unsafe { rust_val.0.unchecked_frozen_value() })
+            },
             _ => unreachable!(),
         }
     }
@@ -41,9 +50,10 @@
     pub fn assign<'v>(
         mut self: Pin<&mut Self>,
         val: starlark::values::Value<'v>,
+        owner: Option<&starlark::values::FrozenHeapRef>,
         settings: &Settings,
         origin: crate::bridge::ParseNodePtr,
-    ) {
+    ) -> starlark::Result<()> {
         if val.is_none() {
             crate::bridge::SetValueNone(self.as_mut(), origin);
         } else if let Some(s) = val.unpack_str() {
@@ -59,20 +69,27 @@
             }
             .into();
             for (el_pin, src) in slice.iter_mut().zip(l.iter()) {
-                el_pin.assign(src, settings, origin);
+                el_pin.assign(src, owner, settings, origin)?;
             }
         } else if let Some(s) = StructRef::from_value(val) {
             let keys: Vec<&str> = s.iter().map(|(k, _)| k.as_str()).collect();
             let (r#struct, mut values) = Scope::new_struct(settings, &keys);
 
             for (v_starlark, v_cxx) in s.iter().map(|(_, v)| v).zip(values.as_slice_mut()) {
-                v_cxx.as_mut().assign(v_starlark, settings, origin);
+                v_cxx.as_mut().assign(v_starlark, owner, settings, origin)?;
             }
 
             crate::bridge::SetValueScope(self.as_mut(), origin, r#struct);
         } else {
-            todo!("Arbitrary starlark values not (yet) supported");
+            let owned_frozen = if let (Some(owner), Some(frozen)) = (owner, val.unpack_frozen()) {
+                // Safety: The caller guarantees that owner owns val.
+                bridge::OwnedFrozenValue(unsafe { OwnedFrozenValue::new(owner.clone(), frozen) })
+            } else {
+                return Err(Error::PassingNonFrozenStarlarkValueToGn(val.to_string()).into());
+            };
+            crate::bridge::SetValueStarlark(self.as_mut(), origin, Box::new(owned_frozen));
         }
+        Ok(())
     }
 }
 
@@ -91,13 +108,17 @@
         let scope = setup.scope();
 
         let mut value = crate::bridge::NewValueForTesting();
-        value.pin_mut().assign(
-            val,
-            scope.settings(),
-            crate::bridge::ParseNodePtr {
-                ptr: std::ptr::null(),
-            },
-        );
+        value
+            .pin_mut()
+            .assign(
+                val,
+                None,
+                scope.settings(),
+                crate::bridge::ParseNodePtr {
+                    ptr: std::ptr::null(),
+                },
+            )
+            .unwrap();
         value.to_rust(heap)
     }
 
diff --git a/src/gn/starlark/crates/rule/src/frozen_rule.rs b/src/gn/starlark/crates/rule/src/frozen_rule.rs
index 68222df..c7aff2e 100644
--- a/src/gn/starlark/crates/rule/src/frozen_rule.rs
+++ b/src/gn/starlark/crates/rule/src/frozen_rule.rs
@@ -117,7 +117,7 @@
                 // Collect all the arguments we don't recognise and pass them to the native
                 // implementation.
                 let kwargs: SmallMap<String, Value<'v>> = param_parser.next()?;
-                let child_scope = scope.copy_with(kwargs.iter().map(|(k, v)| (k.as_str(), *v)));
+                let child_scope = scope.copy_with(kwargs.iter().map(|(k, v)| (k.as_str(), *v)))?;
                 context.create_target(Some(builtin), target_name, &child_scope, me, attrs)?
             } else {
                 context.create_target(None, target_name, scope, me, attrs)?
diff --git a/src/gn/starlark/crates/testutils/src/eval_context.rs b/src/gn/starlark/crates/testutils/src/eval_context.rs
index 3efc0d9..eac8062 100644
--- a/src/gn/starlark/crates/testutils/src/eval_context.rs
+++ b/src/gn/starlark/crates/testutils/src/eval_context.rs
@@ -19,7 +19,7 @@
 pub struct FakeScope(HashMap<String, Value<'static>>);
 
 impl Scope for FakeScope {
-    fn copy_with<'a, 'v>(&self, kv: impl Iterator<Item = (&'a str, Value<'v>)>) -> Self {
+    fn copy_with<'a, 'v>(&self, kv: impl Iterator<Item = (&'a str, Value<'v>)>) -> Result<Self> {
         let mut values = self.0.clone();
         for (k, v) in kv {
             // Safety: Transmuting 'v to 'static is safe because this mock scope
@@ -28,7 +28,7 @@
             let static_val = unsafe { std::mem::transmute::<Value<'v>, Value<'static>>(v) };
             values.insert(k.to_owned(), static_val);
         }
-        Self(values)
+        Ok(Self(values))
     }
 
     fn get<'v>(&self, key: &str, _heap: &Heap<'v>) -> Option<Value<'v>> {
diff --git a/src/gn/starlark/crates/types/src/scope.rs b/src/gn/starlark/crates/types/src/scope.rs
index 870d93a..e3e9490 100644
--- a/src/gn/starlark/crates/types/src/scope.rs
+++ b/src/gn/starlark/crates/types/src/scope.rs
@@ -11,7 +11,12 @@
 /// over that.
 pub trait Scope {
     /// Creates a copy of the scope with some additional values set.
-    fn copy_with<'a, 'v>(&self, kv: impl Iterator<Item = (&'a str, Value<'v>)>) -> Self;
+    fn copy_with<'a, 'v>(
+        &self,
+        kv: impl Iterator<Item = (&'a str, Value<'v>)>,
+    ) -> starlark::Result<Self>
+    where
+        Self: Sized;
 
     /// Retrieves a value from the key-value store.
     /// May allocate the value it retrieves on the heap.
diff --git a/src/gn/value.cc b/src/gn/value.cc
index e2aa11a..85214d7 100644
--- a/src/gn/value.cc
+++ b/src/gn/value.cc
@@ -9,6 +9,7 @@
 
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
+#include "gn/ffi/bridge.h"
 #include "gn/scope.h"
 
 ValueList::ValueList() = default;
@@ -37,6 +38,10 @@
     case SCOPE:
       new (&scope_value_) std::unique_ptr<Scope>();
       break;
+    case STARLARK_VALUE:
+      // There's no "default" value for a starlark value.
+      NOTREACHED();
+      break;
   }
 }
 
@@ -55,6 +60,11 @@
 Value::Value(const ParseNode* origin, std::unique_ptr<Scope> scope)
     : type_(SCOPE), origin_(origin), scope_value_(std::move(scope)) {}
 
+Value::Value(const ParseNode* origin, rust::Box<OwnedFrozenValue> starlark_val)
+    : type_(STARLARK_VALUE),
+      origin_(origin),
+      starlark_value_(std::move(starlark_val)) {}
+
 Value::Value(const Value& other) : type_(other.type_), origin_(other.origin_) {
   switch (type_) {
     case NONE:
@@ -76,6 +86,10 @@
           other.scope_value_.get() ? other.scope_value_->MakeClosure()
                                    : nullptr);
       break;
+    case STARLARK_VALUE:
+      new (&starlark_value_)
+          rust::Box<OwnedFrozenValue>(other.starlark_value().clone());
+      break;
   }
 }
 
@@ -99,6 +113,10 @@
     case SCOPE:
       new (&scope_value_) std::unique_ptr<Scope>(std::move(other.scope_value_));
       break;
+    case STARLARK_VALUE:
+      new (&starlark_value_)
+          rust::Box<OwnedFrozenValue>(std::move(other.starlark_value_));
+      break;
   }
 }
 
@@ -130,6 +148,9 @@
     case SCOPE:
       scope_value_.~unique_ptr();
       break;
+    case STARLARK_VALUE:
+      starlark_value_.~Box();
+      break;
     default:;
   }
 }
@@ -149,6 +170,8 @@
       return "list";
     case SCOPE:
       return "scope";
+    case STARLARK_VALUE:
+      return "starlark value";
     default:
       NOTREACHED();
       return "UNKNOWN";
@@ -245,6 +268,8 @@
 
       return result;
     }
+    case STARLARK_VALUE:
+      return std::string(starlark_value().to_string());
   }
   return std::string();
 }
@@ -276,6 +301,8 @@
       return list_value() == other.list_value();
     case Value::SCOPE:
       return scope_value()->CheckCurrentScopeValuesEqual(other.scope_value());
+    case Value::STARLARK_VALUE:
+      return starlark_value().eq(other.starlark_value());
     case Value::NONE:
       return false;
     default:
diff --git a/src/gn/value.h b/src/gn/value.h
index ec4cb5a..2362c7a 100644
--- a/src/gn/value.h
+++ b/src/gn/value.h
@@ -14,8 +14,10 @@
 #include "base/logging.h"
 #include "base/memory/ref_counted.h"
 #include "base/memory/scoped_refptr.h"
+#include "cxx.h"
 #include "gn/err.h"
 
+struct OwnedFrozenValue;
 class ParseNode;
 class Scope;
 class Value;
@@ -43,6 +45,7 @@
     STRING,
     LIST,
     SCOPE,
+    STARLARK_VALUE,
   };
 
   Value();
@@ -57,6 +60,7 @@
   // you can pass a null scope here if you promise to set it before any other
   // code gets it (code will generally assume the scope is not null).
   Value(const ParseNode* origin, std::unique_ptr<Scope> scope);
+  Value(const ParseNode* origin, rust::Box<OwnedFrozenValue> starlark_val);
 
   Value(const Value& other);
   Value(Value&& other) noexcept;
@@ -114,6 +118,11 @@
   }
   void SetScopeValue(std::unique_ptr<Scope> scope);
 
+  const OwnedFrozenValue& starlark_value() const {
+    DCHECK(type_ == STARLARK_VALUE);
+    return *starlark_value_;
+  }
+
   // Converts the given value to a string. Returns true if strings should be
   // quoted or the ToString of a string should be the string itself. If the
   // string is quoted, it will also enable escaping.
@@ -145,6 +154,7 @@
     // copied, only performing a real copy when a modification is attempted.
     scoped_refptr<ValueList> list_ptr_;
     std::unique_ptr<Scope> scope_value_;
+    rust::Box<OwnedFrozenValue> starlark_value_;
   };
 };