GitHub user carloea2 edited a discussion: py2wf: a compiler from plain Python 
scripts to operator DAG workflows (.py → .pyt → workflow)

Original Program:
```
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt

# Load the dataset
file_path = 'C:/Users/carlo/Downloads/diabetes_migration/diabetes.csv'
data = pd.read_csv(file_path)

# Remove duplicate rows
data = data.drop_duplicates()

# Remove rows with null values
data = data.dropna()

# Print the minimum, maximum, and mean for all fields
print("Minimum values:\n", data.min())
print("\nMaximum values:\n", data.max())
print("\nMean values:\n", data.mean())

# Create a boxplot for the 'Pregnancies' field
plt.figure(figsize=(8, 6))
plt.boxplot(data['Pregnancies'], vert=False, patch_artist=True)
plt.title('Boxplot of Pregnancies')
plt.xlabel('Number of Pregnancies')
plt.show()

# Full overview of the data
data.describe()

# Separate features and target variable
X = data.drop('Outcome', axis=1)
y = data['Outcome']

# Split data into training and testing sets (80% train, 20% test)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, 
random_state=42)

scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# Train Random Forest model
rf_model = RandomForestClassifier(random_state=42)
rf_model.fit(X_train, y_train)
rf_pred = rf_model.predict(X_test)
rf_accuracy = accuracy_score(y_test, rf_pred)
print(f"Random Forest Accuracy: {rf_accuracy:.2%}")

# Train SVM model
svm_model = SVC(random_state=42)
svm_model.fit(X_train, y_train)
svm_pred = svm_model.predict(X_test)
svm_accuracy = accuracy_score(y_test, svm_pred)
print(f"SVM Accuracy: {svm_accuracy:.2%}")

# Train Decision Tree model
dt_model = DecisionTreeClassifier(random_state=42)
dt_model.fit(X_train, y_train)
dt_pred = dt_model.predict(X_test)
dt_accuracy = accuracy_score(y_test, dt_pred)
print(f"Decision Tree Accuracy: {dt_accuracy:.2%}")

# Train Logistic Regression model
lr_model = LogisticRegression(random_state=42)
lr_model.fit(X_train, y_train)
lr_pred = lr_model.predict(X_test)
lr_accuracy = accuracy_score(y_test, lr_pred)
print(f"Logistic Regression Accuracy: {lr_accuracy:.2%}")
```

Manually Translated Workflow:
<img width="1105" height="1285" alt="image" 
src="https://github.com/user-attachments/assets/92c5e1e3-bfc6-4c08-bdf1-5d4e9e1e12d3";
 />

This tool result:
<img width="1254" height="1053" alt="image" 
src="https://github.com/user-attachments/assets/74295e85-4c77-4d4b-98a3-1d1fe0425ef1";
 />

## Summary

I have an MVP of `python_to_workflow` (under 
`py2udf/src/main/python/python_to_workflow/`): a compiler that takes an 
ordinary single-file Python script and turns it into a **runnable operator 
DAG**, using one simple, predictable rule:

> **Every statement becomes one operator.** Then, optionally, a heuristic fuses 
> **N contiguous statements into one operator** to bring the graph down to a 
> human-scale workflow.

The output is a `.pyt` file, still plain, executable Python, but structured so 
each operator is a function with explicit input/output ports, and from there a 
workflow JSON of stock `PythonUDFV2` operators that the engine accepts today.

## The 1-line-1-operator form

`pipeline.compile(source)` emits the finest-grained form, which we call **P⊥**:

- A single `_main()` orchestrator keeps control flow **visible**, 
`for`/`while`/`if` stay as ordinary statements in `_main`.
- **Every value-producing line becomes one operator function** `_<var>_N(ctx, 
live_in) -> live_out`, including loop predicates and iterables.
- **Ports are computed, not declared:** a liveness analysis determines each 
operator's inputs (names read that were bound earlier) and outputs (names 
written that are still live afterwards). A variable rebound after a branch *is* 
the merge, no special merge nodes.
- Imports, class/function definitions, and constants go to a shared **context** 
(`ctx`), registered by reference so operators can read them without threading.

Example (input → P⊥ shape):

```python
# input.py
data = load_rows()
total = 0
for r in data:
    total += r.amount
print(total)
```

```python
# P⊥ .pyt (abridged)
def _data_1(ctx):            return load_rows()
def _total_2(ctx):           return 0
def _total_3(ctx, total, r): return total + r.amount
def _print_4(ctx, total):    print(total)

def _main():
    ctx = _Ctx(); ...
    data = _data_1(ctx)
    total = _total_2(ctx)
    for r in data:
        total = _total_3(ctx, total, r)
    _print_4(ctx, total)
_main()
```

P⊥ is deliberately fine-grained.

## The grouping heuristic: N contiguous lines = 1 operator

One operator per statement is too many operators for a real canvas. The MVP 
coarsening rule is intentionally simple:

- A grouping maps each statement to a block id; **consecutive statements 
sharing a block id fuse into a single operator**.
- Fusing is a re-realization, not new codegen: the fused operator's body is the 
statements spliced together, and its ports are recomputed by the same liveness 
rule (inputs = reads entering the block, outputs = writes still live after the 
block).
- The default heuristic is a fixed chunk size N (with control-flow statements 
acting as natural boundaries), exposed as a knob. Contiguity in program order 
guarantees the fused graph stays acyclic.

So the whole MVP surface is: `compile(source)` → P⊥, and `compile(source, 
group=N)` → the same program as ⌈lines/N⌉-ish fused operators. Both outputs 
run, and both are verified against the original script.

## What the MVP requires of the input

To make "one statement = one operator" well-defined, a small normalization 
front-end runs first (all source-to-source, behavior-preserving): 
comprehensions become loops, functions are inlined toward the main body, and 
`global` state is rewritten into explicit parameter/return threading so every 
dependency between two lines is a visible variable. Scripts using features 
outside that subset are rejected rather than miscompiled.

## Non-goals (for this MVP)

- No data-parallel sharding decisions; each operator is a plain UDF.
- Single-file, synchronous Python only.

## Questions for discussion

- I know there's already work on translating python to workflows with LLMs. Is 
there room for a compiler-based version too? 
- Are we ok merging something this big? It's around 20k lines of compiler passes
- If there's interest,  Is anyone interested in reviewing this?




GitHub link: https://github.com/apache/texera/discussions/6618

----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to: [email protected]

Reply via email to