Support builtin provider types.

Specifically, we add support here for DefaultInfo,
GnSubstititutionsInfo, and GnInputsInfo.

The latter two are only accessible through
load("//builtins:providers.bzl")

Bug: 528225104
Change-Id: I22a18e7f6626c5a882b31cba45ea34ba6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24081
Reviewed-by: Cole Faust <colefaust@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Reviewed-by: Philipp Wollermann <philwo@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/src/gn/starlark/Cargo.lock b/src/gn/starlark/Cargo.lock
index 0be2db9..b4c12dc 100644
--- a/src/gn/starlark/Cargo.lock
+++ b/src/gn/starlark/Cargo.lock
@@ -24,7 +24,7 @@
 [[package]]
 name = "allocative"
 version = "0.3.6"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "allocative_derive",
  "bumpalo",
@@ -37,7 +37,7 @@
 [[package]]
 name = "allocative_derive"
 version = "0.3.6"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -291,7 +291,7 @@
 [[package]]
 name = "cmp_any"
 version = "0.8.1"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 
 [[package]]
 name = "cobs"
@@ -535,7 +535,7 @@
 [[package]]
 name = "display_container"
 version = "0.9.0"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "either",
  "indenter",
@@ -544,7 +544,7 @@
 [[package]]
 name = "dupe"
 version = "0.9.1"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "dupe_derive",
 ]
@@ -552,7 +552,7 @@
 [[package]]
 name = "dupe_derive"
 version = "0.9.1"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1038,7 +1038,7 @@
 [[package]]
 name = "pagable"
 version = "0.4.1"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "allocative",
  "anyhow",
@@ -1074,7 +1074,7 @@
 [[package]]
 name = "pagable_derive"
 version = "0.4.1"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1138,6 +1138,8 @@
 version = "0.1.0"
 dependencies = [
  "allocative",
+ "args",
+ "depset",
  "either",
  "starlark",
  "starlark_derive",
@@ -1492,7 +1494,7 @@
 [[package]]
 name = "starlark"
 version = "0.14.2"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "allocative",
  "anyhow",
@@ -1535,7 +1537,7 @@
 [[package]]
 name = "starlark_derive"
 version = "0.14.2"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "dupe",
  "proc-macro2",
@@ -1546,7 +1548,7 @@
 [[package]]
 name = "starlark_map"
 version = "0.14.2"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "allocative",
  "dupe",
@@ -1561,7 +1563,7 @@
 [[package]]
 name = "starlark_syntax"
 version = "0.14.2"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "allocative",
  "annotate-snippets",
@@ -1599,7 +1601,7 @@
 [[package]]
 name = "strong_hash"
 version = "0.1.0"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "ref-cast",
  "strong_hash_derive",
@@ -1608,7 +1610,7 @@
 [[package]]
 name = "strong_hash_derive"
 version = "0.1.0"
-source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#61ec3746b536d7f7824dd56b7f4cbd2e2d99d77d"
+source = "git+https://github.com/matts1/starlark-rust.git?branch=gn_starlark#24e20d48f4601ccfad4707566f6e461d0973d9e0"
 dependencies = [
  "quote",
  "syn 2.0.119",
diff --git a/src/gn/starlark/crates/args/src/unpack.rs b/src/gn/starlark/crates/args/src/unpack.rs
index 80b7ed7..f583847 100644
--- a/src/gn/starlark/crates/args/src/unpack.rs
+++ b/src/gn/starlark/crates/args/src/unpack.rs
@@ -2,7 +2,7 @@
 use starlark::{
     eval::Evaluator,
     typing::Ty,
-    values::{list::UnpackList, type_repr::StarlarkTypeRepr, UnpackValue, Value, ValueTyped},
+    values::{list::ListRef, type_repr::StarlarkTypeRepr, UnpackValue, Value, ValueTyped},
 };
 
 use crate::{formatter::Formatter, FrozenArgs};
@@ -16,6 +16,7 @@
     }
 }
 
+#[derive(Debug, Default)]
 pub struct FrozenArgsSequence<'v>(pub Vec<Either<&'v str, ValueTyped<'v, FrozenArgs>>>);
 
 impl<'v> StarlarkTypeRepr for FrozenArgsSequence<'v> {
@@ -30,8 +31,15 @@
     type Error = starlark::Error;
 
     fn unpack_value_impl(value: Value<'v>) -> Result<Option<Self>, Self::Error> {
-        let list = <UnpackList<Either<&'v str, ValueTyped<'v, FrozenArgs>>>>::unpack_value(value)?;
-        Ok(list.map(|l| FrozenArgsSequence(l.items)))
+        // We unpack the list elements one by one, because when you pass [1] to
+        // UnpackList<&str>, it says "expected list, but got list".
+        // This way, it says "expected string | Args, but got int"
+        Ok(Some(FrozenArgsSequence(
+            <&ListRef>::unpack_value_err(value)?
+                .iter()
+                .map(<Either<&'v str, ValueTyped<'v, FrozenArgs>>>::unpack_value_err)
+                .collect::<Result<Vec<_>, _>>()?,
+        )))
     }
 }
 
diff --git a/src/gn/starlark/crates/loader/src/loader.rs b/src/gn/starlark/crates/loader/src/loader.rs
index dd4b19b..516f02c 100644
--- a/src/gn/starlark/crates/loader/src/loader.rs
+++ b/src/gn/starlark/crates/loader/src/loader.rs
@@ -55,6 +55,17 @@
 }
 
 impl FileLoader {
+    /// Preloads a frozen module into the loader's cache.
+    /// This should be called once for each builtin module.
+    /// By convention, builtin modules will be named "//builtins:$NAME.scl"
+    pub fn preload(&self, module: FrozenModule) {
+        let mut files = self.files.write().unwrap();
+        files.insert(
+            module.frozen_heap().name().unwrap().to_string(),
+            Arc::new(Mutex::new(FileStatus::Loaded(Ok(Box::pin(module))))),
+        );
+    }
+
     fn wait_for_load(
         &self,
         file_status: &Arc<Mutex<FileStatus>>,
diff --git a/src/gn/starlark/crates/providers/Cargo.toml b/src/gn/starlark/crates/providers/Cargo.toml
index ff8eaf4..80a0dcc 100644
--- a/src/gn/starlark/crates/providers/Cargo.toml
+++ b/src/gn/starlark/crates/providers/Cargo.toml
@@ -11,6 +11,8 @@
 
 [dependencies]
 allocative = { workspace = true }
+args = { path = "../args" }
+depset = { path = "../depset" }
 either = { workspace = true }
 starlark = { workspace = true }
 starlark_derive = { workspace = true }
diff --git a/src/gn/starlark/crates/providers/src/builtins.rs b/src/gn/starlark/crates/providers/src/builtins.rs
new file mode 100644
index 0000000..53e0958
--- /dev/null
+++ b/src/gn/starlark/crates/providers/src/builtins.rs
@@ -0,0 +1,153 @@
+// 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 depset::FrozenDepset;
+use starlark::{
+    environment::{FrozenModule, GlobalsBuilder, Module},
+    eval::ParametersSpecParam::{Defaulted, Required},
+    values::{typing::TypeInstanceId, FrozenValue, FrozenValueTyped},
+};
+
+use crate::{provider_instance::ProviderInstanceGen, provider_type::FrozenProviderType};
+
+// Compile-time assertion that the layout matches u64, in case starlark-rs
+// decides to change this.
+const _: () = {
+    assert!(std::mem::size_of::<TypeInstanceId>() == std::mem::size_of::<u64>());
+    assert!(std::mem::align_of::<TypeInstanceId>() == std::mem::align_of::<u64>());
+};
+
+// Hardcoded stable IDs for builtin providers.
+// These are verified by a unit test to guarantee correctness and prevent drift.
+pub(crate) const DEFAULT_INFO_ID: TypeInstanceId =
+    unsafe { std::mem::transmute::<u64, TypeInstanceId>(6904120031855430705) };
+pub(crate) const INPUTS_INFO_ID: TypeInstanceId =
+    unsafe { std::mem::transmute::<u64, TypeInstanceId>(10321705343315126159) };
+pub(crate) const SUBSTITUTIONS_INFO_ID: TypeInstanceId =
+    unsafe { std::mem::transmute::<u64, TypeInstanceId>(8875227055924591575) };
+
+/// Holds the global built-in provider types and default instances created at
+/// initialization time.
+///
+/// All builtin providers except DefaultInfo (which is available globally) are
+/// available through load("//builtins:providers.scl", "$NAME")
+///
+/// Builtin providers are no different to any other providers, except that
+/// rather than just being metadata passed between targets, GN does something
+/// special with them.
+/// * DefaultInfo(files=depset(...)) (https://bazel.build/rules/lib/providers/DefaultInfo)
+///   * The "outputs" of a rule. Building the alias for the target builds all
+///     files in DefaultInfo.
+///   * Unlike all other providers, available globally without calling
+/// * GnInputsInfo(files=depset(...))
+///   * When the target is a mixed C++/starlark target, this adds the specified
+///     inputs as implicit inputs to all ninja actions C++ generates for this
+///     target.
+/// * GnSubstitutionsInfo(substitutions=struct(foo =
+///   [ctx.actions.args().add("--foo", ctx.file.foo)]))
+///   * Adds "foo = --foo path/to/foo" to the ninja file
+///   * Adding command = "... {{foo}}" to your GN tool will allow you to use
+///     this in GN.
+pub struct BuiltinProviders {
+    /// Default instance of `DefaultInfo`.
+    /// Targets that do not return an explicit DefaultInfo provider will have
+    /// target[DefaultInfo] return this.
+    pub default_defaultinfo: FrozenValue,
+
+    /// A frozen Starlark module containing the built-in provider definitions.
+    /// This module is preloaded as `//builtins:providers.scl` and can be loaded
+    /// in Starlark files.
+    pub module: FrozenModule,
+}
+
+pub(crate) fn register_builtin_providers(builder: &mut GlobalsBuilder) -> BuiltinProviders {
+    let mut default_defaultinfo = None;
+
+    let module = Module::with_temp_heap(|module: Module| {
+        let heap = module.frozen_heap();
+
+        let empty_depset = heap.alloc(FrozenDepset::default());
+        let default_info = FrozenProviderType::new(
+            DEFAULT_INFO_ID,
+            "DefaultInfo",
+            &[
+                ("files", Defaulted(empty_depset)),
+                ("executable", Defaulted(FrozenValue::new_none())),
+            ],
+            heap,
+        );
+        let default_info_value = heap.alloc(default_info);
+        builder.set("DefaultInfo", default_info_value);
+
+        let inputs =
+            FrozenProviderType::new(INPUTS_INFO_ID, "GnInputsInfo", &[("files", Required)], heap);
+
+        let substitutions = FrozenProviderType::new(
+            SUBSTITUTIONS_INFO_ID,
+            "GnSubstitutionsInfo",
+            &[("substitutions", Required)],
+            heap,
+        );
+
+        let inputs_value = heap.alloc(inputs);
+        let substitutions_value = heap.alloc(substitutions);
+
+        module.set("GnInputsInfo", inputs_value.to_value());
+        module.set("GnSubstitutionsInfo", substitutions_value.to_value());
+
+        let defaultinfo_instance = heap.alloc(ProviderInstanceGen {
+            provider_type: FrozenValueTyped::<FrozenProviderType>::new(default_info_value)
+                .unwrap()
+                .as_ref(),
+            values: Box::new([Some(empty_depset), Some(FrozenValue::new_none())]),
+        });
+        default_defaultinfo = Some(defaultinfo_instance);
+
+        module
+            .freeze_named(starlark::values::FrozenHeapName::User(Box::new(
+                "//builtins:providers.scl".to_owned(),
+            )))
+            .unwrap()
+    });
+
+    let default_defaultinfo = default_defaultinfo.unwrap();
+    // DefaultInfo is allocated in the module's heap, so we ensure that the module's
+    // heap will never disappear while the builder is active.
+    builder.frozen_heap().add_reference(module.frozen_heap());
+
+    BuiltinProviders {
+        default_defaultinfo,
+        module,
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use starlark::values::typing::TypeIdDomain;
+
+    use super::*;
+
+    struct GnBuiltinDomain;
+    impl TypeIdDomain for GnBuiltinDomain {
+        fn tag(&self) -> &'static str {
+            "gn.builtin_provider"
+        }
+    }
+
+    #[test]
+    fn test_builtin_provider_ids_match_identity() {
+        assert_eq!(
+            DEFAULT_INFO_ID,
+            TypeInstanceId::from_identity(GnBuiltinDomain, &"DefaultInfo")
+        );
+        assert_eq!(
+            INPUTS_INFO_ID,
+            TypeInstanceId::from_identity(GnBuiltinDomain, &"GnInputsInfo")
+        );
+        assert_eq!(
+            SUBSTITUTIONS_INFO_ID,
+            TypeInstanceId::from_identity(GnBuiltinDomain, &"GnSubstitutionsInfo")
+        );
+    }
+}
diff --git a/src/gn/starlark/crates/providers/src/errors.rs b/src/gn/starlark/crates/providers/src/errors.rs
index 0de8529..63bb393 100644
--- a/src/gn/starlark/crates/providers/src/errors.rs
+++ b/src/gn/starlark/crates/providers/src/errors.rs
@@ -17,6 +17,14 @@
     ProviderNotExported,
     #[error("Cannot construct values of non-frozen provider type")]
     ProviderNotFrozen,
+    #[error("Duplicate provider: {0}")]
+    DuplicateProvider(String),
+    #[error("DefaultInfo.files must be a depset of files, got {0}")]
+    DefaultInfoFilesMustBeFileDepset(String),
+    #[error("GnInputsInfo.files must be a depset of files, got {0}")]
+    GnInputsInfoFilesMustBeFileDepset(String),
+    #[error("GnSubstitutionsInfo.substitutions must be a struct, got {0}")]
+    GnSubstitutionsInfoSubstitutionsMustBeStruct(String),
 }
 
 impl From<Error> for starlark::values::FreezeError {
diff --git a/src/gn/starlark/crates/providers/src/globals.rs b/src/gn/starlark/crates/providers/src/globals.rs
index d503789..5d28c26 100644
--- a/src/gn/starlark/crates/providers/src/globals.rs
+++ b/src/gn/starlark/crates/providers/src/globals.rs
@@ -1,7 +1,7 @@
 use starlark::{environment::GlobalsBuilder, eval::Evaluator, values::Value};
 use starlark_derive::starlark_module;
 
-use crate::Error;
+use crate::{BuiltinProviders, Error};
 
 /// Registers the global `provider()` function.
 #[starlark_module]
@@ -27,6 +27,7 @@
     }
 }
 
-pub fn register_providers(builder: &mut GlobalsBuilder) {
+pub fn register_providers(builder: &mut GlobalsBuilder) -> BuiltinProviders {
     register_providers_globals(builder);
+    crate::builtins::register_builtin_providers(builder)
 }
diff --git a/src/gn/starlark/crates/providers/src/lib.rs b/src/gn/starlark/crates/providers/src/lib.rs
index ab02770..77320b7 100644
--- a/src/gn/starlark/crates/providers/src/lib.rs
+++ b/src/gn/starlark/crates/providers/src/lib.rs
@@ -2,12 +2,16 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+pub mod builtins;
 pub(crate) mod errors;
 pub mod globals;
 pub mod provider_instance;
 pub mod provider_type;
+pub mod providers;
 
+pub use builtins::BuiltinProviders;
 pub(crate) use errors::Error;
 pub use globals::register_providers;
 pub use provider_instance::{FrozenProviderInstance, ProviderInstance};
 pub use provider_type::ProviderType;
+pub use providers::Providers;
diff --git a/src/gn/starlark/crates/providers/src/provider_type.rs b/src/gn/starlark/crates/providers/src/provider_type.rs
index 943fc00..10f282c 100644
--- a/src/gn/starlark/crates/providers/src/provider_type.rs
+++ b/src/gn/starlark/crates/providers/src/provider_type.rs
@@ -157,6 +157,37 @@
         })
     }
 }
+
+impl FrozenProviderType {
+    /// Creates a new builtin provider type with a custom stable TypeInstanceId.
+    /// Unlike regular providers, these providers may have either Defaulted or
+    /// Required parameters.
+    pub fn new(
+        id: TypeInstanceId,
+        name: &'static str,
+        fields: &[(
+            &'static str,
+            starlark::eval::ParametersSpecParam<FrozenValue>,
+        )],
+        heap: &starlark::values::FrozenHeap,
+    ) -> Self {
+        let field_map = fields
+            .iter()
+            .enumerate()
+            .map(|(idx, (name, _))| (name.to_string(), idx))
+            .collect();
+        let name_frozen = heap.alloc_str(name);
+        Self {
+            id,
+            data: ProviderTypeData {
+                name: name_frozen,
+                parameter_spec: ParametersSpec::new_named_only(name, fields.to_vec()),
+            },
+            fields: field_map,
+        }
+    }
+}
+
 impl Freeze for ProviderType {
     type Frozen = FrozenProviderType;
 
diff --git a/src/gn/starlark/crates/providers/src/providers.rs b/src/gn/starlark/crates/providers/src/providers.rs
new file mode 100644
index 0000000..c6c24f5
--- /dev/null
+++ b/src/gn/starlark/crates/providers/src/providers.rs
@@ -0,0 +1,259 @@
+// 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 args::FrozenArgsSequence;
+use depset::UnpackFileDepset;
+use starlark::{
+    collections::SmallMap,
+    typing::Ty,
+    values::{
+        list::FrozenListRef, structs::FrozenStructRef, type_repr::StarlarkTypeRepr,
+        typing::TypeInstanceId, FrozenHeapRef, FrozenValue, OwnedFrozenValue, UnpackValue as _,
+        Value,
+    },
+};
+use types::File;
+
+use crate::ProviderInstance;
+
+/// Helper to unpack a frozen list of providers to useful metadata.
+#[derive(Debug, Default)]
+pub struct Providers {
+    /// The output files produced by this target, parsed from the `DefaultInfo`
+    /// provider.
+    ///
+    /// `DefaultInfo` represents the default outputs of a target rule (similar
+    /// to Bazel's `DefaultInfo`). In GN, it contains the `files` depset
+    /// which lists the direct and transitive outputs of the rule.
+    pub outputs_phony: Option<File>,
+
+    /// Additional input files declared by this target, parsed from the
+    /// `GnInputsInfo` provider.
+    ///
+    /// `GnInputsInfo` is a built-in provider used by target rules to declare
+    /// additional, dynamic input files that the rule's tool dependencies or
+    /// parent targets must track.
+    pub inputs_phony: Option<File>,
+
+    /// Command-line substitution variables propagated by this target, parsed
+    /// from the `GnSubstitutionsInfo` provider.
+    ///
+    /// `GnSubstitutionsInfo` carries key-value substitutions (packaged as a
+    /// struct) that are used by toolchains and command-line execution
+    /// blocks to expand variables and flags dynamically.
+    pub substitutions: SmallMap<&'static str, FrozenArgsSequence<'static>>,
+
+    /// All provider instances mapped by their ProviderType's TypeInstanceId.
+    pub value: SmallMap<TypeInstanceId, FrozenValue>,
+
+    /// The frozen heap for the rule implementation, keeping substitutions and
+    /// value alive.
+    _heap: FrozenHeapRef,
+}
+
+impl StarlarkTypeRepr for Providers {
+    type Canonical = Value<'static>;
+
+    fn starlark_type_repr() -> Ty {
+        Ty::list(Ty::any())
+    }
+}
+
+impl TryFrom<OwnedFrozenValue> for Providers {
+    type Error = starlark::Error;
+
+    fn try_from(value: OwnedFrozenValue) -> Result<Self, Self::Error> {
+        let mut providers = Self {
+            _heap: value.owner().clone(),
+            ..Default::default()
+        };
+        let list = <&FrozenListRef>::unpack_value_err(value.value())?;
+        for &item in list.iter() {
+            let instance = <&ProviderInstance<'static>>::unpack_value_err(item.to_value())?;
+
+            if providers
+                .value
+                .insert(instance.provider_type.id, item)
+                .is_some()
+            {
+                return Err(
+                    crate::errors::Error::DuplicateProvider(instance.ty_name().to_owned()).into(),
+                );
+            }
+
+            match instance.provider_type.id {
+                crate::builtins::DEFAULT_INFO_ID => {
+                    let files = instance.values[0].unwrap();
+                    providers.outputs_phony = UnpackFileDepset::unpack_value_err(files)
+                        .map_err(|_| {
+                            crate::errors::Error::DefaultInfoFilesMustBeFileDepset(files.to_repr())
+                        })?
+                        .0;
+                },
+                crate::builtins::INPUTS_INFO_ID => {
+                    let files = instance.values[0].unwrap();
+                    providers.inputs_phony = UnpackFileDepset::unpack_value_err(files)
+                        .map_err(|_| {
+                            crate::errors::Error::GnInputsInfoFilesMustBeFileDepset(files.to_repr())
+                        })?
+                        .0;
+                },
+                crate::builtins::SUBSTITUTIONS_INFO_ID => {
+                    // GnSubstitutionsInfo(substitutions = struct)
+                    // Safety: We already checked it was frozen earlier.
+                    let substitutions_val = instance.values[0].unwrap();
+                    let substitutions_struct = FrozenStructRef::from_value(unsafe {
+                        substitutions_val.unpack_frozen().unwrap_unchecked()
+                    })
+                    .ok_or_else(|| {
+                        starlark::Error::from(
+                            crate::errors::Error::GnSubstitutionsInfoSubstitutionsMustBeStruct(
+                                substitutions_val.to_repr(),
+                            ),
+                        )
+                    })?;
+
+                    providers.substitutions = substitutions_struct
+                        .iter()
+                        .map(|(k, v)| {
+                            Ok((
+                                k.as_str(),
+                                <FrozenArgsSequence>::unpack_value_err(v.to_value())?,
+                            ))
+                        })
+                        .collect::<Result<_, starlark::Error>>()?;
+                },
+                _ => {},
+            }
+        }
+
+        Ok(providers)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use types::File;
+
+    use crate::Providers;
+
+    fn new_assert() -> testutils::Assert {
+        let mut a = testutils::Assert::default();
+
+        a.modify_globals(|builder| {
+            depset::depset_globals!(builder, testutils::eval_context::FakeEvalContext);
+            let builtin_providers = crate::globals::register_providers(builder);
+            // Note: In production code, the module would be preloaded instead of loading
+            // into globals.
+            builder.set(
+                "GnInputsInfo",
+                builtin_providers.module.get("GnInputsInfo").unwrap(),
+            );
+            builder.set(
+                "GnSubstitutionsInfo",
+                builtin_providers.module.get("GnSubstitutionsInfo").unwrap(),
+            );
+        });
+        a
+    }
+
+    #[test]
+    fn test_providers_unpacking() {
+        let mut a = new_assert();
+
+        let val = a.pass("[]");
+        let providers = Providers::try_from(val).unwrap();
+        assert_eq!(providers.outputs_phony, None);
+        assert_eq!(providers.inputs_phony, None);
+        assert!(providers.substitutions.is_empty());
+        assert!(providers.value.is_empty());
+
+        let custom_info_ty = a.pass("CustomInfo = provider(fields = ['foo']); CustomInfo");
+        a.modify_globals(move |builder| {
+            builder.set("CustomInfo", custom_info_ty.clone());
+        });
+
+        let val = a.pass(
+            r#"[
+    DefaultInfo(files = depset([make_file("a")])),
+    GnInputsInfo(files = depset([make_file("b")])),
+    GnSubstitutionsInfo(substitutions = struct(key = ["val"])),
+    CustomInfo(foo = 1),
+]"#,
+        );
+        let providers = Providers::try_from(val).unwrap();
+
+        assert_eq!(providers.outputs_phony, Some(File::intern("a")));
+        assert_eq!(providers.inputs_phony, Some(File::intern("b")));
+
+        let keys: Vec<&str> = providers.substitutions.keys().copied().collect();
+        assert_eq!(keys, vec!["key"]);
+
+        assert_eq!(providers.value.len(), 4);
+        assert!(providers
+            .value
+            .contains_key(&crate::builtins::DEFAULT_INFO_ID));
+        assert!(providers
+            .value
+            .contains_key(&crate::builtins::INPUTS_INFO_ID));
+        assert!(providers
+            .value
+            .contains_key(&crate::builtins::SUBSTITUTIONS_INFO_ID));
+    }
+
+    #[track_caller]
+    fn assert_unpack_fails(a: &mut testutils::Assert, expr: &str, expected_err: &str) {
+        let val = a.pass(expr);
+        let err = Providers::try_from(val).unwrap_err();
+        assert_eq!(err.to_string(), expected_err);
+    }
+
+    #[test]
+    fn test_providers_unpack_fails() {
+        let mut a = new_assert();
+
+        assert_unpack_fails(
+            &mut a,
+            "[DefaultInfo(files = depset()), DefaultInfo(files = depset())]",
+            "Duplicate provider: DefaultInfo",
+        );
+
+        assert_unpack_fails(
+            &mut a,
+            r#"[DefaultInfo(files = "not-a-depset")]"#,
+            r#"DefaultInfo.files must be a depset of files, got "not-a-depset""#,
+        );
+
+        assert_unpack_fails(
+            &mut a,
+            r#"[DefaultInfo(files = depset(["not-a-file"]))]"#,
+            "DefaultInfo.files must be a depset of files, got depset(...)",
+        );
+
+        assert_unpack_fails(
+            &mut a,
+            r#"[GnInputsInfo(files = "not-a-depset")]"#,
+            r#"GnInputsInfo.files must be a depset of files, got "not-a-depset""#,
+        );
+
+        assert_unpack_fails(
+            &mut a,
+            r#"[GnSubstitutionsInfo(substitutions = {"key": ["val"]})]"#,
+            r#"GnSubstitutionsInfo.substitutions must be a struct, got {"key": ["val"]}"#,
+        );
+
+        assert_unpack_fails(
+            &mut a,
+            r#"[GnSubstitutionsInfo(substitutions = struct(key = "not-a-list"))]"#,
+            r#"Expected `list`, but got `string (repr: "not-a-list")`"#,
+        );
+
+        assert_unpack_fails(
+            &mut a,
+            r#"[GnSubstitutionsInfo(substitutions = struct(key = [123]))]"#,
+            "Expected `Args | str`, but got `int (repr: 123)`",
+        );
+    }
+}