Skip to content

Commit 2a3ec14

Browse files
committed
fix: write-without-read validator now exempts control-flow branches
Operators with a `skip` field (inside if_/else_ branches) are mutually exclusive at runtime, so writing the same field from different branches is legitimate. The validator now skips these ops when checking for write-without-read violations. Bump version to 0.2.4.
1 parent a2f3c43 commit 2a3ec14

4 files changed

Lines changed: 28 additions & 4 deletions

File tree

apple/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.3"
1+
__version__ = "0.2.4"

apple/tests/test_validator.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ def test_read_then_write_ok(self):
6464
function_for_common="g", function_for_item="")
6565
flow.compile() # should not raise
6666

67+
def test_if_else_branches_write_same_field_ok(self):
68+
"""Mutually exclusive if/else branches may write the same field."""
69+
flow = Flow(name="ok", common_input=["x"], common_output=["salt"])
70+
flow.if_("x ~= nil") \
71+
._add_op("transform_by_lua",
72+
common_input=["x"], common_output=["salt"],
73+
lua_script="function f() return x end",
74+
function_for_common="f", function_for_item="") \
75+
.else_() \
76+
._add_op("transform_by_lua",
77+
common_output=["salt"],
78+
lua_script="function g() return 'default' end",
79+
function_for_common="g", function_for_item="") \
80+
.end_if_()
81+
flow.compile() # should not raise
82+
6783

6884
class TestDeadCode:
6985
def test_dead_operator(self):

apple/validator.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,20 @@ def validate_write_without_read(
102102
) -> None:
103103
"""Detect writing a field that already exists (from upstream operator output)
104104
without reading it. Flow-contract inputs are not flagged — operators are
105-
allowed to output fields that match flow inputs without reading them."""
106-
# Track fields written by operators (not flow contract)
105+
allowed to output fields that match flow inputs without reading them.
106+
107+
Operators inside a control-flow branch (``skip`` is set) are exempt from
108+
*being flagged* AND their outputs do not count as "already written" for
109+
downstream checks. This is because mutually exclusive branches (if/else)
110+
may legitimately write the same field without reading it."""
111+
# Track fields written by operators (not flow contract).
112+
# Only unconditional (non-skip) operators contribute.
107113
written_by_ops_common: set[str] = set()
108114
written_by_ops_item: set[str] = set()
109115

110116
for name, op in ops:
117+
if op.skip:
118+
continue # branch-internal ops are exempt
111119
for field in op.common_output:
112120
if field.startswith("_"):
113121
continue

version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ package pine
22

33
// Version is the Pineapple engine version, embedded in every compiled JSON config.
44
// This is the single source of truth — all Go code should reference this constant.
5-
const Version = "0.2.3"
5+
const Version = "0.2.4"

0 commit comments

Comments
 (0)