Skip to content

Add Bool special form to handle type expressions in boolean contexts#31

Merged
dnwpark merged 12 commits intomainfrom
special-bool
Jan 29, 2026
Merged

Add Bool special form to handle type expressions in boolean contexts#31
dnwpark merged 12 commits intomainfrom
special-bool

Conversation

@dnwpark
Copy link
Contributor

@dnwpark dnwpark commented Jan 13, 2026

  • Add _LiteralGeneric as the resulting type of IsSub and other <type-bool>. This handles execution in boolean contexts.
  • Add Matches[Lhs, Rhs] which is equivalent to IsSub[Lhs,Rhs] and IsSub[Rhs,Lhs]
  • Add Bool which converts Literal to _LiteralGeneric (eg. Bool[Literal[False]]
  • Add AllOf, which checks that all elements of a <variadic-type-arg> + are one of Literal[True] or _LiteralGeneric[True].
  • Add AnyOf, which checks that any element of a <variadic-type-arg> + is one of Literal[True] or _LiteralGeneric[True].

Example:

type ContainsAnyInt[Ts] = AnyOf[*[Matches[t, int] for t in Iter[Ts]]]

Notes:

  • Used AnyOf instead of Any because it would clash with typing.Any.
  • Any typing eval that resolves to a bool gets wrapped in a _LiteralGeneric. This is because type NotBool[T] = not Bool[T] will always resolve to a bool.
  • When evaluating type aliases, the globals are wrapped in _GlobalsWrapper which forces the immediate execution of any generics. This allows things like:
type IsIntBool[T] = Bool[IsSub[T, int]]
type IsIntLiteral[T] = Literal[True] if IsIntBool[T] else Literal[False]

This approach may allow us to entirely remove special forms for boolean types: #58

@dnwpark dnwpark requested a review from msullivan January 13, 2026 16:39
@dnwpark dnwpark marked this pull request as draft January 13, 2026 16:45
@msullivan
Copy link
Collaborator

I don't think this is what I was thinking; I don't really want to have a separate "kind" ("sort"?) of boolean type alias.

What I was thinking was maybe we have an abbreviation for Is[Literal[True], T] (or flipped; I'm not sure) or some such, to make it more convenient to use literal types as booleans.

I'm not sure how much it is needed. TypeScript doesn't have anything but subtype check for this

@dnwpark dnwpark marked this pull request as ready for review January 13, 2026 18:12
Comment on lines +181 to +189
@type_eval.register_evaluator(Bool)
def _eval_Bool(tp, *, ctx):
result = _eval_types(tp, ctx)
if isinstance(result, int) and not isinstance(result, bool):
# Python type computations can sometimes return int instead of bool.
# For example, this happens if a type alias is an any/all expression.
return bool(result)
return result

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never trigger, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nevermind, that is fully wrong

Comment on lines +28 to +41
result = evaluator(self)
if isinstance(result, bool):
return result
elif isinstance(result, int):
# Python type computations can sometimes return int instead of
# bool. See: _eval_Bool
return bool(result)
elif (
hasattr(result, '__origin__')
and result.__origin__ is typing.Literal
):
return typing.get_args(result)[0]

raise RuntimeError(f"Expected bool or Literal[bool], got {result}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all this logic should be in _eval_Bool?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified the whole thing so that all we do is "pass through" when in a type computation context and unpack literals when a bool context.

Tried a few things, but I don't think there's a reasonable way to prevent users from doing things like:

type JustFalse = Literal[False]
type ShouldBeStr = (int if JustFalse else str)

We'll just have to document this well.

@_SpecialForm
def Is(self, params):
"""A boolean special form of IsSubSimilar."""
return _BoolGenericAlias(self, params)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we return Bool[IsSubSimilar[*params]]?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some other shortened name we can give IsSubSimilar here, also...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed my changes because you renamed Is to Sub.

Is there still a reason to remove the special form IsX classes? ie.

class IsSubtype[Child, Parent]:
    pass
class IsSubSimilar[Child, Parent]:
    pass

@dnwpark dnwpark force-pushed the special-bool branch 2 times, most recently from d194a31 to 3da742c Compare January 15, 2026 17:48
@vercel
Copy link

vercel bot commented Jan 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
python-typemap Ready Ready Preview, Comment Jan 29, 2026 1:30am


_SpecialForm: typing.Any = typing._SpecialForm
from .type_eval._special_form import (
_BoolGenericAlias,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't depend on type_eval from inside our typing.py. The idea is that these typing.py changes will go into the real typing.py, and so must not depend on the eval library, which will probably be third-party.

typing.special_form_evaluator in particular is part of the public API we are proposing, because it is how third-party eval libraries will hook in to handle bool/iter evals

return False


class _LiteralGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should subtype the real typing._LiteralGenericAlias (and be named something else like _BooleanLiteralGeneric), so that it displays nicely

Comment on lines +259 to +264
if _typing_inspect.is_generic_alias(tp):
if tp.__origin__ is typing.Literal:
return _LiteralGeneric[bool(tp.__args__[0])]
elif tp.__origin__ is _LiteralGeneric:
return _LiteralGeneric[bool(tp.__args__[0])]
raise TypeError(f"Expected Literal type, got {tp}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My inclination is that this should be equivalent to IsSub[T, Literal[True]] and not IsSub[T, Never]

@dnwpark dnwpark merged commit 9f04b91 into main Jan 29, 2026
5 checks passed
@dnwpark dnwpark deleted the special-bool branch January 29, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants