starlark/values/typing/type_compiled/
compiled.rs

1/*
2 * Copyright 2019 The Starlark in Rust Authors.
3 * Copyright (c) Facebook, Inc. and its affiliates.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18use std::fmt;
19use std::fmt::Debug;
20use std::fmt::Display;
21use std::fmt::Formatter;
22use std::hash::Hash;
23use std::hash::Hasher;
24
25use allocative::Allocative;
26use dupe::Dupe;
27use starlark_derive::starlark_module;
28use starlark_derive::starlark_value;
29use starlark_map::StarlarkHasher;
30use starlark_syntax::slice_vec_ext::SliceExt;
31use starlark_syntax::slice_vec_ext::VecExt;
32use thiserror::Error;
33
34use crate as starlark;
35use crate::any::ProvidesStaticType;
36use crate::coerce::Coerce;
37use crate::environment::Methods;
38use crate::environment::MethodsBuilder;
39use crate::environment::MethodsStatic;
40use crate::private::Private;
41use crate::typing::Ty;
42use crate::values::dict::DictRef;
43use crate::values::layout::avalue::alloc_static;
44use crate::values::layout::avalue::AValueBasic;
45use crate::values::layout::avalue::AValueImpl;
46use crate::values::layout::heap::repr::AValueRepr;
47use crate::values::list::ListRef;
48use crate::values::none::NoneType;
49use crate::values::type_repr::StarlarkTypeRepr;
50use crate::values::types::tuple::value::Tuple;
51use crate::values::typing::type_compiled::factory::TypeCompiledFactory;
52use crate::values::typing::type_compiled::matcher::TypeMatcher;
53use crate::values::typing::type_compiled::matchers::IsAny;
54use crate::values::AllocValue;
55use crate::values::Demand;
56use crate::values::Freeze;
57use crate::values::FreezeResult;
58use crate::values::FrozenHeap;
59use crate::values::FrozenValue;
60use crate::values::Heap;
61use crate::values::NoSerialize;
62use crate::values::StarlarkValue;
63use crate::values::StringValue;
64use crate::values::Trace;
65use crate::values::Value;
66use crate::values::ValueLifetimeless;
67use crate::values::ValueLike;
68
69#[derive(Debug, Error)]
70enum TypingError {
71    /// The value does not have the specified type
72    #[error("Value `{0}` of type `{1}` does not match the type annotation `{2}` for {3}")]
73    TypeAnnotationMismatch(String, String, String, String),
74    /// The given type annotation does not represent a type
75    #[error("Type `{0}` is not a valid type annotation")]
76    InvalidTypeAnnotation(String),
77    #[error("`{{A: B}}` cannot be used as type, perhaps you meant `dict[A, B]`")]
78    Dict,
79    #[error("`[X]` cannot be used as type, perhaps you meant `list[X]`")]
80    List,
81    /// The given type annotation does not exist, but the user might have forgotten quotes around
82    /// it
83    #[error(r#"Found `{0}` instead of a valid type annotation. Perhaps you meant `"{1}"`?"#)]
84    PerhapsYouMeant(String, String),
85    #[error("Value of type `{1}` does not match type `{2}`: {0}")]
86    ValueDoesNotMatchType(String, &'static str, String),
87    #[error("String literals are not allowed in type expressions: `{0}`")]
88    StringLiteralNotAllowed(String),
89}
90
91pub(crate) trait TypeCompiledDyn: Debug + Allocative + Send + Sync + 'static {
92    fn as_ty_dyn(&self) -> &Ty;
93    fn is_runtime_wildcard_dyn(&self) -> bool;
94    fn to_frozen_dyn(&self, heap: &FrozenHeap) -> TypeCompiled<FrozenValue>;
95}
96
97// TODO(nga): derive.
98unsafe impl<'v> ProvidesStaticType<'v> for &'v dyn TypeCompiledDyn {
99    type StaticType = &'static dyn TypeCompiledDyn;
100}
101
102impl<T> TypeCompiledDyn for TypeCompiledImplAsStarlarkValue<T>
103where
104    T: TypeMatcher,
105{
106    fn as_ty_dyn(&self) -> &Ty {
107        &self.ty
108    }
109    fn is_runtime_wildcard_dyn(&self) -> bool {
110        self.type_compiled_impl.is_wildcard()
111    }
112    fn to_frozen_dyn(&self, heap: &FrozenHeap) -> TypeCompiled<FrozenValue> {
113        TypeCompiled(heap.alloc_simple::<TypeCompiledImplAsStarlarkValue<T>>(Self::clone(self)))
114    }
115}
116
117#[derive(
118    Clone,
119    Eq,
120    PartialEq,
121    Debug,
122    Allocative,
123    ProvidesStaticType,
124    NoSerialize
125)]
126pub struct TypeCompiledImplAsStarlarkValue<T: 'static> {
127    type_compiled_impl: T,
128    ty: Ty,
129}
130
131impl<T> TypeCompiledImplAsStarlarkValue<T>
132where
133    TypeCompiledImplAsStarlarkValue<T>: StarlarkValue<'static>,
134{
135    pub(crate) const fn alloc_static(
136        imp: T,
137        ty: Ty,
138    ) -> AValueRepr<AValueImpl<'static, AValueBasic<TypeCompiledImplAsStarlarkValue<T>>>> {
139        alloc_static(TypeCompiledImplAsStarlarkValue {
140            type_compiled_impl: imp,
141            ty,
142        })
143    }
144}
145
146#[doc(hidden)]
147#[derive(Hash, Eq, PartialEq, Debug, Clone, Allocative)]
148pub struct DummyTypeMatcher;
149
150impl TypeMatcher for DummyTypeMatcher {
151    fn matches(&self, _value: Value) -> bool {
152        unreachable!()
153    }
154}
155
156#[starlark_value(type = "type")]
157impl<'v, T: 'static> StarlarkValue<'v> for TypeCompiledImplAsStarlarkValue<T>
158where
159    T: TypeMatcher,
160{
161    type Canonical = TypeCompiledImplAsStarlarkValue<DummyTypeMatcher>;
162
163    fn type_matches_value(&self, value: Value<'v>, _private: Private) -> bool {
164        self.type_compiled_impl.matches(value)
165    }
166
167    fn provide(&'v self, demand: &mut Demand<'_, 'v>) {
168        demand.provide_ref_static::<dyn TypeCompiledDyn>(self);
169    }
170
171    fn write_hash(&self, hasher: &mut StarlarkHasher) -> crate::Result<()> {
172        Hash::hash(&self.ty, hasher);
173        Ok(())
174    }
175
176    fn equals(&self, other: Value<'v>) -> crate::Result<bool> {
177        let Some(other) = other.downcast_ref::<Self>() else {
178            return Ok(false);
179        };
180        Ok(self.ty == other.ty)
181    }
182
183    fn eval_type(&self) -> Option<Ty> {
184        // `TypeCompiled::new` handles this type explicitly,
185        // but implement this function to make proc-macro generate `bit_or`.
186        // Also safer to be explicit here.
187        Some(self.ty.clone())
188    }
189
190    fn get_methods() -> Option<&'static Methods>
191    where
192        Self: Sized,
193    {
194        static RES: MethodsStatic = MethodsStatic::new();
195        RES.methods(type_compiled_methods)
196    }
197}
198
199impl<T: TypeMatcher> Display for TypeCompiledImplAsStarlarkValue<T> {
200    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
201        write!(f, "{}", self.ty)
202    }
203}
204
205#[starlark_module]
206fn type_compiled_methods(methods: &mut MethodsBuilder) {
207    /// True iff the value matches this type.
208    fn matches<'v>(this: Value<'v>, value: Value<'v>) -> anyhow::Result<bool> {
209        Ok(this.get_ref().type_matches_value(value))
210    }
211
212    /// Error if the value does not match this type.
213    fn check_matches<'v>(this: Value<'v>, value: Value<'v>) -> anyhow::Result<NoneType> {
214        if !this.get_ref().type_matches_value(value) {
215            return Err(TypingError::ValueDoesNotMatchType(
216                value.to_repr(),
217                value.get_type(),
218                TypeCompiled(this).to_string(),
219            )
220            .into());
221        }
222        Ok(NoneType)
223    }
224}
225
226/// Wrapper for a [`Value`] that acts like a runtime type matcher.
227#[derive(
228    Debug,
229    Allocative,
230    Freeze,
231    Trace,
232    Clone,
233    Copy,
234    Dupe,
235    Coerce,
236    ProvidesStaticType
237)]
238#[repr(transparent)]
239pub struct TypeCompiled<V: ValueLifetimeless>(
240    /// `V` is `TypeCompiledImplAsStarlarkValue`.
241    V,
242);
243
244impl<'v, V: ValueLike<'v>> Display for TypeCompiled<V> {
245    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
246        match self.downcast() {
247            Ok(t) => Display::fmt(&t.as_ty_dyn(), f),
248            Err(_) => {
249                // This is unreachable, but we should not panic in `Display`.
250                Display::fmt(&self.0, f)
251            }
252        }
253    }
254}
255
256impl<V: ValueLifetimeless> StarlarkTypeRepr for TypeCompiled<V> {
257    type Canonical = TypeCompiledImplAsStarlarkValue<DummyTypeMatcher>;
258
259    fn starlark_type_repr() -> Ty {
260        TypeCompiledImplAsStarlarkValue::<DummyTypeMatcher>::starlark_type_repr()
261    }
262}
263
264impl<'v, V: ValueLike<'v>> AllocValue<'v> for TypeCompiled<V> {
265    fn alloc_value(self, _heap: &'v Heap) -> Value<'v> {
266        self.0.to_value()
267    }
268}
269
270impl<'v, V: ValueLike<'v>> TypeCompiled<V> {
271    pub(crate) fn unchecked_new(value: V) -> Self {
272        TypeCompiled(value)
273    }
274
275    fn downcast(self) -> anyhow::Result<&'v dyn TypeCompiledDyn> {
276        self.to_value()
277            .0
278            .request_value::<&dyn TypeCompiledDyn>()
279            .ok_or_else(|| anyhow::anyhow!("Not TypeCompiledImpl (internal error)"))
280    }
281
282    /// Check if given value matches this type.
283    pub fn matches(&self, value: Value<'v>) -> bool {
284        self.0.to_value().get_ref().type_matches_value(value)
285    }
286
287    /// Get the typechecker type for this runtime type.
288    pub fn as_ty(&self) -> &'v Ty {
289        self.downcast().unwrap().as_ty_dyn()
290    }
291
292    /// True if `TypeCompiled` matches any type at runtime.
293    /// However, compile-time/lint typechecker may still check the type.
294    pub(crate) fn is_runtime_wildcard(self) -> bool {
295        self.downcast().unwrap().is_runtime_wildcard_dyn()
296    }
297
298    #[cold]
299    #[inline(never)]
300    fn check_type_error(self, value: Value<'v>, arg_name: Option<&str>) -> crate::Result<()> {
301        Err(crate::Error::new_other(
302            TypingError::TypeAnnotationMismatch(
303                value.to_str(),
304                value.get_type().to_owned(),
305                self.to_string(),
306                match arg_name {
307                    None => "return type".to_owned(),
308                    Some(x) => format!("argument `{}`", x),
309                },
310            ),
311        ))
312    }
313
314    pub(crate) fn check_type(self, value: Value<'v>, arg_name: Option<&str>) -> crate::Result<()> {
315        if self.matches(value) {
316            Ok(())
317        } else {
318            self.check_type_error(value, arg_name)
319        }
320    }
321
322    pub(crate) fn to_value(self) -> TypeCompiled<Value<'v>> {
323        TypeCompiled(self.0.to_value())
324    }
325
326    pub(crate) fn to_inner(self) -> V {
327        self.0
328    }
329
330    pub(crate) fn write_hash(self, hasher: &mut StarlarkHasher) -> crate::Result<()> {
331        self.to_value().0.write_hash(hasher)
332    }
333
334    // Dead code, but may become useful in the future.
335    pub(crate) fn _equals(self, other: Self) -> crate::Result<bool> {
336        self.to_value().0.equals(other.to_value().0)
337    }
338}
339
340impl<'v, V: ValueLike<'v>> Hash for TypeCompiled<V> {
341    fn hash<H: Hasher>(&self, state: &mut H) {
342        match self.0.to_value().get_hash() {
343            Ok(h) => h.hash(state),
344            Err(_) => {
345                // Unreachable, but we should not panic in `Hash`.
346            }
347        }
348    }
349}
350
351impl<'v, V: ValueLike<'v>> PartialEq for TypeCompiled<V> {
352    #[allow(clippy::manual_unwrap_or)]
353    fn eq(&self, other: &Self) -> bool {
354        self.0
355            .to_value()
356            .equals(other.0.to_value())
357            .unwrap_or_default()
358    }
359}
360
361impl<'v, V: ValueLike<'v>> Eq for TypeCompiled<V> {}
362
363impl<'v, V: ValueLike<'v>> TypeCompiled<V> {
364    /// Reallocate the type in a frozen heap.
365    pub fn to_frozen(self, heap: &FrozenHeap) -> TypeCompiled<FrozenValue> {
366        if let Some(v) = self.0.to_value().unpack_frozen() {
367            TypeCompiled(v)
368        } else {
369            self.to_value().downcast().unwrap().to_frozen_dyn(heap)
370        }
371    }
372}
373
374// These functions are small, but are deliberately out-of-line so we get better
375// information in profiling about the origin of these closures
376impl<'v> TypeCompiled<Value<'v>> {
377    pub(crate) fn alloc(
378        type_compiled_impl: impl TypeMatcher,
379        ty: Ty,
380        heap: &'v Heap,
381    ) -> TypeCompiled<Value<'v>> {
382        TypeCompiled(heap.alloc_simple(TypeCompiledImplAsStarlarkValue {
383            type_compiled_impl,
384            ty,
385        }))
386    }
387
388    pub(crate) fn type_list_of(
389        t: TypeCompiled<Value<'v>>,
390        heap: &'v Heap,
391    ) -> TypeCompiled<Value<'v>> {
392        TypeCompiledFactory::alloc_ty(&Ty::list(t.as_ty().clone()), heap)
393    }
394
395    pub(crate) fn type_set_of(
396        t: TypeCompiled<Value<'v>>,
397        heap: &'v Heap,
398    ) -> TypeCompiled<Value<'v>> {
399        TypeCompiledFactory::alloc_ty(&Ty::set(t.as_ty().clone()), heap)
400    }
401
402    pub(crate) fn type_any_of_two(
403        t0: TypeCompiled<Value<'v>>,
404        t1: TypeCompiled<Value<'v>>,
405        heap: &'v Heap,
406    ) -> TypeCompiled<Value<'v>> {
407        let ty = Ty::union2(t0.as_ty().clone(), t1.as_ty().clone());
408        TypeCompiledFactory::alloc_ty(&ty, heap)
409    }
410
411    pub(crate) fn type_any_of(
412        ts: Vec<TypeCompiled<Value<'v>>>,
413        heap: &'v Heap,
414    ) -> TypeCompiled<Value<'v>> {
415        let ty = Ty::unions(ts.into_map(|t| t.as_ty().clone()));
416        TypeCompiledFactory::alloc_ty(&ty, heap)
417    }
418
419    pub(crate) fn type_dict_of(
420        kt: TypeCompiled<Value<'v>>,
421        vt: TypeCompiled<Value<'v>>,
422        heap: &'v Heap,
423    ) -> TypeCompiled<Value<'v>> {
424        let ty = Ty::dict(kt.as_ty().clone(), vt.as_ty().clone());
425        TypeCompiledFactory::alloc_ty(&ty, heap)
426    }
427
428    /// Parse `[t1, t2, ...]` as type.
429    fn from_list(t: &ListRef<'v>, heap: &'v Heap) -> anyhow::Result<TypeCompiled<Value<'v>>> {
430        match t.content() {
431            [] | [_] => Err(TypingError::List.into()),
432            ts @ [_, _, ..] => {
433                // A union type, can match any
434                let ts = ts.try_map(|t| TypeCompiled::new(*t, heap))?;
435                Ok(TypeCompiled::type_any_of(ts, heap))
436            }
437        }
438    }
439
440    pub(crate) fn from_ty(ty: &Ty, heap: &'v Heap) -> Self {
441        TypeCompiledFactory::alloc_ty(ty, heap)
442    }
443
444    /// Evaluate type annotation at runtime.
445    pub fn new(ty: Value<'v>, heap: &'v Heap) -> anyhow::Result<Self> {
446        if let Some(s) = StringValue::new(ty) {
447            return Err(TypingError::StringLiteralNotAllowed(s.to_string()).into());
448        } else if ty.is_none() {
449            Ok(TypeCompiledFactory::alloc_ty(&Ty::none(), heap))
450        } else if let Some(t) = Tuple::from_value(ty) {
451            let elems = t
452                .content()
453                .try_map(|t| anyhow::Ok(TypeCompiled::new(*t, heap)?.as_ty().clone()))?;
454            Ok(TypeCompiled::from_ty(&Ty::tuple(elems), heap))
455        } else if let Some(t) = ListRef::from_value(ty) {
456            TypeCompiled::from_list(t, heap)
457        } else if ty.request_value::<&dyn TypeCompiledDyn>().is_some() {
458            // This branch is optimization: `TypeCompiledAsStarlarkValue` implements `eval_type`,
459            // but this branch avoids copying the type.
460            Ok(TypeCompiled(ty))
461        } else if let Some(ty) = ty.get_ref().eval_type() {
462            Ok(TypeCompiled::from_ty(&ty, heap))
463        } else {
464            Err(invalid_type_annotation(ty, heap).into())
465        }
466    }
467}
468
469impl TypeCompiled<FrozenValue> {
470    /// Evaluate type annotation at runtime.
471    pub(crate) fn new_frozen(ty: FrozenValue, frozen_heap: &FrozenHeap) -> anyhow::Result<Self> {
472        // TODO(nga): trip to a heap is not free.
473        let heap = Heap::new();
474        let ty = TypeCompiled::new(ty.to_value(), &heap)?;
475        Ok(ty.to_frozen(frozen_heap))
476    }
477
478    /// `typing.Any`.
479    pub fn any() -> TypeCompiled<FrozenValue> {
480        static ANYTHING: AValueRepr<
481            AValueImpl<'static, AValueBasic<TypeCompiledImplAsStarlarkValue<IsAny>>>,
482        > = TypeCompiledImplAsStarlarkValue::alloc_static(IsAny, Ty::any());
483
484        TypeCompiled::unchecked_new(FrozenValue::new_repr(&ANYTHING))
485    }
486}
487
488fn invalid_type_annotation<'v>(ty: Value<'v>, heap: &'v Heap) -> TypingError {
489    if DictRef::from_value(ty).is_some() {
490        TypingError::Dict
491    } else if ListRef::from_value(ty).is_some() {
492        TypingError::List
493    } else if let Some(name) = ty
494        .get_attr("type", heap)
495        .ok()
496        .flatten()
497        .and_then(|v| v.unpack_str())
498    {
499        TypingError::PerhapsYouMeant(ty.to_str(), name.into())
500    } else {
501        TypingError::InvalidTypeAnnotation(ty.to_str())
502    }
503}