|
ESBMC
Efficient SMT-based Context-Bounded Model Checker
|
Functions | |
| None | _emit_error (str source_filename, int|str line, str message) |
| None | reject_unsupported_threading_usage (ast.AST tree, str source_filename) |
| tuple[set[str], set[str]] | _collect_thread_aliases (ast.AST tree) |
| bool | _is_thread_constructor (ast.Call call_node, set[str] module_aliases, set[str] thread_aliases) |
| bool | _is_thread_construction_with_target_kw (ast.expr|None value, set[str] module_aliases, set[str] thread_aliases) |
| bool | _base_is_thread (ast.expr base, set[str] module_aliases, set[str] thread_aliases) |
| dict[str, ast.ClassDef] | _collect_thread_subclass_defs (ast.AST tree, set[str] module_aliases, set[str] thread_aliases) |
| bool | _is_subclass_constructor (ast.Call call_node, set[str] subclass_names) |
| tuple[str|None, ast.expr|None] | _extract_name_binding (ast.stmt stmt) |
| str | _call_class_name (ast.Call call_node) |
| bool | _is_super_call_expr (ast.AST node) |
| bool | _is_super_init_call_stmt (ast.stmt stmt) |
| str|None | _target_name_chain (ast.AST node) |
| list[list[ast.stmt]] | _scope_bodies (ast.Module tree) |
| list[ast.stmt] | _collect_scope_statements (list[ast.stmt] body) |
| set[str] | _declared_outer_names (list[ast.stmt] body) |
| Iterator[ast.AST] | _iter_scope_nodes (list[ast.stmt] body) |
| Iterator[list[ast.stmt]] | _iter_scope_stmt_lists (list[ast.stmt] body) |
| dict[int, ast.AST] | _parent_map (ast.AST tree) |
| bool | _inside_loop_within_scope (ast.AST node, dict[int, ast.AST] parents) |
| dict[str, int] | _scope_name_assign_counts (list[ast.stmt] body) |
| None | validate_threading_thread_usage (ast.Module tree, str source_filename) |
| str|None | _thread_method_receiver (ast.AST node, set[str] thread_var_names, str method) |
| tuple[dict[int, dict[str, tuple[int, ast.Call]]], int] | _collect_thread_var_sites (ast.Module tree, set[str] module_aliases, set[str] thread_aliases, int starting_id=0) |
| tuple[dict[str, tuple[int, ast.Call]], int] | _collect_subclass_sites (ast.Module tree, set[str] subclass_names, int starting_id) |
| tuple[dict[int, dict[str, tuple[int, ast.Call]]], int] | _collect_func_subclass_sites (ast.Module tree, set[str] subclass_names, int starting_id) |
| None | _hoist_func_subclass_binding (list[ast.stmt] scope_body, str var_name, str obj_name) |
| tuple[ast.expr, list[ast.expr]] | _thread_call_keywords (ast.Call call_node) |
| ast.expr | _build_target_call (ast.expr target_expr, int site_id, int n_args) |
| ast.FunctionDef | _build_trampoline (int site_id, ast.expr target_expr, int n_args) |
| ast.FunctionDef | _build_subclass_trampoline (int site_id, str var_name) |
| ast.AnnAssign | _build_obj_declaration (str obj_name, str class_name) |
| ast.Assign | _build_tid_declaration (int site_id) |
| None | _strip_thread_inheritance (ast.ClassDef class_node, set[str] module_aliases, set[str] thread_aliases) |
| ast.stmt | _build_arg_declaration (int site_id, int arg_index, ast.expr arg_value) |
| ast.Assign | _build_arg_assignment (int site_id, int arg_index, ast.expr value) |
| ast.Call | _build_bare_thread_call (ast.Call original_call) |
| list[ast.stmt] | _build_start_statements (int site_id) |
| ast.Expr | _build_join_call (int site_id) |
| list[ast.stmt] | _rewrite_construction_stmt (ast.stmt stmt, ast.Call call_node, int site_id) |
| None | lower_threading_thread_usage (ast.Module tree, str source_filename) |
| int|None | _find_binding_stmt_index (ast.Module tree, ast.Call call_node) |
| None | _rewrite_body_in_place (list[ast.stmt] body, dict[str, tuple[int, ast.Call]] scope_sites, set[str] module_aliases, set[str] thread_aliases) |
| list[ast.stmt]|None | _try_rewrite_statement (ast.stmt stmt, dict[str, tuple[int, ast.Call]] scope_sites, set[str] module_aliases, set[str] thread_aliases) |
Variables | |
| SUPPORTED_THREADING_SYMBOLS = frozenset({"Lock", "Thread"}) | |
| UNSUPPORTED_THREAD_KWARGS = frozenset({"daemon", "name", "kwargs", "group"}) | |
| THREAD_OVERRIDE_REJECTED_METHODS = frozenset({"start", "join"}) | |
| tuple | _SCOPE_NODES |
| lineno | |
| body | |
Threading validation and lowering helpers for the Python parser frontend.
|
protected |
Return ``True`` iff ``base`` resolves to ``threading.Thread``. Recognises both ``threading.Thread`` (qualified through an ``import threading [as X]`` alias) and the bare name when it came in via ``from threading import Thread [as X]``.
|
protected |
Return ``__pythread_arg_<N>_<i> = <value>`` as an AST node. The deep-copied ``value`` is the construction-site expression; the assignment runs in the user's calling frame just before the rewritten bare ``Thread()`` construction so module globals carry the construction-time value into the spawned thread.
|
protected |
Return a module-top declaration for ``__pythread_arg_<N>_<i>``.
A plain ``Assign`` or an ``AnnAssign`` with a non-null ``value`` is
required so the ESBMC Python frontend creates a *module-level*
binding: an annotation-only declaration silently degrades to a
function-local at the first construction-site write, and the
spawned trampoline would then read an uninitialised global rather
than the user-supplied argument.
The declared shape is chosen by ``arg_value`` (the construction-
site expression, treated read-only):
* Numeric / string constants — emit ``= <zero-of-that-kind>`` so
the symbol's inferred type matches the construction-site rebind
and the trampoline call site type-checks against the target's
parameter.
* Anything else (Name references, attribute chains, calls,
list/dict/set literals, ``None``) — emit ``: object = None``.
``object`` lowers to ``any_type()`` (``void *``) in the Python
frontend, so a class-instance arg carrying a struct pointer is
forwarded to the trampoline call site without the
int-degradation that ``= 0`` would force.
|
protected |
Return a no-kwargs ``Thread()`` call mirroring ``original_call.func``. Preserves the original constructor reference (``threading.Thread`` vs ``MyThread``) so the Python frontend's class resolution still finds the skeleton, but strips the ``target=`` / ``args=`` keywords so the skeleton's zero-arg ``__init__`` accepts the call.
|
protected |
Return ``__pyt_join(__pythread_tid_<N>)`` as a statement.
|
protected |
Return ``<obj_name>: <class_name> = None`` as a module-top declaration. The ``= None`` is load-bearing. A value-less annotation gives the symbol the *value* type ``<class_name>``, and a value-typed module global rebound from a function is not observed by the spawned thread — it reads the pre-assignment contents. Initialising to ``None`` types the symbol as ``<class_name> *`` instead, matching what a module-scope ``<name> = <class_name>(...)`` produces, and the rebind is then visible across threads. Constructing here instead would run the user's ``__init__`` a second time with made-up arguments.
|
protected |
Return the atomic spawn-and-init sequence replacing ``t.start()``.
Emits, in order:
1. ``global __pythread_tid_<N>`` — binds the tid assignment to
the module-level slot the prelude declared, even when
``t.start()`` is called inside a function scope.
2. ``__ESBMC_atomic_begin()``.
3. ``__pythread_tid_<N> = __ESBMC_spawn_thread(__pythread_trampoline_<N>)``.
4. ``__pyt_init_tid(__pythread_tid_<N>)``.
5. ``__ESBMC_atomic_end()``.
The atomic block is essential: without it, the spawned trampoline
can run (and reach ``__pyt_terminate``, setting
``pthread_thread_ended[tid] = 1``) between the spawn and the init,
after which ``__pyt_init_tid`` would reset the ended flag back to
0 and the subsequent ``__pyt_join`` would deadlock-falsely.
|
protected |
Synthesise the trampoline for a Thread subclass instance bound to ``var_name``.
Body is::
global <var_name>
<var_name>.run()
__pyt_terminate()
The ``global`` lets the trampoline read the user's module-level
instance binding (validator C2 guarantees module scope). The
trampoline must be inserted *after* the user's ``<var_name> = X(...)``
statement so the Python frontend resolves the type of ``<var_name>``
by the time it sees the trampoline body.
|
protected |
Return an AST for ``<target>(<arg0>, ..., <argN-1>)``. ``<argI>`` is the module-level global ``__pythread_arg_<site>_<i>`` populated at construction time. ``<target>`` is a deep copy of the original ``target=`` chain so the trampoline calls the same function the user named.
|
protected |
Return ``__pythread_tid_<N> = 0`` as a module-top assignment.
|
protected |
Synthesise the per-site Python trampoline ``FunctionDef``.
Body is::
target(__pythread_arg_<N>_0, ..., __pythread_arg_<N>_n-1)
__pyt_terminate()
The trampoline is invoked by ``__ESBMC_spawn_thread`` as a spawned
thread; ``__pyt_terminate`` marks the thread ended so ``__pyt_join``
can observe completion. The leading ``global`` declaration is what
lets the trampoline read the construction-site arg globals — without
it Python's name resolution treats every ``__pythread_arg_<N>_<i>``
as a local lookup, which the ESBMC frontend rejects with an
undefined-variable error.
|
protected |
Return the class name from a ``Name(...)`` call for error messages.
|
protected |
Return ``({id(scope_body): {var_name: (site_id, call_node)}}, next_id)``.
The function-scope counterpart of :func:`_collect_subclass_sites`.
Module-top bindings are excluded — they keep the simpler lowering
that reads the user's own global — so every site returned here needs
the hoist performed by :func:`_hoist_func_subclass_binding`.
|
protected |
Yield every statement in a scope, descending control-flow constructs. Descends into ``If``/``For``/``While``/``With``/``Try`` and their ``orelse``/``finalbody``/handler bodies (those share the enclosing scope's name bindings) but stops at nested ``FunctionDef`` / ``AsyncFunctionDef`` / ``ClassDef`` — those introduce new scopes that the caller already handles via :func:`_scope_bodies`.
|
protected |
Return ``({var_name: (site_id, call_node)}, next_id)`` for module-top subclass bindings.
Validator C2 guarantees subclass instance bindings are module-scope
only, so we walk ``tree.body`` (not the per-scope helper) and the
returned map is flat — every entry is a module-global var name
visible to every other scope. Site ids continue from
``starting_id`` so target= and subclass sites share one id space
and the generated trampoline / tid names never collide.
|
protected |
Return ``(module_aliases, thread_aliases)`` for the ``threading`` module. ``module_aliases`` are the names bound by ``import threading [as X]``; ``thread_aliases`` are the names bound by ``from threading import Thread [as X]``. The two sets disambiguate qualified (``X.Thread``) from bare (``X``) references at every Thread construction site.
|
protected |
Return ``{class_name: ClassDef}`` for module-top direct subclasses.
Only direct subclasses (``class X(threading.Thread)``) at module
scope are picked up. Transitive subclasses (``class B(A)`` where
``A`` is itself a Thread subclass) and nested subclass defs are out
of MVP scope; the validator's C1 rule rejects nested defs and
transitive subclassing fails the constructor-recognition check at
the call site.
|
protected |
Return ``({id(scope_body): {var_name: (site_id, construction_call)}}, next_id)``.
Each scope (module top, each function body) gets its own inner map
because the validator's single-definition check is per-scope: the
same name ``t`` may legally bind a Thread in two distinct function
scopes, and each binding must allocate its own site id (otherwise
the second scope's ``t.start()`` would silently reuse the first
scope's trampoline). Site ids are assigned in deterministic source
order so the generated globals / trampolines stay stable across
parser runs.
``starting_id`` lets the caller chain this collector with the
subclass-site collector under one continuous id space.
|
protected |
Return names bound outside a scope via ``global``/``nonlocal``, nested scopes included. Nested declarations count. ``Global``/``Nonlocal`` hold their names as plain strings, so the hoist's ``Name`` rename cannot rewrite them: an inner ``nonlocal w`` would survive pointing at a name the rename has just retired.
|
protected |
Emit a parser-stage threading error and abort.
|
protected |
Return ``(target_name, value)`` for ``Name = value`` / ``Name: T = value``. Returns ``(None, None)`` for tuple-targets, attribute-targets, augmented assigns, and any other statement shape. Used wherever we need to ask "is this a simple single-name binding, and what does it assign?"
|
protected |
Return the module-top index of the Assign/AnnAssign whose value is ``call_node``.
|
protected |
Rename a function-local subclass instance to the module global ``obj_name``. The spawned trampoline runs at module scope and can only reach the instance through a global, so the local binding is renamed rather than copied: one object, no aliasing question about whether the frontend assigns instances by reference. Both passes stop at nested scope boundaries — an inner ``def``'s parameter or local of the same name is a different variable, and renaming it would either orphan the parameter or redirect the inner binding at this scope's thread object. Annotations on the target are dropped: a name cannot be both annotated and ``global``, and re-annotating inside the function would restore the value type that :func:`_build_obj_declaration` exists to avoid.
|
protected |
Return ``True`` iff ``node`` is inside a loop body in its own scope. Walks the ``parents`` chain rooted at ``node`` and stops at a scope boundary (``FunctionDef``/``AsyncFunctionDef``/``Lambda``/``ClassDef`` /``Module``). Loop ancestors are ``For``/``AsyncFor``/``While`` plus the comprehension forms (``ListComp``/``SetComp``/``DictComp``/ ``GeneratorExp``) — each re-evaluates the node per iteration, which the per-site converter state cannot represent.
|
protected |
Return ``True`` iff ``call_node`` constructs a tracked Thread subclass.
|
protected |
Return ``True`` iff ``node`` is a call whose function is ``super().X``.
Matches the AST shape ``Call(func=Attribute(value=Call(func=Name('super'))))``
for any attribute. Used to enumerate every super-method call inside
a subclass body so the validator can refuse the ones we cannot
soundly strip.
|
protected |
Return ``True`` iff ``stmt`` is ``super().__init__()`` at statement level. Strict: zero positional args, zero keywords, both for the ``super(...)`` call (must be bare ``super()``) and the ``.__init__(...)`` call. Anything richer falls through to the validator, which refuses non-trivial super calls.
|
protected |
Return ``True`` iff ``value`` is a real ``Thread(target=...)`` call. Used by the threading-rewrite path to recognise the construction arm: a ``threading.Thread`` call with no positional arguments and a ``target=`` keyword. A user subclass whose ``__init__`` accepts a parameter named ``target`` is excluded by the ``_is_thread_constructor`` check.
|
protected |
Return ``True`` iff ``call_node`` constructs ``threading.Thread``. Recognises both ``threading.Thread(...)`` (qualified through an ``import threading [as X]`` alias) and ``Thread(...)`` (bare, through a ``from threading import Thread [as X]`` alias).
|
protected |
Yield every node reachable from ``body`` without crossing a scope boundary.
|
protected |
Yield ``body`` and every statement list nested below it in the same scope.
|
protected |
Build a child-to-parent map for ``tree`` keyed by ``id(node)``. ``ast`` does not expose parent pointers; the validator needs them to answer "is this Thread construction lexically inside a loop body within its own scope" without walking the whole tree per call site.
|
protected |
Walk ``body`` recursively, rewriting this scope's Thread sites. ``scope_sites`` is the inner map from :func:`_collect_thread_var_sites` for *this* scope only — sibling scopes are visited separately so their Thread variables with the same name resolve to distinct site ids. Descends into control-flow constructs (``If``/``For``/``While`` /``With``/``Try``) but not into nested defs (those are independent scopes already enrolled in the outer site map). ``module_aliases`` / ``thread_aliases`` are forwarded so the construction-rewrite predicate in :func:`_try_rewrite_statement` can require a real ``threading.Thread`` call rather than misfiring on a subclass constructor whose own ``__init__`` happens to take a ``target=`` keyword argument.
|
protected |
Return the replacement statements for a Thread construction.
The original ``t = Thread(target=f, args=(x, y))`` becomes::
global __pythread_arg_<N>_0, __pythread_arg_<N>_1
__pythread_arg_<N>_0 = x
__pythread_arg_<N>_1 = y
t = Thread()
The leading ``global`` ensures the arg writes bind to the module-
level placeholders the prelude declared, not to function locals
that the spawned trampoline could never see. (For module-scope
constructions the ``global`` is redundant but harmless.) The spawn
happens later at ``t.start()``, by which time the args are visible
to the spawned trampoline (cross-thread visibility follows from
the spawn happens-before edge).
|
protected |
Return the statement list of every Python scope in ``tree``. A "scope" is a region with its own local-name binding rules: the module top level and the body of every (sync or async) function definition. Class bodies and lambdas are intentionally excluded: Thread variables cannot meaningfully live in either under the MVP.
|
protected |
Count direct Name-target value bindings per name within a single scope. Counts every ``Assign``, ``AnnAssign`` *with* a value, and ``AugAssign`` whose target is a plain ``Name``, descending into control-flow bodies but not into nested defs. ``AnnAssign`` without a value (e.g. ``t: Thread``) is intentionally skipped: Python evaluates the annotation but does not bind a value, so it is not a definition. The result drives the single-definition check that lets the converter bind each Thread variable to a unique site id without runtime resolution. Rebinds via ``def t(...)``, ``class t``, ``import t``, ``for t in ...``, ``with ... as t``, and walrus expressions are not counted — those are acceptable MVP gaps documented in :func:`validate_threading_thread_usage`.
|
protected |
Remove ``threading.Thread`` from ``class_node.bases`` and neuter ``super().__init__()``.
Two in-place transforms required to make the user's subclass
survive the Python frontend's class converter:
* Strip every base that resolves to ``threading.Thread``. The
converter cannot resolve the operational-model ``Thread``
skeleton as a base (probe: ``nlohmann::json`` key-not-found
assertion). After stripping, ``class X(threading.Thread)``
becomes ``class X:`` (implicit ``object`` base).
* Replace any bare ``super().__init__()`` statement inside
``__init__`` with ``Pass``. The converter cannot resolve
``super().__init__()`` on an implicit ``object`` base either
(probe: ``_init_undefined`` / ``migrate expr failed``). The
validator (C5) refuses every other ``super(...)`` shape, so
we only need to neutralise the bare-init shape here.
|
protected |
Return the dotted form of a ``Name``/``Attribute`` chain, else ``None``.
Used to enforce the MVP rule that ``Thread(target=...)`` resolves to
a statically-known function. ``Name('f')`` returns ``"f"``;
``Attribute(Name('m'), 'f')`` returns ``"m.f"``; anything else
(lambdas, calls, subscripts, arbitrary expressions) returns ``None``.
|
protected |
Return ``(target_expr, list_of_arg_exprs)`` for a validated Thread call. ``args=`` (when omitted) yields an empty arg list. The validator has already proven ``target=`` is present and ``args=`` (if present) is a tuple literal, so this function only does the lookup.
|
protected |
Return the receiver var name iff ``node`` is ``var.<method>()`` on a tracked Thread. Recognises ``Name(var).method()`` shape only; attribute-chain receivers (``obj.t.start()``) are out of MVP scope. Returning the name directly (rather than a bool) lets the caller skip a separate type-narrowing pass on ``call.func``.
|
protected |
Rewrite a single Thread construction / start / join statement.
Returns ``None`` if ``stmt`` is unrelated to threading; otherwise
returns the replacement statement list. ``scope_sites`` is the
per-scope ``{var_name: (site_id, call_node)}`` map.
The construction-rewrite arm requires the call to be a real
``threading.Thread`` constructor (via :func:`_is_thread_constructor`)
— *not* merely "has a ``target=`` keyword". A user subclass whose
``__init__`` accepts a parameter named ``target`` would otherwise
have its keyword silently dropped by :func:`_build_bare_thread_call`,
a soundness defect.
| None parser.threading_lowering.lower_threading_thread_usage | ( | ast.Module | tree, |
| str | source_filename ) |
Rewrite ``threading.Thread`` usage into pthread-backed intrinsics.
Runs after :func:`validate_threading_thread_usage` has refused every
unsupported shape. For each accepted ``Thread(...)`` construction
site:
* allocate a unique site id;
* emit module-level globals ``__pythread_arg_<N>_<i>`` for each
``args=`` element and a ``__pythread_tid_<N>`` slot for the
spawned thread's id;
* emit a module-level trampoline ``__pythread_trampoline_<N>``
whose body calls the user-supplied target with the per-site
arg globals, then calls ``__pyt_terminate``;
* rewrite the construction statement to populate the arg globals
and bind the user's name to a bare ``Thread()``;
* rewrite every ``t.start()`` call to an atomic block that
captures the spawned tid into ``__pythread_tid_<N>`` and calls
``__pyt_init_tid``, then rewrite every ``t.join()`` call to
``__pyt_join(__pythread_tid_<N>)``.
Producing module-level globals (rather than function-locals
captured by closure) was the fix for #4571's three soundness
defects: it gives the spawned trampoline an unambiguous reader of
the construction-site values, and ``__pyt_join`` provides
the real synchronisation edge symex's interleaving search needs.
| None parser.threading_lowering.reject_unsupported_threading_usage | ( | ast.AST | tree, |
| str | source_filename ) |
Refuse to compile programs using unsupported ``threading`` names. Walks the AST for usages of names from the ``threading`` module that ESBMC does not yet model and exits with a clear error rather than silently emitting a weaker abstraction. The supported set is ``SUPPORTED_THREADING_SYMBOLS``. Detects three import shapes: ``import threading`` → ``threading.<X>`` attribute access ``import threading as t`` → ``t.<X>`` attribute access ``from threading import X`` → bare ``X`` reference ``from threading import *`` is refused outright because static name resolution would require importing the real ``threading`` module.
| None parser.threading_lowering.validate_threading_thread_usage | ( | ast.Module | tree, |
| str | source_filename ) |
Refuse ``threading.Thread`` usage outside the MVP-supported shape.
ESBMC's Thread lowering (tracked in #4568) is intentionally narrow so
no concurrency model can be silently weakened. This pass refuses,
with a precise error message and a non-zero exit, every construct
outside that shape:
* subclassing ``threading.Thread``;
* positional arguments to ``Thread()`` (real signature is
``Thread(group, target, name, args, ...)`` and ESBMC only
accepts the keyword form);
* ``**kwargs`` splats and ``daemon=``/``name=``/``kwargs=``/
``group=`` keyword arguments;
* ``Thread()`` constructions with no ``target=`` keyword;
* ``target=`` values that are not a ``Name`` or dotted attribute
chain (lambdas, calls, runtime variables);
* ``args=`` values that are not a tuple literal (lists, sets,
runtime variables);
* ``Thread()`` constructions lexically inside a loop body within
their own scope (per-site converter state would be aliased);
* reassignment of a name that already binds a ``Thread`` instance
within the same scope (breaks the single-site binding the
``.start()`` / ``.join()`` lowering relies on).
Structurally-valid ``Thread()`` constructions are passed through to
:func:`lower_threading_thread_usage`, which rewrites them into
module-level globals, a per-site Python trampoline, and calls to
the C intrinsics ``__ESBMC_spawn_thread`` / ``__pyt_join`` so
symex's interleaving search can explore real concurrent behaviour.
Known gaps (acceptable for the MVP — soundness is preserved because
each is either over-rejected by the structural fallback or has no
silent failure path):
* a ``target=`` bound to a runtime variable holding a callable is
syntactically a ``Name`` and passes ``_target_name_chain`` —
AST-only validation cannot distinguish a function name from a
variable-bound callable without a name-resolution pass. The
lowering generates a direct call by name; a mismatch surfaces
as an undefined-symbol error at conversion time, not silent
weakening of the concurrency model;
* tuple-target assigns (``a, b = Thread(...), Thread(...)``) and
walrus-bound Threads are not counted by the reassignment check;
* rebinds via ``def``/``class``/``import``/``for``-target/
``with``-target are not counted by the reassignment check;
* Thread-subclass name collision with a function-local class of
the same name — the C2 walk matches subclass constructors by
bare ``func.id``, so a function-local ``class Worker:`` that
shadows the module-top ``class Worker(threading.Thread)`` has
its ``Worker(...)`` calls wrongly refused. This is a false
positive (valid code rejected), not a false verdict; the user
can rename the local class. Resolving it would require a
name-resolution pass.
|
protected |
| parser.threading_lowering.body |
| parser.threading_lowering.lineno |
| parser.threading_lowering.SUPPORTED_THREADING_SYMBOLS = frozenset({"Lock", "Thread"}) |
| parser.threading_lowering.THREAD_OVERRIDE_REJECTED_METHODS = frozenset({"start", "join"}) |
| parser.threading_lowering.UNSUPPORTED_THREAD_KWARGS = frozenset({"daemon", "name", "kwargs", "group"}) |