Rework API to create targets.

Targets creation will now follow the same API regardless of whether the
target is a native GN target or a starlark target.

This requires us to pass a scope-like object to GN.

Bug: 528225104
Change-Id: I8bec09858d2d500dfa9e5e9b1ed2efad6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24180
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/src/gn/ffi/bridge.cc b/src/gn/ffi/bridge.cc
index e7eb883..792758f 100644
--- a/src/gn/ffi/bridge.cc
+++ b/src/gn/ffi/bridge.cc
@@ -605,6 +605,11 @@
   new (return$) ::SliceAny(GetScopeItems$(scope));
 }
 
+::Value const *cxxbridge1$196$GetValue(::Scope const &scope, ::rust::Str ident) noexcept {
+  ::Value const *(*GetValue$)(::Scope const &, ::rust::Str) = ::GetValue;
+  return GetValue$(scope, ident);
+}
+
 ::Settings const *cxxbridge1$196$Scope$settings_cxx(::Scope const &self) noexcept {
   ::Settings const *(::Scope::*settings_cxx$)() const = &::Scope::settings;
   return (self.*settings_cxx$)();
diff --git a/src/gn/ffi/scope.cc b/src/gn/ffi/scope.cc
index f3a658e..84abed9 100644
--- a/src/gn/ffi/scope.cc
+++ b/src/gn/ffi/scope.cc
@@ -65,3 +65,8 @@
   }
   return IntoSlice(std::move(vec));
 }
+
+const Value* GetValue(const Scope& scope, rust::Str ident) {
+  std::string_view ident_sv(ident.data(), ident.size());
+  return scope.GetValue(ident_sv);
+}
diff --git a/src/gn/ffi/scope.h b/src/gn/ffi/scope.h
index dc3e309..fd018b0 100644
--- a/src/gn/ffi/scope.h
+++ b/src/gn/ffi/scope.h
@@ -10,6 +10,7 @@
 #include "cxx.h"
 
 class Scope;
+class Value;
 struct SliceAny;
 
 // Constructs a new child Scope, populates placeholder Values for the given
@@ -26,4 +27,7 @@
 // Safety: Rust is required to convert this to an OwnedSlice<KeyValue>.
 SliceAny GetScopeItems(const Scope& scope);
 
+// Returns a pointer to the value in the scope or nullptr if not found.
+const Value* GetValue(const Scope& scope, rust::Str ident);
+
 #endif  // TOOLS_GN_FFI_SCOPE_H_
diff --git a/src/gn/starlark/Cargo.lock b/src/gn/starlark/Cargo.lock
index 8e1fb30..7095a02 100644
--- a/src/gn/starlark/Cargo.lock
+++ b/src/gn/starlark/Cargo.lock
@@ -787,6 +787,12 @@
 ]
 
 [[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
 name = "home"
 version = "0.5.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1599,6 +1605,28 @@
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.119",
+]
+
+[[package]]
 name = "subtle"
 version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1711,6 +1739,7 @@
  "cxx",
  "starlark",
  "starlark_derive",
+ "strum",
  "thiserror",
 ]
 
diff --git a/src/gn/starlark/Cargo.toml b/src/gn/starlark/Cargo.toml
index 5d03132..dfc681c 100644
--- a/src/gn/starlark/Cargo.toml
+++ b/src/gn/starlark/Cargo.toml
@@ -31,6 +31,7 @@
 starlark = "0.14"
 starlark_derive = "0.14"
 thiserror = "2.0"
+strum = { version = "0.26", features = ["derive"] }
 
 # A short-lived fork until my PR gets merged.
 # Basically all it does is make some pub(crate) things public, so that we can
diff --git a/src/gn/starlark/crates/attr/src/lib.rs b/src/gn/starlark/crates/attr/src/lib.rs
index 5eb676c..1729bd7 100644
--- a/src/gn/starlark/crates/attr/src/lib.rs
+++ b/src/gn/starlark/crates/attr/src/lib.rs
@@ -16,7 +16,7 @@
 pub use attr::{Attr, LabelOrFile};
 pub use cfg::AttrCfg;
 pub use ctx::{CtxAttr, CtxAttrSchema};
-pub use errors::Error;
+pub(crate) use errors::Error;
 pub use globals::{AttrModule, AttrSpecArgs};
 pub use schema::{AllowFilesSchema, AttrKind, AttrSchema};
 pub use traits::{EvalContext, EvalContextAttrExt, Session, TargetRef};
diff --git a/src/gn/starlark/crates/attr/src/traits.rs b/src/gn/starlark/crates/attr/src/traits.rs
index 87132af..7f67ad1 100644
--- a/src/gn/starlark/crates/attr/src/traits.rs
+++ b/src/gn/starlark/crates/attr/src/traits.rs
@@ -3,13 +3,15 @@
 // found in the LICENSE file.
 
 /// Re-export the traits from types so caller crates can access them seamlessly.
-pub use types::{EvalContext, Session, TargetRef};
+pub use types::{EvalContext, OutputType, Session, TargetRef};
 
 /// Extension trait for EvalContext to support target creation.
 pub trait EvalContextAttrExt: types::EvalContext {
-    fn create_starlark_target(
+    fn create_target(
         &self,
+        target_type: Option<OutputType>,
         target_name: &str,
+        scope: &Self::Scope,
         rule: starlark::values::FrozenValue,
         attrs: Vec<crate::Attr>,
     ) -> starlark::Result<<Self::Session as types::Session>::TargetRef>;
diff --git a/src/gn/starlark/crates/attr/src/value.rs b/src/gn/starlark/crates/attr/src/value.rs
index af6a928..acdf29c 100644
--- a/src/gn/starlark/crates/attr/src/value.rs
+++ b/src/gn/starlark/crates/attr/src/value.rs
@@ -525,7 +525,6 @@
                 target_label.clone(),
                 FakeTargetRef::new(FakeTarget {
                     outputs: vec![file1.clone(), File::new("out.h")],
-                    attrs: vec![],
                     ..Default::default()
                 }),
             );
diff --git a/src/gn/starlark/crates/ffi/src/bridge.rs b/src/gn/starlark/crates/ffi/src/bridge.rs
index 96f55d4..dd32aee 100644
--- a/src/gn/starlark/crates/ffi/src/bridge.rs
+++ b/src/gn/starlark/crates/ffi/src/bridge.rs
@@ -84,6 +84,7 @@
         ) -> SliceAny;
         // Returns an OwnedSlice<KeyValue> corresponding to references to each element.
         pub(in crate::scope) fn GetScopeItems(scope: &Scope) -> SliceAny;
+        pub(in crate::scope) fn GetValue(scope: &Scope, ident: &str) -> *const Value;
         #[rust_name = "settings_cxx"]
         pub(in crate::scope) fn settings(self: &Scope) -> *const Settings;
 
diff --git a/src/gn/starlark/crates/ffi/src/lib.rs b/src/gn/starlark/crates/ffi/src/lib.rs
index 2e698a0..988080a 100644
--- a/src/gn/starlark/crates/ffi/src/lib.rs
+++ b/src/gn/starlark/crates/ffi/src/lib.rs
@@ -30,5 +30,6 @@
 pub use bridge::{KeyValue, Label, OutputFile, Scope, Settings, SourceDir, Value, ValueType};
 pub use mutability::Immutable;
 pub use opaque::{NonOpaque, OpaqueSized};
+pub use scope::OwnedScope;
 pub use slice::{OwnedSlice, Slice};
 pub use test_with_scope::TestWithScope;
diff --git a/src/gn/starlark/crates/ffi/src/scope.rs b/src/gn/starlark/crates/ffi/src/scope.rs
index e063d7a..54a43a7 100644
--- a/src/gn/starlark/crates/ffi/src/scope.rs
+++ b/src/gn/starlark/crates/ffi/src/scope.rs
@@ -4,7 +4,7 @@
 
 use std::pin::Pin;
 
-use starlark::values::FrozenValue;
+use starlark::values::{Heap, Value as StarlarkValue};
 
 use crate::{bridge::Value, Immutable, OwnedSlice, Scope};
 
@@ -31,16 +31,74 @@
     }
 
     /// Converts Scope items to Starlark key-value pairs.
-    pub fn get_kv<'a>(
-        &'a self,
-        frozen_heap: &starlark::values::FrozenHeap,
-    ) -> Vec<(&'a str, FrozenValue)> {
+    pub fn get_kv<'v>(&self, heap: &Heap<'v>) -> Vec<(&str, StarlarkValue<'v>)> {
         let owned = self.items();
         let mut items = Vec::new();
-        // Iterate over the KeyValue contiguous slice:
         for pair in owned.as_slice() {
-            items.push((pair.key, pair.value.to_rust(frozen_heap)));
+            items.push((pair.key, pair.value.to_rust(heap)));
         }
         items
     }
 }
+
+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 {
+        let parent = self.0.as_ref().unwrap();
+        let (keys, vals): (Vec<&str>, Vec<StarlarkValue<'v>>) = kv.unzip();
+        let (mut child_scope, mut placeholders) = Scope::new(parent, &keys);
+
+        // Safety: The child scope will always be non-null.
+        let child_ref = unsafe { child_scope.as_mut().unwrap().get_unchecked_mut() };
+        for (placeholder, val) in placeholders.as_slice_mut().iter_mut().zip(vals) {
+            placeholder
+                .as_mut()
+                .assign(val, child_ref, std::ptr::null());
+        }
+
+        Self(child_scope)
+    }
+
+    fn get<'v>(&self, key: &str, heap: &Heap<'v>) -> Option<StarlarkValue<'v>> {
+        let parent = self.0.as_ref().unwrap();
+        let val_ptr = crate::bridge::GetValue(parent, key);
+        // Safety: val_ptr is either null or a valid pointer backed by the parent scope
+        // lifetime.
+        unsafe { val_ptr.as_ref() }.map(|val| val.to_rust(heap))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use types::Scope as _;
+
+    use super::*;
+    use crate::TestWithScope;
+
+    #[test]
+    fn test_owned_scope_methods() {
+        let mut setup = TestWithScope::new();
+        let parent_scope = setup.scope();
+
+        let (child_ptr, _) = Scope::new(parent_scope, &[]);
+        let owned_scope = OwnedScope(child_ptr);
+
+        starlark::environment::Module::with_temp_heap(|module| {
+            let heap = module.heap();
+
+            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());
+
+            assert_eq!(grandchild.get("foo", &heap).unwrap().unpack_i32(), Some(42));
+            assert_eq!(
+                grandchild.get("bar", &heap).unwrap().unpack_str(),
+                Some("hello")
+            );
+            assert!(grandchild.get("baz", &heap).is_none());
+        });
+    }
+}
diff --git a/src/gn/starlark/crates/ffi/src/value.rs b/src/gn/starlark/crates/ffi/src/value.rs
index 95a26c0..39bd977 100644
--- a/src/gn/starlark/crates/ffi/src/value.rs
+++ b/src/gn/starlark/crates/ffi/src/value.rs
@@ -4,7 +4,7 @@
 
 use std::pin::Pin;
 
-use starlark::values::{list::ListRef, structs::StructRef, FrozenValue};
+use starlark::values::{list::ListRef, structs::StructRef};
 
 pub use crate::bridge::ParseNode;
 use crate::{
@@ -17,25 +17,23 @@
         Immutable::from(Slice::from(crate::bridge::list_value_cxx(self)))
     }
 
-    pub fn to_rust(&self, frozen_heap: &starlark::values::FrozenHeap) -> FrozenValue {
+    pub fn to_rust<'v>(&self, heap: &starlark::values::Heap<'v>) -> starlark::values::Value<'v> {
         match self.kind() {
-            ValueType::None => FrozenValue::new_none(),
-            ValueType::Boolean => FrozenValue::new_bool(*self.boolean_value()),
-            ValueType::Integer => frozen_heap.alloc(*self.int_value()),
-            ValueType::String => frozen_heap.alloc(self.string_value()),
+            ValueType::None => starlark::values::Value::new_none(),
+            ValueType::Boolean => starlark::values::Value::new_bool(*self.boolean_value()),
+            ValueType::Integer => heap.alloc(*self.int_value()),
+            ValueType::String => heap.alloc(self.string_value()),
             ValueType::List => {
                 let slice = self.list_value();
-                let items: Vec<_> = slice.iter().map(|item| item.to_rust(frozen_heap)).collect();
-                frozen_heap.alloc(items)
+                let items: Vec<_> = slice.iter().map(|item| item.to_rust(heap)).collect();
+                heap.alloc(items)
             },
             ValueType::Scope => {
                 let scope_ptr = self.scope_value();
                 // Safety: C++ Value invariants guarantee that scope_value() is never null
                 // when the type is SCOPE.
                 let scope = unsafe { &*scope_ptr };
-                frozen_heap.alloc(starlark::values::structs::AllocStruct(
-                    scope.get_kv(frozen_heap),
-                ))
+                heap.alloc(starlark::values::structs::AllocStruct(scope.get_kv(heap)))
             },
             _ => unreachable!(),
         }
@@ -97,13 +95,13 @@
 
 #[cfg(test)]
 mod tests {
-    use starlark::values::FrozenHeap;
+    use starlark::values::{FrozenValue, Heap, ValueLike as _};
 
     use super::*;
     use crate::TestWithScope;
 
     fn back_and_forth<'v>(
-        heap: &FrozenHeap,
+        heap: &Heap<'v>,
         val: starlark::values::Value<'v>,
     ) -> starlark::values::Value<'v> {
         let mut setup = TestWithScope::new();
@@ -111,89 +109,101 @@
 
         let mut value = crate::bridge::NewValueForTesting();
         value.pin_mut().assign(val, scope, std::ptr::null());
-        value.to_rust(heap).to_value()
+        value.to_rust(heap)
     }
 
     #[test]
     fn test_none_conversion() {
-        let heap = FrozenHeap::new();
-        assert!(back_and_forth(&heap, FrozenValue::new_none().to_value()).is_none());
+        starlark::environment::Module::with_temp_heap(|module| {
+            let heap = module.heap();
+            assert!(back_and_forth(&heap, FrozenValue::new_none().to_value()).is_none());
+        });
     }
 
     #[test]
     fn test_bool_conversion() {
-        let heap = FrozenHeap::new();
-        assert_eq!(
-            back_and_forth(&heap, heap.alloc(true).to_value()).unpack_bool(),
-            Some(true)
-        );
-        assert_eq!(
-            back_and_forth(&heap, heap.alloc(false).to_value()).unpack_bool(),
-            Some(false)
-        );
+        starlark::environment::Module::with_temp_heap(|module| {
+            let heap = module.heap();
+            assert_eq!(
+                back_and_forth(&heap, heap.alloc(true).to_value()).unpack_bool(),
+                Some(true)
+            );
+            assert_eq!(
+                back_and_forth(&heap, heap.alloc(false).to_value()).unpack_bool(),
+                Some(false)
+            );
+        });
     }
 
     #[test]
     fn test_int_conversion() {
-        let heap = FrozenHeap::new();
-        assert_eq!(
-            back_and_forth(&heap, heap.alloc(123456789i32).to_value()).unpack_i32(),
-            Some(123456789)
-        );
+        starlark::environment::Module::with_temp_heap(|module| {
+            let heap = module.heap();
+            assert_eq!(
+                back_and_forth(&heap, heap.alloc(123456789i32).to_value()).unpack_i32(),
+                Some(123456789)
+            );
+        });
     }
 
     #[test]
     fn test_string_conversion() {
-        let heap = FrozenHeap::new();
-        assert_eq!(
-            back_and_forth(&heap, heap.alloc("hello world").to_value()).unpack_str(),
-            Some("hello world")
-        );
-        assert_eq!(
-            back_and_forth(
-                &heap,
-                heap.alloc("hello long string without SSO optimizations")
-                    .to_value(),
-            )
-            .unpack_str(),
-            Some("hello long string without SSO optimizations")
-        );
+        starlark::environment::Module::with_temp_heap(|module| {
+            let heap = module.heap();
+            assert_eq!(
+                back_and_forth(&heap, heap.alloc("hello world").to_value()).unpack_str(),
+                Some("hello world")
+            );
+            assert_eq!(
+                back_and_forth(
+                    &heap,
+                    heap.alloc("hello long string without SSO optimizations")
+                        .to_value(),
+                )
+                .unpack_str(),
+                Some("hello long string without SSO optimizations")
+            );
+        });
     }
 
     #[test]
     fn test_list_conversion() {
-        let heap = FrozenHeap::new();
-        let list_ref = ListRef::from_value(back_and_forth(
-            &heap,
-            heap.alloc(vec![heap.alloc(42), heap.alloc("hello")])
-                .to_value(),
-        ))
-        .unwrap();
-        assert_eq!(list_ref.len(), 2);
-        let mut iter = list_ref.iter();
-        assert_eq!(iter.next().unwrap().unpack_i32(), Some(42));
-        assert_eq!(iter.next().unwrap().unpack_str(), Some("hello"));
+        starlark::environment::Module::with_temp_heap(|module| {
+            let heap = module.heap();
+            let list_ref = ListRef::from_value(back_and_forth(
+                &heap,
+                heap.alloc(vec![heap.alloc(42), heap.alloc("hello")])
+                    .to_value(),
+            ))
+            .unwrap();
+            assert_eq!(list_ref.len(), 2);
+            let mut iter = list_ref.iter();
+            assert_eq!(iter.next().unwrap().unpack_i32(), Some(42));
+            assert_eq!(iter.next().unwrap().unpack_str(), Some("hello"));
+        });
     }
 
     #[test]
     fn test_struct_conversion() {
-        let heap = FrozenHeap::new();
-        let struct_ref = StructRef::from_value(back_and_forth(
-            &heap,
-            heap.alloc(starlark::values::structs::AllocStruct(vec![
-                ("foo", heap.alloc(100)),
-                ("bar", heap.alloc("baz")),
-            ]))
-            .to_value(),
-        ))
-        .unwrap();
-        let get_field = |name: &str| {
-            struct_ref
-                .iter()
-                .find(|(k, _)| k.as_str() == name)
-                .map(|(_, v)| v)
-        };
-        assert_eq!(get_field("foo").unwrap().unpack_i32(), Some(100));
-        assert_eq!(get_field("bar").unwrap().unpack_str(), Some("baz"));
+        starlark::environment::Module::with_temp_heap(|module| {
+            let heap = module.heap();
+            let struct_ref = StructRef::from_value(back_and_forth(
+                &heap,
+                heap.alloc(starlark::values::structs::AllocStruct(vec![
+                    ("foo", heap.alloc(100)),
+                    ("bar", heap.alloc("baz")),
+                ]))
+                .to_value(),
+            ))
+            .unwrap();
+            let get_field = |name: &str| {
+                struct_ref
+                    .iter()
+                    .find(|(k, _)| k.as_str() == name)
+                    .map(|(_, v)| v)
+            };
+            assert_eq!(get_field("foo").unwrap().unpack_i32(), Some(100));
+            assert_eq!(get_field("bar").unwrap().unpack_str(), Some("baz"));
+        });
     }
 }
diff --git a/src/gn/starlark/crates/testutils/src/eval_context.rs b/src/gn/starlark/crates/testutils/src/eval_context.rs
index b5707e1..36c721c 100644
--- a/src/gn/starlark/crates/testutils/src/eval_context.rs
+++ b/src/gn/starlark/crates/testutils/src/eval_context.rs
@@ -1,16 +1,43 @@
 // Copyright 2026 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
+use std::collections::HashMap;
 
 use attr::{Attr, EvalContext as AttrEvalContext, EvalContextAttrExt, Session as AttrSession};
 use starlark::{
-    values::{FrozenValue, ProvidesStaticType},
+    values::{FrozenValue, Heap, ProvidesStaticType, Value},
     Result,
 };
-use types::{CtxState, Label, LabelRef, Package, PackageRef, PathResolver, Session};
+use types::{
+    CtxState, Label, LabelRef, OutputType, Package, PackageRef, PathResolver, Scope, Session,
+};
 
 use crate::{FakeSession, FakeTarget, FakeTargetRef};
 
+#[derive(Clone, Default, Debug)]
+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 {
+        let mut values = self.0.clone();
+        for (k, v) in kv {
+            // Safety: Transmuting 'v to 'static is safe because this mock scope
+            // is only used in tests, where the Starlark heap outlives the evaluation
+            // context.
+            let static_val = unsafe { std::mem::transmute::<Value<'v>, Value<'static>>(v) };
+            values.insert(k.to_owned(), static_val);
+        }
+        Self(values)
+    }
+
+    fn get<'v>(&self, key: &str, _heap: &Heap<'v>) -> Option<Value<'v>> {
+        self.0.get(key).map(|v| {
+            // Safety: Shortening the lifetime is always safe.
+            unsafe { std::mem::transmute::<Value<'static>, Value<'v>>(*v) }
+        })
+    }
+}
+
 /// A simple implementation of the evaluation context used in Starlark unit
 /// tests.
 #[derive(allocative::Allocative)]
@@ -28,6 +55,9 @@
     /// The fake rule state.
     #[allocative(skip)]
     pub rule_state: CtxState<FakeTargetRef>,
+    /// The fake scope.
+    #[allocative(skip)]
+    pub scope: FakeScope,
 }
 
 unsafe impl<'v> ProvidesStaticType<'v> for FakeEvalContext {
@@ -50,12 +80,14 @@
             session,
             path_resolver: PathResolver::new_for_testing(),
             rule_state: CtxState::new(FakeTargetRef::default()),
+            scope: FakeScope::default(),
         }
     }
 }
 
 impl AttrEvalContext for FakeEvalContext {
     type Session = FakeSession;
+    type Scope = FakeScope;
 
     fn session(&self) -> &Self::Session {
         &self.session
@@ -73,8 +105,8 @@
         self.current_toolchain.as_ref()
     }
 
-    fn require_macro(&self) -> Result<()> {
-        Ok(())
+    fn require_macro(&self) -> Result<&FakeScope> {
+        Ok(&self.scope)
     }
 
     fn require_bzl(&self) -> Result<()> {
@@ -93,14 +125,19 @@
 }
 
 impl EvalContextAttrExt for FakeEvalContext {
-    fn create_starlark_target(
+    fn create_target(
         &self,
+        target_type: Option<OutputType>,
         target_name: &str,
-        _rule: FrozenValue,
+        scope: &FakeScope,
+        rule: FrozenValue,
         attrs: Vec<Attr>,
     ) -> Result<<Self::Session as AttrSession>::TargetRef> {
         let label = Label::new(self.package.clone(), target_name.to_owned());
         let target = FakeTargetRef::new(FakeTarget {
+            output_type: target_type,
+            rule,
+            cxx_attrs: scope.0.clone(),
             outputs: vec![],
             attrs,
             ..Default::default()
diff --git a/src/gn/starlark/crates/testutils/src/target.rs b/src/gn/starlark/crates/testutils/src/target.rs
index d0be24c..da1fa2c 100644
--- a/src/gn/starlark/crates/testutils/src/target.rs
+++ b/src/gn/starlark/crates/testutils/src/target.rs
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 use std::{
-    collections::HashSet,
+    collections::{HashMap, HashSet},
     hash::Hasher,
     sync::{Arc, Mutex},
 };
@@ -12,10 +12,10 @@
 use attr::Attr;
 use starlark::{
     starlark_simple_value,
-    values::{ProvidesStaticType, StarlarkValue, Value, ValueLike},
+    values::{FrozenValue, ProvidesStaticType, StarlarkValue, Value, ValueLike},
 };
 use starlark_derive::{starlark_value, NoSerialize};
-use types::{File, IPromiseToImplementStarlarkEqAndHash, Label, TargetRef};
+use types::{File, IPromiseToImplementStarlarkEqAndHash, Label, OutputType, TargetRef};
 
 /// A fake target struct for testing.
 #[derive(Debug, Allocative, Default)]
@@ -24,11 +24,33 @@
     pub outputs: Vec<File>,
     /// A list of attributes.
     pub attrs: Vec<Attr>,
+    pub output_type: Option<OutputType>,
+    pub rule: FrozenValue,
+    #[allocative(skip)]
+    pub cxx_attrs: HashMap<String, Value<'static>>,
     /// Registered target dependencies.
     #[allocative(skip)]
     pub dependencies: Mutex<HashSet<(Label, Label)>>,
 }
 
+impl PartialEq for FakeTarget {
+    fn eq(&self, other: &Self) -> bool {
+        self.outputs == other.outputs
+            && self.attrs == other.attrs
+            && self.output_type == other.output_type
+            && self.rule == other.rule
+            && self.cxx_attrs.len() == other.cxx_attrs.len()
+            && self.cxx_attrs.iter().all(|(k, v)| {
+                other.cxx_attrs.get(k).map_or(false, |ov| {
+                    v.equals(*ov).unwrap_or(false)
+                })
+            })
+            && *self.dependencies.lock().unwrap() == *other.dependencies.lock().unwrap()
+    }
+}
+impl Eq for FakeTarget {}
+
+
 /// A reference to a fake target.
 #[derive(Debug, ProvidesStaticType, NoSerialize, Allocative, Clone)]
 pub struct FakeTargetRef(#[allocative(skip)] Arc<FakeTarget>);
diff --git a/src/gn/starlark/crates/types/Cargo.toml b/src/gn/starlark/crates/types/Cargo.toml
index ee19c49..120aacb 100644
--- a/src/gn/starlark/crates/types/Cargo.toml
+++ b/src/gn/starlark/crates/types/Cargo.toml
@@ -10,8 +10,9 @@
 doctest = false
 
 [dependencies]
+allocative = { workspace = true }
+cxx = { workspace = true }
 starlark = { workspace = true }
 starlark_derive = { workspace = true }
+strum = { workspace = true }
 thiserror = { workspace = true }
-cxx = { workspace = true }
-allocative = { workspace = true }
diff --git a/src/gn/starlark/crates/types/src/eval_context.rs b/src/gn/starlark/crates/types/src/eval_context.rs
index f23ae6c..73b8f87 100644
--- a/src/gn/starlark/crates/types/src/eval_context.rs
+++ b/src/gn/starlark/crates/types/src/eval_context.rs
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use crate::{LabelRef, PackageRef, PathResolver, Session};
+use crate::{LabelRef, PackageRef, PathResolver, Scope, Session};
 
 /// The starlark Evaluator has an "extra" object that you can use to store
 /// whatever metadata you want, accessible to any custom functions you write
@@ -20,6 +20,9 @@
     /// The session type associated with this context.
     type Session: Session;
 
+    /// The scope type associated with this context.
+    type Scope: Scope;
+
     /// Returns the package currently being evaluated.
     fn current_package(&self) -> &PackageRef;
 
@@ -34,8 +37,9 @@
 
     /// Asserts that the current evaluation context is a macro context.
     /// Use when a function cannot be called from other contexts (eg.
-    /// `static_library` cannot be called inside rule evaluation)
-    fn require_macro(&self) -> starlark::Result<()>;
+    /// `static_library` cannot be called inside rule evaluation).
+    /// Returns the scope of the place the macro was called from.
+    fn require_macro(&self) -> starlark::Result<&Self::Scope>;
 
     /// Asserts that the current evaluation context is a bzl file context.
     fn require_bzl(&self) -> starlark::Result<()>;
diff --git a/src/gn/starlark/crates/types/src/lib.rs b/src/gn/starlark/crates/types/src/lib.rs
index 5a9d617..33c7fd3 100644
--- a/src/gn/starlark/crates/types/src/lib.rs
+++ b/src/gn/starlark/crates/types/src/lib.rs
@@ -11,6 +11,8 @@
 pub mod package;
 pub mod package_ref;
 pub mod path_resolver;
+pub mod output_type;
+pub mod scope;
 pub mod session;
 pub mod target_ref;
 pub mod unpacked_owned_value;
@@ -22,9 +24,11 @@
 pub use file::File;
 pub use label::Label;
 pub use label_ref::LabelRef;
+pub use output_type::OutputType;
 pub use package::Package;
 pub use package_ref::PackageRef;
 pub use path_resolver::PathResolver;
+pub use scope::Scope;
 pub use session::Session;
 pub use target_ref::{IPromiseToImplementStarlarkEqAndHash, TargetRef};
 pub use unpacked_owned_value::UnpackedOwnedValue;
diff --git a/src/gn/starlark/crates/types/src/output_type.rs b/src/gn/starlark/crates/types/src/output_type.rs
new file mode 100644
index 0000000..d3af62c
--- /dev/null
+++ b/src/gn/starlark/crates/types/src/output_type.rs
@@ -0,0 +1,28 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use allocative::Allocative;
+use strum::{Display, EnumIter, IntoStaticStr};
+
+#[repr(u8)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Display, IntoStaticStr, Allocative)]
+#[strum(serialize_all = "snake_case")]
+/// Copied from target.h's OutputType enum.
+pub enum OutputType {
+    // Unknown = 0
+    Group = 1,
+    Executable,
+    SharedLibrary,
+    LoadableModule,
+    StaticLibrary,
+    SourceSet,
+    Copy,
+    Action,
+    ActionForEach,
+    BundleData,
+    CreateBundle,
+    GeneratedFile,
+    RustLibrary,
+    RustProcMacro,
+}
diff --git a/src/gn/starlark/crates/types/src/scope.rs b/src/gn/starlark/crates/types/src/scope.rs
new file mode 100644
index 0000000..870d93a
--- /dev/null
+++ b/src/gn/starlark/crates/types/src/scope.rs
@@ -0,0 +1,19 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use starlark::values::{Heap, Value};
+
+/// This trait is an abstraction over GN's Scope object.
+///
+/// We intentionally expose, rather than methods that C++ scope objects support,
+/// the API we actually wish to use from rust, which may be an abstraction
+/// 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;
+
+    /// Retrieves a value from the key-value store.
+    /// May allocate the value it retrieves on the heap.
+    fn get<'v>(&self, key: &str, heap: &Heap<'v>) -> Option<Value<'v>>;
+}