blob: db620f3f02e636fdb79da6e0139c1c0f91b56376 [file]
// 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::{
fmt,
fmt::{Display, Formatter},
};
use allocative::Allocative;
use starlark::{
environment::{Methods, MethodsBuilder, MethodsStatic},
starlark_complex_value,
typing::Ty,
values::{
type_repr::StarlarkTypeRepr, Freeze, FreezeResult, Freezer, Heap, ProvidesStaticType,
StarlarkValue, Trace, UnpackValue, Value, ValueLike,
},
};
use starlark_derive::{starlark_module, starlark_value, Coerce, NoSerialize};
use types::File;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Allocative)]
/// All orderings are guaranteed to be deterministic.
pub enum Order {
/// Our unspecified order is postorder. However, this should not be relied
/// upon.
/// This is named "default" in bazel, but we call it Unspecified to clarify
/// exactly how it should work from the depset implementation side of
/// things.
Unspecified,
/// Guaranteed to traverse through direct dependencies in left-to-right
/// order, then transitive in left-to-right order.
Preorder,
/// Guaranteed to traverse through transitive dependencies in left-to-right
/// order, then direct in left-to-right order.
Postorder,
/// Our topological order is reverse postorder. However, this should not be
/// relied upon. Note: Topological order is much slower and less memory
/// efficient as it requires an intermediate Vec to be created and then
/// reversed.
Topological,
}
impl StarlarkTypeRepr for Order {
type Canonical = String;
fn starlark_type_repr() -> Ty {
String::starlark_type_repr()
}
}
impl<'v> UnpackValue<'v> for Order {
type Error = starlark::Error;
fn unpack_value_impl(value: Value<'v>) -> Result<Option<Self>, Self::Error> {
match <&'v str>::unpack_value_err(value)? {
"default" => Ok(Some(Self::Unspecified)),
"preorder" => Ok(Some(Self::Preorder)),
"postorder" => Ok(Some(Self::Postorder)),
"topological" => Ok(Some(Self::Topological)),
s => Err(crate::Error::InvalidOrder(s.to_owned()).into()),
}
}
}
impl Display for Order {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
// For consistency with bazel, the user sees unspecified as "default".
Self::Unspecified => write!(f, "default"),
Self::Preorder => write!(f, "preorder"),
Self::Postorder => write!(f, "postorder"),
Self::Topological => write!(f, "topological"),
}
}
}
/// The type of elements contained in a depset.
#[derive(Debug, Clone, Copy, PartialEq, Eq, allocative::Allocative)]
pub enum Kind {
Empty,
Unknown,
File,
}
impl Display for Kind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "empty"),
Self::Unknown => write!(f, "unknown"),
Self::File => write!(f, "File"),
}
}
}
/// A generic implementation of a Starlark Depset.
#[derive(Debug, Trace, Coerce, ProvidesStaticType, NoSerialize, Allocative)]
// By implementing coerce, freezing depsets is zero-cost.
// Coerce requires repr(C).
// Starlark knows that it can just do a reinterpret cast of the memory.
#[repr(C)]
pub struct DepsetGen<V> {
/// The traversal order of this depset.
pub(crate) order: Order,
/// The direct elements of this depset. De-duped on creation.
pub(crate) direct: Vec<V>,
/// Transitive depsets. Each entry is guaranteed to be a non-empty depset.
pub(crate) transitive: Vec<V>,
/// The element type kind of this depset.
pub(crate) kind: Kind,
/// The single phony file for this depset. Set for `depset[File]` only.
/// If the depset only has a single element, it will just be that element.
pub(crate) phony: Option<File>,
}
impl<'v> Depset<'v> {
/// Creates a new `Depset` containing `File` elements.
pub fn new_file_depset<C: types::EvalContext>(
direct: Vec<File>,
heap: &Heap<'v>,
ctx: &C,
) -> starlark::Result<Self> {
let phony = if direct.len() == 1 {
Some(direct[0].clone())
} else if !direct.is_empty() {
Some(ctx.require_rule_impl()?.new_phony(direct.clone()))
} else {
None
};
Ok(Self {
order: Order::Unspecified,
transitive: Vec::new(),
kind: if direct.is_empty() {
Kind::Empty
} else {
Kind::File
},
direct: direct.into_iter().map(|f| heap.alloc(f)).collect(),
phony,
})
}
}
impl<V> Default for DepsetGen<V> {
fn default() -> Self {
Self {
order: Order::Unspecified,
direct: Vec::new(),
transitive: Vec::new(),
kind: Kind::Empty,
phony: None,
}
}
}
impl<V> DepsetGen<V> {
pub(crate) fn order(&self) -> Order {
self.order
}
pub(crate) fn direct(&self) -> &[V] {
&self.direct
}
pub(crate) fn transitive(&self) -> &[V] {
&self.transitive
}
/// Returns the element kind of this depset.
pub fn kind(&self) -> &Kind {
&self.kind
}
/// Returns the single phony File if this is a file depset containing
/// exactly one element.
pub fn phony(&self) -> &Option<File> {
&self.phony
}
/// Returns true if this depset has no elements (its kind is Empty).
pub fn is_empty(&self) -> bool {
self.kind == Kind::Empty
}
}
starlark_complex_value!(pub Depset);
impl<'v, V: ValueLike<'v>> Display for DepsetGen<V>
where
Self: ProvidesStaticType<'v>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// Explicitly DO NOT flatten a depset implicitly, they can get massive.
// We may consider printing some fields in the future, but for now
// we'll leave this empty to be safe.
if self.is_empty() {
write!(f, "depset([])")
} else {
write!(f, "depset(...)")
}
}
}
#[starlark_value(type = "depset")]
impl<'v, V: ValueLike<'v>> StarlarkValue<'v> for DepsetGen<V>
where
Self: ProvidesStaticType<'v>,
{
fn get_methods() -> Option<&'static Methods> {
static RES: MethodsStatic = MethodsStatic::new("depset", depset_methods);
Some(RES.methods())
}
fn to_bool(&self) -> bool {
!self.is_empty()
}
}
impl Freeze for Depset<'_> {
type Frozen = FrozenDepset;
fn freeze(self, freezer: &Freezer) -> FreezeResult<Self::Frozen> {
Ok(DepsetGen {
order: self.order,
direct: self.direct.freeze(freezer)?,
transitive: self.transitive.freeze(freezer)?,
kind: self.kind,
phony: self.phony,
})
}
}
/// Declares the Starlark methods for the `depset` type.
#[starlark_module]
pub fn depset_methods(builder: &mut MethodsBuilder) {
fn to_list<'v>(this: &Depset<'v>) -> starlark::Result<Vec<Value<'v>>> {
Ok(this.iter().collect())
}
}
#[cfg(test)]
mod tests {
use testutils::Assert;
use super::*;
use crate::{globals::tests::new_assert, UnpackFileDepset};
#[test]
fn test_depset_deduplication() {
let mut a = new_assert();
let depset_val = a.pass("depset(['a', 'a'])");
let depset = depset_val.value().downcast_ref::<FrozenDepset>().unwrap();
assert_eq!(depset.direct().len(), 1);
}
#[test]
fn test_depset_invalid_transitive() {
let mut a = new_assert();
a.fail(
"depset(['c'], transitive=['not a depset'])",
"Expected value of type `depset` but got `string (repr: \"not a depset\")`",
);
}
#[test]
fn test_depset_conflicting_orders() {
let mut a = new_assert();
a.fail(
"depset(transitive=[depset(['a'], order='preorder'), depset(['b'], order='postorder')])",
"conflicting orders: depset has order preorder, but transitive child has order postorder",
);
// When outer depset specifies non-default order, we can't reuse the inner
// depset.
a.equivalent(
"depset(transitive=[depset(['c'], transitive=[depset(['a'])])], order='preorder').to_list()",
"['c', 'a']",
);
}
#[test]
fn test_depset_type_validation() {
let mut a = new_assert();
let frozen_str_depset = a.pass("depset(['a', 'b'])");
let frozen_file_depset = a.pass("depset([make_file('a.txt'), make_file('b.txt')])");
a.modify_globals(move |builder: &mut starlark::environment::GlobalsBuilder| {
builder.set("frozen_str_depset", frozen_str_depset.clone());
builder.set("frozen_file_depset", frozen_file_depset.clone());
});
// Homogeneous File depset.
a.equivalent(
"[f.path for f in depset([make_file('a.txt'), make_file('b.txt')]).to_list()]",
"['a.txt', 'b.txt']",
);
// Homogeneous string depset.
a.equivalent("depset(['a', 'b']).to_list()", "['a', 'b']");
// Mixing File and String in direct elements.
a.fail(
"depset([make_file('a.txt'), 'b'])",
"depset elements must be of the same type, expected File, got unknown",
);
// Mixing String and File in direct elements.
a.fail(
"depset(['a', make_file('b.txt')])",
"depset elements must be of the same type, expected unknown, got File",
);
// Mixing File depset and String depset in transitive elements.
a.fail(
"depset(transitive=[depset([make_file('a.txt')]), depset(['b'])])",
"depset elements must be of the same type, expected File, got unknown",
);
// Direct File elements mixed with transitive String depset.
a.fail(
"depset([make_file('a.txt')], transitive=[depset(['b'])])",
"depset elements must be of the same type, expected File, got unknown",
);
// Transitive depset containing a frozen depset.
a.equivalent(
"depset(['c'], transitive=[frozen_str_depset], order='postorder').to_list()",
"['a', 'b', 'c']",
);
// Transitive depset containing a frozen File depset.
a.equivalent(
"[f.path for f in depset([make_file('c.txt')], transitive=[frozen_file_depset], order='postorder').to_list()]",
"['a.txt', 'b.txt', 'c.txt']",
);
}
#[test]
fn test_depset_starlark_conversions() {
let mut a = new_assert();
// Truthiness checks.
a.equivalent("bool(depset(['a']))", "True");
a.equivalent("bool(depset())", "False");
a.equivalent("bool(depset(transitive=[depset()]))", "False");
a.equivalent("repr(depset())", "\"depset([])\"");
}
#[test]
fn test_depset_phony() {
let mut a = new_assert();
// This is to ensure we don't accidentally use File::new.
// We cannot mix File::new and File::from_rust to get the same file object.
let f = |s: &str| File::intern(s);
let mut upto_phony = 0;
// Collect the phonies we've seen since last time we called new_phonies.
let mut new_phonies = |a: &Assert| {
use types::EvalContext as _;
let phonies = &a.context().require_rule_impl().unwrap().phonies;
let result = &phonies[upto_phony..];
upto_phony = phonies.len();
result.to_vec()
};
assert!(UnpackFileDepset::unpack_value_err(a.pass("depset([1])").value()).is_err());
a.eq("depset()", UnpackFileDepset(None));
assert_eq!(new_phonies(&a), &[]);
// Because the outer depset only phonies to one item, we shouldn't make a phony.
a.eq(
"depset([make_file('a.txt')])",
UnpackFileDepset(Some(f("a.txt"))),
);
assert_eq!(new_phonies(&a), &[]);
// We should make a phony here for the inner depset.
// Because the outer depset only phonies to one phony, it shouldn't make a
// phony.
a.eq(
"depset(transitive = [depset([make_file('a.txt'), make_file('b.txt')])])",
UnpackFileDepset(Some(f("phony/$TOOLCHAIN/$LABEL_0"))),
);
assert_eq!(
new_phonies(&a),
&[(f("phony/$TOOLCHAIN/$LABEL_0"), vec![f("a.txt"), f("b.txt")])]
);
a.eq(
"depset([make_file('c.txt')], transitive=[depset([make_file('a.txt'), make_file('b.txt')])])",
UnpackFileDepset(Some(f("phony/$TOOLCHAIN/$LABEL_2"))),
);
assert_eq!(
new_phonies(&a),
&[
(f("phony/$TOOLCHAIN/$LABEL_1"), vec![f("a.txt"), f("b.txt")]),
(
f("phony/$TOOLCHAIN/$LABEL_2"),
vec![f("c.txt"), f("phony/$TOOLCHAIN/$LABEL_1")]
),
]
);
assert_eq!(new_phonies(&a), &[]);
a.eq(
"new_file_depset([make_file('a.txt')])",
UnpackFileDepset(Some(f("a.txt"))),
);
assert_eq!(new_phonies(&a), &[]);
a.eq(
"new_file_depset([make_file('a.txt'), make_file('b.txt')])",
UnpackFileDepset(Some(f("phony/$TOOLCHAIN/$LABEL_3"))),
);
assert_eq!(
new_phonies(&a),
&[(f("phony/$TOOLCHAIN/$LABEL_3"), vec![f("a.txt"), f("b.txt")])]
);
}
}