Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,47 @@ docs = [
srdatalog = "srdatalog.cli:main"

# ---------------------------------------------------------------------------
# Plugin entry points (srdatalog.plugins)
# Plugin entry points (PR-1 split — per docs/phase_decomposition_redesign.md
# § 3.3.4)
#
# Phase E validation: built-in dialects ship as discoverable plugins via
# the same `srdatalog.plugins` entry-point group used by external
# packages. `Compiler.with_default_plugins()` walks this group at
# bootstrap. Spec: docs/phase_e_plugin_extensibility.md §2, §5.
# Two production groups + a legacy back-compat group:
#
# `srdatalog.dialects` — data-side dialect contributions (target-
# agnostic IR: ops, pragmas, lowerings). Walked by
# `Compiler.with_default_plugins()`.
# `srdatalog.targets` — render-side target contributions (per-target
# `@register_render(op, target=T)` registrations). Walked by
# `Compiler.with_default_plugins()`. Empty today — built-in CUDA
# target stays loaded via the `sorted_array` dialect's
# `_register_passes` until subsequent PRs separate render
# registrations into a `srdatalog.codegen.cuda:register` entry.
# `srdatalog.plugins` — LEGACY group. Walked for one release cycle
# with a `DeprecationWarning`. External plugins that haven't
# migrated continue to work.
#
# Spec: docs/phase_e_plugin_extensibility.md §2 + docs/phase_decomposition
# _redesign.md § 3.3.4.
# ---------------------------------------------------------------------------

[project.entry-points."srdatalog.plugins"]
[project.entry-points."srdatalog.dialects"]
d2l = "srdatalog.ir.dialects.relation.d2l:register"
iir_cf = "srdatalog.ir.dialects.iir.cf:register"
sorted_array = "srdatalog.ir.dialects.relation.sorted_array:register"
parallel_data = "srdatalog.ir.dialects.parallel.data:register"
parallel_atomic_ws = "srdatalog.ir.dialects.parallel.atomic_ws:register"
parallel_block_group = "srdatalog.ir.dialects.parallel.block_group:register"

[project.entry-points."srdatalog.targets"]
# (no entries yet — this group exists so external CPU/TBB / SYCL /
# WASM targets can register. PR-2+ wires the built-in CUDA target
# under a `srdatalog.codegen.cuda:register` entry.)

# LEGACY group — kept for back-compat for one release cycle per the
# § 3.3.4 transition contract. External plugins (e.g. the jaccard
# demo) discoverable here keep working; loading emits a
# `DeprecationWarning` exactly once per process.
[project.entry-points."srdatalog.plugins"]

[project.urls]
Homepage = "https://github.com/anthropics/srdatalog-python"
Issues = "https://github.com/anthropics/srdatalog-python/issues"
Expand Down
3 changes: 3 additions & 0 deletions src/srdatalog/ir/codegen/cuda/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def compile_kernel_body(
rel_index_types: dict[str, str] | None = None,
tiled_cartesian: bool = False,
bg_enabled: bool = False,
target: str = 'cuda',
) -> str:
'''Emit the operator() body for one kernel — view_decls followed by
the dialect-emitted kernel logic. Caller is responsible for the
Expand Down Expand Up @@ -159,8 +160,10 @@ def compile_kernel_body(
rel_index_types=rel_index_types,
tiled_cartesian=tiled_cartesian,
bg_enabled=bg_enabled,
target=target,
),
pipeline=DEFAULT_KERNEL_PIPELINE,
target=target,
)
body_text = state.body_text
assert body_text is not None, 'compile_kernel_body: CudaRenderShim left body_text unset'
Expand Down
2 changes: 1 addition & 1 deletion src/srdatalog/ir/codegen/cuda/complete_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
has_tiled_cartesian_eligible,
)
from srdatalog.ir.codegen.cuda.plugin import plugin_gen_host_view_setup, plugin_view_count
from srdatalog.ir.mir.passes import ep_has_work_stealing

# Pure-template phase emitters now live in the dialect's runner module.
# Local aliases preserve the legacy call sites until the rest of this
Expand Down Expand Up @@ -75,6 +74,7 @@
compute_total_view_count,
source_spec_key,
)
from srdatalog.ir.mir.passes import ep_has_work_stealing

# -----------------------------------------------------------------------------
# Source spec extraction helpers (mirror Nim's inline lambdas)
Expand Down
97 changes: 97 additions & 0 deletions src/srdatalog/ir/codegen/cuda/lower_ctx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'''CudaRenderCtx — CUDA-render-side identifier scratch state.

Spec: `docs/phase_decomposition_redesign.md` § 3.2.1 (Wave T1).

Today's dialect-level `LoweringCtx` (in
`src/srdatalog/ir/dialects/relation/sorted_array/lowerings/__init__.py`)
mixes generic lowering-walk scope state (bound vars, indent, counter)
with CUDA C++ identifier scratch (view variable names, output variable
name, per-handle pre-narrow info, …). The spec calls out the second
category as CUDA-render-specific — strings that the CUDA renderer's
own helpers emit and consume. Those fields belong here, under
`codegen/cuda/`, not on a target-agnostic `LoweringCtx`.

Per `docs/code_discipline.md` D10, the framework `LowerCtx` (in
`core/lower_ctx.py`) is pinned at 5 fields. This file ships the
CUDA-render-side companion: a target-private dataclass aggregating
every CUDA identifier the lowering walk produces. When future targets
(CPU/TBB, SYCL, …) come online, each ships its own analogous
`<target>RenderCtx`; the lowering walk threads the right one
based on the active target.

The PR-1 transition (per `docs/phase_decomposition_redesign.md` § 6.1)
keeps the dialect `LoweringCtx` as the surface every lowering helper
sees — the CUDA-render fields are still accessible via
`ctx.<field>` for byte-equivalence (the existing helpers don't change
their reads), AND via the canonical `ctx.render_ctx.<field>` form.
Future PRs migrate every read to the canonical form; PR-1 establishes
the shape.
'''

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass
class CudaRenderCtx:
'''CUDA-render-side identifier scratch.

Every field is a CUDA C++ identifier or a per-handle CUDA-render
side-table. Nothing here is consumed by a non-CUDA target.

Fields:
view_var_names — handle_idx (str) -> view variable name
(`view_<rel>_<handle_idx>`).
output_var — name of the single-output OutputContext
variable.
output_var_overrides — per-relation OutputContext name map for
multi-head rules.
view_slot_bases — handle_idx (str) -> base slot in `views[]`.
Populated from `emit_view_declarations`
so D2L segment-loop emission can
reference the right HEAD/FULL pair.
rel_index_types — rel_name -> custom index type code (e.g.,
`Device2LevelIndex`). Empty string /
missing entry = plain DSAI.
tiled_cartesian_valid_var
— name of the C++ ballot validity var
threaded by `_lower_nested_cart` when
rendering the tiled-mode body.
ws_cartesian_valid_var — name of the C++ WS ballot validity var.
ws_cartesian_bound_vars
— names of C++ WS-loop-bound vars.
neg_pre_narrow — handle_idx -> NegPreNarrowInfo (pre-
narrowed handle info for Negations
following a Cartesian).
dedup_hash_vars — names of C++ vars feeding the dedup hash
key. (Carried for parity with the legacy
CodeGenContext field; today populated by
the runner-side plumbing, not by the
lowering walk itself.)
debug — emit `Comment` nodes describing the
source MIR shape for each emitted block.
'''

view_var_names: dict[str, str] = field(default_factory=dict)
output_var: str = 'output'
output_var_overrides: dict[str, str] = field(default_factory=dict)
view_slot_bases: dict[str, int] = field(default_factory=dict)
rel_index_types: dict[str, str] = field(default_factory=dict)
tiled_cartesian_valid_var: str = ''
ws_cartesian_valid_var: str = ''
ws_cartesian_bound_vars: list[str] = field(default_factory=list)
# `neg_pre_narrow` is typed loosely (`dict[int, Any]`) because the
# value class — `NegPreNarrowInfo` — exists in two parallel forms
# today: the legacy CUDA-side `codegen.cuda.context.NegPreNarrowInfo`
# and the dialect-side `relation.sorted_array.lowerings.NegPreNarrowInfo`.
# Both carry the same payload; a future PR collapses the duplicate.
# Until then, `Any` lets both forms flow through without type-check
# noise. The runtime behavior is identical.
neg_pre_narrow: dict[int, Any] = field(default_factory=dict)
dedup_hash_vars: list[str] = field(default_factory=list)
debug: bool = True


__all__ = ['CudaRenderCtx']
64 changes: 54 additions & 10 deletions src/srdatalog/ir/core/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
PluginConflictError,
PluginInfo,
PluginLoadError,
_discover_all_groups,
_discover_entry_points,
_info_for,
_topo_sort_plugins,
Expand Down Expand Up @@ -159,7 +160,13 @@ def dialects(self) -> list[Dialect]:
'''All registered dialects, in registration order.'''
return list(self._dialects.values())

def run(self, prog: Any, *, pipeline: list[Any]) -> Any:
def run(
self,
prog: Any,
*,
pipeline: list[Any],
target: str = 'cuda',
) -> Any:
'''Drive a list of `Pass` instances over `prog`. Returns the
(possibly transformed) prog.

Expand All @@ -171,9 +178,19 @@ def run(self, prog: Any, *, pipeline: list[Any]) -> Any:

Per `docs/compiler_redesign.md` §4 and the R4 research report:
pipelines are data; ordering errors are caught up-front.

PR-1 (per `docs/phase_decomposition_redesign.md` § 3.3.1): the
`target` kwarg threads the active render target into the per-
pass through-state when the state carries a `target` field
(e.g. `KernelCtx`). This lets `RenderShim` dispatch the per-op
`@register_render(target=T)` resolution without hardcoding the
target. Default preserves the current single-target ('cuda')
behavior.
'''
# Local import to avoid a circular at module-load time
# (passes.py imports Compiler from this module).
import dataclasses

from srdatalog.ir.core.passes import PassOrderingError

available: set[str] = {d.name for d in self.dialects}
Expand All @@ -183,6 +200,16 @@ def run(self, prog: Any, *, pipeline: list[Any]) -> Any:
raise PassOrderingError(p.name, needed, i)
available |= set(p.produces)

# If the through-state dataclass exposes a `target` field, thread
# the active target into it. This keeps the per-pass shims target-
# parametric: each shim reads `state.target` to drive per-target
# dispatch (e.g. RenderShim's @register_render lookup).
if dataclasses.is_dataclass(prog) and not isinstance(prog, type):
if any(f.name == 'target' for f in dataclasses.fields(prog)):
current = getattr(prog, 'target', None)
if current != target:
prog = dataclasses.replace(prog, target=target)

for p in pipeline:
prog = p.apply(prog, self)
return prog
Expand Down Expand Up @@ -252,11 +279,16 @@ def _resolve_plugin(plugin: Any) -> tuple[str, Callable[[Any], None]]:
`register.plugin_name` if set, else `register.__name__`.
'''
if isinstance(plugin, str):
# Look up by entry-point name in the default group.
for ep in _discover_entry_points():
# Look up by entry-point name across every registered group
# (dialects + targets + legacy). PR-1 (per § 3.3.4) split the
# legacy group; the resolver follows.
for ep in _discover_all_groups():
if ep.name == plugin:
return ep.name, ep.load()
raise LookupError(f'no plugin named {plugin!r} in entry-point group {ENTRY_POINT_GROUP!r}')
raise LookupError(
f'no plugin named {plugin!r} across entry-point groups '
f"('srdatalog.dialects', 'srdatalog.targets', {ENTRY_POINT_GROUP!r})"
)

if hasattr(plugin, 'name') and hasattr(plugin, 'load'):
# EntryPoint-shaped (importlib.metadata.EntryPoint or a test fake).
Expand All @@ -276,23 +308,35 @@ def _resolve_plugin(plugin: Any) -> tuple[str, Callable[[Any], None]]:
# -- discovery factory -----------------------------------------------------

@classmethod
def with_default_plugins(cls, *, group: str = ENTRY_POINT_GROUP) -> Compiler:
def with_default_plugins(cls, *, group: str | None = None) -> Compiler:
'''Construct a `Compiler` and auto-discover all entry-point plugins.

Walks `importlib.metadata.entry_points(group=group)`,
topo-sorts by each plugin's `provides` / `requires` attributes
Walks the production entry-point groups (per PR-1, per
`docs/phase_decomposition_redesign.md` § 3.3.4):

- `srdatalog.dialects` — data-side dialect contributions.
- `srdatalog.targets` — render-side target contributions.
- `srdatalog.plugins` — legacy group (kept for back-compat
for one release cycle; emits `DeprecationWarning` when
non-empty).

Topo-sorts by each plugin's `provides` / `requires` attributes
on the `register` callable (defaulting to `()` when absent),
then calls `register_plugin` on each in order.

The `group` keyword exists for test isolation — production code
uses the default. See
`docs/phase_e_plugin_extensibility.md` §2.
uses the default (walks all groups). Passing a specific group
name walks only that group; passing `None` (the default) walks
every registered group.

See `docs/phase_e_plugin_extensibility.md` §2 and
`docs/phase_decomposition_redesign.md` § 3.3.4.
'''
compiler = cls()

# Discover, then resolve each EP to (name, register_fn) once so
# we can both topo-sort and register without double-loading.
eps = _discover_entry_points(group)
eps = _discover_entry_points(group) if group is not None else _discover_all_groups()
resolved: dict[str, tuple[Any, Callable[[Any], None]]] = {}
infos: list[PluginInfo] = []
for ep in eps:
Expand Down
Loading
Loading