Move dg_stack into delta_generator.py and ensure it's never empty
diff --git a/lib/streamlit/__init__.py b/lib/streamlit/__init__.py
index 49791c066473..4c53855f54fe 100644
--- a/lib/streamlit/__init__.py
+++ b/lib/streamlit/__init__.py
@@ -66,8 +66,12 @@
 # Give the package a version.
 __version__ = _STREAMLIT_VERSION_STRING
 
-from streamlit.delta_generator import DeltaGenerator as _DeltaGenerator
-from streamlit.proto.RootContainer_pb2 import RootContainer as _RootContainer
+from streamlit.delta_generator import (
+    main_dg as _main_dg,
+    sidebar_dg as _sidebar_dg,
+    event_dg as _event_dg,
+    bottom_dg as _bottom_dg,
+)
 from streamlit.runtime.caching import (
     cache_resource as _cache_resource,
     cache_data as _cache_data,
@@ -121,15 +125,15 @@ def _update_logger() -> None:
 _config.on_config_parsed(_update_logger, True)
 
 
-_main = _DeltaGenerator(root_container=_RootContainer.MAIN)
-sidebar = _DeltaGenerator(root_container=_RootContainer.SIDEBAR, parent=_main)
-_event = _DeltaGenerator(root_container=_RootContainer.EVENT, parent=_main)
-_bottom = _DeltaGenerator(root_container=_RootContainer.BOTTOM, parent=_main)
-
 secrets = _secrets_singleton
 
 # DeltaGenerator methods:
 
+_main = _main_dg
+sidebar = _sidebar_dg
+_event = _event_dg
+_bottom = _bottom_dg
+
 altair_chart = _main.altair_chart
 area_chart = _main.area_chart
 audio = _main.audio
diff --git a/lib/streamlit/delta_generator.py b/lib/streamlit/delta_generator.py
index 4b64bb57a442..eec5371d41fd 100644
--- a/lib/streamlit/delta_generator.py
+++ b/lib/streamlit/delta_generator.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import sys
+from contextvars import ContextVar
 from copy import deepcopy
 from typing import (
     TYPE_CHECKING,
@@ -93,7 +94,6 @@
 from streamlit.proto.RootContainer_pb2 import RootContainer
 from streamlit.runtime import caching, legacy_caching
 from streamlit.runtime.scriptrunner import get_script_run_ctx
-from streamlit.runtime.scriptrunner.script_run_context import dg_stack
 from streamlit.runtime.state import NoValue
 
 if TYPE_CHECKING:
@@ -313,7 +313,7 @@ def _active_dg(self) -> DeltaGenerator:
             # We're being invoked via an `st.foo` pattern - use the current
             # `with` dg (aka the top of the stack).
             current_stack = dg_stack.get()
-            if len(current_stack) > 0:
+            if len(current_stack) > 1:
                 return current_stack[-1]
 
         # We're being invoked via an `st.sidebar.foo` pattern - ignore the
@@ -781,6 +781,19 @@ def _arrow_add_rows(
         return self
 
 
+main_dg = DeltaGenerator(root_container=RootContainer.MAIN)
+sidebar_dg = DeltaGenerator(root_container=RootContainer.SIDEBAR, parent=main_dg)
+event_dg = DeltaGenerator(root_container=RootContainer.EVENT, parent=main_dg)
+bottom_dg = DeltaGenerator(root_container=RootContainer.BOTTOM, parent=main_dg)
+
+# The dg_stack tracks the currently active DeltaGenerator, and is pushed to when
+# a DeltaGenerator is entered via a `with` block. This is implemented as a ContextVar
+# so that different threads or async tasks can have their own stacks.
+dg_stack: ContextVar[tuple[DeltaGenerator, ...]] = ContextVar(
+    "dg_stack", default=(main_dg,)
+)
+
+
 def _prep_data_for_add_rows(
     data: Data,
     delta_type: str,
diff --git a/lib/streamlit/elements/form.py b/lib/streamlit/elements/form.py
index f13818c04e5c..b6e10ef7e175 100644
--- a/lib/streamlit/elements/form.py
+++ b/lib/streamlit/elements/form.py
@@ -21,7 +21,6 @@
 from streamlit.proto import Block_pb2
 from streamlit.runtime.metrics_util import gather_metrics
 from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
-from streamlit.runtime.scriptrunner.script_run_context import dg_stack
 from streamlit.runtime.state import WidgetArgs, WidgetCallback, WidgetKwargs
 
 if TYPE_CHECKING:
@@ -42,6 +41,9 @@ def _current_form(this_dg: DeltaGenerator) -> FormData | None:
     To find the current form, we walk up the dg_stack until we find
     a DeltaGenerator that has FormData.
     """
+    # Avoid circular imports.
+    from streamlit.delta_generator import dg_stack
+
     if not runtime.exists():
         return None
 
diff --git a/lib/streamlit/runtime/scriptrunner/script_run_context.py b/lib/streamlit/runtime/scriptrunner/script_run_context.py
index 030ca861d7b3..d723203dba5b 100644
--- a/lib/streamlit/runtime/scriptrunner/script_run_context.py
+++ b/lib/streamlit/runtime/scriptrunner/script_run_context.py
@@ -15,7 +15,6 @@
 from __future__ import annotations
 
 import collections
-import contextvars
 import threading
 from dataclasses import dataclass, field
 from typing import Callable, Counter, Dict, Final, Union
@@ -37,14 +36,6 @@
 UserInfo: TypeAlias = Dict[str, Union[str, None]]
 
 
-# The dg_stack tracks the currently active DeltaGenerator, and is pushed to when
-# a DeltaGenerator is entered via a `with` block. This is implemented as a ContextVar
-# so that different threads or async tasks can have their own stacks.
-dg_stack: contextvars.ContextVar[
-    tuple["streamlit.delta_generator.DeltaGenerator", ...]
-] = contextvars.ContextVar("dg_stack", default=tuple())
-
-
 @dataclass
 class ScriptRunContext:
     """A context object that contains data for a "script run" - that is,

This work started out as an attempt to fix some bugs that currently exist in `feature/st.experimental_fragment`,
but we may want to merge it directly into `develop` as it potentially improves how understandable some of our
`DeltaGenerator`-related code is.

Currently, `dg_stack` is a bit weird in that the way that it works is that the main `DeltaGenerator` is special and is
never contained in it. In other words, `dg_stack` is the empty tuple when the `main` `DeltaGenerator` is active.
When inside of one or more `with` blocks corresponding to containers (which by construction must be descendants
of the `main` container), `dg_stack` is then populated.

This PR changes the way this works so that `dg_stack` is initialized to a singleton tuple containing only the `main`
container, and at that point everything works as before. The benefit of this is that there's never any discrepancy
between the state of `dg_stack` and the currently active `DeltaGenerator`, which should make things easier
to reason about.