Skip to content

Commit 7af5790

Browse files
authored
Emit a warning if a result is implicitly ignored (#127)
1 parent 21189eb commit 7af5790

File tree

5 files changed

+181
-9
lines changed

5 files changed

+181
-9
lines changed

examples/callback/repair_prompt.pdl

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
- |
2-
Given the following code:
3-
```python
4-
${code_line}
5-
```
6-
and the following error:
7-
${error_msg}
8-
Please repair the code!
1+
- text: |
2+
Given the following code:
3+
```python
4+
${code_line}
5+
```
6+
and the following error:
7+
${error_msg}
8+
Please repair the code!
9+
contribute: [context]
910

1011
- def: raw_output
1112
model: watsonx/ibm/granite-34b-code-instruct

src/pdl/pdl_analysis.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import sys
2+
from dataclasses import dataclass
3+
from typing import Sequence
4+
5+
from .pdl_ast import (
6+
AdvancedBlockType,
7+
ArrayBlock,
8+
Block,
9+
BlockType,
10+
CallBlock,
11+
CodeBlock,
12+
ContributeTarget,
13+
DataBlock,
14+
EmptyBlock,
15+
ErrorBlock,
16+
ForBlock,
17+
FunctionBlock,
18+
GetBlock,
19+
IfBlock,
20+
IncludeBlock,
21+
LastOfBlock,
22+
MessageBlock,
23+
ModelBlock,
24+
ObjectBlock,
25+
Program,
26+
ReadBlock,
27+
RepeatBlock,
28+
RepeatUntilBlock,
29+
TextBlock,
30+
)
31+
from .pdl_ast_utils import iter_block_children
32+
33+
34+
@dataclass
35+
class UnusedConfig:
36+
implicit_ignore: bool
37+
38+
def with_implicit_ignore(self, b):
39+
return UnusedConfig(implicit_ignore=b)
40+
41+
42+
_DISPLAY_UNUSED_HINT = True
43+
44+
45+
def unused_warning(block: BlockType):
46+
global _DISPLAY_UNUSED_HINT # pylint: disable= global-statement
47+
print(f"Warning: the result of block `{block}` is not used.", file=sys.stderr)
48+
if _DISPLAY_UNUSED_HINT:
49+
_DISPLAY_UNUSED_HINT = False
50+
print(
51+
" You might want to use a `text` block around the list or explicitly ignore the result with `contribute: [context]`.",
52+
file=sys.stderr,
53+
)
54+
55+
56+
def unused_program(prog: Program) -> None:
57+
state = UnusedConfig(implicit_ignore=False)
58+
unused_advanced_block(state, LastOfBlock(lastOf=prog.root))
59+
60+
61+
def unused_block(state, block: BlockType) -> None:
62+
if not isinstance(block, Block):
63+
if state.implicit_ignore:
64+
unused_warning(block)
65+
return
66+
unused_advanced_block(state, block)
67+
68+
69+
def unused_advanced_block(state: UnusedConfig, block: AdvancedBlockType) -> None:
70+
if block.assign is not None:
71+
state = state.with_implicit_ignore(False)
72+
if ContributeTarget.RESULT not in block.contribute:
73+
state = state.with_implicit_ignore(False)
74+
match block:
75+
case LastOfBlock():
76+
if not isinstance(block.lastOf, str) and isinstance(block.lastOf, Sequence):
77+
state_with_ignore = state.with_implicit_ignore(True)
78+
for b in block.lastOf[:-1]:
79+
unused_block(state_with_ignore, b)
80+
unused_block(state, block.lastOf[-1])
81+
else:
82+
unused_block(state, block.lastOf)
83+
# Leaf blocks without side effects
84+
case DataBlock() | FunctionBlock() | GetBlock() | ModelBlock() | ReadBlock():
85+
if state.implicit_ignore:
86+
unused_warning(block)
87+
return
88+
# Leaf blocks with side effects
89+
case CallBlock() | CodeBlock() | EmptyBlock() | ErrorBlock():
90+
return
91+
# Non-leaf blocks
92+
case (
93+
ArrayBlock()
94+
| ForBlock()
95+
| IfBlock()
96+
| IncludeBlock()
97+
| MessageBlock()
98+
| ObjectBlock()
99+
| RepeatBlock()
100+
| RepeatUntilBlock()
101+
| TextBlock()
102+
):
103+
iter_block_children((lambda b: unused_block(state, b)), block)
104+
case _:
105+
assert False

src/pdl/pdl_parser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import yaml
55
from pydantic import ValidationError
66

7+
from .pdl_analysis import unused_program
78
from .pdl_ast import LocationType, PDLException, Program
89
from .pdl_location_utils import get_line_map
910
from .pdl_schema_error_analyzer import analyze_errors
@@ -25,6 +26,7 @@ def parse_str(pdl_str: str, file_name: str = "") -> tuple[Program, LocationType]
2526
loc = LocationType(path=[], file=file_name, table=line_table)
2627
try:
2728
prog = Program.model_validate(prog_yaml)
29+
unused_program(prog)
2830
except ValidationError as exc:
2931
pdl_schema_file = Path(__file__).parent / "pdl-schema.json"
3032
with open(pdl_schema_file, "r", encoding="utf-8") as schema_fp:

tests/test_examples_parse.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pathlib
22

3+
from pytest import CaptureFixture
4+
35
from pdl.pdl_parser import PDLParseError, parse_file
46

57
EXPECTED_INVALID = [
@@ -14,15 +16,20 @@
1416
]
1517

1618

17-
def test_valid_programs() -> None:
19+
def test_valid_programs(capsys: CaptureFixture[str]) -> None:
1820
actual_invalid: set[str] = set()
21+
with_warnings: set[str] = set()
1922
for yaml_file_name in pathlib.Path(".").glob("**/*.pdl"):
2023
try:
2124
_ = parse_file(yaml_file_name)
25+
captured = capsys.readouterr()
26+
if len(captured.err) > 0:
27+
with_warnings |= {str(yaml_file_name)}
2228
except PDLParseError:
2329
actual_invalid |= {str(yaml_file_name)}
2430
expected_invalid = set(str(p) for p in EXPECTED_INVALID)
2531
unexpected_invalid = sorted(list(actual_invalid - expected_invalid))
2632
assert len(unexpected_invalid) == 0, unexpected_invalid
2733
unexpected_valid = sorted(list(expected_invalid - actual_invalid))
2834
assert len(unexpected_valid) == 0, unexpected_valid
35+
assert len(with_warnings) == 0

tests/test_implicit_ignore.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from pdl.pdl import exec_str
2+
3+
4+
def do_test(capsys, test):
5+
result = exec_str(test["prog"])
6+
captured = capsys.readouterr()
7+
warnings = {line.strip() for line in captured.err.split("\n")} - {
8+
"You might want to use a `text` block around the list or explicitly ignore the result with `contribute: [context]`."
9+
}
10+
assert result == test["result"]
11+
assert set(warnings) == set(test["warnings"])
12+
13+
14+
def test_strings(capsys):
15+
test = {
16+
"prog": """
17+
- Hello
18+
- How are you?
19+
- Bye
20+
""",
21+
"result": "Bye",
22+
"warnings": [
23+
"Warning: the result of block `Hello` is not used.",
24+
"Warning: the result of block `How are you?` is not used.",
25+
"",
26+
],
27+
}
28+
do_test(capsys, test)
29+
30+
31+
def test_no_warning1(capsys):
32+
test = {
33+
"prog": """
34+
text:
35+
- Hello
36+
- How are you?
37+
- Bye
38+
""",
39+
"result": "HelloHow are you?Bye",
40+
"warnings": [""],
41+
}
42+
do_test(capsys, test)
43+
44+
45+
def test_no_warning2(capsys):
46+
test = {
47+
"prog": """
48+
- def: x
49+
text: Hello
50+
- text: How are you?
51+
contribute: []
52+
- Bye
53+
""",
54+
"result": "Bye",
55+
"warnings": [""],
56+
}
57+
do_test(capsys, test)

0 commit comments

Comments
 (0)