diff --git a/examples/hello/hello-type.pdl b/examples/hello/hello-type.pdl index 9f6514312..7d13fd180 100644 --- a/examples/hello/hello-type.pdl +++ b/examples/hello/hello-type.pdl @@ -9,7 +9,8 @@ text: language: str spec: int return: - - "\nTranslate the sentence '${ sentence }' to ${ language }\n" + lastOf: + - "\nTranslate the sentence '${ sentence }' to ${ language }.\n" - model: replicate/ibm-granite/granite-3.0-8b-instruct parameters: stop_sequences: "\n" diff --git a/examples/talk/4-function.pdl b/examples/talk/4-function.pdl index 15751c436..2d33c387f 100644 --- a/examples/talk/4-function.pdl +++ b/examples/talk/4-function.pdl @@ -5,11 +5,12 @@ text: sentence: str language: str return: - - "\nTranslate the sentence '${ sentence }' to ${ language }.\n" - - model: replicate/ibm-granite/granite-3.0-8b-instruct - parameters: - stop_sequences: "\n" - temperature: 0 + lastOf: + - "\nTranslate the sentence '${ sentence }' to ${ language }.\n" + - model: replicate/ibm-granite/granite-3.0-8b-instruct + parameters: + stop_sequences: "\n" + temperature: 0 - call: translate args: sentence: I love Paris! diff --git a/examples/talk/8-tools.pdl b/examples/talk/8-tools.pdl index c90faba59..ac6468ef7 100644 --- a/examples/talk/8-tools.pdl +++ b/examples/talk/8-tools.pdl @@ -31,6 +31,7 @@ text: - "\n" - if: ${ action.name == "Calc" } then: - - "Obs: " - - lang: python - code: result = ${ action.arguments.expr } + text: + - "Obs: " + - lang: python + code: result = ${ action.arguments.expr } diff --git a/examples/tools/calc.pdl b/examples/tools/calc.pdl index c90faba59..ac6468ef7 100644 --- a/examples/tools/calc.pdl +++ b/examples/tools/calc.pdl @@ -31,6 +31,7 @@ text: - "\n" - if: ${ action.name == "Calc" } then: - - "Obs: " - - lang: python - code: result = ${ action.arguments.expr } + text: + - "Obs: " + - lang: python + code: result = ${ action.arguments.expr } diff --git a/examples/tutorial/function_definition.pdl b/examples/tutorial/function_definition.pdl index 15751c436..fd47bc40d 100644 --- a/examples/tutorial/function_definition.pdl +++ b/examples/tutorial/function_definition.pdl @@ -5,7 +5,8 @@ text: sentence: str language: str return: - - "\nTranslate the sentence '${ sentence }' to ${ language }.\n" + - text: "\nTranslate the sentence '${ sentence }' to ${ language }.\n" + contribute: [context] - model: replicate/ibm-granite/granite-3.0-8b-instruct parameters: stop_sequences: "\n" diff --git a/examples/tutorial/grouping_definitions.pdl b/examples/tutorial/grouping_definitions.pdl index 88cf3e05b..81dd7060f 100644 --- a/examples/tutorial/grouping_definitions.pdl +++ b/examples/tutorial/grouping_definitions.pdl @@ -5,7 +5,8 @@ defs: sentence: str language: str return: - - "\nTranslate the sentence '${ sentence }' to ${ language }.\n" + - text: "\nTranslate the sentence '${ sentence }' to ${ language }.\n" + contribute: [context] - model: replicate/ibm-granite/granite-3.0-8b-instruct parameters: stop_sequences: "\n" diff --git a/examples/tutorial/muting_block_output.pdl b/examples/tutorial/muting_block_output.pdl index ce1b0bf80..842cc01dd 100644 --- a/examples/tutorial/muting_block_output.pdl +++ b/examples/tutorial/muting_block_output.pdl @@ -5,7 +5,8 @@ defs: sentence: str language: str return: - - "\nTranslate the sentence '${ sentence }' to ${ language }.\n" + - text: "\nTranslate the sentence '${ sentence }' to ${ language }.\n" + contribute: [context] - model: replicate/ibm-granite/granite-3.0-8b-instruct parameters: stop_sequences: "\n" diff --git a/src/pdl/pdl_analysis.py b/src/pdl/pdl_analysis.py index 91aa501f7..5c31a0333 100644 --- a/src/pdl/pdl_analysis.py +++ b/src/pdl/pdl_analysis.py @@ -6,6 +6,7 @@ AdvancedBlockType, ArrayBlock, Block, + BlocksType, BlockType, CallBlock, CodeBlock, @@ -29,41 +30,66 @@ TextBlock, ) from .pdl_ast_utils import iter_block_children +from .pdl_dumper import blocks_to_dict, dump_yaml @dataclass class UnusedConfig: implicit_ignore: bool + implicit_lastOf: bool # pylint: disable=invalid-name def with_implicit_ignore(self, b): - return UnusedConfig(implicit_ignore=b) + return UnusedConfig(implicit_ignore=b, implicit_lastOf=self.implicit_lastOf) + + def with_implicit_lastOf(self, b): # pylint: disable=invalid-name + return UnusedConfig(implicit_ignore=self.implicit_ignore, implicit_lastOf=b) _DISPLAY_UNUSED_HINT = True def unused_warning(block: BlockType): - global _DISPLAY_UNUSED_HINT # pylint: disable= global-statement - print(f"Warning: the result of block `{block}` is not used.", file=sys.stderr) + global _DISPLAY_UNUSED_HINT # pylint: disable=global-statement + print( + f"Warning: the result of block `{dump_yaml(blocks_to_dict(block, json_compatible=True))}` is not used.", + file=sys.stderr, + ) if _DISPLAY_UNUSED_HINT: _DISPLAY_UNUSED_HINT = False print( - " You might want to use a `text` block around the list or explicitly ignore the result with `contribute: [context]`.", + " You might want to use a `text` block around the list or explicitly ignore the result with a `lastOf` block or `contribute: [context]`.", file=sys.stderr, ) def unused_program(prog: Program) -> None: - state = UnusedConfig(implicit_ignore=False) - unused_advanced_block(state, LastOfBlock(lastOf=prog.root)) + try: + state = UnusedConfig(implicit_ignore=False, implicit_lastOf=True) + unused_blocks(state, prog.root) + except Exception as exc: + print(f"Unexpected error in implicit ignored analysis: {exc}") + + +def unused_blocks(state: UnusedConfig, blocks: BlocksType) -> None: + if not isinstance(blocks, str) and isinstance(blocks, Sequence): + if state.implicit_lastOf: + state_with_ignore = state.with_implicit_ignore(True) + for b in blocks[:-1]: + unused_block(state_with_ignore, b) + unused_block(state, blocks[-1]) + else: + for b in blocks: + unused_block(state, b) + else: + unused_block(state, blocks) -def unused_block(state, block: BlockType) -> None: - if not isinstance(block, Block): +def unused_block(state, blocks: BlockType) -> None: + if isinstance(blocks, Block): + unused_advanced_block(state, blocks) + else: if state.implicit_ignore: - unused_warning(block) - return - unused_advanced_block(state, block) + unused_warning(blocks) def unused_advanced_block(state: UnusedConfig, block: AdvancedBlockType) -> None: @@ -72,34 +98,54 @@ def unused_advanced_block(state: UnusedConfig, block: AdvancedBlockType) -> None if ContributeTarget.RESULT not in block.contribute: state = state.with_implicit_ignore(False) match block: - case LastOfBlock(): - if not isinstance(block.lastOf, str) and isinstance(block.lastOf, Sequence): - state_with_ignore = state.with_implicit_ignore(True) - for b in block.lastOf[:-1]: - unused_block(state_with_ignore, b) - unused_block(state, block.lastOf[-1]) - else: - unused_block(state, block.lastOf) - # Leaf blocks without side effects - case DataBlock() | FunctionBlock() | GetBlock() | ModelBlock() | ReadBlock(): + case ArrayBlock() | LastOfBlock() | ObjectBlock() | TextBlock(): if state.implicit_ignore: unused_warning(block) - return - # Leaf blocks with side effects - case CallBlock() | CodeBlock() | EmptyBlock() | ErrorBlock(): - return - # Non-leaf blocks + state = state.with_implicit_lastOf(False) + iter_block_children( + (lambda blocks: used_blocks(state, blocks)), + block, + ) + # Leaf blocks case ( - ArrayBlock() - | ForBlock() - | IfBlock() - | IncludeBlock() + DataBlock() + | FunctionBlock() + | GetBlock() | MessageBlock() - | ObjectBlock() - | RepeatBlock() - | RepeatUntilBlock() - | TextBlock() + | ModelBlock() + | CallBlock() + | CodeBlock() + | ReadBlock() ): - iter_block_children((lambda b: unused_block(state, b)), block) + if state.implicit_ignore: + unused_warning(block) + state = state.with_implicit_ignore(False).with_implicit_lastOf(True) + iter_block_children( + (lambda blocks: unused_blocks(state, blocks)), + block, + ) + case EmptyBlock(): + state = state.with_implicit_ignore(False).with_implicit_lastOf(True) + iter_block_children( + (lambda blocks: unused_blocks(state, blocks)), + block, + ) + # Non-leaf blocks + case IfBlock() | IncludeBlock(): + state = state.with_implicit_lastOf(True) + iter_block_children((lambda blocks: unused_blocks(state, blocks)), block) + # Loops blocks + case ForBlock() | RepeatBlock() | RepeatUntilBlock(): + iter_block_children((lambda blocks: unused_blocks(state, blocks)), block) + case ErrorBlock(): + pass case _: assert False + + +def used_blocks(state: UnusedConfig, blocks: BlocksType) -> None: + if not isinstance(blocks, str) and isinstance(blocks, Sequence): + for block in blocks: + unused_block(state.with_implicit_ignore(False), block) + else: + unused_block(state.with_implicit_ignore(False), blocks) diff --git a/src/pdl/pdl_ast_utils.py b/src/pdl/pdl_ast_utils.py index d8af00658..8a764319b 100644 --- a/src/pdl/pdl_ast_utils.py +++ b/src/pdl/pdl_ast_utils.py @@ -32,69 +32,69 @@ ) -def iter_block_children(f: Callable[[BlockType], None], block: BlockType) -> None: +def iter_block_children(f: Callable[[BlocksType], None], block: BlockType) -> None: if not isinstance(block, Block): return for blocks in block.defs.values(): - iter_blocks(f, blocks) + f(blocks) match block: case FunctionBlock(): if block.returns is not None: - iter_blocks(f, block.returns) + f(block.returns) case CallBlock(): if block.trace is not None: - iter_blocks(f, block.trace) + f(block.trace) case ModelBlock(): if block.input is not None: - iter_blocks(f, block.input) + f(block.input) if block.trace is not None: - iter_blocks(f, block.trace) + f(block.trace) case CodeBlock(): - iter_blocks(f, block.code) + f(block.code) case GetBlock(): pass case DataBlock(): pass case TextBlock(): - iter_blocks(f, block.text) + f(block.text) case LastOfBlock(): - iter_blocks(f, block.lastOf) + f(block.lastOf) case ArrayBlock(): - iter_blocks(f, block.array) + f(block.array) case ObjectBlock(): if isinstance(block.object, dict): for blocks in block.object.values(): - iter_blocks(f, blocks) + f(blocks) else: - iter_blocks(f, block.object) + f(block.object) case MessageBlock(): - iter_blocks(f, block.content) + f(block.content) case IfBlock(): - iter_blocks(f, block.then) + f(block.then) if block.elses is not None: - iter_blocks(f, block.elses) + f(block.elses) case RepeatBlock(): - iter_blocks(f, block.repeat) + f(block.repeat) if block.trace is not None: for trace in block.trace: - iter_blocks(f, trace) + f(trace) case RepeatUntilBlock(): - iter_blocks(f, block.repeat) + f(block.repeat) if block.trace is not None: for trace in block.trace: - iter_blocks(f, trace) + f(trace) case ForBlock(): - iter_blocks(f, block.repeat) + f(block.repeat) if block.trace is not None: for trace in block.trace: - iter_blocks(f, trace) + f(trace) case ErrorBlock(): - iter_blocks(f, block.program) + f(block.program) case ReadBlock(): pass case IncludeBlock(): if block.trace is not None: - iter_blocks(f, block.trace) + f(block.trace) case EmptyBlock(): pass case _: @@ -105,17 +105,9 @@ def iter_block_children(f: Callable[[BlockType], None], block: BlockType) -> Non case "json" | "yaml" | RegexParser(): pass case PdlParser(): - iter_blocks(f, block.parser.pdl) + f(block.parser.pdl) if block.fallback is not None: - iter_blocks(f, block.fallback) - - -def iter_blocks(f: Callable[[BlockType], None], blocks: BlocksType) -> None: - if not isinstance(blocks, str) and isinstance(blocks, Sequence): - for block in blocks: - f(block) - else: - f(blocks) + f(block.fallback) class MappedFunctions: diff --git a/src/pdl/pdl_dumper.py b/src/pdl/pdl_dumper.py index 8c6dd261f..368bb7621 100644 --- a/src/pdl/pdl_dumper.py +++ b/src/pdl/pdl_dumper.py @@ -21,6 +21,10 @@ GetBlock, IfBlock, IncludeBlock, + JoinArray, + JoinLastOf, + JoinText, + JoinType, LastOfBlock, LitellmModelBlock, LitellmParameters, @@ -76,7 +80,7 @@ def block_to_dict(block: pdl_ast.BlockType, json_compatible: bool) -> DumpedBloc if not isinstance(block, Block): return block d: dict[str, Any] = {} - d["kind"] = block.kind + d["kind"] = str(block.kind) if block.description is not None: d["description"] = block.description if block.spec is not None: @@ -87,7 +91,7 @@ def block_to_dict(block: pdl_ast.BlockType, json_compatible: bool) -> DumpedBloc } match block: case BamModelBlock(): - d["platform"] = block.platform + d["platform"] = str(block.platform) d["model"] = block.model if block.input is not None: d["input"] = blocks_to_dict(block.input, json_compatible) @@ -105,7 +109,7 @@ def block_to_dict(block: pdl_ast.BlockType, json_compatible: bool) -> DumpedBloc if block.constraints is not None: d["constraints"] = block.constraints case LitellmModelBlock(): - d["platform"] = block.platform + d["platform"] = str(block.platform) d["model"] = block.model if block.input is not None: d["input"] = blocks_to_dict(block.input, json_compatible) @@ -159,7 +163,7 @@ def block_to_dict(block: pdl_ast.BlockType, json_compatible: bool) -> DumpedBloc case RepeatBlock(): d["repeat"] = blocks_to_dict(block.repeat, json_compatible) d["num_iterations"] = block.num_iterations - d["join"] = block.join.model_dump(by_alias=True) + d["join"] = join_to_dict(block.join) if block.trace is not None: d["trace"] = [ blocks_to_dict(blocks, json_compatible) for blocks in block.trace @@ -167,7 +171,7 @@ def block_to_dict(block: pdl_ast.BlockType, json_compatible: bool) -> DumpedBloc case RepeatUntilBlock(): d["repeat"] = blocks_to_dict(block.repeat, json_compatible) d["until"] = block.until - d["join"] = block.join.model_dump(by_alias=True) + d["join"] = join_to_dict(block.join) if block.trace is not None: d["trace"] = [ blocks_to_dict(blocks, json_compatible) for blocks in block.trace @@ -175,7 +179,7 @@ def block_to_dict(block: pdl_ast.BlockType, json_compatible: bool) -> DumpedBloc case ForBlock(): d["for"] = block.fors d["repeat"] = blocks_to_dict(block.repeat, json_compatible) - d["join"] = block.join.model_dump(by_alias=True) + d["join"] = join_to_dict(block.join) if block.trace is not None: d["trace"] = [ blocks_to_dict(blocks, json_compatible) for blocks in block.trace @@ -217,6 +221,16 @@ def block_to_dict(block: pdl_ast.BlockType, json_compatible: bool) -> DumpedBloc return d +def join_to_dict(join: JoinType) -> dict[str, Any]: + d = {} + match join: + case JoinText(): + d["with"] = join.join_string + case JoinArray() | JoinLastOf(): + d["as"] = str(join.iteration_type) + return d + + JsonType: TypeAlias = None | bool | int | float | str | dict[str, "JsonType"] diff --git a/tests/data/line/hello14.pdl b/tests/data/line/hello14.pdl index 8a601442f..fb049f368 100644 --- a/tests/data/line/hello14.pdl +++ b/tests/data/line/hello14.pdl @@ -14,6 +14,7 @@ text: language: str spec: int return: + lastOf: - "\nTranslate the sentence '${ sentence }' to ${ language }\n" - model: watsonx/ibm/granite-20b-multilingual parameters: diff --git a/tests/data/line/hello15.pdl b/tests/data/line/hello15.pdl index fa3c031ed..f07835947 100644 --- a/tests/data/line/hello15.pdl +++ b/tests/data/line/hello15.pdl @@ -3,6 +3,7 @@ text: - def: stutter function: return: + lastOf: - get: boolean - ${ something } - "Hello World!\n" diff --git a/tests/data/line/hello24.pdl b/tests/data/line/hello24.pdl index 8934e8e92..b9a0020a1 100644 --- a/tests/data/line/hello24.pdl +++ b/tests/data/line/hello24.pdl @@ -14,6 +14,7 @@ text: language: str spec: int return: + lastOf: - "\nTranslate the sentence '${ sentence }' to ${ language }\n" - model: watsonx/ibm/granite-34b-code-instruct parameters: diff --git a/tests/data/line/hello25.pdl b/tests/data/line/hello25.pdl index 0f42e5fea..aad49031d 100644 --- a/tests/data/line/hello25.pdl +++ b/tests/data/line/hello25.pdl @@ -12,6 +12,7 @@ text: sentence: str language: str return: + lastOf: - "\nTranslate the sentence '${ sentence1 }' to ${ language }\n" - model: watsonx/ibm/granite-20b-multilingual parameters: diff --git a/tests/test_implicit_ignore.py b/tests/test_implicit_ignore.py index be2fc7637..2c11713dc 100644 --- a/tests/test_implicit_ignore.py +++ b/tests/test_implicit_ignore.py @@ -5,7 +5,7 @@ def do_test(capsys, test): result = exec_str(test["prog"]) captured = capsys.readouterr() warnings = {line.strip() for line in captured.err.split("\n")} - { - "You might want to use a `text` block around the list or explicitly ignore the result with `contribute: [context]`." + "You might want to use a `text` block around the list or explicitly ignore the result with a `lastOf` block or `contribute: [context]`." } assert result == test["result"] assert set(warnings) == set(test["warnings"]) @@ -20,8 +20,12 @@ def test_strings(capsys): """, "result": "Bye", "warnings": [ - "Warning: the result of block `Hello` is not used.", - "Warning: the result of block `How are you?` is not used.", + "Warning: the result of block `Hello", + "...", + "` is not used.", + "Warning: the result of block `How are you?", + "...", + "` is not used.", "", ], } @@ -55,3 +59,30 @@ def test_no_warning2(capsys): "warnings": [""], } do_test(capsys, test) + + +def test_function(capsys): + test = { + "prog": """ +defs: + f: + function: + return: + - Hello + - How are you? + - Bye +call: f +args: {} +""", + "result": "Bye", + "warnings": [ + "Warning: the result of block `Hello", + "...", + "` is not used.", + "Warning: the result of block `How are you?", + "...", + "` is not used.", + "", + ], + } + do_test(capsys, test) diff --git a/tests/test_line_table.py b/tests/test_line_table.py index 60713e974..f236f7b97 100644 --- a/tests/test_line_table.py +++ b/tests/test_line_table.py @@ -169,7 +169,7 @@ def test_line13(capsys: CaptureFixture[str]): "file": "tests/data/line/hello14.pdl", "errors": [ "", - "tests/data/line/hello14.pdl:24 - Type errors in result of function call to translate:", + "tests/data/line/hello14.pdl:25 - Type errors in result of function call to translate:", "tests/data/line/hello14.pdl:16 - Bonjour le monde! should be of type ", ], } @@ -183,7 +183,7 @@ def test_line14(capsys: CaptureFixture[str]): "file": "tests/data/line/hello15.pdl", "errors": [ "", - "tests/data/line/hello15.pdl:6 - Error during the evaluation of ${ boolean }: 'boolean' is undefined", + "tests/data/line/hello15.pdl:7 - Error during the evaluation of ${ boolean }: 'boolean' is undefined", ], } @@ -304,7 +304,7 @@ def test_line23(capsys: CaptureFixture[str]): "file": "tests/data/line/hello24.pdl", "errors": [ "", - "tests/data/line/hello24.pdl:24 - Error during the evaluation of Hello,${ GEN1 }: 'GEN1' is undefined", + "tests/data/line/hello24.pdl:25 - Error during the evaluation of Hello,${ GEN1 }: 'GEN1' is undefined", ], }