Skip to content

Commit 548ef90

Browse files
authored
Begin adding goal to assist with the call-by-name @rule syntax migration. (#20574)
As discussed in #20572, this change adds a built-in goal `migrate-call-by-name` which emits all `Get` calls which should be migrated to call-by-name syntax. A followup change should use the resulting information (`@rule` function pointers and `Get` signatures) to execute rewrites of the relevant files. Emits lines like: ``` <function infer_python_conftest_dependencies at 0x113633af0> Get(<class 'pants.engine.target.Targets'>, [<class 'pants.engine.addresses.Addresses'>]) -> <function resolve_targets at 0x112335d30> Get(<class 'pants.engine.internals.graph.Owners'>, [<class 'pants.engine.internals.graph.OwnersRequest'>]) -> <function find_owners at 0x112349160> Get(<class 'pants.backend.python.util_rules.ancestor_files.AncestorFiles'>, [<class 'pants.backend.python.util_rules.ancestor_files.AncestorFilesRequest'>]) -> <function find_ancestor_files at 0x1131e6790> ```
1 parent f15bbea commit 548ef90

File tree

8 files changed

+121
-11
lines changed

8 files changed

+121
-11
lines changed

src/python/pants/engine/internals/native_engine.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,9 @@ def validate_reachability(scheduler: PyScheduler) -> None: ...
722722
def rule_graph_consumed_types(
723723
scheduler: PyScheduler, param_types: Sequence[type], product_type: type
724724
) -> list[type]: ...
725+
def rule_graph_rule_gets(
726+
scheduler: PyScheduler,
727+
) -> dict[Callable, list[tuple[type, list[type], Callable]]]: ...
725728
def rule_graph_visualize(scheduler: PyScheduler, path: str) -> None: ...
726729
def rule_subgraph_visualize(
727730
scheduler: PyScheduler, param_types: Sequence[type], product_type: type, path: str

src/python/pants/engine/internals/scheduler.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dataclasses import dataclass
1010
from pathlib import PurePath
1111
from types import CoroutineType
12-
from typing import Any, Dict, Iterable, NoReturn, Sequence, cast
12+
from typing import Any, Callable, Dict, Iterable, NoReturn, Sequence, cast
1313

1414
from typing_extensions import TypedDict
1515

@@ -405,6 +405,9 @@ def visualize_graph_to_file(self, filename: str) -> None:
405405
def visualize_rule_graph_to_file(self, filename: str) -> None:
406406
self._scheduler.visualize_rule_graph_to_file(filename)
407407

408+
def rule_graph_rule_gets(self) -> dict[Callable, list[tuple[type, list[type], Callable]]]:
409+
return native_engine.rule_graph_rule_gets(self.py_scheduler)
410+
408411
def execution_request(
409412
self,
410413
requests: Sequence[tuple[type, Any | Params]],

src/python/pants/goal/builtins.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pants.goal import help
99
from pants.goal.builtin_goal import BuiltinGoal
1010
from pants.goal.explorer import ExplorerBuiltinGoal
11+
from pants.goal.migrate_call_by_name import MigrateCallByNameBuiltinGoal
1112

1213

1314
def register_builtin_goals(build_configuration: BuildConfiguration.Builder) -> None:
@@ -18,6 +19,7 @@ def builtin_goals() -> tuple[type[BuiltinGoal], ...]:
1819
return (
1920
BSPGoal,
2021
ExplorerBuiltinGoal,
22+
MigrateCallByNameBuiltinGoal,
2123
help.AllHelpBuiltinGoal,
2224
help.NoGoalHelpBuiltinGoal,
2325
help.ThingHelpBuiltinGoal,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import annotations
5+
6+
import logging
7+
8+
from pants.base.exiter import ExitCode
9+
from pants.base.specs import Specs
10+
from pants.build_graph.build_configuration import BuildConfiguration
11+
from pants.engine.unions import UnionMembership
12+
from pants.goal.builtin_goal import BuiltinGoal
13+
from pants.init.engine_initializer import GraphSession
14+
from pants.option.options import Options
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class MigrateCallByNameBuiltinGoal(BuiltinGoal):
20+
name = "migrate-call-by-name"
21+
help = "Migrate from `Get` syntax to call-by-name syntax. See #19730."
22+
23+
def run(
24+
self,
25+
build_config: BuildConfiguration,
26+
graph_session: GraphSession,
27+
options: Options,
28+
specs: Specs,
29+
union_membership: UnionMembership,
30+
) -> ExitCode:
31+
# Emit all `@rules` which use non-union Gets.
32+
for rule, dependencies in graph_session.scheduler_session.rule_graph_rule_gets().items():
33+
print(f"{rule}")
34+
for output_type, input_types, rule_dep in dependencies:
35+
print(f" Get({output_type}, {input_types}) -> {rule_dep}")
36+
37+
return 0

src/rust/engine/rule_graph/src/lib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,34 @@ impl<R: Rule> RuleGraph<R> {
307307
.collect()
308308
}
309309

310+
///
311+
/// Returns a mapping from a `Rule` to the dependencies it has that are themselves satisfied by
312+
/// `Rule`s.
313+
///
314+
#[allow(clippy::type_complexity)]
315+
pub fn rule_dependencies(&self) -> HashMap<&R, Vec<(&DependencyKey<R::TypeId>, &R)>> {
316+
let mut result = HashMap::default();
317+
for (entry, rule_edges) in &self.rule_dependency_edges {
318+
let EntryWithDeps::Rule(RuleEntry { rule, .. }) = entry.as_ref() else {
319+
continue;
320+
};
321+
322+
let mut function_satisfied_gets = Vec::new();
323+
for (dependency, source) in &rule_edges.dependencies {
324+
let Entry::WithDeps(entry_with_deps) = source.as_ref() else {
325+
continue;
326+
};
327+
let EntryWithDeps::Rule(RuleEntry { rule, .. }) = entry_with_deps.as_ref() else {
328+
continue;
329+
};
330+
331+
function_satisfied_gets.push((dependency, rule));
332+
}
333+
result.insert(rule, function_satisfied_gets);
334+
}
335+
result
336+
}
337+
310338
///
311339
/// Find the entrypoint in this RuleGraph for the given product and params.
312340
///

src/rust/engine/src/externs/interface.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ fn native_engine(py: Python, m: &PyModule) -> PyO3Result<()> {
125125

126126
m.add_function(wrap_pyfunction!(validate_reachability, m)?)?;
127127
m.add_function(wrap_pyfunction!(rule_graph_consumed_types, m)?)?;
128+
m.add_function(wrap_pyfunction!(rule_graph_rule_gets, m)?)?;
128129
m.add_function(wrap_pyfunction!(rule_graph_visualize, m)?)?;
129130
m.add_function(wrap_pyfunction!(rule_subgraph_visualize, m)?)?;
130131

@@ -1399,6 +1400,48 @@ fn rule_graph_consumed_types<'py>(
13991400
})
14001401
}
14011402

1403+
#[pyfunction]
1404+
fn rule_graph_rule_gets<'p>(py: Python<'p>, py_scheduler: &PyScheduler) -> PyO3Result<&'p PyDict> {
1405+
let core = &py_scheduler.0.core;
1406+
core.executor.enter(|| {
1407+
let result = PyDict::new(py);
1408+
for (rule, rule_dependencies) in core.rule_graph.rule_dependencies() {
1409+
let Rule::Task(task) = rule else { continue };
1410+
1411+
let function = &task.func;
1412+
let mut dependencies = Vec::new();
1413+
for (dependency_key, rule) in rule_dependencies {
1414+
// NB: We are only migrating non-union Gets, which are those in the `gets` list
1415+
// which do not have `in_scope_params` marking them as being for unions, or a call
1416+
// signature marking them as already being call-by-name.
1417+
if dependency_key.call_signature.is_some()
1418+
|| dependency_key.in_scope_params.is_some()
1419+
|| !task.gets.contains(dependency_key)
1420+
{
1421+
continue;
1422+
}
1423+
let Rule::Task(task) = rule else { continue };
1424+
1425+
let provided_params = dependency_key
1426+
.provided_params
1427+
.iter()
1428+
.map(|p| p.as_py_type(py))
1429+
.collect::<Vec<_>>();
1430+
dependencies.push((
1431+
dependency_key.product.as_py_type(py),
1432+
provided_params,
1433+
task.func.0.value.into_py(py),
1434+
));
1435+
}
1436+
if dependencies.is_empty() {
1437+
continue;
1438+
}
1439+
result.set_item(function.0.value.into_py(py), dependencies.into_py(py))?;
1440+
}
1441+
Ok(result)
1442+
})
1443+
}
1444+
14021445
#[pyfunction]
14031446
fn rule_graph_visualize(py_scheduler: &PyScheduler, path: PathBuf) -> PyO3Result<()> {
14041447
let core = &py_scheduler.0.core;

src/rust/engine/src/externs/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub struct PyFailure(pub Failure);
6666
impl PyFailure {
6767
fn get_error(&self, py: Python) -> PyErr {
6868
match &self.0 {
69-
Failure::Throw { val, .. } => val.into_py(py),
69+
Failure::Throw { val, .. } => PyErr::from_value(val.as_ref().as_ref(py)),
7070
f @ (Failure::Invalidated | Failure::MissingDigest { .. }) => {
7171
EngineError::new_err(format!("{f}"))
7272
}

src/rust/engine/src/python.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,15 +378,9 @@ impl From<PyObject> for Value {
378378
}
379379
}
380380

381-
impl From<PyErr> for Value {
382-
fn from(py_err: PyErr) -> Self {
383-
Python::with_gil(|py| Value::new(py_err.into_py(py)))
384-
}
385-
}
386-
387-
impl IntoPy<PyErr> for &Value {
388-
fn into_py(self, py: Python) -> PyErr {
389-
PyErr::from_value((*self.0).as_ref(py))
381+
impl IntoPy<PyObject> for &Value {
382+
fn into_py(self, py: Python) -> PyObject {
383+
(*self.0).as_ref(py).into_py(py)
390384
}
391385
}
392386

0 commit comments

Comments
 (0)