diff --git a/mypy/cache.py b/mypy/cache.py index b9cd8ad7a9050..96e85931f146a 100644 --- a/mypy/cache.py +++ b/mypy/cache.py @@ -69,7 +69,7 @@ from mypy_extensions import u8 # High-level cache layout format -CACHE_VERSION: Final = 8 +CACHE_VERSION: Final = 9 # Type used internally to represent errors: # (path, line, column, end_line, end_column, severity, message, code) @@ -308,6 +308,7 @@ def read(cls, data: ReadBuffer) -> CacheMetaEx | None: LITERAL_BYTES: Final[Tag] = 5 LITERAL_FLOAT: Final[Tag] = 6 LITERAL_COMPLEX: Final[Tag] = 7 +LITERAL_SENTINEL: Final[Tag] = 8 # Collections. LIST_GEN: Final[Tag] = 20 diff --git a/mypy/messages.py b/mypy/messages.py index 3de66c7c6082c..68351a81cf1cd 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2777,6 +2777,8 @@ def format_literal_value(typ: LiteralType) -> str: modifier += "=" items.append(f"{item_name!r}{modifier}: {format(item_type)}") return f"TypedDict({{{', '.join(items)}}})" + elif isinstance(typ, LiteralType) and typ.is_sentinel_literal(): + return format_literal_value(typ) elif isinstance(typ, LiteralType): return f"Literal[{format_literal_value(typ)}]" elif isinstance(typ, UnionType): @@ -2784,6 +2786,9 @@ def format_literal_value(typ: LiteralType) -> str: if not isinstance(typ, UnionType): return format(typ) literal_items, union_items = separate_union_literals(typ) + sentinel_items = [item for item in literal_items if item.is_sentinel_literal()] + literal_items = [item for item in literal_items if not item.is_sentinel_literal()] + union_items = [*sentinel_items, *union_items] # Coalesce multiple Literal[] members. This also changes output order. # If there's just one Literal item, retain the original ordering. diff --git a/mypy/nodes.py b/mypy/nodes.py index 32a694560b24b..889dc608cb7e5 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1414,6 +1414,7 @@ def is_dynamic(self) -> bool: "from_module_getattr", "has_explicit_value", "allow_incompatible_override", + "is_sentinel", ] @@ -1452,6 +1453,7 @@ class Var(SymbolNode): "allow_incompatible_override", "invalid_partial_type", "is_argument", + "is_sentinel", ) __match_args__ = ("name", "type", "final_value") @@ -1514,6 +1516,8 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None: self.invalid_partial_type = False # Is it a variable symbol for a function argument? self.is_argument = False + # Was this variable created by PEP 661 sentinel()/Sentinel() syntax? + self.is_sentinel = False @property def name(self) -> str: @@ -1596,6 +1600,7 @@ def write(self, data: WriteBuffer) -> None: self.from_module_getattr, self.has_explicit_value, self.allow_incompatible_override, + self.is_sentinel, ], ) write_literal(data, self.final_value) @@ -1633,7 +1638,8 @@ def read(cls, data: ReadBuffer) -> Var: v.from_module_getattr, v.has_explicit_value, v.allow_incompatible_override, - ) = read_flags(data, num_flags=19) + v.is_sentinel, + ) = read_flags(data, num_flags=20) tag = read_tag(data) if tag == LITERAL_COMPLEX: v.final_value = complex(read_float_bare(data), read_float_bare(data)) diff --git a/mypy/semanal.py b/mypy/semanal.py index a958043fa35c2..605398f5d3349 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -269,6 +269,7 @@ OVERRIDE_DECORATOR_NAMES, PROTOCOL_NAMES, REVEAL_TYPE_NAMES, + SENTINEL_TYPE_NAMES, TPDICT_NAMES, TYPE_ALIAS_NAMES, TYPE_CHECK_ONLY_NAMES, @@ -287,6 +288,7 @@ ParamSpecType, PlaceholderType, ProperType, + SentinelValue, TrivialSyntheticTypeTranslator, TupleType, Type, @@ -3357,9 +3359,15 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: # may be set to True while there were still placeholders due to forward refs. s.is_alias_def = False + sentinel_definition = self.is_sentinel_declaration(s) + # OK, this is a regular assignment, perform the necessary analysis steps. s.is_final_def = self.unwrap_final(s) + if sentinel_definition: + s.is_final_def = True self.analyze_lvalues(s) + if sentinel_definition: + self.setup_sentinel_var(s) self.check_final_implicit_def(s) self.store_final_status(s) self.check_classvar(s) @@ -3372,6 +3380,51 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.process__deletable__(s) self.process__slots__(s) + def is_sentinel_declaration(self, s: AssignmentStmt) -> bool: + """Does this assignment define a PEP 661 sentinel singleton?""" + if self.is_func_scope() or s.unanalyzed_type is not None: + return False + if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr): + return False + if not isinstance(s.rvalue, CallExpr): + return False + call = s.rvalue + if not isinstance(call.callee, RefExpr): + return False + if call.callee.fullname not in SENTINEL_TYPE_NAMES: + return False + if not call.args or call.arg_kinds[0] != ARG_POS or not isinstance(call.args[0], StrExpr): + return False + return True + + def setup_sentinel_var(self, s: AssignmentStmt) -> None: + lvalue = s.lvalues[0] + assert isinstance(lvalue, NameExpr) + if not isinstance(lvalue.node, Var): + return + var = lvalue.node + var.is_sentinel = True + typ = self.sentinel_type_for_var(var, s.rvalue) + if typ is not None: + s.type = typ + + def sentinel_type_for_var(self, var: Var, rvalue: Expression) -> Instance | None: + assert isinstance(rvalue, CallExpr) + callee = rvalue.callee + assert isinstance(callee, RefExpr) + typ = self.named_type_or_none(callee.fullname) + if typ is None: + return None + name = f"{self.type.name}.{var.name}" if self.type is not None else var.name + return typ.copy_modified( + last_known_value=LiteralType( + SentinelValue(var.fullname, name), + fallback=typ, + line=rvalue.line, + column=rvalue.column, + ) + ) + def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool: """Special case 'X = X' in global scope. @@ -3536,6 +3589,8 @@ def is_type_ref(self, rv: Expression, bare: bool = False) -> bool: # Assignment color = Color['RED'] defines a variable, not an alias. return not rv.node.is_enum if isinstance(rv.node, Var): + if rv.node.is_sentinel: + return True return rv.node.fullname in NEVER_NAMES if isinstance(rv, NameExpr): @@ -4717,6 +4772,7 @@ def store_declared_types(self, lvalue: Lvalue, typ: Type) -> None: var.is_final and isinstance(typ, Instance) and typ.last_known_value + and not isinstance(typ.last_known_value.value, SentinelValue) and (not self.type or not self.type.is_enum) ): var.final_value = typ.last_known_value.value diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index ec9af3e669344..9160b3de99ac7 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -5,6 +5,9 @@ import re from unittest import TestCase, skipUnless +from librt.internal import ReadBuffer, WriteBuffer + +from mypy.cache import read_tag from mypy.erasetype import erase_type, remove_instance_last_known_values from mypy.indirection import TypeIndirectionVisitor from mypy.join import join_types @@ -30,6 +33,7 @@ from mypy.test.typefixture import InterfaceTypeFixture, TypeFixture from mypy.typeops import false_only, make_simplified_union, true_only from mypy.types import ( + LITERAL_TYPE, AnyType, CallableType, Instance, @@ -37,6 +41,7 @@ NoneType, Overloaded, ProperType, + SentinelValue, TupleType, Type, TypeOfAny, @@ -65,6 +70,25 @@ def setUp(self) -> None: def test_any(self) -> None: assert_equal(str(AnyType(TypeOfAny.special_form)), "Any") + def test_sentinel_literal_json_roundtrip(self) -> None: + literal = LiteralType(SentinelValue("__main__.MISSING", "MISSING"), self.fx.a) + assert_equal(str(literal), "MISSING") + data = literal.serialize() + assert isinstance(data, dict) + roundtrip = LiteralType.deserialize(data) + self.assertEqual(roundtrip.value, literal.value) + self.assertEqual(roundtrip.fallback.type_ref, self.fx.a.type.fullname) + + def test_sentinel_literal_ff_roundtrip(self) -> None: + literal = LiteralType(SentinelValue("__main__.MISSING", "MISSING"), self.fx.a) + data = WriteBuffer() + literal.write(data) + buffer = ReadBuffer(data.getvalue()) + assert read_tag(buffer) == LITERAL_TYPE + roundtrip = LiteralType.read(buffer) + self.assertEqual(roundtrip.value, literal.value) + self.assertEqual(roundtrip.fallback.type_ref, self.fx.a.type.fullname) + def test_simple_unbound_type(self) -> None: u = UnboundType("Foo") assert_equal(str(u), "Foo?") diff --git a/mypy/typeanal.py b/mypy/typeanal.py index db56256192625..10185c773fb38 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1000,6 +1000,16 @@ def analyze_unbound_type_without_type_info( column=t.column, ) + if isinstance(sym.node, Var) and sym.node.is_sentinel: + typ = get_proper_type(sym.node.type) + if isinstance(typ, Instance) and typ.last_known_value is not None: + return LiteralType( + value=typ.last_known_value.value, + fallback=typ.last_known_value.fallback, + line=t.line, + column=t.column, + ) + # None of the above options worked. We parse the args (if there are any) # to make sure there are no remaining semanal-only types, then give up. t = t.copy_modified(args=self.anal_array(t.args)) diff --git a/mypy/typeops.py b/mypy/typeops.py index e13d6dd0b1732..0f78835abe9fe 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1025,7 +1025,7 @@ def is_singleton_identity_type(typ: ProperType) -> bool: or (typ.type.fullname in NOT_IMPLEMENTED_TYPE_NAMES) ) if isinstance(typ, LiteralType): - return typ.is_enum_literal() or isinstance(typ.value, bool) + return typ.is_enum_literal() or typ.is_sentinel_literal() or isinstance(typ.value, bool) if isinstance(typ, TypeType) and isinstance(typ.item, Instance) and typ.item.type.is_final: return True if isinstance(typ, FunctionLike) and typ.is_type_obj() and typ.type_object().is_final: diff --git a/mypy/types.py b/mypy/types.py index 40c3839e2efca..b661a8a25b2c6 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -9,6 +9,7 @@ Any, ClassVar, Final, + NamedTuple, NewType, TypeAlias as _TypeAlias, TypeGuard, @@ -33,6 +34,7 @@ EXTRA_ATTRS, LIST_GEN, LITERAL_NONE, + LITERAL_SENTINEL, ReadBuffer, Tag, WriteBuffer, @@ -64,6 +66,7 @@ JsonDict: _TypeAlias = dict[str, Any] + # The set of all valid expressions that can currently be contained # inside of a Literal[...]. # @@ -94,7 +97,12 @@ # # Note: Float values are only used internally. They are not accepted within # Literal[...]. -LiteralValue: _TypeAlias = int | str | bool | float +class SentinelValue(NamedTuple): + fullname: str + name: str + + +LiteralValue: _TypeAlias = int | str | bool | float | SentinelValue TUPLE_NAMES: Final = ("builtins.tuple", "typing.Tuple") @@ -109,6 +117,12 @@ "typing_extensions.TypeVarTuple", ) +SENTINEL_TYPE_NAMES: Final = ( + "builtins.sentinel", + "typing_extensions.sentinel", + "typing_extensions.Sentinel", +) + TYPED_NAMEDTUPLE_NAMES: Final = ("typing.NamedTuple", "typing_extensions.NamedTuple") # Supported names of TypedDict type constructors. @@ -3226,11 +3240,15 @@ def __init__( # almost no test cases where we would redundantly compute # `can_be_false`/`can_be_true`. def can_be_false_default(self) -> bool: + if isinstance(self.value, SentinelValue): + return False if self.fallback.type.is_enum: return self.fallback.can_be_false return not self.value def can_be_true_default(self) -> bool: + if isinstance(self.value, SentinelValue): + return True if self.fallback.type.is_enum: return self.fallback.can_be_true return bool(self.value) @@ -3251,6 +3269,9 @@ def __eq__(self, other: object) -> bool: def is_enum_literal(self) -> bool: return self.fallback.type.is_enum + def is_sentinel_literal(self) -> bool: + return isinstance(self.value, SentinelValue) + def value_repr(self) -> str: """Returns the string representation of the underlying type. @@ -3258,6 +3279,9 @@ def value_repr(self) -> str: except it includes some additional logic to correctly handle cases where the value is a string, byte string, a unicode string, or an enum. """ + if isinstance(self.value, SentinelValue): + return self.value.name + raw = repr(self.value) fallback_name = self.fallback.type.fullname @@ -3276,21 +3300,29 @@ def value_repr(self) -> str: return raw def serialize(self) -> JsonDict | str: - return { - ".class": "LiteralType", - "value": self.value, - "fallback": self.fallback.serialize(), - } + value: LiteralValue | JsonDict = self.value + if isinstance(value, SentinelValue): + value = {".class": "SentinelValue", "fullname": value.fullname, "name": value.name} + return {".class": "LiteralType", "value": value, "fallback": self.fallback.serialize()} @classmethod def deserialize(cls, data: JsonDict) -> LiteralType: assert data[".class"] == "LiteralType" - return LiteralType(value=data["value"], fallback=Instance.deserialize(data["fallback"])) + value = data["value"] + if isinstance(value, dict): + assert value[".class"] == "SentinelValue" + value = SentinelValue(value["fullname"], value["name"]) + return LiteralType(value=value, fallback=Instance.deserialize(data["fallback"])) def write(self, data: WriteBuffer) -> None: write_tag(data, LITERAL_TYPE) self.fallback.write(data) - write_literal(data, self.value) + if isinstance(self.value, SentinelValue): + write_tag(data, LITERAL_SENTINEL) + write_str_bare(data, self.value.fullname) + write_str_bare(data, self.value.name) + else: + write_literal(data, self.value) write_tag(data, END_TAG) @classmethod @@ -3298,7 +3330,11 @@ def read(cls, data: ReadBuffer) -> LiteralType: assert read_tag(data) == INSTANCE fallback = Instance.read(data) tag = read_tag(data) - ret = LiteralType(read_literal(data, tag), fallback) + if tag == LITERAL_SENTINEL: + value = SentinelValue(read_str_bare(data), read_str_bare(data)) + else: + value = read_literal(data, tag) + ret = LiteralType(value, fallback) assert read_tag(data) == END_TAG return ret @@ -3958,6 +3994,8 @@ def visit_raw_expression_type(self, t: RawExpressionType, /) -> str: return repr(t.literal_value) def visit_literal_type(self, t: LiteralType, /) -> str: + if isinstance(t.value, SentinelValue): + return t.value_repr() return f"Literal[{t.value_repr()}]" def visit_union_type(self, t: UnionType, /) -> str: diff --git a/mypyc/lib-rt/internal/librt_internal.c b/mypyc/lib-rt/internal/librt_internal.c index 04de7610736c0..dc811efbabad1 100644 --- a/mypyc/lib-rt/internal/librt_internal.c +++ b/mypyc/lib-rt/internal/librt_internal.c @@ -930,6 +930,7 @@ write_tag(PyObject *self, PyObject *const *args, size_t nargs) { #define LITERAL_BYTES 5 #define LITERAL_FLOAT 6 #define LITERAL_COMPLEX 7 +#define LITERAL_SENTINEL 8 // Supported builtin collections. #define LIST_GEN 20 @@ -1161,6 +1162,11 @@ _skip_object(PyObject *data, uint8_t tag) { return _skip(data, 8); if (tag == LITERAL_COMPLEX) return _skip(data, 16); + if (tag == LITERAL_SENTINEL) { + if (unlikely(_skip_str_bytes(data) == CPY_NONE_ERROR)) + return CPY_NONE_ERROR; + return _skip_str_bytes(data); + } PyErr_Format(PyExc_ValueError, "Unsupported tag: %d", tag); return CPY_NONE_ERROR; } diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 39172a6385696..c99dff7b8fe40 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2827,7 +2827,7 @@ from mypy_extensions import u8 from librt.internal import ( ReadBuffer, WriteBuffer, write_bool, read_bool, write_str, read_str, write_float, read_float, write_int, read_int, write_tag, read_tag, write_bytes, read_bytes, - cache_version, + cache_version, extract_symbol, ) from testutil import assertRaises @@ -3045,6 +3045,16 @@ def test_buffer_str_size() -> None: b = ReadBuffer(b.getvalue()) assert read_str(b) == s +def test_extract_symbol_sentinel_literal() -> None: + data = WriteBuffer() + write_tag(data, 8) # LITERAL_SENTINEL + write_str(data, "__main__.MISSING") + write_str(data, "MISSING") + write_tag(data, 255) # END_TAG + + payload = data.getvalue() + assert extract_symbol(ReadBuffer(payload)) == payload + [file driver.py] from native import * @@ -3059,6 +3069,7 @@ test_buffer_str_size() test_buffer_int_powers() test_positive_long_int_serialized_bytes() test_negative_long_int_serialized_bytes() +test_extract_symbol_sentinel_literal() def test_buffer_basic_interpreted() -> None: b = WriteBuffer() diff --git a/test-data/unit/check-sentinels.test b/test-data/unit/check-sentinels.test new file mode 100644 index 0000000000000..ca4768a4d4aab --- /dev/null +++ b/test-data/unit/check-sentinels.test @@ -0,0 +1,155 @@ +[case testSentinelTypeExpressionsAndNarrowing] +from typing_extensions import Sentinel, assert_type, sentinel + +MISSING = Sentinel("", repr="missing") +SPECIAL = sentinel("SPECIAL") + +class Cls: + IN_CLASS = Sentinel("Cls.IN_CLASS") + +def func2(x: int | MISSING | SPECIAL = MISSING) -> None: + if x is MISSING: + assert_type(x, MISSING) + else: + assert_type(x, int | SPECIAL) + +def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None: + if x is Cls.IN_CLASS: + assert_type(x, Cls.IN_CLASS) + else: + assert_type(x, int) + +func2(1) +func2(MISSING) +func2(SPECIAL) +func2(Cls.IN_CLASS) # E: Argument 1 to "func2" has incompatible type "Cls.IN_CLASS"; expected "int | MISSING | SPECIAL" + +func3(1) +func3(MISSING) # E: Argument 1 to "func3" has incompatible type "MISSING"; expected "int | Cls.IN_CLASS" +func3(Cls.IN_CLASS) +[builtins fixtures/tuple.pyi] + +[case testSentinelEqualityNarrowingCurrentBehavior] +from typing_extensions import Sentinel, assert_type + +MISSING = Sentinel("MISSING") +SPECIAL = Sentinel("SPECIAL") + +def func(x: int | MISSING | SPECIAL) -> None: + # We could reasonably do narrowing here, but currently we're pretty conservative + # about narrowing on ==. + if x == MISSING: + assert_type(x, int | MISSING | SPECIAL) + else: + assert_type(x, int | MISSING | SPECIAL) +[builtins fixtures/tuple.pyi] + +[case testSentinelSameReprDistinctTypes] +from typing_extensions import Sentinel + +FIRST = Sentinel("DUPLICATE") +SECOND = Sentinel("DUPLICATE") + +def takes_first(x: FIRST) -> None: ... + +takes_first(FIRST) +takes_first(SECOND) # E: Argument 1 to "takes_first" has incompatible type "SECOND"; expected "FIRST" +[builtins fixtures/tuple.pyi] + +[case testSentinelFunctionLocalNotTypeExpression] +from typing_extensions import Sentinel + +def outer() -> None: + LOCAL = Sentinel("LOCAL") + def inner(x: LOCAL) -> None: ... # E: Variable "LOCAL" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +[builtins fixtures/tuple.pyi] + +[case testSentinelImplicitlyFinal] +from typing_extensions import Sentinel + +MISSING = Sentinel("MISSING") +MISSING = Sentinel("OTHER") # E: Cannot redefine an existing name as final +[builtins fixtures/tuple.pyi] + +[case testSentinelImplicitlyFinalWithLaterReference] +from typing_extensions import Sentinel + +_SENTINEL = Sentinel("_SENTINEL") + +def func(x: int | _SENTINEL = _SENTINEL) -> int: + if x is _SENTINEL: + return 0 + return x +[builtins fixtures/tuple.pyi] + +[case testBuiltinsSentinelTypeExpression] +# flags: --python-version 3.15 +from typing import assert_type + +MISSING = sentinel("MISSING") + +def func(x: int | MISSING = MISSING) -> None: + if x is not MISSING: + assert_type(x, int) + else: + assert_type(x, MISSING) + +[builtins fixtures/sentinel.pyi] + +[case testPrivateSentinelNameCanBeReusedAcrossModules] +from typing_extensions import Sentinel + +_SENTINEL = Sentinel("_SENTINEL") + +[file other.py] +from typing_extensions import Sentinel + +_SENTINEL = Sentinel("_SENTINEL") +[builtins fixtures/tuple.pyi] + +[case testPrivateSentinelNameCanBeReusedAfterStarImport] +from other import * +from typing_extensions import Sentinel + +_SENTINEL = Sentinel("_SENTINEL") + +[file other.py] +from typing_extensions import Sentinel + +_SENTINEL = Sentinel("_SENTINEL") +[builtins fixtures/tuple.pyi] + +[case testPrivateSentinelNameCanBeReusedAfterStarImportWithAll] +from other import * +from typing_extensions import Sentinel + +_SENTINEL = Sentinel("_SENTINEL") + +[file other.py] +from typing_extensions import Sentinel + +_SENTINEL = Sentinel("_SENTINEL") +__all__ = ("public",) +public = 1 +[builtins fixtures/tuple.pyi] + +[case testIsinstanceSentinel] +# flags: --python-version 3.15 +from typing import assert_type +from typing_extensions import Sentinel + +BUILTIN = sentinel("BUILTIN") +TYPEXT = Sentinel("TYPEXT") + +def func(x: int | BUILTIN | TYPEXT) -> None: + if isinstance(x, sentinel): + assert_type(x, BUILTIN) + else: + assert_type(x, int | TYPEXT) + if isinstance(x, Sentinel): + assert_type(x, TYPEXT) + else: + assert_type(x, int) + +[builtins fixtures/sentinel.pyi] diff --git a/test-data/unit/fixtures/sentinel.pyi b/test-data/unit/fixtures/sentinel.pyi new file mode 100644 index 0000000000000..690efb0df32ac --- /dev/null +++ b/test-data/unit/fixtures/sentinel.pyi @@ -0,0 +1,24 @@ +# Builtins stub used in sentinel-related test cases. + +from typing import Self + +class object: + def __init__(self) -> None: pass + def __new__(cls) -> Self: ... + +class type: pass +class function: + __name__: str +class ellipsis: pass + +class int: pass +class bool(int): pass +class str: pass + +class sentinel: + def __init__(self, name: str, /) -> None: ... + +class dict: pass +class tuple: pass + +def isinstance(x: object, t: type) -> bool: pass diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 43b5ef3a09501..6d614d8f1b915 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -42,6 +42,11 @@ TypeGuard: _SpecialForm TypeIs: _SpecialForm Never: _SpecialForm +class Sentinel: + def __init__(self, name: str, repr: str | None = None) -> None: ... + def __eq__(self, other: object) -> bool: ... +sentinel = Sentinel + TypeVarTuple: _SpecialForm Unpack: _SpecialForm Required: _SpecialForm